forked from BRETT/BRETT
Initial Commit
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
<script setup lang="ts">
|
||||
import { Color, Mesh, Program, Renderer, Triangle } from 'ogl'
|
||||
import { 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 uTime;
|
||||
uniform float uAmplitude;
|
||||
uniform vec3 uColorStops[3];
|
||||
uniform vec2 uResolution;
|
||||
uniform float uBlend;
|
||||
uniform float uRadius;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
vec3 permute(vec3 x) {
|
||||
return mod(((x * 34.0) + 1.0) * x, 289.0);
|
||||
}
|
||||
|
||||
float snoise(vec2 v){
|
||||
const vec4 C = vec4(
|
||||
0.211324865405187, 0.366025403784439,
|
||||
-0.577350269189626, 0.024390243902439
|
||||
);
|
||||
vec2 i = floor(v + dot(v, C.yy));
|
||||
vec2 x0 = v - i + dot(i, C.xx);
|
||||
vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
|
||||
vec4 x12 = x0.xyxy + C.xxzz;
|
||||
x12.xy -= i1;
|
||||
i = mod(i, 289.0);
|
||||
|
||||
vec3 p = permute(
|
||||
permute(i.y + vec3(0.0, i1.y, 1.0))
|
||||
+ i.x + vec3(0.0, i1.x, 1.0)
|
||||
);
|
||||
|
||||
vec3 m = max(
|
||||
0.5 - vec3(
|
||||
dot(x0, x0),
|
||||
dot(x12.xy, x12.xy),
|
||||
dot(x12.zw, x12.zw)
|
||||
),
|
||||
0.0
|
||||
);
|
||||
m = m * m;
|
||||
m = m * m;
|
||||
|
||||
vec3 x = 2.0 * fract(p * C.www) - 1.0;
|
||||
vec3 h = abs(x) - 0.5;
|
||||
vec3 ox = floor(x + 0.5);
|
||||
vec3 a0 = x - ox;
|
||||
m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
|
||||
|
||||
vec3 g;
|
||||
g.x = a0.x * x0.x + h.x * x0.y;
|
||||
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
|
||||
return 130.0 * dot(m, g);
|
||||
}
|
||||
|
||||
struct ColorStop {
|
||||
vec3 color;
|
||||
float position;
|
||||
};
|
||||
|
||||
#define COLOR_RAMP(colors, factor, finalColor) { \\
|
||||
int index = 0; \\
|
||||
for (int i = 0; i < 2; i++) { \\
|
||||
ColorStop currentColor = colors[i]; \\
|
||||
bool isInBetween = currentColor.position <= factor; \\
|
||||
index = int(mix(float(index), float(i), float(isInBetween))); \\
|
||||
} \\
|
||||
ColorStop currentColor = colors[index]; \\
|
||||
ColorStop nextColor = colors[index + 1]; \\
|
||||
float range = nextColor.position - currentColor.position; \\
|
||||
float lerpFactor = (factor - currentColor.position) / range; \\
|
||||
finalColor = mix(currentColor.color, nextColor.color, lerpFactor); \\
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / uResolution;
|
||||
|
||||
ColorStop colors[3];
|
||||
colors[0] = ColorStop(uColorStops[0], 0.0);
|
||||
colors[1] = ColorStop(uColorStops[1], 0.5);
|
||||
colors[2] = ColorStop(uColorStops[2], 1.0);
|
||||
|
||||
vec3 rampColor;
|
||||
COLOR_RAMP(colors, uv.x, rampColor);
|
||||
|
||||
// Noise x-Koordinate pixelkonsistent (~2 Einheiten pro 320px), damit
|
||||
// die Wellen in schmalen/breiten Karten nicht gestaucht/gestreckt wirken
|
||||
float height = snoise(vec2(gl_FragCoord.x / 160.0 + uTime * 0.1, uTime * 0.25)) * 0.5 * uAmplitude;
|
||||
height = exp(height);
|
||||
height = (uv.y * 2.0 - height + 0.2);
|
||||
float intensity = 0.6 * height;
|
||||
|
||||
float midPoint = 0.20;
|
||||
float auroraAlpha = smoothstep(midPoint - uBlend * 0.5, midPoint + uBlend * 0.5, intensity);
|
||||
|
||||
vec3 auroraColor = intensity * rampColor;
|
||||
|
||||
// Rounded-Rect-Maske direkt im Shader — zuverlässig auch wenn der
|
||||
// Compositor den Canvas-Layer nicht an CSS-Clips bindet (Waterfox)
|
||||
vec2 p = gl_FragCoord.xy - uResolution * 0.5;
|
||||
vec2 hb = uResolution * 0.5 - vec2(uRadius);
|
||||
float rd = length(max(abs(p) - hb, 0.0)) - uRadius;
|
||||
float rm = 1.0 - smoothstep(-0.75, 0.75, rd);
|
||||
auroraAlpha *= rm;
|
||||
|
||||
fragColor = vec4(auroraColor * auroraAlpha, auroraAlpha);
|
||||
}
|
||||
`
|
||||
|
||||
const ctnDom = ref<HTMLDivElement | null>(null)
|
||||
|
||||
let animateId = 0
|
||||
let teardown: (() => void) | null = null
|
||||
|
||||
onMounted(() => {
|
||||
const ctn = ctnDom.value
|
||||
if (!ctn) return
|
||||
|
||||
const renderer = new Renderer({ alpha: true, premultipliedAlpha: true, antialias: true })
|
||||
const gl = renderer.gl
|
||||
gl.clearColor(0, 0, 0, 0)
|
||||
gl.enable(gl.BLEND)
|
||||
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
|
||||
// WebGL-Canvas bleibt detached: Waterfox/Firefox zeichnet WebGL-Layer nach
|
||||
// Scrollen teils an veralteter Position und ignoriert CSS-Clips — stattdessen
|
||||
// wird pro Frame in einen normalen 2D-Canvas geblittet
|
||||
const outCanvas = document.createElement('canvas')
|
||||
outCanvas.className = 'absolute inset-0 size-full rounded-[inherit]'
|
||||
const outCtx = outCanvas.getContext('2d')
|
||||
ctn.appendChild(outCanvas)
|
||||
|
||||
const geometry = new Triangle(gl)
|
||||
if (geometry.attributes.uv) {
|
||||
delete geometry.attributes.uv
|
||||
}
|
||||
|
||||
const colorStops = [shadeHex(props.color, -0.5), props.color, shadeHex(props.color, 0.4)]
|
||||
.map((hex) => {
|
||||
const c = new Color(hex)
|
||||
return [c.r, c.g, c.b]
|
||||
})
|
||||
|
||||
const program = new Program(gl, {
|
||||
vertex: VERT,
|
||||
fragment: FRAG,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uAmplitude: { value: 1.0 },
|
||||
uColorStops: { value: colorStops },
|
||||
uResolution: { value: [ctn.offsetWidth, ctn.offsetHeight] },
|
||||
uBlend: { value: 0.5 },
|
||||
uRadius: { value: 0 },
|
||||
},
|
||||
})
|
||||
|
||||
const mesh = new Mesh(gl, { geometry, program })
|
||||
|
||||
// Farbwechsel live übernehmen (Settings-Vorschau)
|
||||
watch(() => props.color, (c) => {
|
||||
program.uniforms.uColorStops!.value = [shadeHex(c, -0.5), c, shadeHex(c, 0.4)]
|
||||
.map((hex) => {
|
||||
const col = new Color(hex)
|
||||
return [col.r, col.g, col.b]
|
||||
})
|
||||
})
|
||||
|
||||
const resizeHandler = () => {
|
||||
renderer.setSize(ctn.offsetWidth, ctn.offsetHeight)
|
||||
program.uniforms.uResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight]
|
||||
const cssRadius = parseFloat(getComputedStyle(ctn).borderTopLeftRadius) || 0
|
||||
program.uniforms.uRadius.value = ctn.offsetWidth > 0
|
||||
? cssRadius * (gl.drawingBufferWidth / ctn.offsetWidth)
|
||||
: 0
|
||||
outCanvas.width = gl.drawingBufferWidth
|
||||
outCanvas.height = gl.drawingBufferHeight
|
||||
}
|
||||
const ro = new ResizeObserver(resizeHandler)
|
||||
ro.observe(ctn)
|
||||
resizeHandler()
|
||||
|
||||
const update = (t: number) => {
|
||||
animateId = requestAnimationFrame(update)
|
||||
program.uniforms.uTime.value = t * 0.01 * 0.1
|
||||
renderer.render({ scene: mesh })
|
||||
outCtx?.clearRect(0, 0, outCanvas.width, outCanvas.height)
|
||||
outCtx?.drawImage(gl.canvas, 0, 0, outCanvas.width, outCanvas.height)
|
||||
}
|
||||
animateId = requestAnimationFrame(update)
|
||||
|
||||
teardown = () => {
|
||||
cancelAnimationFrame(animateId)
|
||||
ro.disconnect()
|
||||
if (outCanvas.parentNode === ctn) ctn.removeChild(outCanvas)
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
teardown?.()
|
||||
teardown = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="ctnDom" class="relative size-full overflow-hidden rounded-[inherit]" />
|
||||
</template>
|
||||
Reference in New Issue
Block a user