Files
2026-10-03 20:07:49 +02:00

182 lines
5.3 KiB
Vue

<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>