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

247 lines
7.4 KiB
Vue

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