mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
15 KiB
JSON
1 line
15 KiB
JSON
{"name":"PrismaticBurst","title":"PrismaticBurst","description":"Burst of light rays with controllable color, distortion, amount.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { Renderer, Program, Mesh, Triangle, Texture } from 'ogl';\nimport { onMounted, onUnmounted, ref, useTemplateRef, watch, type CSSProperties } from 'vue';\n\ntype Offset = { x?: number | string; y?: number | string };\ntype AnimationType = 'rotate' | 'rotate3d' | 'hover';\n\nexport type PrismaticBurstProps = {\n intensity?: number;\n speed?: number;\n animationType?: AnimationType;\n colors?: string[];\n distort?: number;\n paused?: boolean;\n offset?: Offset;\n hoverDampness?: number;\n rayCount?: number;\n mixBlendMode?: CSSProperties['mixBlendMode'] | 'none';\n};\n\nconst vertexShader = `#version 300 es\nin vec2 position;\nin vec2 uv;\nout vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragmentShader = `#version 300 es\nprecision highp float;\nprecision highp int;\n\nout vec4 fragColor;\n\nuniform vec2 uResolution;\nuniform float uTime;\n\nuniform float uIntensity;\nuniform float uSpeed;\nuniform int uAnimType;\nuniform vec2 uMouse;\nuniform int uColorCount;\nuniform float uDistort;\nuniform vec2 uOffset;\nuniform sampler2D uGradient;\nuniform float uNoiseAmount;\nuniform int uRayCount;\n\nfloat hash21(vec2 p){\n p = floor(p);\n float f = 52.9829189 * fract(dot(p, vec2(0.065, 0.005)));\n return fract(f);\n}\n\nmat2 rot30(){ return mat2(0.8, -0.5, 0.5, 0.8); }\n\nfloat layeredNoise(vec2 fragPx){\n vec2 p = mod(fragPx + vec2(uTime * 30.0, -uTime * 21.0), 1024.0);\n vec2 q = rot30() * p;\n float n = 0.0;\n n += 0.40 * hash21(q);\n n += 0.25 * hash21(q * 2.0 + 17.0);\n n += 0.20 * hash21(q * 4.0 + 47.0);\n n += 0.10 * hash21(q * 8.0 + 113.0);\n n += 0.05 * hash21(q * 16.0 + 191.0);\n return n;\n}\n\nvec3 rayDir(vec2 frag, vec2 res, vec2 offset, float dist){\n float focal = res.y * max(dist, 1e-3);\n return normalize(vec3(2.0 * (frag - offset) - res, focal));\n}\n\nfloat edgeFade(vec2 frag, vec2 res, vec2 offset){\n vec2 toC = frag - 0.5 * res - offset;\n float r = length(toC) / (0.5 * min(res.x, res.y));\n float x = clamp(r, 0.0, 1.0);\n float q = x * x * x * (x * (x * 6.0 - 15.0) + 10.0);\n float s = q * 0.5;\n s = pow(s, 1.5);\n float tail = 1.0 - pow(1.0 - s, 2.0);\n s = mix(s, tail, 0.2);\n float dn = (layeredNoise(frag * 0.15) - 0.5) * 0.0015 * s;\n return clamp(s + dn, 0.0, 1.0);\n}\n\nmat3 rotX(float a){ float c = cos(a), s = sin(a); return mat3(1.0,0.0,0.0, 0.0,c,-s, 0.0,s,c); }\nmat3 rotY(float a){ float c = cos(a), s = sin(a); return mat3(c,0.0,s, 0.0,1.0,0.0, -s,0.0,c); }\nmat3 rotZ(float a){ float c = cos(a), s = sin(a); return mat3(c,-s,0.0, s,c,0.0, 0.0,0.0,1.0); }\n\nvec3 sampleGradient(float t){\n t = clamp(t, 0.0, 1.0);\n return texture(uGradient, vec2(t, 0.5)).rgb;\n}\n\nvec2 rot2(vec2 v, float a){\n float s = sin(a), c = cos(a);\n return mat2(c, -s, s, c) * v;\n}\n\nfloat bendAngle(vec3 q, float t){\n float a = 0.8 * sin(q.x * 0.55 + t * 0.6)\n + 0.7 * sin(q.y * 0.50 - t * 0.5)\n + 0.6 * sin(q.z * 0.60 + t * 0.7);\n return a;\n}\n\nvoid main(){\n vec2 frag = gl_FragCoord.xy;\n float t = uTime * uSpeed;\n float jitterAmp = 0.1 * clamp(uNoiseAmount, 0.0, 1.0);\n vec3 dir = rayDir(frag, uResolution, uOffset, 1.0);\n float marchT = 0.0;\n vec3 col = vec3(0.0);\n float n = layeredNoise(frag);\n vec4 c = cos(t * 0.2 + vec4(0.0, 33.0, 11.0, 0.0));\n mat2 M2 = mat2(c.x, c.y, c.z, c.w);\n float amp = clamp(uDistort, 0.0, 50.0) * 0.15;\n\n mat3 rot3dMat = mat3(1.0);\n if(uAnimType == 1){\n vec3 ang = vec3(t * 0.31, t * 0.21, t * 0.17);\n rot3dMat = rotZ(ang.z) * rotY(ang.y) * rotX(ang.x);\n }\n mat3 hoverMat = mat3(1.0);\n if(uAnimType == 2){\n vec2 m = uMouse * 2.0 - 1.0;\n vec3 ang = vec3(m.y * 0.6, m.x * 0.6, 0.0);\n hoverMat = rotY(ang.y) * rotX(ang.x);\n }\n\n for (int i = 0; i < 44; ++i) {\n vec3 P = marchT * dir;\n P.z -= 2.0;\n float rad = length(P);\n vec3 Pl = P * (10.0 / max(rad, 1e-6));\n\n if(uAnimType == 0){\n Pl.xz *= M2;\n } else if(uAnimType == 1){\n Pl = rot3dMat * Pl;\n } else {\n Pl = hoverMat * Pl;\n }\n\n float stepLen = min(rad - 0.3, n * jitterAmp) + 0.1;\n\n float grow = smoothstep(0.35, 3.0, marchT);\n float a1 = amp * grow * bendAngle(Pl * 0.6, t);\n float a2 = 0.5 * amp * grow * bendAngle(Pl.zyx * 0.5 + 3.1, t * 0.9);\n vec3 Pb = Pl;\n Pb.xz = rot2(Pb.xz, a1);\n Pb.xy = rot2(Pb.xy, a2);\n\n float rayPattern = smoothstep(\n 0.5, 0.7,\n sin(Pb.x + cos(Pb.y) * cos(Pb.z)) *\n sin(Pb.z + sin(Pb.y) * cos(Pb.x + t))\n );\n\n if (uRayCount > 0) {\n float ang = atan(Pb.y, Pb.x);\n float comb = 0.5 + 0.5 * cos(float(uRayCount) * ang);\n comb = pow(comb, 3.0);\n rayPattern *= smoothstep(0.15, 0.95, comb);\n }\n\n vec3 spectralDefault = 1.0 + vec3(\n cos(marchT * 3.0 + 0.0),\n cos(marchT * 3.0 + 1.0),\n cos(marchT * 3.0 + 2.0)\n );\n\n float saw = fract(marchT * 0.25);\n float tRay = saw * saw * (3.0 - 2.0 * saw);\n vec3 userGradient = 2.0 * sampleGradient(tRay);\n vec3 spectral = (uColorCount > 0) ? userGradient : spectralDefault;\n vec3 base = (0.05 / (0.4 + stepLen))\n * smoothstep(5.0, 0.0, rad)\n * spectral;\n\n col += base * rayPattern;\n marchT += stepLen;\n }\n\n col *= edgeFade(frag, uResolution, uOffset);\n col *= uIntensity;\n\n fragColor = vec4(clamp(col, 0.0, 1.0), 1.0);\n}`;\n\nconst hexToRgb01 = (hex: string): [number, number, number] => {\n let h = hex.trim();\n if (h.startsWith('#')) h = h.slice(1);\n if (h.length === 3) {\n const r = h[0],\n g = h[1],\n b = h[2];\n h = r + r + g + g + b + b;\n }\n const intVal = parseInt(h, 16);\n if (isNaN(intVal) || (h.length !== 6 && h.length !== 8)) return [1, 1, 1];\n const r = ((intVal >> 16) & 255) / 255;\n const g = ((intVal >> 8) & 255) / 255;\n const b = (intVal & 255) / 255;\n return [r, g, b];\n};\n\nconst toPx = (v: number | string | undefined): number => {\n if (v == null) return 0;\n if (typeof v === 'number') return v;\n const s = String(v).trim();\n const num = parseFloat(s.replace('px', ''));\n return isNaN(num) ? 0 : num;\n};\n\nconst props = withDefaults(defineProps<PrismaticBurstProps>(), {\n intensity: 2,\n speed: 0.5,\n animationType: 'rotate3d',\n distort: 0,\n paused: false,\n offset: () => ({ x: 0, y: 0 }),\n hoverDampness: 0,\n mixBlendMode: 'lighten'\n});\n\nconst containerRef = useTemplateRef('containerRef');\nconst programRef = ref<Program | null>(null);\nconst rendererRef = ref<Renderer | null>(null);\nconst mouseTargetRef = ref<[number, number]>([0.5, 0.5]);\nconst mouseSmoothRef = ref<[number, number]>([0.5, 0.5]);\nconst pausedRef = ref<boolean>(props.paused);\nconst gradTexRef = ref<Texture | null>(null);\nconst hoverDampRef = ref<number>(props.hoverDampness);\nconst isVisibleRef = ref<boolean>(true);\nconst meshRef = ref<Mesh | null>(null);\nconst triRef = ref<Triangle | null>(null);\n\nonMounted(() => {\n const container = containerRef.value;\n if (!container) return;\n\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n const renderer = new Renderer({ dpr, alpha: false, antialias: false });\n rendererRef.value = renderer;\n\n const gl = renderer.gl;\n gl.canvas.style.position = 'absolute';\n gl.canvas.style.inset = '0';\n gl.canvas.style.width = '100%';\n gl.canvas.style.height = '100%';\n gl.canvas.style.mixBlendMode = props.mixBlendMode && props.mixBlendMode !== 'none' ? props.mixBlendMode : '';\n container.appendChild(gl.canvas);\n\n const white = new Uint8Array([255, 255, 255, 255]);\n const gradientTex = new Texture(gl, {\n image: white,\n width: 1,\n height: 1,\n generateMipmaps: false,\n flipY: false\n });\n\n gradientTex.minFilter = gl.LINEAR;\n gradientTex.magFilter = gl.LINEAR;\n gradientTex.wrapS = gl.CLAMP_TO_EDGE;\n gradientTex.wrapT = gl.CLAMP_TO_EDGE;\n gradTexRef.value = gradientTex;\n\n const program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms: {\n uResolution: { value: [1, 1] as [number, number] },\n uTime: { value: 0 },\n\n uIntensity: { value: 1 },\n uSpeed: { value: 1 },\n uAnimType: { value: 0 },\n uMouse: { value: [0.5, 0.5] as [number, number] },\n uColorCount: { value: 0 },\n uDistort: { value: 0 },\n uOffset: { value: [0, 0] as [number, number] },\n uGradient: { value: gradientTex },\n uNoiseAmount: { value: 0.8 },\n uRayCount: { value: 0 }\n }\n });\n\n programRef.value = program;\n\n const triangle = new Triangle(gl);\n const mesh = new Mesh(gl, { geometry: triangle, program });\n triRef.value = triangle;\n meshRef.value = mesh;\n\n const resize = () => {\n const w = container.clientWidth || 1;\n const h = container.clientHeight || 1;\n renderer.setSize(w, h);\n program.uniforms.uResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight];\n };\n\n let ro: ResizeObserver | null = null;\n if ('ResizeObserver' in window) {\n ro = new ResizeObserver(resize);\n ro.observe(container);\n } else {\n (window as Window).addEventListener('resize', resize);\n }\n resize();\n\n const onPointer = (e: PointerEvent) => {\n const rect = container.getBoundingClientRect();\n const x = (e.clientX - rect.left) / Math.max(rect.width, 1);\n const y = (e.clientY - rect.top) / Math.max(rect.height, 1);\n mouseTargetRef.value = [Math.min(Math.max(x, 0), 1), Math.min(Math.max(y, 0), 1)];\n };\n container.addEventListener('pointermove', onPointer, { passive: true });\n\n let io: IntersectionObserver | null = null;\n if ('IntersectionObserver' in window) {\n io = new IntersectionObserver(\n entries => {\n if (entries[0]) isVisibleRef.value = entries[0].isIntersecting;\n },\n { root: null, threshold: 0.01 }\n );\n io.observe(container);\n }\n const onVis = () => {};\n document.addEventListener('visibilitychange', onVis);\n\n let raf = 0;\n let last = performance.now();\n let accumTime = 0;\n\n const update = (now: number) => {\n const dt = Math.max(0, now - last) * 0.001;\n last = now;\n const visible = isVisibleRef.value && !document.hidden;\n if (!pausedRef.value) accumTime += dt;\n if (!visible) {\n raf = requestAnimationFrame(update);\n return;\n }\n const tau = 0.02 + Math.max(0, Math.min(1, hoverDampRef.value)) * 0.5;\n const alpha = 1 - Math.exp(-dt / tau);\n const tgt = mouseTargetRef.value;\n const sm = mouseSmoothRef.value;\n sm[0] += (tgt[0] - sm[0]) * alpha;\n sm[1] += (tgt[1] - sm[1]) * alpha;\n program.uniforms.uMouse.value = sm as [number, number];\n program.uniforms.uTime.value = accumTime;\n renderer.render({ scene: meshRef.value! });\n raf = requestAnimationFrame(update);\n };\n raf = requestAnimationFrame(update);\n\n onUnmounted(() => {\n cancelAnimationFrame(raf);\n container.removeEventListener('pointermove', onPointer);\n ro?.disconnect();\n if (!ro) window.removeEventListener('resize', resize);\n io?.disconnect();\n document.removeEventListener('visibilitychange', onVis);\n try {\n container.removeChild(gl.canvas);\n } catch (e) {\n void e;\n }\n meshRef.value = null;\n triRef.value = null;\n programRef.value = null;\n try {\n const glCtx = rendererRef.value?.gl;\n if (glCtx && gradTexRef.value?.texture) glCtx.deleteTexture(gradTexRef.value.texture);\n } catch (e) {\n void e;\n }\n rendererRef.value = null;\n gradTexRef.value = null;\n });\n});\n\nwatch(\n () => props.paused,\n v => (pausedRef.value = v)\n);\nwatch(\n () => props.hoverDampness,\n v => (hoverDampRef.value = v)\n);\n\nwatch(\n () => props.mixBlendMode,\n mode => {\n const canvas = rendererRef.value?.gl?.canvas as HTMLCanvasElement | undefined;\n if (canvas) {\n canvas.style.mixBlendMode = mode && mode !== 'none' ? mode : '';\n }\n }\n);\n\nwatch(\n () => [props.intensity, props.speed, props.animationType, props.colors, props.distort, props.offset, props.rayCount],\n () => {\n const program = programRef.value;\n const renderer = rendererRef.value;\n const gradTex = gradTexRef.value;\n if (!program || !renderer || !gradTex) return;\n\n program.uniforms.uIntensity.value = props.intensity ?? 1;\n program.uniforms.uSpeed.value = props.speed ?? 1;\n\n const animTypeMap: Record<AnimationType, number> = {\n rotate: 0,\n rotate3d: 1,\n hover: 2\n };\n program.uniforms.uAnimType.value = animTypeMap[props.animationType ?? 'rotate'];\n\n program.uniforms.uDistort.value = typeof props.distort === 'number' ? props.distort : 0;\n\n const ox = toPx(props.offset?.x);\n const oy = toPx(props.offset?.y);\n program.uniforms.uOffset.value = [ox, oy];\n program.uniforms.uRayCount.value = Math.max(0, Math.floor(props.rayCount ?? 0));\n\n let count = 0;\n if (Array.isArray(props.colors) && props.colors.length > 0) {\n const gl = renderer.gl;\n const capped = props.colors.slice(0, 64);\n count = capped.length;\n const data = new Uint8Array(count * 4);\n for (let i = 0; i < count; i++) {\n const [r, g, b] = hexToRgb01(capped[i]);\n data[i * 4 + 0] = Math.round(r * 255);\n data[i * 4 + 1] = Math.round(g * 255);\n data[i * 4 + 2] = Math.round(b * 255);\n data[i * 4 + 3] = 255;\n }\n gradTex.image = data;\n gradTex.width = count;\n gradTex.height = 1;\n gradTex.minFilter = gl.LINEAR;\n gradTex.magFilter = gl.LINEAR;\n gradTex.wrapS = gl.CLAMP_TO_EDGE;\n gradTex.wrapT = gl.CLAMP_TO_EDGE;\n gradTex.flipY = false;\n gradTex.generateMipmaps = false;\n gradTex.format = gl.RGBA;\n gradTex.type = gl.UNSIGNED_BYTE;\n gradTex.needsUpdate = true;\n } else {\n count = 0;\n }\n program.uniforms.uColorCount.value = count;\n },\n { immediate: true }\n);\n</script>\n\n<template>\n <div class=\"relative w-full h-full overflow-hidden\" ref=\"containerRef\" />\n</template>\n","path":"PrismaticBurst/PrismaticBurst.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"ogl","version":"^1.0.11"}],"devDependencies":[],"categories":["Backgrounds"]} |