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

134 lines
3.5 KiB
Vue

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