Initial Commit

This commit is contained in:
2026-10-03 20:07:49 +02:00
commit a1346c98b1
427 changed files with 59320 additions and 0 deletions
@@ -0,0 +1,45 @@
<script setup lang="ts">
import type { CardEffectKey } from '~~/shared/utils/cardEffects'
import EffectAcidSquares from '@/components/effects/EffectAcidSquares.vue'
import EffectAurora from '@/components/effects/EffectAurora.vue'
import EffectCrtWarp from '@/components/effects/EffectCrtWarp.vue'
import EffectDither from '@/components/effects/EffectDither.vue'
import EffectGradientWaves from '@/components/effects/EffectGradientWaves.vue'
import EffectLetterGlitch from '@/components/effects/EffectLetterGlitch.vue'
import EffectLineWaves from '@/components/effects/EffectLineWaves.vue'
import EffectParticles from '@/components/effects/EffectParticles.vue'
import EffectPlasma from '@/components/effects/EffectPlasma.vue'
import EffectScanner from '@/components/effects/EffectScanner.vue'
import EffectShapeGrid from '@/components/effects/EffectShapeGrid.vue'
import EffectSilk from '@/components/effects/EffectSilk.vue'
import EffectSoftAurora from '@/components/effects/EffectSoftAurora.vue'
import EffectWaves from '@/components/effects/EffectWaves.vue'
const props = defineProps<{
effect: CardEffectKey
color: string
}>()
const COMPONENTS = {
'gradient-waves': EffectGradientWaves,
'aurora': EffectAurora,
'particles': EffectParticles,
'letter-glitch': EffectLetterGlitch,
'waves': EffectWaves,
'scanner': EffectScanner,
'acid-squares': EffectAcidSquares,
'crt-warp': EffectCrtWarp,
'dither': EffectDither,
'soft-aurora': EffectSoftAurora,
'silk': EffectSilk,
'shape-grid': EffectShapeGrid,
'plasma': EffectPlasma,
'line-waves': EffectLineWaves,
} as const
const component = computed(() => COMPONENTS[props.effect] ?? null)
</script>
<template>
<component :is="component" v-if="component" :color="color" />
</template>
+206
View File
@@ -0,0 +1,206 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue'
import type { CardBorderEffectKey } from '~~/shared/utils/cardBorderEffects'
import { hexToRgbTuple } from '@/lib/color'
// Electric-Variante portiert aus Vue Bits (DavidHDev/vue-bits, ElectricBorder)
const props = withDefaults(defineProps<{
effect: CardBorderEffectKey
color?: string
}>(), {
color: '#6366f1',
})
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef')
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
function random(x: number): number {
return (Math.sin(x * 12.9898) * 43758.5453) % 1
}
function noise2D(x: number, y: number): number {
const i = Math.floor(x)
const j = Math.floor(y)
const fx = x - i
const fy = y - j
const a = random(i + j * 57)
const b = random(i + 1 + j * 57)
const c = random(i + (j + 1) * 57)
const d = random(i + 1 + (j + 1) * 57)
const ux = fx * fx * (3 - 2 * fx)
const uy = fy * fy * (3 - 2 * fy)
return a * (1 - ux) * (1 - uy) + b * ux * (1 - uy) + c * (1 - ux) * uy + d * ux * uy
}
function octavedNoise(x: number, time: number, seed: number): number {
let y = 0
let amplitude = 0.12
let frequency = 10
for (let i = 0; i < 10; i++) {
y += amplitude * noise2D(frequency * x + seed * 100, time * frequency * 0.3)
frequency *= 1.6
amplitude *= 0.7
}
return y
}
function roundedRectPoint(t: number, w: number, h: number, r: number): { x: number, y: number } {
const sw = w - 2 * r
const sh = h - 2 * r
const arc = (Math.PI * r) / 2
const total = 2 * sw + 2 * sh + 4 * arc
let d = t * total
if (d <= sw) return { x: r + d, y: 0 }
d -= sw
if (d <= arc) {
const a = -Math.PI / 2 + (d / arc) * (Math.PI / 2)
return { x: w - r + r * Math.cos(a), y: r + r * Math.sin(a) }
}
d -= arc
if (d <= sh) return { x: w, y: r + d }
d -= sh
if (d <= arc) {
const a = (d / arc) * (Math.PI / 2)
return { x: w - r + r * Math.cos(a), y: h - r + r * Math.sin(a) }
}
d -= arc
if (d <= sw) return { x: w - r - d, y: h }
d -= sw
if (d <= arc) {
const a = Math.PI / 2 + (d / arc) * (Math.PI / 2)
return { x: r + r * Math.cos(a), y: h - r + r * Math.sin(a) }
}
d -= arc
if (d <= sh) return { x: 0, y: h - r - d }
d -= sh
const a = Math.PI + (d / arc) * (Math.PI / 2)
return { x: r + r * Math.cos(a), y: r + r * Math.sin(a) }
}
let cleanup: (() => void) | null = null
onMounted(() => {
if (props.effect !== 'electric') return
const canvas = canvasRef.value
const container = containerRef.value
if (!canvas || !container) return
const ctx = canvas.getContext('2d')
if (!ctx) return
let width = 0
let height = 0
let radius = 0
const dpr = Math.min(window.devicePixelRatio || 1, 2)
const updateSize = () => {
const rect = container.getBoundingClientRect()
width = rect.width
height = rect.height
canvas.width = width * dpr
canvas.height = height * dpr
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
radius = parseFloat(getComputedStyle(container).borderRadius) || 0
}
updateSize()
const ro = new ResizeObserver(updateSize)
ro.observe(container)
let raf = 0
let time = 0
let last = 0
const draw = (now: number) => {
time += ((now - last) / 1000) * 1.2
last = now
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
ctx.clearRect(0, 0, width, height)
ctx.strokeStyle = props.color
ctx.lineWidth = 1.5
ctx.lineCap = 'round'
ctx.lineJoin = 'round'
const inset = 1
const w = width - inset * 2
const h = height - inset * 2
const r = Math.max(0, radius - inset)
const perimeter = 2 * (w + h) + 2 * Math.PI * r
const samples = Math.floor(perimeter / 2)
ctx.beginPath()
for (let i = 0; i <= samples; i++) {
const p = i / samples
const pt = roundedRectPoint(p, w, h, r)
const nx = octavedNoise(p * 8, time, 0) * 14
const ny = octavedNoise(p * 8, time, 1) * 14
if (i === 0) ctx.moveTo(inset + pt.x + nx, inset + pt.y + ny)
else ctx.lineTo(inset + pt.x + nx, inset + pt.y + ny)
}
ctx.closePath()
ctx.stroke()
raf = requestAnimationFrame(draw)
}
raf = requestAnimationFrame(draw)
cleanup = () => {
cancelAnimationFrame(raf)
ro.disconnect()
}
})
onBeforeUnmount(() => {
cleanup?.()
cleanup = null
})
const rgb = computed(() => hexToRgbTuple(props.color).map(v => Math.round(v * 255)).join(' '))
const cssVars = computed<CSSProperties>(() => ({ '--be-color': `rgb(${rgb.value})` }) as CSSProperties)
</script>
<template>
<div ref="containerRef" class="pointer-events-none absolute inset-0 rounded-[inherit]" :style="cssVars">
<canvas v-if="effect === 'electric'" ref="canvasRef" class="absolute inset-0" />
<div v-else-if="effect === 'beam'" class="border-beam absolute inset-0 rounded-[inherit]" />
<div v-else-if="effect === 'pulse'" class="border-pulse absolute inset-0 rounded-[inherit]" />
<div v-else-if="effect === 'rainbow'" class="border-rainbow absolute inset-0 rounded-[inherit]" />
</div>
</template>
<style scoped>
@property --beam-angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.border-beam {
border: 2px solid transparent;
background: conic-gradient(from var(--beam-angle), transparent 60%, var(--be-color) 80%, transparent 100%) border-box;
mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
mask-composite: exclude;
animation: be-rotate 3s linear infinite;
}
.border-pulse {
border: 2px solid var(--be-color);
box-shadow: 0 0 12px 1px var(--be-color);
animation: be-pulse 2s ease-in-out infinite;
}
.border-rainbow {
border: 2px solid transparent;
background: conic-gradient(from var(--beam-angle), var(--be-color), #ffffff, var(--be-color)) border-box;
mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
mask-composite: exclude;
animation: be-rotate 4s linear infinite;
}
@keyframes be-rotate {
to { --beam-angle: 360deg; }
}
@keyframes be-pulse {
0%, 100% { opacity: 0.35; }
50% { opacity: 1; }
}
</style>
+16
View File
@@ -0,0 +1,16 @@
<script setup lang="ts">
// Optionale Vignette über Karten-Hintergrundeffekten: dunkelt die Ränder
// zur Surface-Farbe hin ab, damit darüberliegender Text lesbar bleibt.
// `surface` muss die CSS-Farbe der Kartenfläche sein (popover/card/background),
// damit die Kante optisch in die Karte übergeht.
withDefaults(defineProps<{ surface?: string }>(), {
surface: 'var(--color-popover)',
})
</script>
<template>
<div
class="pointer-events-none absolute inset-0 rounded-[inherit]"
:style="{ background: `radial-gradient(ellipse at center, transparent 25%, ${surface} 92%)` }"
/>
</template>
@@ -0,0 +1,152 @@
<script setup lang="ts">
import { hexToRgbTuple, shadeHex } from '@/lib/color'
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop,
// Blur-Post-Pass entfernt (blur: 0 war Standard)
const props = withDefaults(defineProps<{ color?: string }>(), {
color: '#6366f1',
})
const VERT = `#version 300 es
in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`
const FRAG = `#version 300 es
precision highp float;
uniform float iTime;
uniform float uSpeed;
uniform float uWaveDepth;
uniform float uZoom;
uniform float uDensity;
uniform float uSpread;
uniform float uStepSize;
uniform float uGlow;
uniform float uExposure;
uniform float uColorShift;
uniform float uContrast;
uniform float uBrightness;
uniform float uOpacity;
uniform float uSteps;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform vec3 uColor3;
uniform vec2 uMouse;
uniform float uMouseStrength;
uniform float uMouseRadius;
uniform float uMouseActive;
uniform float uGrainIntensity;
uniform vec2 uCardRes;
uniform float uRadius;
out vec4 fragColor;
float cardMask(vec2 fc) {
vec2 p = fc - uCardRes * 0.5;
vec2 hb = uCardRes * 0.5 - vec2(uRadius);
float rd = length(max(abs(p) - hb, 0.0)) - uRadius;
return 1.0 - smoothstep(-0.75, 0.75, rd);
}
void main() {
vec2 frag = gl_FragCoord.xy;
float zoom = max(uZoom, 0.05);
// Pixelkonsistent: Referenzhöhe 180px, damit die Zellen in flachen
// Containern ihre reale Pixelgröße behalten statt zu stauchen
vec2 ndc = (2.0 * frag - uCardRes.xy) / 180.0;
vec2 dir = ndc * (0.5 / zoom);
vec2 mouseNdc = uMouse * (uCardRes / 360.0);
float mr = max(uMouseRadius, 0.01);
vec2 md = ndc - mouseNdc;
float dent = exp(-dot(md, md) / (mr * mr)) * (3.0 * uMouseStrength * uMouseActive);
float travel = sin(iTime * uSpeed) * uWaveDepth;
float density = max(uDensity, 1.0);
float spread = clamp(uSpread, 0.05, 0.6);
float stepSize = max(uStepSize, 0.0005);
float glowGain = max(uGlow, 0.0);
vec3 tOffset = vec3(0.0, dent, travel);
vec3 p = vec3(0.0);
float s = 0.0;
float glow = 0.0;
for (int i = 0; i < 64; i++) {
if (float(i) >= uSteps) break;
p += vec3(dir * s, s);
vec3 q = p + tOffset;
s += density - length(q.xz) + length(ceil(q).xy);
s = stepSize + abs(s) * spread;
glow += glowGain / s;
}
float e = glow / max(uExposure, 1.0);
float shimmer = 0.5 + 0.5 * dot(cos(iTime * uColorShift + p), vec3(0.3333));
float v = tanh(e * uBrightness * mix(0.7, 1.05, shimmer));
v = clamp((v - 0.5) * uContrast + 0.5, 0.0, 1.0);
vec3 col = mix(uColor1, uColor2, smoothstep(0.0, 0.55, v));
col = mix(col, uColor3, smoothstep(0.55, 1.0, v));
col *= v;
float a = clamp(v, 0.0, 1.0) * uOpacity;
vec3 outRgb = col;
if (a > 0.01) {
float gv = (fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233)) + iTime) * 43758.5453) - 0.5) * uGrainIntensity;
outRgb = clamp(outRgb + gv, 0.0, 1.0);
a = clamp(a + gv, 0.0, 1.0);
}
a *= cardMask(frag);
fragColor = vec4(outRgb * a, a);
}
`
const ctnDom = ref<HTMLDivElement | null>(null)
useCardShader(ctnDom, {
vertex: VERT,
fragment: FRAG,
mouse: true,
uniforms: {
iTime: { value: 0 },
uSpeed: { value: 0.7 },
uWaveDepth: { value: 1 },
uZoom: { value: 1.3 },
uDensity: { value: 10.0 },
uSpread: { value: 0.3 },
uStepSize: { value: 0.002 },
uGlow: { value: 1.0 },
uExposure: { value: 2700 },
uColorShift: { value: 0 },
uContrast: { value: 1 },
uBrightness: { value: 1.0 },
uOpacity: { value: 1.0 },
uSteps: { value: 32 },
uColor1: { value: new Float32Array(hexToRgbTuple(shadeHex(props.color, -0.35))) },
uColor2: { value: new Float32Array(hexToRgbTuple(props.color)) },
uColor3: { value: new Float32Array([1, 1, 1]) },
uMouse: { value: new Float32Array([0, 0]) },
uMouseStrength: { value: 0.1 },
uMouseRadius: { value: 0.35 },
uMouseActive: { value: 0 },
uGrainIntensity: { value: 0.05 },
},
color: () => props.color,
colorUniforms: { uColor1: -0.35, uColor2: 0 },
frame: ({ t, program, mouse }) => {
const u = program.uniforms
u.iTime.value = t * 0.001
const m = u.uMouse.value as Float32Array
m[0] = mouse.x * 2 - 1
m[1] = mouse.y * 2 - 1
u.uMouseActive.value = mouse.active
},
})
</script>
<template>
<div ref="ctnDom" class="relative size-full overflow-hidden rounded-[inherit]" />
</template>
+223
View File
@@ -0,0 +1,223 @@
<script setup lang="ts">
import { Color, Mesh, Program, Renderer, Triangle } from 'ogl'
import { shadeHex } from '@/lib/color'
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop
const props = withDefaults(defineProps<{ color?: string }>(), {
color: '#6366f1',
})
const VERT = `#version 300 es
in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`
const FRAG = `#version 300 es
precision highp float;
uniform float uTime;
uniform float uAmplitude;
uniform vec3 uColorStops[3];
uniform vec2 uResolution;
uniform float uBlend;
uniform float uRadius;
out vec4 fragColor;
vec3 permute(vec3 x) {
return mod(((x * 34.0) + 1.0) * x, 289.0);
}
float snoise(vec2 v){
const vec4 C = vec4(
0.211324865405187, 0.366025403784439,
-0.577350269189626, 0.024390243902439
);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod(i, 289.0);
vec3 p = permute(
permute(i.y + vec3(0.0, i1.y, 1.0))
+ i.x + vec3(0.0, i1.x, 1.0)
);
vec3 m = max(
0.5 - vec3(
dot(x0, x0),
dot(x12.xy, x12.xy),
dot(x12.zw, x12.zw)
),
0.0
);
m = m * m;
m = m * m;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
struct ColorStop {
vec3 color;
float position;
};
#define COLOR_RAMP(colors, factor, finalColor) { \\
int index = 0; \\
for (int i = 0; i < 2; i++) { \\
ColorStop currentColor = colors[i]; \\
bool isInBetween = currentColor.position <= factor; \\
index = int(mix(float(index), float(i), float(isInBetween))); \\
} \\
ColorStop currentColor = colors[index]; \\
ColorStop nextColor = colors[index + 1]; \\
float range = nextColor.position - currentColor.position; \\
float lerpFactor = (factor - currentColor.position) / range; \\
finalColor = mix(currentColor.color, nextColor.color, lerpFactor); \\
}
void main() {
vec2 uv = gl_FragCoord.xy / uResolution;
ColorStop colors[3];
colors[0] = ColorStop(uColorStops[0], 0.0);
colors[1] = ColorStop(uColorStops[1], 0.5);
colors[2] = ColorStop(uColorStops[2], 1.0);
vec3 rampColor;
COLOR_RAMP(colors, uv.x, rampColor);
// Noise x-Koordinate pixelkonsistent (~2 Einheiten pro 320px), damit
// die Wellen in schmalen/breiten Karten nicht gestaucht/gestreckt wirken
float height = snoise(vec2(gl_FragCoord.x / 160.0 + uTime * 0.1, uTime * 0.25)) * 0.5 * uAmplitude;
height = exp(height);
height = (uv.y * 2.0 - height + 0.2);
float intensity = 0.6 * height;
float midPoint = 0.20;
float auroraAlpha = smoothstep(midPoint - uBlend * 0.5, midPoint + uBlend * 0.5, intensity);
vec3 auroraColor = intensity * rampColor;
// Rounded-Rect-Maske direkt im Shader — zuverlässig auch wenn der
// Compositor den Canvas-Layer nicht an CSS-Clips bindet (Waterfox)
vec2 p = gl_FragCoord.xy - uResolution * 0.5;
vec2 hb = uResolution * 0.5 - vec2(uRadius);
float rd = length(max(abs(p) - hb, 0.0)) - uRadius;
float rm = 1.0 - smoothstep(-0.75, 0.75, rd);
auroraAlpha *= rm;
fragColor = vec4(auroraColor * auroraAlpha, auroraAlpha);
}
`
const ctnDom = ref<HTMLDivElement | null>(null)
let animateId = 0
let teardown: (() => void) | null = null
onMounted(() => {
const ctn = ctnDom.value
if (!ctn) return
const renderer = new Renderer({ alpha: true, premultipliedAlpha: true, antialias: true })
const gl = renderer.gl
gl.clearColor(0, 0, 0, 0)
gl.enable(gl.BLEND)
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
// WebGL-Canvas bleibt detached: Waterfox/Firefox zeichnet WebGL-Layer nach
// Scrollen teils an veralteter Position und ignoriert CSS-Clips — stattdessen
// wird pro Frame in einen normalen 2D-Canvas geblittet
const outCanvas = document.createElement('canvas')
outCanvas.className = 'absolute inset-0 size-full rounded-[inherit]'
const outCtx = outCanvas.getContext('2d')
ctn.appendChild(outCanvas)
const geometry = new Triangle(gl)
if (geometry.attributes.uv) {
delete geometry.attributes.uv
}
const colorStops = [shadeHex(props.color, -0.5), props.color, shadeHex(props.color, 0.4)]
.map((hex) => {
const c = new Color(hex)
return [c.r, c.g, c.b]
})
const program = new Program(gl, {
vertex: VERT,
fragment: FRAG,
uniforms: {
uTime: { value: 0 },
uAmplitude: { value: 1.0 },
uColorStops: { value: colorStops },
uResolution: { value: [ctn.offsetWidth, ctn.offsetHeight] },
uBlend: { value: 0.5 },
uRadius: { value: 0 },
},
})
const mesh = new Mesh(gl, { geometry, program })
// Farbwechsel live übernehmen (Settings-Vorschau)
watch(() => props.color, (c) => {
program.uniforms.uColorStops!.value = [shadeHex(c, -0.5), c, shadeHex(c, 0.4)]
.map((hex) => {
const col = new Color(hex)
return [col.r, col.g, col.b]
})
})
const resizeHandler = () => {
renderer.setSize(ctn.offsetWidth, ctn.offsetHeight)
program.uniforms.uResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight]
const cssRadius = parseFloat(getComputedStyle(ctn).borderTopLeftRadius) || 0
program.uniforms.uRadius.value = ctn.offsetWidth > 0
? cssRadius * (gl.drawingBufferWidth / ctn.offsetWidth)
: 0
outCanvas.width = gl.drawingBufferWidth
outCanvas.height = gl.drawingBufferHeight
}
const ro = new ResizeObserver(resizeHandler)
ro.observe(ctn)
resizeHandler()
const update = (t: number) => {
animateId = requestAnimationFrame(update)
program.uniforms.uTime.value = t * 0.01 * 0.1
renderer.render({ scene: mesh })
outCtx?.clearRect(0, 0, outCanvas.width, outCanvas.height)
outCtx?.drawImage(gl.canvas, 0, 0, outCanvas.width, outCanvas.height)
}
animateId = requestAnimationFrame(update)
teardown = () => {
cancelAnimationFrame(animateId)
ro.disconnect()
if (outCanvas.parentNode === ctn) ctn.removeChild(outCanvas)
gl.getExtension('WEBGL_lose_context')?.loseContext()
}
})
onUnmounted(() => {
teardown?.()
teardown = null
})
</script>
<template>
<div ref="ctnDom" class="relative size-full overflow-hidden rounded-[inherit]" />
</template>
+194
View File
@@ -0,0 +1,194 @@
<script setup lang="ts">
import { hexToRgbTuple, shadeHex } from '@/lib/color'
// Portiert aus Vue Bits (DavidHDev/vue-bits), von three.js auf ogl umgeschrieben,
// vereinfacht auf color-Prop
const props = withDefaults(defineProps<{ color?: string }>(), {
color: '#6366f1',
})
const VERT = `
attribute vec2 uv;
attribute vec2 position;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0.0, 1.0);
}
`
const FRAG = `
precision highp float;
varying vec2 vUv;
uniform float uTime;
uniform float uSpeed;
uniform vec3 uColor;
uniform vec3 uBackgroundColor;
uniform float uCurvature;
uniform float uScanlineStrength;
uniform float uScanlineFrequency;
uniform float uWaveAmplitude;
uniform float uWaveFrequency;
uniform float uBloom;
uniform float uBloomRadius;
uniform float uNoise;
uniform float uVignette;
uniform float uBrightness;
uniform float uPixelation;
uniform float uRgbShift;
uniform vec2 uPointer;
uniform float uMouseStrength;
uniform vec2 uCardRes;
uniform float uRadius;
float hash21(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
vec2 crtCurve(vec2 uv, float radius) {
vec2 p = (uv - 0.5) * 2.0;
float safeRadius = max(radius, 1.415);
float cornerScale = safeRadius / sqrt(max(safeRadius * safeRadius - 2.0, 0.001));
p = safeRadius * p / sqrt(max(safeRadius * safeRadius - dot(p, p), 0.001));
p /= cornerScale;
return p * 0.5 + 0.5;
}
float referencePlasma(vec2 uv, float t) {
float frequencyScale = max(uWaveFrequency / 2.2, 0.001);
uv = (uv - 0.5) * frequencyScale + 0.5;
float scanline = 0.5 - 0.5 * cos(uv.y * 3.14159265 * uScanlineFrequency);
scanline = mix(1.0, scanline, uScanlineStrength);
// Pixelkonsistente Zellen (~16px) statt fixem 80x24-Raster,
// damit das CRT-Pixeln in flachen Containern nicht staucht
vec2 cells = max(round(uCardRes / 16.0), vec2(4.0));
uv *= cells;
uv = ceil(uv);
uv /= cells;
float amplitude = uWaveAmplitude / 0.28;
float field = 0.0;
field += 0.7 * sin(0.5 * uv.x + t / 5.0);
field += 3.0 * sin(1.6 * uv.y + t / 5.0);
field += sin(10.0 * (uv.y * sin(t / 2.0) + uv.x * cos(t / 5.0)) + t / 2.0);
float cx = uv.x + 0.5 * sin(t / 2.0);
float cy = uv.y + 0.5 * cos(t / 4.0);
field += 0.4 * sin(sqrt(100.0 * cx * cx + 100.0 * cy * cy + 1.0) + t);
field += 0.9 * sin(sqrt(75.0 * cx * cx + 25.0 * cy * cy + 1.0) + t);
field -= 1.4 * sin(sqrt(256.0 * cx * cx + 25.0 * cy * cy + 1.0) + t);
field += 0.3 * sin(0.5 * uv.y + uv.x + sin(t));
return scanline * floor(3.0 * (0.5 + 0.499 * sin(field * amplitude))) / 3.0;
}
float cardMask(vec2 fc) {
vec2 p = fc - uCardRes * 0.5;
vec2 hb = uCardRes * 0.5 - vec2(uRadius);
float rd = length(max(abs(p) - hb, 0.0)) - uRadius;
return 1.0 - smoothstep(-0.75, 0.75, rd);
}
void main() {
vec2 uv = vUv;
if (uPixelation > 1.001) {
vec2 cells = max(uCardRes / uPixelation, vec2(1.0));
uv = (floor(uv * cells) + 0.5) / cells;
}
float curveRadius = 1.1 + 0.42 / max(uCurvature, 0.001);
curveRadius *= exp(-uPointer.y * uMouseStrength * 0.4);
vec2 curvedUv = crtCurve(uv, curveRadius);
curvedUv.x -= uPointer.x * uMouseStrength * 0.035;
float t = uTime * uSpeed;
float signal = referencePlasma(curvedUv, t);
float radius = 0.01 * uBloomRadius;
float glow = signal * 0.2;
glow += referencePlasma(curvedUv + vec2(radius, 0.0), t) * 0.12;
glow += referencePlasma(curvedUv - vec2(radius, 0.0), t) * 0.12;
glow += referencePlasma(curvedUv + vec2(0.0, radius), t) * 0.12;
glow += referencePlasma(curvedUv - vec2(0.0, radius), t) * 0.12;
glow += referencePlasma(curvedUv + vec2(radius), t) * 0.08;
glow += referencePlasma(curvedUv - vec2(radius), t) * 0.08;
glow += referencePlasma(curvedUv + vec2(radius, -radius), t) * 0.08;
glow += referencePlasma(curvedUv + vec2(-radius, radius), t) * 0.08;
float redSignal = referencePlasma(curvedUv + vec2(uRgbShift, 0.0), t);
float blueSignal = referencePlasma(curvedUv - vec2(uRgbShift, 0.0), t);
vec3 channelSignal = vec3(redSignal, signal, blueSignal);
vec3 waveColor = uColor * (0.3 + signal * 0.7 + glow * uBloom * 0.65);
waveColor += (channelSignal - signal) * 0.42;
float edge = clamp(1.0 - dot(vUv - 0.5, vUv - 0.5) * 2.0, 0.0, 1.0);
float edgeFade = mix(1.0, smoothstep(0.0, 1.0, edge), uVignette);
float waveMask = clamp(signal * 0.82 + glow * 0.52, 0.0, 1.0) * edgeFade;
waveColor = max(waveColor * uBrightness, vec3(0.0));
float a = clamp(waveMask, 0.0, 1.0) * cardMask(gl_FragCoord.xy);
vec3 col = waveColor;
if (a > 0.01) {
float grain = (hash21(gl_FragCoord.xy + vec2(fract(uTime) * 173.0)) - 0.5) * uNoise;
col = max(col + grain, vec3(0.0));
}
gl_FragColor = vec4(col * a, a);
}
`
const ctnDom = ref<HTMLDivElement | null>(null)
let elapsed = 0
let lastT = 0
useCardShader(ctnDom, {
vertex: VERT,
fragment: FRAG,
mouse: true,
uniforms: {
uTime: { value: 0 },
uSpeed: { value: 0.5 },
uColor: { value: new Float32Array(hexToRgbTuple(props.color)) },
uBackgroundColor: { value: new Float32Array(hexToRgbTuple(shadeHex(props.color, -0.93))) },
uCurvature: { value: 0.25 },
uScanlineStrength: { value: 0.25 },
uScanlineFrequency: { value: 200 },
uWaveAmplitude: { value: 0.3 },
uWaveFrequency: { value: 2.5 },
uBloom: { value: 1.5 },
uBloomRadius: { value: 1 },
uNoise: { value: 0.1 },
uVignette: { value: 0 },
uBrightness: { value: 1.25 },
uPixelation: { value: 1 },
uRgbShift: { value: 0.015 },
uPointer: { value: new Float32Array([0, 0]) },
uMouseStrength: { value: 0.5 },
},
color: () => props.color,
colorUniforms: { uColor: 0, uBackgroundColor: -0.93 },
frame: ({ t, program, mouse }) => {
// fps-Cap ~30 wie im Original (CRT-Flackern)
const u = program.uniforms
const delta = Math.min((t - lastT) * 0.001, 0.1)
lastT = t
elapsed += delta
u.uTime.value = elapsed
const p = u.uPointer.value as Float32Array
p[0] = (mouse.x * 2 - 1) * mouse.active
p[1] = (mouse.y * 2 - 1) * mouse.active
// Scanline-Dichte in Pixel-Pitch (eine Linie alle ~6 Buffer-Pixel)
// statt fixer Zeilenzahl — flache Karten sonst subpixel-gestaucht
const res = u.uCardRes!.value as Float32Array
u.uScanlineFrequency!.value = res[1] / 6
},
})
</script>
<template>
<div ref="ctnDom" class="relative size-full overflow-hidden rounded-[inherit]" />
</template>
+240
View File
@@ -0,0 +1,240 @@
<script setup lang="ts">
import { hexToRgbTuple } from '@/lib/color'
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop
const props = withDefaults(defineProps<{ color?: string }>(), {
color: '#6366f1',
})
const VERT = `
attribute vec2 uv;
attribute vec2 position;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0.0, 1.0);
}
`
const FRAG = `
precision highp float;
uniform float time;
uniform float waveSpeed;
uniform float waveFrequency;
uniform float waveAmplitude;
uniform vec3 waveColor;
uniform vec2 mousePos;
uniform float mouseActive;
uniform float mouseRadius;
uniform float colorNum;
uniform float pixelSize;
uniform vec2 uCardRes;
uniform float uRadius;
varying vec2 vUv;
vec4 mod289(vec4 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
vec2 fade(vec2 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }
float cnoise(vec2 P) {
vec4 Pi = floor(P.xyxy) + vec4(0.0,0.0,1.0,1.0);
vec4 Pf = fract(P.xyxy) - vec4(0.0,0.0,1.0,1.0);
Pi = mod289(Pi);
vec4 ix = Pi.xzxz;
vec4 iy = Pi.yyww;
vec4 fx = Pf.xzxz;
vec4 fy = Pf.yyww;
vec4 i = permute(permute(ix) + iy);
vec4 gx = fract(i * (1.0/41.0)) * 2.0 - 1.0;
vec4 gy = abs(gx) - 0.5;
vec4 tx = floor(gx + 0.5);
gx = gx - tx;
vec2 g00 = vec2(gx.x, gy.x);
vec2 g10 = vec2(gx.y, gy.y);
vec2 g01 = vec2(gx.z, gy.z);
vec2 g11 = vec2(gx.w, gy.w);
vec4 norm = taylorInvSqrt(vec4(dot(g00,g00), dot(g01,g01), dot(g10,g10), dot(g11,g11)));
g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w;
float n00 = dot(g00, vec2(fx.x, fy.x));
float n10 = dot(g10, vec2(fx.y, fy.y));
float n01 = dot(g01, vec2(fx.z, fy.z));
float n11 = dot(g11, vec2(fx.w, fy.w));
vec2 fade_xy = fade(Pf.xy);
float n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);
}
const int OCTAVES = 8;
float fbm(vec2 p) {
float value = 0.0;
float amp = 1.0;
float freq = waveFrequency;
for (int i = 0; i < OCTAVES; i++) {
value += amp * abs(cnoise(p));
p *= freq;
amp *= waveAmplitude;
}
return value;
}
float pattern(vec2 p) {
vec2 p2 = p - time * waveSpeed;
return fbm(p - fbm(p + fbm(p2)));
}
float getBayerValue(int x, int y) {
if (y == 0) {
if (x == 0) return 0.0/64.0;
if (x == 1) return 48.0/64.0;
if (x == 2) return 12.0/64.0;
if (x == 3) return 60.0/64.0;
if (x == 4) return 3.0/64.0;
if (x == 5) return 51.0/64.0;
if (x == 6) return 15.0/64.0;
if (x == 7) return 63.0/64.0;
} else if (y == 1) {
if (x == 0) return 32.0/64.0;
if (x == 1) return 16.0/64.0;
if (x == 2) return 44.0/64.0;
if (x == 3) return 28.0/64.0;
if (x == 4) return 35.0/64.0;
if (x == 5) return 19.0/64.0;
if (x == 6) return 47.0/64.0;
if (x == 7) return 31.0/64.0;
} else if (y == 2) {
if (x == 0) return 8.0/64.0;
if (x == 1) return 56.0/64.0;
if (x == 2) return 4.0/64.0;
if (x == 3) return 52.0/64.0;
if (x == 4) return 11.0/64.0;
if (x == 5) return 59.0/64.0;
if (x == 6) return 7.0/64.0;
if (x == 7) return 55.0/64.0;
} else if (y == 3) {
if (x == 0) return 40.0/64.0;
if (x == 1) return 24.0/64.0;
if (x == 2) return 36.0/64.0;
if (x == 3) return 20.0/64.0;
if (x == 4) return 43.0/64.0;
if (x == 5) return 27.0/64.0;
if (x == 6) return 39.0/64.0;
if (x == 7) return 23.0/64.0;
} else if (y == 4) {
if (x == 0) return 2.0/64.0;
if (x == 1) return 50.0/64.0;
if (x == 2) return 14.0/64.0;
if (x == 3) return 62.0/64.0;
if (x == 4) return 1.0/64.0;
if (x == 5) return 49.0/64.0;
if (x == 6) return 13.0/64.0;
if (x == 7) return 61.0/64.0;
} else if (y == 5) {
if (x == 0) return 34.0/64.0;
if (x == 1) return 18.0/64.0;
if (x == 2) return 46.0/64.0;
if (x == 3) return 30.0/64.0;
if (x == 4) return 33.0/64.0;
if (x == 5) return 17.0/64.0;
if (x == 6) return 45.0/64.0;
if (x == 7) return 29.0/64.0;
} else if (y == 6) {
if (x == 0) return 10.0/64.0;
if (x == 1) return 58.0/64.0;
if (x == 2) return 6.0/64.0;
if (x == 3) return 54.0/64.0;
if (x == 4) return 9.0/64.0;
if (x == 5) return 57.0/64.0;
if (x == 6) return 5.0/64.0;
if (x == 7) return 53.0/64.0;
} else if (y == 7) {
if (x == 0) return 42.0/64.0;
if (x == 1) return 26.0/64.0;
if (x == 2) return 38.0/64.0;
if (x == 3) return 22.0/64.0;
if (x == 4) return 41.0/64.0;
if (x == 5) return 25.0/64.0;
if (x == 6) return 37.0/64.0;
if (x == 7) return 21.0/64.0;
}
return 0.0;
}
vec3 dither(vec2 uv, vec3 color) {
vec2 scaledCoord = floor(uv * uCardRes / pixelSize);
int x = int(mod(scaledCoord.x, 8.0));
int y = int(mod(scaledCoord.y, 8.0));
float threshold = getBayerValue(x, y) - 0.25;
float step = 1.0 / (colorNum - 1.0);
color += threshold * step;
float bias = 0.2;
color = clamp(color - bias, 0.0, 1.0);
return floor(color * (colorNum - 1.0) + 0.5) / (colorNum - 1.0);
}
float cardMask(vec2 fc) {
vec2 p = fc - uCardRes * 0.5;
vec2 hb = uCardRes * 0.5 - vec2(uRadius);
float rd = length(max(abs(p) - hb, 0.0)) - uRadius;
return 1.0 - smoothstep(-0.75, 0.75, rd);
}
void main() {
vec2 uv = gl_FragCoord.xy / uCardRes.xy;
// Pixelkonsistent: auf Referenzhöhe 180px normieren,
// damit das Dither-Muster in flachen Containern nicht staucht
vec2 centeredUv = (uv - 0.5) * (uCardRes / 180.0);
float f = pattern(centeredUv);
vec2 mouseNDC = ((mousePos / uCardRes - 0.5) * vec2(1.0, -1.0)) * (uCardRes / 180.0);
float dist = length(centeredUv - mouseNDC);
float effect = (1.0 - smoothstep(0.0, mouseRadius, dist)) * mouseActive;
f -= 0.5 * effect;
vec3 col = mix(vec3(0.0), waveColor, f);
col = dither(uv, col);
float m = cardMask(gl_FragCoord.xy);
float a = clamp(max(col.r, max(col.g, col.b)), 0.0, 1.0) * m;
gl_FragColor = vec4(col * a, a);
}
`
const ctnDom = ref<HTMLDivElement | null>(null)
useCardShader(ctnDom, {
vertex: VERT,
fragment: FRAG,
mouse: true,
uniforms: {
time: { value: 0 },
waveSpeed: { value: 0.05 },
waveFrequency: { value: 3 },
waveAmplitude: { value: 0.3 },
waveColor: { value: hexToRgbTuple(props.color) },
mousePos: { value: new Float32Array([0, 0]) },
mouseActive: { value: 0 },
mouseRadius: { value: 1 },
colorNum: { value: 4 },
pixelSize: { value: 2 },
},
color: () => props.color,
colorUniforms: { waveColor: 0 },
frame: ({ t, program, mouse }) => {
const u = program.uniforms
u.time.value = t * 0.001
const res = u.uCardRes.value as Float32Array
const m = u.mousePos.value as Float32Array
m[0] = mouse.x * res[0]
m[1] = (1 - mouse.y) * res[1]
u.mouseActive.value = mouse.active
},
})
</script>
<template>
<div ref="ctnDom" class="relative size-full overflow-hidden rounded-[inherit]" />
</template>
@@ -0,0 +1,246 @@
<script setup lang="ts">
import { Mesh, Program, Renderer, Triangle } from 'ogl'
import { hexToRgbTuple, shadeHex } from '@/lib/color'
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop
const props = withDefaults(defineProps<{ color?: string }>(), {
color: '#6366f1',
})
const vertex = `#version 300 es
in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`
const fragment = `#version 300 es
precision highp float;
uniform vec2 iResolution;
uniform float iTime;
uniform float uSpeed;
uniform float uAmplitude;
uniform float uWaveScale;
uniform float uWaveRatio;
uniform float uSwell;
uniform float uTurbulence;
uniform float uTilt;
uniform float uZoom;
uniform float uHeight;
uniform float uFogDepth;
uniform float uSteps;
uniform float uBrightness;
uniform float uOpacity;
uniform float uGrain;
uniform float uGrainIntensity;
uniform vec2 uMouse;
uniform float uParallax;
uniform bool uEnableMouse;
uniform vec3 uHorizonColor;
uniform vec3 uWaveColor;
uniform vec3 uCrestColor;
uniform float uRadius;
out vec4 fragColor;
const float MAX_DIST = 20000.0;
float hash21(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
float plasma(vec3 r, vec2 freq, vec4 tc) {
float mx = r.x + tc.x;
mx += uSwell * sin((r.y + mx) / 20.0 + tc.y);
float my = r.y - tc.z;
my += uTurbulence * cos(r.x / 23.0 + tc.w);
return r.z - (sin(mx * freq.x) * uAmplitude + sin(my * freq.y) * uAmplitude + uHeight);
}
float raymarch(vec3 pos, vec3 dir, vec2 freq, vec4 tc) {
float dist = 0.0;
for (int i = 0; i < 128; i++) {
if (float(i) >= uSteps) break;
float dscene = plasma(pos + dist * dir, freq, tc);
if (abs(dscene) < 0.1) break;
dist += 0.9 * dscene;
if (!(abs(dist) < MAX_DIST)) return MAX_DIST;
}
return dist;
}
void main() {
float T = iTime * uSpeed;
vec2 freq = vec2(uWaveScale / 7.0, (uWaveScale * uWaveRatio) / 3.0);
vec4 tc = vec4(T / 0.130, T / 0.810, T / 0.200, T / 0.710);
float c, s;
float vfov = (3.14159 / 2.3) / max(uZoom, 0.05);
vec3 cam = vec3(0.0, 0.0, 30.0);
vec2 uv = (gl_FragCoord.xy / iResolution.xy) - 0.5;
uv.x *= iResolution.x / iResolution.y;
uv.y *= -1.0;
vec3 dir = vec3(0.0, 0.0, -1.0);
float ulen = length(uv);
float xrot = vfov * ulen;
c = cos(xrot); s = sin(xrot);
dir = mat3(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c) * dir;
vec2 nuv = ulen > 1e-5 ? uv / ulen : vec2(1.0, 0.0);
c = nuv.x; s = nuv.y;
dir = mat3(c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0) * dir;
c = cos(uTilt); s = sin(uTilt);
dir = mat3(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c) * dir;
if (uEnableMouse) {
float yaw = (uMouse.x - 0.5) * uParallax * 0.4;
float pitch = (uMouse.y - 0.5) * uParallax * 0.4;
c = cos(yaw); s = sin(yaw);
dir = mat3(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c) * dir;
c = cos(pitch); s = sin(pitch);
dir = mat3(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c) * dir;
}
float dist = raymarch(cam, dir, freq, tc);
vec3 pos = cam + dist * dir;
float t = clamp(uFogDepth / max(dist, 0.001), 0.0, 1.0);
vec3 body = mix(uWaveColor, uCrestColor, clamp(pos.z * 0.08 + 0.5, 0.0, 1.0));
vec3 col = mix(uHorizonColor, body, t);
col *= uBrightness;
col = clamp(col, 0.0, 1.0);
float alpha = clamp(t, 0.0, 1.0) * uOpacity;
if (uGrain > 0.5) {
float g = hash21(gl_FragCoord.xy + mod(iTime, 64.0) * 11.0);
alpha += (g - 0.5) * uGrainIntensity;
}
alpha = clamp(alpha, 0.0, 1.0);
// Rounded-Rect-Maske im Shader (Compositor-sicher, z. B. Waterfox)
vec2 p = gl_FragCoord.xy - iResolution * 0.5;
vec2 hb = iResolution * 0.5 - vec2(uRadius);
float rd = length(max(abs(p) - hb, 0.0)) - uRadius;
alpha *= 1.0 - smoothstep(-0.75, 0.75, rd);
fragColor = vec4(col * alpha, alpha);
}
`
const containerRef = ref<HTMLDivElement | null>(null)
let teardown: (() => void) | null = null
onMounted(() => {
const container = containerRef.value
if (!container) return
const renderer = new Renderer({
webgl: 2,
alpha: true,
premultipliedAlpha: true,
antialias: false,
dpr: Math.min(window.devicePixelRatio || 1, 1.5),
})
const gl = renderer.gl
gl.clearColor(0, 0, 0, 0)
// WebGL-Canvas bleibt detached; Anzeige über 2D-Blit-Canvas — siehe EffectAurora
const canvas = document.createElement('canvas')
canvas.style.width = '100%'
canvas.style.height = '100%'
canvas.style.display = 'block'
canvas.style.borderRadius = 'inherit'
const ctx2d = canvas.getContext('2d')
container.appendChild(canvas)
const geometry = new Triangle(gl)
const program = new Program(gl, {
vertex,
fragment,
uniforms: {
iTime: { value: 0 },
iResolution: { value: new Float32Array([1, 1]) },
uSpeed: { value: 0.4 },
uAmplitude: { value: 2.5 },
uWaveScale: { value: 0.6 },
uWaveRatio: { value: 0.9 },
uSwell: { value: 35 },
uTurbulence: { value: 20 },
uTilt: { value: 1.11 },
uZoom: { value: 1.0 },
uHeight: { value: 5.5 },
uFogDepth: { value: 15 },
uSteps: { value: 40.0 }, // 'low' — kleine Card, muss nicht schwer raymarch-en
uBrightness: { value: 1.0 },
uOpacity: { value: 0.9 },
uGrain: { value: 1.0 },
uGrainIntensity: { value: 0.05 },
uMouse: { value: new Float32Array([0.5, 0.5]) },
uParallax: { value: 0.5 },
uEnableMouse: { value: false },
uHorizonColor: { value: new Float32Array(hexToRgbTuple(shadeHex(props.color, -0.55))) },
uWaveColor: { value: new Float32Array(hexToRgbTuple(props.color)) },
uCrestColor: { value: new Float32Array(hexToRgbTuple(shadeHex(props.color, 0.55))) },
uRadius: { value: 0 },
},
})
const mesh = new Mesh(gl, { geometry, program })
// Farbwechsel live übernehmen (Settings-Vorschau)
watch(() => props.color, (c) => {
const u = program.uniforms
u.uHorizonColor!.value = new Float32Array(hexToRgbTuple(shadeHex(c, -0.55)))
u.uWaveColor!.value = new Float32Array(hexToRgbTuple(c))
u.uCrestColor!.value = new Float32Array(hexToRgbTuple(shadeHex(c, 0.55)))
})
const setSize = () => {
const rect = container.getBoundingClientRect()
const w = Math.max(1, Math.floor(rect.width))
const h = Math.max(1, Math.floor(rect.height))
renderer.setSize(w, h)
const res = program.uniforms.iResolution.value as Float32Array
res[0] = gl.drawingBufferWidth
res[1] = gl.drawingBufferHeight
const cssRadius = parseFloat(getComputedStyle(container).borderTopLeftRadius) || 0
program.uniforms.uRadius.value = w > 0 ? cssRadius * (gl.drawingBufferWidth / w) : 0
canvas.width = gl.drawingBufferWidth
canvas.height = gl.drawingBufferHeight
}
const ro = new ResizeObserver(setSize)
ro.observe(container)
setSize()
let raf = 0
const t0 = performance.now()
const loop = (t: number) => {
program.uniforms.iTime.value = (t - t0) * 0.001
renderer.render({ scene: mesh })
ctx2d?.clearRect(0, 0, canvas.width, canvas.height)
ctx2d?.drawImage(gl.canvas, 0, 0, canvas.width, canvas.height)
raf = requestAnimationFrame(loop)
}
raf = requestAnimationFrame(loop)
teardown = () => {
cancelAnimationFrame(raf)
ro.disconnect()
try {
container.removeChild(canvas)
}
catch { /* bereits detached */ }
gl.getExtension('WEBGL_lose_context')?.loseContext()
}
})
onUnmounted(() => {
teardown?.()
teardown = null
})
</script>
<template>
<div ref="containerRef" class="relative size-full overflow-hidden rounded-[inherit]" />
</template>
@@ -0,0 +1,148 @@
<script setup lang="ts">
import { hexToRgbTuple, mixHex, shadeHex } from '@/lib/color'
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop
const props = withDefaults(defineProps<{ color?: string }>(), {
color: '#61dca3',
})
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef')
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
const fontSize = 14
const charWidth = 9
const charHeight = 18
const lettersAndSymbols = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$&*()-_+=/[]{};:<>?0123456789'
const glitchColors = computed(() => [
shadeHex(props.color, -0.5),
props.color,
shadeHex(props.color, 0.35),
])
interface GlitchLetter {
char: string
color: string
targetColor: string
colorProgress: number
}
let letters: GlitchLetter[] = []
let grid = { columns: 0, rows: 0 }
let ctx: CanvasRenderingContext2D | null = null
let animationId = 0
let lastGlitchTime = 0
let ro: ResizeObserver | null = null
let cornerRadius = 0
const getRandomChar = () => lettersAndSymbols[Math.floor(Math.random() * lettersAndSymbols.length)]!
const getRandomColor = () => glitchColors.value[Math.floor(Math.random() * glitchColors.value.length)]!
// Farbwechsel live übernehmen (Settings-Vorschau)
watch(() => props.color, () => {
for (const l of letters) {
l.color = getRandomColor()
l.targetColor = getRandomColor()
l.colorProgress = 1
}
draw()
})
function resizeCanvas() {
const canvas = canvasRef.value
const container = containerRef.value
if (!canvas || !container) return
const dpr = window.devicePixelRatio || 1
const width = container.offsetWidth
const height = container.offsetHeight
canvas.width = width * dpr
canvas.height = height * dpr
ctx?.setTransform(dpr, 0, 0, dpr, 0, 0)
cornerRadius = parseFloat(getComputedStyle(container).borderTopLeftRadius) || 0
grid = {
columns: Math.ceil(width / charWidth),
rows: Math.ceil(height / charHeight),
}
letters = Array.from({ length: grid.columns * grid.rows }, () => ({
char: getRandomChar(),
color: getRandomColor(),
targetColor: getRandomColor(),
colorProgress: 1,
}))
draw()
}
function draw() {
const canvas = canvasRef.value
if (!ctx || !canvas || !letters.length) return
const { width, height } = canvas.getBoundingClientRect()
ctx.clearRect(0, 0, width, height)
// Rounded-Rect-Clip direkt im Canvas — zuverlässig auch wenn der
// Compositor den Layer nicht an CSS-Clips bindet (Waterfox)
ctx.save()
ctx.beginPath()
ctx.roundRect(0, 0, width, height, cornerRadius)
ctx.clip()
ctx.font = `${fontSize}px monospace`
ctx.textBaseline = 'top'
letters.forEach((letter, index) => {
const x = (index % grid.columns) * charWidth
const y = Math.floor(index / grid.columns) * charHeight
ctx!.fillStyle = letter.color
ctx!.fillText(letter.char, x, y)
})
ctx.restore()
}
function animate() {
const now = Date.now()
if (now - lastGlitchTime >= 60) {
const updateCount = Math.max(1, Math.floor(letters.length * 0.05))
for (let i = 0; i < updateCount; i++) {
const index = Math.floor(Math.random() * letters.length)
const letter = letters[index]
if (!letter) continue
letter.char = getRandomChar()
letter.targetColor = getRandomColor()
letter.colorProgress = 0
}
lastGlitchTime = now
}
let needsRedraw = false
for (const letter of letters) {
if (letter.colorProgress < 1) {
letter.colorProgress = Math.min(1, letter.colorProgress + 0.05)
letter.color = mixHex(letter.color, letter.targetColor, letter.colorProgress)
needsRedraw = true
}
}
if (needsRedraw || now - lastGlitchTime < 70) draw()
animationId = requestAnimationFrame(animate)
}
onMounted(() => {
const canvas = canvasRef.value
if (!canvas) return
ctx = canvas.getContext('2d')
resizeCanvas()
animate()
ro = new ResizeObserver(resizeCanvas)
if (containerRef.value) ro.observe(containerRef.value)
})
onUnmounted(() => {
cancelAnimationFrame(animationId)
ro?.disconnect()
})
</script>
<template>
<div ref="containerRef" class="relative size-full overflow-hidden rounded-[inherit] opacity-40">
<canvas ref="canvasRef" class="absolute inset-0 size-full rounded-[inherit]" />
</div>
</template>
+170
View File
@@ -0,0 +1,170 @@
<script setup lang="ts">
import { hexToRgbTuple, shadeHex } from '@/lib/color'
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop
const props = withDefaults(defineProps<{ color?: string }>(), {
color: '#6366f1',
})
const VERT = `
attribute vec2 uv;
attribute vec2 position;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0.0, 1.0);
}
`
const FRAG = `
precision highp float;
uniform float uTime;
uniform float uSpeed;
uniform float uInnerLines;
uniform float uOuterLines;
uniform float uWarpIntensity;
uniform float uRotation;
uniform float uEdgeFadeWidth;
uniform float uColorCycleSpeed;
uniform float uBrightness;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform vec3 uColor3;
uniform vec2 uMouse;
uniform float uMouseActive;
uniform float uMouseInfluence;
uniform vec2 uCardRes;
uniform float uRadius;
#define HALF_PI 1.5707963
float hashF(float n) {
return fract(sin(n * 127.1) * 43758.5453123);
}
float smoothNoise(float x) {
float i = floor(x);
float f = fract(x);
float u = f * f * (3.0 - 2.0 * f);
return mix(hashF(i), hashF(i + 1.0), u);
}
float displaceA(float coord, float t) {
float result = sin(coord * 2.123) * 0.2;
result += sin(coord * 3.234 + t * 4.345) * 0.1;
result += sin(coord * 0.589 + t * 0.934) * 0.5;
return result;
}
float displaceB(float coord, float t) {
float result = sin(coord * 1.345) * 0.3;
result += sin(coord * 2.734 + t * 3.345) * 0.2;
result += sin(coord * 0.189 + t * 0.934) * 0.3;
return result;
}
vec2 rotate2D(vec2 p, float angle) {
float c = cos(angle);
float s = sin(angle);
return vec2(p.x * c - p.y * s, p.x * s + p.y * c);
}
float cardMask(vec2 fc) {
vec2 p = fc - uCardRes * 0.5;
vec2 hb = uCardRes * 0.5 - vec2(uRadius);
float rd = length(max(abs(p) - hb, 0.0)) - uRadius;
return 1.0 - smoothstep(-0.75, 0.75, rd);
}
void main() {
// Pixelkonsistent: Koordinaten auf eine Referenzhöhe von 180px normieren,
// damit die Liniendichte in flachen Containern nicht zusammengestaucht wird
vec2 coords = (gl_FragCoord.xy * 2.0 - uCardRes.xy) / 180.0;
coords = rotate2D(coords, uRotation);
float halfT = uTime * uSpeed * 0.5;
float fullT = uTime * uSpeed;
vec2 mPos = rotate2D((uMouse * 2.0 - 1.0) * (uCardRes / 360.0), uRotation);
float mDist = length(coords - mPos);
float mouseWarp = uMouseInfluence * exp(-mDist * mDist * 4.0) * uMouseActive;
float warpAx = coords.x + displaceA(coords.y, halfT) * uWarpIntensity + mouseWarp;
float warpAy = coords.y - displaceA(coords.x * cos(fullT) * 1.235, halfT) * uWarpIntensity;
float warpBx = coords.x + displaceB(coords.y, halfT) * uWarpIntensity + mouseWarp;
float warpBy = coords.y - displaceB(coords.x * sin(fullT) * 1.235, halfT) * uWarpIntensity;
vec2 fieldA = vec2(warpAx, warpAy);
vec2 fieldB = vec2(warpBx, warpBy);
vec2 blended = mix(fieldA, fieldB, mix(fieldA, fieldB, 0.5));
float fadeTop = smoothstep(uEdgeFadeWidth, uEdgeFadeWidth + 0.4, blended.y);
float fadeBottom = smoothstep(-uEdgeFadeWidth, -(uEdgeFadeWidth + 0.4), blended.y);
float vMask = 1.0 - max(fadeTop, fadeBottom);
float tileCount = mix(uOuterLines, uInnerLines, vMask);
float scaledY = blended.y * tileCount;
float nY = smoothNoise(abs(scaledY));
float ridge = pow(
step(abs(nY - blended.x) * 2.0, HALF_PI) * cos(2.0 * (nY - blended.x)),
5.0
);
float waveProfile = max(0.0, (max(fract(scaledY), fract(-scaledY)) - 0.55) / 0.45);
float lines = pow(waveProfile, 2.0) + pow(waveProfile, 4.0);
float pattern = vMask * lines;
float cycleT = fullT * uColorCycleSpeed;
float rChannel = (pattern + lines * ridge) * (cos(blended.y + cycleT * 0.234) * 0.5 + 1.0);
float gChannel = (pattern + vMask * ridge) * (sin(blended.x + cycleT * 1.745) * 0.5 + 1.0);
float bChannel = (pattern + lines * ridge) * (cos(blended.x + cycleT * 0.534) * 0.5 + 1.0);
vec3 col = (rChannel * uColor1 + gChannel * uColor2 + bChannel * uColor3) * uBrightness;
float alpha = clamp(length(col), 0.0, 1.0) * cardMask(gl_FragCoord.xy);
gl_FragColor = vec4(col * alpha, alpha);
}
`
const ctnDom = ref<HTMLDivElement | null>(null)
useCardShader(ctnDom, {
vertex: VERT,
fragment: FRAG,
mouse: true,
uniforms: {
uTime: { value: 0 },
uSpeed: { value: 0.3 },
uInnerLines: { value: 32 },
uOuterLines: { value: 36 },
uWarpIntensity: { value: 1 },
uRotation: { value: -45 * Math.PI / 180 },
uEdgeFadeWidth: { value: 0 },
uColorCycleSpeed: { value: 1 },
uBrightness: { value: 0.35 },
uColor1: { value: new Float32Array(hexToRgbTuple(props.color)) },
uColor2: { value: new Float32Array(hexToRgbTuple(shadeHex(props.color, 0.35))) },
uColor3: { value: new Float32Array(hexToRgbTuple(shadeHex(props.color, -0.25))) },
uMouse: { value: new Float32Array([0.5, 0.5]) },
uMouseActive: { value: 0 },
uMouseInfluence: { value: 2.0 },
},
color: () => props.color,
colorUniforms: { uColor1: 0, uColor2: 0.35, uColor3: -0.25 },
frame: ({ t, program, mouse }) => {
const u = program.uniforms
u.uTime.value = t * 0.001
const m = u.uMouse.value as Float32Array
m[0] = mouse.x
m[1] = mouse.y
u.uMouseActive.value = mouse.active
},
})
</script>
<template>
<div ref="ctnDom" class="relative size-full overflow-hidden rounded-[inherit]" />
</template>
+197
View File
@@ -0,0 +1,197 @@
<script setup lang="ts">
import { Camera, Geometry, Mesh, Program, Renderer } from 'ogl'
import { hexToRgbTuple, shadeHex } from '@/lib/color'
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop
const props = withDefaults(defineProps<{ color?: string }>(), {
color: '#6366f1',
})
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
const vertex = /* glsl */ `
attribute vec3 position;
attribute vec4 random;
attribute vec3 color;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
uniform float uTime;
uniform float uSpread;
uniform float uBaseSize;
uniform float uSizeRandomness;
varying vec4 vRandom;
varying vec3 vColor;
void main() {
vRandom = random;
vColor = color;
vec3 pos = position * uSpread;
pos.z *= 10.0;
vec4 mPos = modelMatrix * vec4(pos, 1.0);
float t = uTime;
mPos.x += sin(t * random.z + 6.28 * random.w) * mix(0.1, 1.5, random.x);
mPos.y += sin(t * random.y + 6.28 * random.x) * mix(0.1, 1.5, random.w);
mPos.z += sin(t * random.w + 6.28 * random.y) * mix(0.1, 1.5, random.z);
vec4 mvPos = viewMatrix * mPos;
gl_PointSize = (uBaseSize * (1.0 + uSizeRandomness * (random.x - 0.5))) / length(mvPos.xyz);
gl_Position = projectionMatrix * mvPos;
}
`
const fragment = /* glsl */ `
precision highp float;
uniform float uTime;
uniform vec2 uResolution;
uniform float uRadius;
varying vec4 vRandom;
varying vec3 vColor;
void main() {
vec2 uv = gl_PointCoord.xy;
float d = length(uv - vec2(0.5));
float circle = smoothstep(0.5, 0.4, d) * 0.8;
// Rounded-Rect-Maske im Shader (Compositor-sicher, z. B. Waterfox)
vec2 p = gl_FragCoord.xy - uResolution * 0.5;
vec2 hb = uResolution * 0.5 - vec2(uRadius);
float rd = length(max(abs(p) - hb, 0.0)) - uRadius;
circle *= 1.0 - smoothstep(-0.75, 0.75, rd);
gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), circle);
}
`
let cleanup: (() => void) | null = null
onMounted(() => {
const container = containerRef.value
if (!container) return
const renderer = new Renderer({ dpr: 1, depth: false, alpha: true })
const gl = renderer.gl
// WebGL-Canvas bleibt detached; Anzeige über 2D-Blit-Canvas — siehe EffectAurora
const outCanvas = document.createElement('canvas')
outCanvas.className = 'absolute inset-0 size-full rounded-[inherit]'
const outCtx = outCanvas.getContext('2d')
container.appendChild(outCanvas)
gl.clearColor(0, 0, 0, 0)
const camera = new Camera(gl, { fov: 15 })
camera.position.set(0, 0, 20)
const resize = () => {
renderer.setSize(container.clientWidth, container.clientHeight)
camera.perspective({ aspect: gl.canvas.width / gl.canvas.height })
const res = program.uniforms.uResolution.value as Float32Array
res[0] = gl.drawingBufferWidth
res[1] = gl.drawingBufferHeight
const cssRadius = parseFloat(getComputedStyle(container).borderTopLeftRadius) || 0
program.uniforms.uRadius.value = container.clientWidth > 0
? cssRadius * (gl.drawingBufferWidth / container.clientWidth)
: 0
outCanvas.width = gl.drawingBufferWidth
outCanvas.height = gl.drawingBufferHeight
}
const ro = new ResizeObserver(resize)
const count = 120
const positions = new Float32Array(count * 3)
const randoms = new Float32Array(count * 4)
const colors = new Float32Array(count * 3)
const palette = [props.color, shadeHex(props.color, 0.4), '#ffffff']
for (let i = 0; i < count; i++) {
let x = 0
let y = 0
let z = 0
let len = 0
do {
x = Math.random() * 2 - 1
y = Math.random() * 2 - 1
z = Math.random() * 2 - 1
len = x * x + y * y + z * z
} while (len > 1 || len === 0)
const r = Math.cbrt(Math.random())
positions.set([x * r, y * r, z * r], i * 3)
randoms.set([Math.random(), Math.random(), Math.random(), Math.random()], i * 4)
colors.set(hexToRgbTuple(palette[Math.floor(Math.random() * palette.length)]!), i * 3)
}
const geometry = new Geometry(gl, {
position: { size: 3, data: positions },
random: { size: 4, data: randoms },
color: { size: 3, data: colors },
})
const program = new Program(gl, {
vertex,
fragment,
uniforms: {
uTime: { value: 0 },
uSpread: { value: 10 },
uBaseSize: { value: 100 },
uSizeRandomness: { value: 1 },
uResolution: { value: new Float32Array([1, 1]) },
uRadius: { value: 0 },
},
transparent: true,
depthTest: false,
})
const particles = new Mesh(gl, { mode: gl.POINTS, geometry, program })
// Farbwechsel live übernehmen (Settings-Vorschau)
watch(() => props.color, (c) => {
const p = [c, shadeHex(c, 0.4), '#ffffff']
for (let i = 0; i < count; i++) {
colors.set(hexToRgbTuple(p[Math.floor(Math.random() * p.length)]!), i * 3)
}
geometry.attributes.color!.needsUpdate = true
})
ro.observe(container)
resize()
let animationFrameId = 0
let lastTime = performance.now()
let elapsed = 0
const update = (t: number) => {
animationFrameId = requestAnimationFrame(update)
const delta = t - lastTime
lastTime = t
elapsed += delta * 0.1
program.uniforms.uTime.value = elapsed * 0.001
particles.rotation.x = Math.sin(elapsed * 0.0002) * 0.1
particles.rotation.y = Math.cos(elapsed * 0.0005) * 0.15
particles.rotation.z += 0.001
renderer.render({ scene: particles, camera })
outCtx?.clearRect(0, 0, outCanvas.width, outCanvas.height)
outCtx?.drawImage(gl.canvas, 0, 0, outCanvas.width, outCanvas.height)
}
animationFrameId = requestAnimationFrame(update)
cleanup = () => {
ro.disconnect()
cancelAnimationFrame(animationFrameId)
if (container.contains(outCanvas)) container.removeChild(outCanvas)
gl.getExtension('WEBGL_lose_context')?.loseContext()
}
})
onBeforeUnmount(() => {
cleanup?.()
cleanup = null
})
</script>
<template>
<div ref="containerRef" class="relative size-full overflow-hidden rounded-[inherit]" />
</template>
+133
View File
@@ -0,0 +1,133 @@
<script setup lang="ts">
import { hexToRgbTuple } from '@/lib/color'
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop
const props = withDefaults(defineProps<{ color?: string }>(), {
color: '#6366f1',
})
const VERT = `#version 300 es
precision highp float;
in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`
const FRAG = `#version 300 es
precision highp float;
uniform float iTime;
uniform vec3 uCustomColor;
uniform float uSpeed;
uniform float uScale;
uniform float uOpacity;
uniform vec2 uMouse;
uniform float uMouseActive;
uniform float uIterations;
uniform float uStepScale;
uniform vec2 uCardRes;
uniform float uRadius;
out vec4 fragColor;
void mainImage(out vec4 o, vec2 C) {
// Pixelkonsistent: Koordinatenrahmen auf Referenzhöhe 180px normieren —
// r.y ist die Brennweite und würde in flachen Containern die
// Perspektive extrem verzerren
float pxScale = 180.0 / uCardRes.y;
vec2 r = uCardRes * pxScale;
C *= pxScale;
vec2 center = r * 0.5;
C = (C - center) / uScale + center;
vec2 mouseOffset = (uMouse * pxScale - center) * 0.0002;
C += mouseOffset * length(C - center) * uMouseActive;
float i, d, z, T = iTime * uSpeed;
vec3 O, p, S;
for (vec2 Q; ++i < 60.; O += o.w/d*o.xyz) {
p = z*normalize(vec3(C-.5*r,r.y));
p.z -= 4.;
S = p;
d = p.y-T;
p.x += .4*(1.+p.y)*sin(d + p.x*0.1)*cos(.34*d + p.x*0.05);
Q = p.xz *= mat2(cos(p.y+vec4(0,11,33,0)-T));
z+= d = (abs(sqrt(length(Q*Q)) - .25*(5.+S.y))/3.+8e-4)*uStepScale;
o = 1.+sin(S.y+p.z*.5+S.z-length(S-p)+vec4(2,1,0,8));
if (i >= uIterations) break;
}
o.xyz = tanh(O/1e4);
}
bool finite1(float x){ return !(isnan(x) || isinf(x)); }
vec3 sanitize(vec3 c){
return vec3(
finite1(c.r) ? c.r : 0.0,
finite1(c.g) ? c.g : 0.0,
finite1(c.b) ? c.b : 0.0
);
}
float cardMask(vec2 fc) {
vec2 p = fc - uCardRes * 0.5;
vec2 hb = uCardRes * 0.5 - vec2(uRadius);
float rd = length(max(abs(p) - hb, 0.0)) - uRadius;
return 1.0 - smoothstep(-0.75, 0.75, rd);
}
void main() {
vec4 o = vec4(0.0);
mainImage(o, gl_FragCoord.xy);
vec3 rgb = sanitize(o.rgb);
float intensity = (rgb.r + rgb.g + rgb.b) / 3.0;
vec3 finalColor = intensity * uCustomColor;
float a = length(rgb) * uOpacity * cardMask(gl_FragCoord.xy);
fragColor = vec4(finalColor * a, a);
}`
const ctnDom = ref<HTMLDivElement | null>(null)
let elapsed = 0
let lastT = 0
useCardShader(ctnDom, {
vertex: VERT,
fragment: FRAG,
mouse: true,
renderScale: 0.55,
uniforms: {
iTime: { value: 0 },
uCustomColor: { value: new Float32Array(hexToRgbTuple(props.color)) },
uSpeed: { value: 0.4 },
uScale: { value: 1 },
uOpacity: { value: 1 },
uMouse: { value: new Float32Array([0, 0]) },
uMouseActive: { value: 0 },
uIterations: { value: 48 },
uStepScale: { value: 60 / 48 },
},
color: () => props.color,
colorUniforms: { uCustomColor: 0 },
frame: ({ t, program, mouse }) => {
const u = program.uniforms
// Zeit akkumulieren, damit Pausen (Tab-Wechsel) nicht springen
const delta = Math.min((t - lastT) * 0.001, 0.1)
lastT = t
elapsed += delta
u.iTime.value = elapsed
const res = u.uCardRes.value as Float32Array
const m = u.uMouse.value as Float32Array
m[0] = mouse.x * res[0]
m[1] = (1 - mouse.y) * res[1]
u.uMouseActive.value = mouse.active
},
})
</script>
<template>
<div ref="ctnDom" class="relative size-full overflow-hidden rounded-[inherit]" />
</template>
+181
View File
@@ -0,0 +1,181 @@
<script setup lang="ts">
import { hexToRgbTuple, shadeHex } from '@/lib/color'
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop
const props = withDefaults(defineProps<{ color?: string }>(), {
color: '#6366f1',
})
const VERT = `#version 300 es
in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`
const FRAG = `#version 300 es
precision highp float;
uniform float iTime;
uniform float uSpeed;
uniform float uSweepSpeed;
uniform float uSweepWidth;
uniform float uSweepFalloff;
uniform float uScale;
uniform float uFrequency;
uniform float uRipple;
uniform float uBandDensity;
uniform float uLineSharpness;
uniform float uGlow;
uniform float uColorSpread;
uniform float uBrightness;
uniform float uContrast;
uniform float uSoftness;
uniform float uVignette;
uniform float uOpacity;
uniform float uScanline;
uniform float uGrain;
uniform float uGrainIntensity;
uniform vec2 uMouse;
uniform float uMouseRadius;
uniform float uMouseStrength;
uniform float uMouseActive;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform vec3 uColor3;
uniform vec2 uCardRes;
uniform float uRadius;
out vec4 fragColor;
const float TAU = 6.2831853;
float signalField(vec2 p, float t) {
float w = sin(p.x * 1.3 + t * 0.7);
w += sin(p.y * 1.7 - t * 0.52) * 0.8;
w += sin((p.x + p.y) * 0.9 + t * 0.91) * 0.6;
w += sin((p.x - p.y) * 1.53 - t * 0.63) * 0.42;
return w * 0.35;
}
vec3 palette(float f) {
f = clamp(f, 0.0, 1.0);
f = pow(f, uContrast);
vec3 c = mix(uColor1, uColor2, smoothstep(0.08, 0.6, f));
return mix(c, uColor3, smoothstep(0.68, 1.0, f));
}
float scanBand(float x, float aa, float sharp) {
float v = mix(0.5, 0.5 + 0.5 * cos(x * TAU), aa);
return pow(v, sharp);
}
float cardMask(vec2 fc) {
vec2 p = fc - uCardRes * 0.5;
vec2 hb = uCardRes * 0.5 - vec2(uRadius);
float rd = length(max(abs(p) - hb, 0.0)) - uRadius;
return 1.0 - smoothstep(-0.75, 0.75, rd);
}
void main() {
// Pixelkonsistent: auf Referenzhöhe 180px normieren,
// damit Bänder/Wellen in flachen Containern nicht stauchen
vec2 uv0 = (gl_FragCoord.xy * 2.0 - uCardRes.xy) / 180.0;
vec2 p = uv0 / max(uScale, 0.001);
float t = iTime * uSpeed;
vec2 mUv = (uMouse * 2.0 - 1.0) * (uCardRes / 360.0);
vec2 md = uv0 - mUv;
float r = max(uMouseRadius, 0.001);
float mouseBoost = exp(-dot(md, md) / (r * r)) * uMouseStrength * uMouseActive;
float axis = p.y;
float sig = signalField(p * uFrequency, t);
float coord = axis + sig * uRipple;
float phase = coord / max(uSweepWidth, 0.05) - t * uSweepSpeed;
float sweep = pow(0.5 + 0.5 * cos(phase * TAU), max(uSweepFalloff, 0.1));
float lc = coord * uBandDensity;
float aa = 1.0 / (1.0 + uSoftness * fwidth(lc) * 3.0);
aa = clamp(aa * (1.0 + mouseBoost * 0.6), 0.0, 1.0);
float bodyBase = clamp(0.5 + 0.5 * sig, 0.0, 1.0);
float body = bodyBase * bodyBase * uGlow * sweep;
float sharp = max(uLineSharpness, 0.1);
float split = uColorSpread * 0.16;
float fr = clamp(scanBand(lc + split, aa, sharp) * sweep + body, 0.0, 1.0);
float fg = clamp(scanBand(lc, aa, sharp) * sweep + body, 0.0, 1.0);
float fb = clamp(scanBand(lc - split, aa, sharp) * sweep + body, 0.0, 1.0);
vec3 col = vec3(palette(fr).r, palette(fg).g, palette(fb).b);
float inten = (fr + fg + fb) * 0.3333333 * uBrightness;
inten *= 1.0 + mouseBoost * 0.9;
inten *= 1.0 - 0.18 * (0.5 + 0.5 * cos(gl_FragCoord.y * 1.7));
if (inten > 0.01) {
float g = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233)) + iTime) * 43758.5453);
inten += (g - 0.5) * uGrainIntensity;
}
inten *= clamp(1.0 - uVignette * smoothstep(0.55, 1.65, length(uv0)), 0.0, 1.0);
inten = clamp(inten, 0.0, 1.0);
float a = clamp(inten * uOpacity, 0.0, 1.0) * cardMask(gl_FragCoord.xy);
fragColor = vec4(clamp(col, 0.0, 1.0) * a, a);
}
`
const ctnDom = ref<HTMLDivElement | null>(null)
useCardShader(ctnDom, {
vertex: VERT,
fragment: FRAG,
mouse: true,
uniforms: {
iTime: { value: 0 },
uSpeed: { value: 0.5 },
uSweepSpeed: { value: 0.25 },
uSweepWidth: { value: 1.6 },
uSweepFalloff: { value: 6 },
uScale: { value: 1.5 },
uFrequency: { value: 2 },
uRipple: { value: 0.22 },
uBandDensity: { value: 11 },
uLineSharpness: { value: 5.5 },
uGlow: { value: 0.22 },
uColorSpread: { value: 0.7 },
uBrightness: { value: 1.0 },
uContrast: { value: 1.15 },
uSoftness: { value: 1.4 },
uVignette: { value: 0.45 },
uOpacity: { value: 1.0 },
uScanline: { value: 1.0 },
uGrain: { value: 1.0 },
uGrainIntensity: { value: 0.05 },
uMouse: { value: new Float32Array([0.5, 0.5]) },
uMouseRadius: { value: 0.5 },
uMouseStrength: { value: 0.5 },
uMouseActive: { value: 0 },
uColor1: { value: new Float32Array(hexToRgbTuple(shadeHex(props.color, -0.55))) },
uColor2: { value: new Float32Array(hexToRgbTuple(props.color)) },
uColor3: { value: new Float32Array([1, 1, 1]) },
},
color: () => props.color,
colorUniforms: { uColor1: -0.55, uColor2: 0 },
frame: ({ t, program, mouse }) => {
const u = program.uniforms
u.iTime.value = t * 0.001
const m = u.uMouse.value as Float32Array
m[0] = mouse.x
m[1] = mouse.y
u.uMouseActive.value = mouse.active
},
})
</script>
<template>
<div ref="ctnDom" class="relative size-full overflow-hidden rounded-[inherit]" />
</template>
+304
View File
@@ -0,0 +1,304 @@
<script setup lang="ts">
import { hexToRgbTuple, shadeHex } from '@/lib/color'
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop,
// Scroll-Grid + Hover-Fill/Trail auf 2D-Canvas
const props = withDefaults(defineProps<{
color?: string
shape?: 'square' | 'hexagon' | 'circle' | 'triangle'
}>(), {
color: '#6366f1',
shape: 'square',
})
const canvasRef = ref<HTMLCanvasElement | null>(null)
let cleanup: (() => void) | null = null
onMounted(() => {
const canvas = canvasRef.value
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const squareSize = 30
const speed = 0.6
const hoverTrailAmount = 10
let borderColor = ''
let hoverFill = ''
let edgeRgba = ''
const updateColors = () => {
const [r, g, b] = hexToRgbTuple(props.color)
borderColor = `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, 0.3)`
hoverFill = props.color
const edgeColor = hexToRgbTuple(shadeHex(props.color, -0.85))
edgeRgba = `rgba(${Math.round(edgeColor[0] * 255)}, ${Math.round(edgeColor[1] * 255)}, ${Math.round(edgeColor[2] * 255)}, 0.9)`
}
updateColors()
// Farbwechsel live übernehmen (Settings-Vorschau)
watch(() => props.color, updateColors)
const isHex = props.shape === 'hexagon'
const isTri = props.shape === 'triangle'
const hexHoriz = squareSize * 1.5
const hexVert = squareSize * Math.sqrt(3)
const offset = { x: 0, y: 0 }
let hovered: { x: number, y: number } | null = null
const trail: { x: number, y: number }[] = []
const opacities = new Map<string, number>()
const resizeCanvas = () => {
canvas.width = canvas.offsetWidth
canvas.height = canvas.offsetHeight
}
const drawHex = (cx: number, cy: number, size: number) => {
ctx.beginPath()
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i
const vx = cx + size * Math.cos(angle)
const vy = cy + size * Math.sin(angle)
if (i === 0) ctx.moveTo(vx, vy)
else ctx.lineTo(vx, vy)
}
ctx.closePath()
}
const drawCircle = (cx: number, cy: number, size: number) => {
ctx.beginPath()
ctx.arc(cx, cy, size / 2, 0, Math.PI * 2)
}
const drawTriangle = (cx: number, cy: number, size: number, flip: boolean) => {
ctx.beginPath()
if (flip) {
ctx.moveTo(cx, cy + size / 2)
ctx.lineTo(cx + size / 2, cy - size / 2)
ctx.lineTo(cx - size / 2, cy - size / 2)
}
else {
ctx.moveTo(cx, cy - size / 2)
ctx.lineTo(cx + size / 2, cy + size / 2)
ctx.lineTo(cx - size / 2, cy + size / 2)
}
ctx.closePath()
}
const drawGrid = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
const cell = (key: string, draw: () => void) => {
const alpha = opacities.get(key)
ctx.strokeStyle = borderColor
ctx.globalAlpha = 0.55
if (alpha) {
ctx.globalAlpha = alpha
ctx.fillStyle = hoverFill
draw()
ctx.fill()
ctx.globalAlpha = 1
draw()
ctx.stroke()
}
else {
draw()
ctx.stroke()
}
ctx.globalAlpha = 1
}
if (isHex) {
const colShift = Math.floor(offset.x / hexHoriz)
const offsetX = ((offset.x % hexHoriz) + hexHoriz) % hexHoriz
const offsetY = ((offset.y % hexVert) + hexVert) % hexVert
const cols = Math.ceil(canvas.width / hexHoriz) + 3
const rows = Math.ceil(canvas.height / hexVert) + 3
for (let col = -2; col < cols; col++) {
for (let row = -2; row < rows; row++) {
const cx = col * hexHoriz + offsetX
const cy = row * hexVert + ((col + colShift) % 2 !== 0 ? hexVert / 2 : 0) + offsetY
cell(`${col},${row}`, () => drawHex(cx, cy, squareSize))
}
}
}
else if (isTri) {
const halfW = squareSize / 2
const colShift = Math.floor(offset.x / halfW)
const rowShift = Math.floor(offset.y / squareSize)
const offsetX = ((offset.x % halfW) + halfW) % halfW
const offsetY = ((offset.y % squareSize) + squareSize) % squareSize
const cols = Math.ceil(canvas.width / halfW) + 4
const rows = Math.ceil(canvas.height / squareSize) + 4
for (let col = -2; col < cols; col++) {
for (let row = -2; row < rows; row++) {
const cx = col * halfW + offsetX
const cy = row * squareSize + squareSize / 2 + offsetY
const flip = (((col + colShift + row + rowShift) % 2) + 2) % 2 !== 0
cell(`${col},${row}`, () => drawTriangle(cx, cy, squareSize, flip))
}
}
}
else if (props.shape === 'circle') {
const offsetX = ((offset.x % squareSize) + squareSize) % squareSize
const offsetY = ((offset.y % squareSize) + squareSize) % squareSize
const cols = Math.ceil(canvas.width / squareSize) + 3
const rows = Math.ceil(canvas.height / squareSize) + 3
for (let col = -2; col < cols; col++) {
for (let row = -2; row < rows; row++) {
const cx = col * squareSize + squareSize / 2 + offsetX
const cy = row * squareSize + squareSize / 2 + offsetY
cell(`${col},${row}`, () => drawCircle(cx, cy, squareSize))
}
}
}
else {
const offsetX = ((offset.x % squareSize) + squareSize) % squareSize
const offsetY = ((offset.y % squareSize) + squareSize) % squareSize
const cols = Math.ceil(canvas.width / squareSize) + 3
const rows = Math.ceil(canvas.height / squareSize) + 3
for (let col = -2; col < cols; col++) {
for (let row = -2; row < rows; row++) {
const sx = col * squareSize + offsetX
const sy = row * squareSize + offsetY
cell(`${col},${row}`, () => {
ctx.beginPath()
ctx.rect(sx, sy, squareSize, squareSize)
})
}
}
}
// dunkle Rand-Vignette, aus der Effektfarbe abgeleitet
const gradient = ctx.createRadialGradient(
canvas.width / 2, canvas.height / 2, 0,
canvas.width / 2, canvas.height / 2,
Math.sqrt(canvas.width ** 2 + canvas.height ** 2) / 2,
)
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)')
gradient.addColorStop(1, edgeRgba)
ctx.fillStyle = gradient
ctx.fillRect(0, 0, canvas.width, canvas.height)
}
const updateCellOpacities = () => {
const targets = new Map<string, number>()
if (hovered) targets.set(`${hovered.x},${hovered.y}`, 1)
for (let i = 0; i < trail.length; i++) {
const t = trail[i]
const key = `${t.x},${t.y}`
if (!targets.has(key)) targets.set(key, (trail.length - i) / (trail.length + 1))
}
for (const [key] of targets) {
if (!opacities.has(key)) opacities.set(key, 0)
}
for (const [key, opacity] of opacities) {
const next = opacity + ((targets.get(key) || 0) - opacity) * 0.15
if (next < 0.005) opacities.delete(key)
else opacities.set(key, next)
}
}
let raf = 0
let visible = true
const updateAnimation = () => {
if (!visible || document.hidden) {
raf = 0
return
}
raf = requestAnimationFrame(updateAnimation)
const wrapX = isHex ? hexHoriz * 2 : squareSize
const wrapY = isHex ? hexVert : isTri ? squareSize * 2 : squareSize
// diagonal
offset.x = (offset.x - speed + wrapX) % wrapX
offset.y = (offset.y - speed + wrapY) % wrapY
updateCellOpacities()
drawGrid()
}
const updateHovered = (col: number, row: number) => {
if (!hovered || hovered.x !== col || hovered.y !== row) {
if (hovered) {
trail.unshift({ ...hovered })
if (trail.length > hoverTrailAmount) trail.length = hoverTrailAmount
}
hovered = { x: col, y: row }
}
}
const onPointerMove = (event: PointerEvent) => {
const rect = canvas.getBoundingClientRect()
if (event.clientX < rect.left || event.clientX > rect.right
|| event.clientY < rect.top || event.clientY > rect.bottom) {
if (hovered) {
trail.unshift({ ...hovered })
if (trail.length > hoverTrailAmount) trail.length = hoverTrailAmount
hovered = null
}
return
}
const mouseX = event.clientX - rect.left
const mouseY = event.clientY - rect.top
if (isHex) {
const colShift = Math.floor(offset.x / hexHoriz)
const offsetX = ((offset.x % hexHoriz) + hexHoriz) % hexHoriz
const offsetY = ((offset.y % hexVert) + hexVert) % hexVert
const adjustedX = mouseX - offsetX
const adjustedY = mouseY - offsetY
const col = Math.round(adjustedX / hexHoriz)
const rowOffset = (col + colShift) % 2 !== 0 ? hexVert / 2 : 0
const row = Math.round((adjustedY - rowOffset) / hexVert)
updateHovered(col, row)
}
else if (isTri) {
const halfW = squareSize / 2
const offsetX = ((offset.x % halfW) + halfW) % halfW
const offsetY = ((offset.y % squareSize) + squareSize) % squareSize
const col = Math.round((mouseX - offsetX) / halfW)
const row = Math.floor((mouseY - offsetY) / squareSize)
updateHovered(col, row)
}
else {
const offsetX = ((offset.x % squareSize) + squareSize) % squareSize
const offsetY = ((offset.y % squareSize) + squareSize) % squareSize
const rounded = props.shape === 'circle' ? Math.round : Math.floor
const col = rounded((mouseX - offsetX) / squareSize)
const row = rounded((mouseY - offsetY) / squareSize)
updateHovered(col, row)
}
}
const io = new IntersectionObserver(([entry]) => {
visible = entry?.isIntersecting ?? true
if (visible && !raf) raf = requestAnimationFrame(updateAnimation)
})
io.observe(canvas)
const ro = new ResizeObserver(resizeCanvas)
ro.observe(canvas)
window.addEventListener('pointermove', onPointerMove, { passive: true })
resizeCanvas()
raf = requestAnimationFrame(updateAnimation)
cleanup = () => {
cancelAnimationFrame(raf)
io.disconnect()
ro.disconnect()
window.removeEventListener('pointermove', onPointerMove)
}
})
onBeforeUnmount(() => {
cleanup?.()
cleanup = null
})
</script>
<template>
<canvas ref="canvasRef" class="absolute inset-0 block size-full rounded-[inherit] border-none" />
</template>
+110
View File
@@ -0,0 +1,110 @@
<script setup lang="ts">
import { hexToRgbTuple } from '@/lib/color'
// Portiert aus Vue Bits (DavidHDev/vue-bits), Plane+Camera auf Fullscreen-Triangle
// vereinfacht, nur color-Prop
const props = withDefaults(defineProps<{ color?: string }>(), {
color: '#6366f1',
})
const VERT = `
attribute vec2 uv;
attribute vec2 position;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0.0, 1.0);
}
`
const FRAG = `
precision highp float;
varying vec2 vUv;
uniform float uTime;
uniform vec3 uColor;
uniform float uSpeed;
uniform float uScale;
uniform float uRotation;
uniform float uNoiseIntensity;
uniform vec2 uCardRes;
uniform float uRadius;
const float e = 2.71828182845904523536;
float noise(vec2 texCoord) {
float G = e;
vec2 r = (G * sin(G * texCoord));
return fract(r.x * r.y * (1.0 + texCoord.x));
}
vec2 rotateUvs(vec2 uv, float angle) {
float c = cos(angle);
float s = sin(angle);
mat2 rot = mat2(c, -s, s, c);
return rot * uv;
}
float cardMask(vec2 fc) {
vec2 p = fc - uCardRes * 0.5;
vec2 hb = uCardRes * 0.5 - vec2(uRadius);
float rd = length(max(abs(p) - hb, 0.0)) - uRadius;
return 1.0 - smoothstep(-0.75, 0.75, rd);
}
void main() {
float rnd = noise(gl_FragCoord.xy);
// Pixelkonsistent: Muster-Koordinaten auf eine Referenzgröße von 180px
// normieren, damit flache Container den Effekt nicht vertikal stauchen
vec2 suv = vUv * (uCardRes / 180.0);
vec2 uv = rotateUvs(suv * uScale, uRotation);
vec2 tex = uv * uScale;
float tOffset = uSpeed * uTime;
tex.y += 0.03 * sin(8.0 * tex.x - tOffset);
float pattern = 0.6 +
0.4 * sin(5.0 * (tex.x + tex.y +
cos(3.0 * tex.x + 5.0 * tex.y) +
0.02 * tOffset) +
sin(20.0 * (tex.x + tex.y - 0.1 * tOffset)));
vec4 col = vec4(uColor, 1.0) * vec4(pattern) - rnd / 15.0 * uNoiseIntensity;
float m = cardMask(gl_FragCoord.xy);
float a = clamp(pattern * 0.45, 0.0, 1.0) * m;
gl_FragColor = vec4(col.rgb * a, a);
}
`
const ctnDom = ref<HTMLDivElement | null>(null)
let elapsed = 0
let lastT = 0
useCardShader(ctnDom, {
vertex: VERT,
fragment: FRAG,
uniforms: {
uTime: { value: 0 },
uColor: { value: new Float32Array(hexToRgbTuple(props.color)) },
uSpeed: { value: 5 },
uScale: { value: 1 },
uNoiseIntensity: { value: 1.5 },
uRotation: { value: 0 },
},
color: () => props.color,
colorUniforms: { uColor: 0 },
frame: ({ t, program }) => {
const delta = Math.min((t - lastT) * 0.001, 0.1)
lastT = t
elapsed += 0.1 * delta
program.uniforms.uTime.value = elapsed
},
})
</script>
<template>
<div ref="ctnDom" class="relative size-full overflow-hidden rounded-[inherit]" />
</template>
+185
View File
@@ -0,0 +1,185 @@
<script setup lang="ts">
import { hexToRgbTuple } from '@/lib/color'
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop
const props = withDefaults(defineProps<{ color?: string }>(), {
color: '#6366f1',
})
const VERT = `
attribute vec2 uv;
attribute vec2 position;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0.0, 1.0);
}
`
const FRAG = `
precision highp float;
uniform float uTime;
uniform float uSpeed;
uniform float uScale;
uniform float uBrightness;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform float uNoiseFreq;
uniform float uNoiseAmp;
uniform float uBandHeight;
uniform float uBandSpread;
uniform float uOctaveDecay;
uniform float uLayerOffset;
uniform float uColorSpeed;
uniform vec2 uMouse;
uniform float uMouseInfluence;
uniform vec2 uCardRes;
uniform float uRadius;
#define TAU 6.28318
vec3 gradientHash(vec3 p) {
p = vec3(
dot(p, vec3(127.1, 311.7, 234.6)),
dot(p, vec3(269.5, 183.3, 198.3)),
dot(p, vec3(169.5, 283.3, 156.9))
);
vec3 h = fract(sin(p) * 43758.5453123);
float phi = acos(2.0 * h.x - 1.0);
float theta = TAU * h.y;
return vec3(cos(theta) * sin(phi), sin(theta) * cos(phi), cos(phi));
}
float quinticSmooth(float t) {
float t2 = t * t;
float t3 = t * t2;
return 6.0 * t3 * t2 - 15.0 * t2 * t2 + 10.0 * t3;
}
vec3 cosineGradient(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
return a + b * cos(TAU * (c * t + d));
}
float perlin3D(float amplitude, float frequency, float px, float py, float pz) {
float x = px * frequency;
float y = py * frequency;
float fx = floor(x); float fy = floor(y); float fz = floor(pz);
float cx = ceil(x); float cy = ceil(y); float cz = ceil(pz);
vec3 g000 = gradientHash(vec3(fx, fy, fz));
vec3 g100 = gradientHash(vec3(cx, fy, fz));
vec3 g010 = gradientHash(vec3(fx, cy, fz));
vec3 g110 = gradientHash(vec3(cx, cy, fz));
vec3 g001 = gradientHash(vec3(fx, fy, cz));
vec3 g101 = gradientHash(vec3(cx, fy, cz));
vec3 g011 = gradientHash(vec3(fx, cy, cz));
vec3 g111 = gradientHash(vec3(cx, cy, cz));
float d000 = dot(g000, vec3(x - fx, y - fy, pz - fz));
float d100 = dot(g100, vec3(x - cx, y - fy, pz - fz));
float d010 = dot(g010, vec3(x - fx, y - cy, pz - fz));
float d110 = dot(g110, vec3(x - cx, y - cy, pz - fz));
float d001 = dot(g001, vec3(x - fx, y - fy, pz - cz));
float d101 = dot(g101, vec3(x - cx, y - fy, pz - cz));
float d011 = dot(g011, vec3(x - fx, y - cy, pz - cz));
float d111 = dot(g111, vec3(x - cx, y - cy, pz - cz));
float sx = quinticSmooth(x - fx);
float sy = quinticSmooth(y - fy);
float sz = quinticSmooth(pz - fz);
float lx00 = mix(d000, d100, sx);
float lx10 = mix(d010, d110, sx);
float lx01 = mix(d001, d101, sx);
float lx11 = mix(d011, d111, sx);
float ly0 = mix(lx00, lx10, sy);
float ly1 = mix(lx01, lx11, sy);
return amplitude * mix(ly0, ly1, sz);
}
float auroraGlow(float t, vec2 shift) {
// Pixelkonsistent: Referenzhöhe 180px — die Noise-/Band-Struktur behält
// in flachen Containern ihre reale Größe statt zu stauchen
vec2 uv = gl_FragCoord.xy / 180.0;
uv += shift;
float noiseVal = 0.0;
float freq = uNoiseFreq;
float amp = uNoiseAmp;
vec2 samplePos = uv * uScale;
for (float i = 0.0; i < 3.0; i += 1.0) {
noiseVal += perlin3D(amp, freq, samplePos.x, samplePos.y, t);
amp *= uOctaveDecay;
freq *= 2.0;
}
float yBand = uv.y * 10.0 - uBandHeight * 10.0;
return 0.3 * max(exp(uBandSpread * (1.0 - 1.1 * abs(noiseVal + yBand))), 0.0);
}
float cardMask(vec2 fc) {
vec2 p = fc - uCardRes * 0.5;
vec2 hb = uCardRes * 0.5 - vec2(uRadius);
float rd = length(max(abs(p) - hb, 0.0)) - uRadius;
return 1.0 - smoothstep(-0.75, 0.75, rd);
}
void main() {
vec2 uv = gl_FragCoord.xy / uCardRes.xy;
float t = uSpeed * 0.4 * uTime;
vec2 shift = (uMouse - 0.5) * uMouseInfluence;
vec3 col = vec3(0.0);
col += 0.99 * auroraGlow(t, shift) * cosineGradient(uv.x + uTime * uSpeed * 0.2 * uColorSpeed, vec3(0.5), vec3(0.5), vec3(1.0), vec3(0.3, 0.20, 0.20)) * uColor1;
col += 0.99 * auroraGlow(t + uLayerOffset, shift) * cosineGradient(uv.x + uTime * uSpeed * 0.1 * uColorSpeed, vec3(0.5), vec3(0.5), vec3(2.0, 1.0, 0.0), vec3(0.5, 0.20, 0.25)) * uColor2;
col *= uBrightness;
float alpha = clamp(length(col), 0.0, 1.0) * cardMask(gl_FragCoord.xy);
gl_FragColor = vec4(col * alpha, alpha);
}
`
const ctnDom = ref<HTMLDivElement | null>(null)
useCardShader(ctnDom, {
vertex: VERT,
fragment: FRAG,
mouse: true,
uniforms: {
uTime: { value: 0 },
uSpeed: { value: 0.6 },
uScale: { value: 1.5 },
uBrightness: { value: 1.0 },
uColor1: { value: new Float32Array([0.97, 0.97, 0.97]) },
uColor2: { value: new Float32Array(hexToRgbTuple(props.color)) },
uNoiseFreq: { value: 2.5 },
uNoiseAmp: { value: 1.0 },
uBandHeight: { value: 0.5 },
uBandSpread: { value: 1.0 },
uOctaveDecay: { value: 0.1 },
uLayerOffset: { value: 0 },
uColorSpeed: { value: 1.0 },
uMouse: { value: new Float32Array([0.5, 0.5]) },
uMouseInfluence: { value: 0.25 },
},
color: () => props.color,
colorUniforms: { uColor2: 0 },
frame: ({ t, program, mouse }) => {
const u = program.uniforms
u.uTime.value = t * 0.001
const m = u.uMouse.value as Float32Array
m[0] = mouse.x
m[1] = mouse.y
},
})
</script>
<template>
<div ref="ctnDom" class="relative size-full overflow-hidden rounded-[inherit]" />
</template>
+203
View File
@@ -0,0 +1,203 @@
<script setup lang="ts">
// Portiert aus Vue Bits (DavidHDev/vue-bits), vereinfacht auf color-Prop,
// ohne Cursor-Interaktion (Hover-Card ist zu klein dafür)
const props = withDefaults(defineProps<{ color?: string }>(), {
color: '#6366f1',
})
class Grad {
constructor(
public x: number,
public y: number,
public z: number,
) {}
dot2(x: number, y: number): number {
return this.x * x + this.y * y
}
}
class Noise {
grad3 = [
new Grad(1, 1, 0), new Grad(-1, 1, 0), new Grad(1, -1, 0), new Grad(-1, -1, 0),
new Grad(1, 0, 1), new Grad(-1, 0, 1), new Grad(1, 0, -1), new Grad(-1, 0, -1),
new Grad(0, 1, 1), new Grad(0, -1, 1), new Grad(0, 1, -1), new Grad(0, -1, -1),
]
p = [
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240,
21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88,
237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83,
111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216,
80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186,
3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58,
17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193,
238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157,
184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128,
195, 78, 66, 215, 61, 156, 180,
]
perm = new Array(512)
gradP = new Array(512)
constructor(seed = 0) {
this.seed(seed)
}
seed(seed: number) {
if (seed > 0 && seed < 1) seed *= 65536
seed = Math.floor(seed)
if (seed < 256) seed |= seed << 8
for (let i = 0; i < 256; i++) {
const v = i & 1 ? this.p[i]! ^ (seed & 255) : this.p[i]! ^ ((seed >> 8) & 255)
this.perm[i] = this.perm[i + 256] = v
this.gradP[i] = this.gradP[i + 256] = this.grad3[v % 12]
}
}
fade(t: number): number {
return t * t * t * (t * (t * 6 - 15) + 10)
}
lerp(a: number, b: number, t: number): number {
return (1 - t) * a + t * b
}
perlin2(x: number, y: number): number {
let X = Math.floor(x)
let Y = Math.floor(y)
x -= X
y -= Y
X &= 255
Y &= 255
const n00 = this.gradP[X + this.perm[Y]!]!.dot2(x, y)
const n01 = this.gradP[X + this.perm[Y + 1]!]!.dot2(x, y - 1)
const n10 = this.gradP[X + 1 + this.perm[Y]!]!.dot2(x - 1, y)
const n11 = this.gradP[X + 1 + this.perm[Y + 1]!]!.dot2(x - 1, y - 1)
const u = this.fade(x)
return this.lerp(this.lerp(n00, n10, u), this.lerp(n01, n11, u), this.fade(y))
}
}
interface Point {
x: number
y: number
wave: { x: number, y: number }
}
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef')
let ctx: CanvasRenderingContext2D | null = null
let bounding = { width: 0, height: 0 }
let cornerRadius = 0
let noise: Noise | null = null
let lines: Point[][] = []
let frameId: number | null = null
const xGap = 10
const yGap = 26
function setSize() {
const container = containerRef.value
const canvas = canvasRef.value
if (!container || !canvas) return
const rect = container.getBoundingClientRect()
bounding = { width: rect.width, height: rect.height }
const dpr = window.devicePixelRatio || 1
canvas.width = rect.width * dpr
canvas.height = rect.height * dpr
ctx?.setTransform(dpr, 0, 0, dpr, 0, 0)
cornerRadius = parseFloat(getComputedStyle(container).borderTopLeftRadius) || 0
}
function setLines() {
const { width, height } = bounding
lines = []
const oWidth = width + 200
const oHeight = height + 30
const totalLines = Math.ceil(oWidth / xGap)
const totalPoints = Math.ceil(oHeight / yGap)
const xStart = (width - xGap * totalLines) / 2
const yStart = (height - yGap * totalPoints) / 2
for (let i = 0; i <= totalLines; i++) {
const pts: Point[] = []
for (let j = 0; j <= totalPoints; j++) {
pts.push({ x: xStart + xGap * i, y: yStart + yGap * j, wave: { x: 0, y: 0 } })
}
lines.push(pts)
}
}
function tick(t: number) {
if (!ctx || !noise) return
const { width, height } = bounding
for (const pts of lines) {
for (const p of pts) {
const move = noise.perlin2((p.x + t * 0.0125) * 0.002, (p.y + t * 0.005) * 0.0015) * 12
p.wave.x = Math.cos(move) * 32
p.wave.y = Math.sin(move) * 16
}
}
ctx.clearRect(0, 0, width, height)
// Rounded-Rect-Clip direkt im Canvas — zuverlässig auch wenn der
// Compositor den Layer nicht an CSS-Clips bindet (Waterfox)
ctx.save()
ctx.beginPath()
ctx.roundRect(0, 0, width, height, cornerRadius)
ctx.clip()
ctx.beginPath()
ctx.strokeStyle = props.color
for (const points of lines) {
let p1 = points[0]!
ctx.moveTo(p1.x + p1.wave.x, p1.y + p1.wave.y)
for (const p of points) {
ctx.lineTo(Math.round((p.x + p.wave.x) * 10) / 10, Math.round((p.y + p.wave.y) * 10) / 10)
}
p1 = points[points.length - 1]!
ctx.moveTo(p1.x + p1.wave.x, p1.y + p1.wave.y)
}
ctx.stroke()
ctx.restore()
frameId = requestAnimationFrame(tick)
}
let ro: ResizeObserver | null = null
onMounted(() => {
const canvas = canvasRef.value
const container = containerRef.value
if (!canvas || !container) return
ctx = canvas.getContext('2d')
noise = new Noise(Math.random())
setSize()
setLines()
frameId = requestAnimationFrame(tick)
ro = new ResizeObserver(() => {
setSize()
setLines()
})
ro.observe(container)
})
onUnmounted(() => {
ro?.disconnect()
if (frameId !== null) cancelAnimationFrame(frameId)
})
</script>
<template>
<div ref="containerRef" class="relative size-full overflow-hidden rounded-[inherit] opacity-50">
<canvas ref="canvasRef" class="block size-full rounded-[inherit]" />
</div>
</template>