forked from BRETT/BRETT
247 lines
7.4 KiB
Vue
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>
|