Files
BRETT/app/components/effects/EffectCrtWarp.vue
T
2026-10-03 20:07:49 +02:00

195 lines
6.3 KiB
Vue

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