forked from BRETT/BRETT
Initial Commit
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
<script setup lang="ts">
|
||||
import { Camera, Geometry, Mesh, Program, Renderer } 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 containerRef = useTemplateRef<HTMLDivElement>('containerRef')
|
||||
|
||||
const vertex = /* glsl */ `
|
||||
attribute vec3 position;
|
||||
attribute vec4 random;
|
||||
attribute vec3 color;
|
||||
|
||||
uniform mat4 modelMatrix;
|
||||
uniform mat4 viewMatrix;
|
||||
uniform mat4 projectionMatrix;
|
||||
uniform float uTime;
|
||||
uniform float uSpread;
|
||||
uniform float uBaseSize;
|
||||
uniform float uSizeRandomness;
|
||||
|
||||
varying vec4 vRandom;
|
||||
varying vec3 vColor;
|
||||
|
||||
void main() {
|
||||
vRandom = random;
|
||||
vColor = color;
|
||||
|
||||
vec3 pos = position * uSpread;
|
||||
pos.z *= 10.0;
|
||||
|
||||
vec4 mPos = modelMatrix * vec4(pos, 1.0);
|
||||
float t = uTime;
|
||||
mPos.x += sin(t * random.z + 6.28 * random.w) * mix(0.1, 1.5, random.x);
|
||||
mPos.y += sin(t * random.y + 6.28 * random.x) * mix(0.1, 1.5, random.w);
|
||||
mPos.z += sin(t * random.w + 6.28 * random.y) * mix(0.1, 1.5, random.z);
|
||||
|
||||
vec4 mvPos = viewMatrix * mPos;
|
||||
gl_PointSize = (uBaseSize * (1.0 + uSizeRandomness * (random.x - 0.5))) / length(mvPos.xyz);
|
||||
gl_Position = projectionMatrix * mvPos;
|
||||
}
|
||||
`
|
||||
|
||||
const fragment = /* glsl */ `
|
||||
precision highp float;
|
||||
|
||||
uniform float uTime;
|
||||
uniform vec2 uResolution;
|
||||
uniform float uRadius;
|
||||
varying vec4 vRandom;
|
||||
varying vec3 vColor;
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_PointCoord.xy;
|
||||
float d = length(uv - vec2(0.5));
|
||||
float circle = smoothstep(0.5, 0.4, d) * 0.8;
|
||||
|
||||
// Rounded-Rect-Maske im Shader (Compositor-sicher, z. B. 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;
|
||||
circle *= 1.0 - smoothstep(-0.75, 0.75, rd);
|
||||
|
||||
gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), circle);
|
||||
}
|
||||
`
|
||||
|
||||
let cleanup: (() => void) | null = null
|
||||
|
||||
onMounted(() => {
|
||||
const container = containerRef.value
|
||||
if (!container) return
|
||||
|
||||
const renderer = new Renderer({ dpr: 1, depth: false, alpha: true })
|
||||
const gl = renderer.gl
|
||||
// WebGL-Canvas bleibt detached; Anzeige über 2D-Blit-Canvas — siehe EffectAurora
|
||||
const outCanvas = document.createElement('canvas')
|
||||
outCanvas.className = 'absolute inset-0 size-full rounded-[inherit]'
|
||||
const outCtx = outCanvas.getContext('2d')
|
||||
container.appendChild(outCanvas)
|
||||
gl.clearColor(0, 0, 0, 0)
|
||||
|
||||
const camera = new Camera(gl, { fov: 15 })
|
||||
camera.position.set(0, 0, 20)
|
||||
|
||||
const resize = () => {
|
||||
renderer.setSize(container.clientWidth, container.clientHeight)
|
||||
camera.perspective({ aspect: gl.canvas.width / gl.canvas.height })
|
||||
const res = program.uniforms.uResolution.value as Float32Array
|
||||
res[0] = gl.drawingBufferWidth
|
||||
res[1] = gl.drawingBufferHeight
|
||||
const cssRadius = parseFloat(getComputedStyle(container).borderTopLeftRadius) || 0
|
||||
program.uniforms.uRadius.value = container.clientWidth > 0
|
||||
? cssRadius * (gl.drawingBufferWidth / container.clientWidth)
|
||||
: 0
|
||||
outCanvas.width = gl.drawingBufferWidth
|
||||
outCanvas.height = gl.drawingBufferHeight
|
||||
}
|
||||
const ro = new ResizeObserver(resize)
|
||||
|
||||
const count = 120
|
||||
const positions = new Float32Array(count * 3)
|
||||
const randoms = new Float32Array(count * 4)
|
||||
const colors = new Float32Array(count * 3)
|
||||
const palette = [props.color, shadeHex(props.color, 0.4), '#ffffff']
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let x = 0
|
||||
let y = 0
|
||||
let z = 0
|
||||
let len = 0
|
||||
do {
|
||||
x = Math.random() * 2 - 1
|
||||
y = Math.random() * 2 - 1
|
||||
z = Math.random() * 2 - 1
|
||||
len = x * x + y * y + z * z
|
||||
} while (len > 1 || len === 0)
|
||||
const r = Math.cbrt(Math.random())
|
||||
positions.set([x * r, y * r, z * r], i * 3)
|
||||
randoms.set([Math.random(), Math.random(), Math.random(), Math.random()], i * 4)
|
||||
colors.set(hexToRgbTuple(palette[Math.floor(Math.random() * palette.length)]!), i * 3)
|
||||
}
|
||||
|
||||
const geometry = new Geometry(gl, {
|
||||
position: { size: 3, data: positions },
|
||||
random: { size: 4, data: randoms },
|
||||
color: { size: 3, data: colors },
|
||||
})
|
||||
|
||||
const program = new Program(gl, {
|
||||
vertex,
|
||||
fragment,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uSpread: { value: 10 },
|
||||
uBaseSize: { value: 100 },
|
||||
uSizeRandomness: { value: 1 },
|
||||
uResolution: { value: new Float32Array([1, 1]) },
|
||||
uRadius: { value: 0 },
|
||||
},
|
||||
transparent: true,
|
||||
depthTest: false,
|
||||
})
|
||||
|
||||
const particles = new Mesh(gl, { mode: gl.POINTS, geometry, program })
|
||||
|
||||
// Farbwechsel live übernehmen (Settings-Vorschau)
|
||||
watch(() => props.color, (c) => {
|
||||
const p = [c, shadeHex(c, 0.4), '#ffffff']
|
||||
for (let i = 0; i < count; i++) {
|
||||
colors.set(hexToRgbTuple(p[Math.floor(Math.random() * p.length)]!), i * 3)
|
||||
}
|
||||
geometry.attributes.color!.needsUpdate = true
|
||||
})
|
||||
|
||||
ro.observe(container)
|
||||
resize()
|
||||
|
||||
let animationFrameId = 0
|
||||
let lastTime = performance.now()
|
||||
let elapsed = 0
|
||||
|
||||
const update = (t: number) => {
|
||||
animationFrameId = requestAnimationFrame(update)
|
||||
const delta = t - lastTime
|
||||
lastTime = t
|
||||
elapsed += delta * 0.1
|
||||
program.uniforms.uTime.value = elapsed * 0.001
|
||||
particles.rotation.x = Math.sin(elapsed * 0.0002) * 0.1
|
||||
particles.rotation.y = Math.cos(elapsed * 0.0005) * 0.15
|
||||
particles.rotation.z += 0.001
|
||||
renderer.render({ scene: particles, camera })
|
||||
outCtx?.clearRect(0, 0, outCanvas.width, outCanvas.height)
|
||||
outCtx?.drawImage(gl.canvas, 0, 0, outCanvas.width, outCanvas.height)
|
||||
}
|
||||
animationFrameId = requestAnimationFrame(update)
|
||||
|
||||
cleanup = () => {
|
||||
ro.disconnect()
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
if (container.contains(outCanvas)) container.removeChild(outCanvas)
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanup?.()
|
||||
cleanup = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="relative size-full overflow-hidden rounded-[inherit]" />
|
||||
</template>
|
||||
Reference in New Issue
Block a user