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":"LightRays","title":"LightRays","description":"Volumetric light rays/beams with customizable direction.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div ref=\"containerRef\" :class=\"['w-full h-full relative pointer-events-none z-[3] overflow-hidden', className]\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, watch, useTemplateRef, computed, nextTick } from 'vue';\nimport { Renderer, Program, Triangle, Mesh } from 'ogl';\n\nexport type RaysOrigin =\n | 'top-center'\n | 'top-left'\n | 'top-right'\n | 'right'\n | 'left'\n | 'bottom-center'\n | 'bottom-right'\n | 'bottom-left';\n\ninterface LightRaysProps {\n raysOrigin?: RaysOrigin;\n raysColor?: string;\n raysSpeed?: number;\n lightSpread?: number;\n rayLength?: number;\n pulsating?: boolean;\n fadeDistance?: number;\n saturation?: number;\n followMouse?: boolean;\n mouseInfluence?: number;\n noiseAmount?: number;\n distortion?: number;\n className?: string;\n}\n\ninterface MousePosition {\n x: number;\n y: number;\n}\n\ninterface AnchorAndDirection {\n anchor: [number, number];\n dir: [number, number];\n}\n\ninterface WebGLUniforms {\n iTime: { value: number };\n iResolution: { value: [number, number] };\n rayPos: { value: [number, number] };\n rayDir: { value: [number, number] };\n raysColor: { value: [number, number, number] };\n raysSpeed: { value: number };\n lightSpread: { value: number };\n rayLength: { value: number };\n pulsating: { value: number };\n fadeDistance: { value: number };\n saturation: { value: number };\n mousePos: { value: [number, number] };\n mouseInfluence: { value: number };\n noiseAmount: { value: number };\n distortion: { value: number };\n}\n\nconst props = withDefaults(defineProps<LightRaysProps>(), {\n raysOrigin: 'top-center',\n raysColor: '#ffffff',\n raysSpeed: 1,\n lightSpread: 1,\n rayLength: 2,\n pulsating: false,\n fadeDistance: 1.0,\n saturation: 1.0,\n followMouse: true,\n mouseInfluence: 0.1,\n noiseAmount: 0.0,\n distortion: 0.0,\n className: ''\n});\n\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\n\nconst uniformsRef = ref<WebGLUniforms | null>(null);\nconst rendererRef = ref<Renderer | null>(null);\nconst mouseRef = ref<MousePosition>({ x: 0.5, y: 0.5 });\nconst smoothMouseRef = ref<MousePosition>({ x: 0.5, y: 0.5 });\nconst animationIdRef = ref<number | null>(null);\nconst meshRef = ref<Mesh | null>(null);\nconst cleanupFunctionRef = ref<(() => void) | null>(null);\nconst isVisible = ref<boolean>(false);\nconst observerRef = ref<IntersectionObserver | null>(null);\nconst resizeTimeoutRef = ref<number | null>(null);\n\nconst rgbColor = computed<[number, number, number]>(() => hexToRgb(props.raysColor));\nconst pulsatingValue = computed<number>(() => (props.pulsating ? 1.0 : 0.0));\nconst devicePixelRatio = computed<number>(() => Math.min(window.devicePixelRatio || 1, 2));\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n const m = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1];\n};\n\nconst getAnchorAndDir = (origin: RaysOrigin, w: number, h: number): AnchorAndDirection => {\n const outside = 0.2;\n switch (origin) {\n case 'top-left':\n return { anchor: [0, -outside * h], dir: [0, 1] };\n case 'top-right':\n return { anchor: [w, -outside * h], dir: [0, 1] };\n case 'left':\n return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] };\n case 'right':\n return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] };\n case 'bottom-left':\n return { anchor: [0, (1 + outside) * h], dir: [0, -1] };\n case 'bottom-center':\n return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] };\n case 'bottom-right':\n return { anchor: [w, (1 + outside) * h], dir: [0, -1] };\n default:\n return { anchor: [0.5 * w, -outside * h], dir: [0, 1] };\n }\n};\n\nconst debouncedUpdatePlacement = (() => {\n let timeoutId: number | null = null;\n\n return (updateFn: () => void): void => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId);\n }\n timeoutId = window.setTimeout(() => {\n updateFn();\n timeoutId = null;\n }, 16);\n };\n})();\n\nconst vertexShader: string = `\nattribute vec2 position;\nvarying vec2 vUv;\nvoid main() {\n vUv = position * 0.5 + 0.5;\n gl_Position = vec4(position, 0.0, 1.0);\n}`;\n\nconst fragmentShader: string = `precision highp float;\n\nuniform float iTime;\nuniform vec2 iResolution;\n\nuniform vec2 rayPos;\nuniform vec2 rayDir;\nuniform vec3 raysColor;\nuniform float raysSpeed;\nuniform float lightSpread;\nuniform float rayLength;\nuniform float pulsating;\nuniform float fadeDistance;\nuniform float saturation;\nuniform vec2 mousePos;\nuniform float mouseInfluence;\nuniform float noiseAmount;\nuniform float distortion;\n\nvarying vec2 vUv;\n\nfloat noise(vec2 st) {\n return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);\n}\n\nfloat rayStrength(vec2 raySource, vec2 rayRefDirection, vec2 coord,\n float seedA, float seedB, float speed) {\n vec2 sourceToCoord = coord - raySource;\n vec2 dirNorm = normalize(sourceToCoord);\n float cosAngle = dot(dirNorm, rayRefDirection);\n\n float distortedAngle = cosAngle + distortion * sin(iTime * 2.0 + length(sourceToCoord) * 0.01) * 0.2;\n\n float spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(lightSpread, 0.001));\n\n float distance = length(sourceToCoord);\n float maxDistance = iResolution.x * rayLength;\n float lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0);\n\n float fadeFalloff = clamp((iResolution.x * fadeDistance - distance) / (iResolution.x * fadeDistance), 0.5, 1.0);\n float pulse = pulsating > 0.5 ? (0.8 + 0.2 * sin(iTime * speed * 3.0)) : 1.0;\n\n float baseStrength = clamp(\n (0.45 + 0.15 * sin(distortedAngle * seedA + iTime * speed)) +\n (0.3 + 0.2 * cos(-distortedAngle * seedB + iTime * speed)),\n 0.0, 1.0\n );\n\n return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 coord = vec2(fragCoord.x, iResolution.y - fragCoord.y);\n\n vec2 finalRayDir = rayDir;\n if (mouseInfluence > 0.0) {\n vec2 mouseScreenPos = mousePos * iResolution.xy;\n vec2 mouseDirection = normalize(mouseScreenPos - rayPos);\n finalRayDir = normalize(mix(rayDir, mouseDirection, mouseInfluence));\n }\n\n vec4 rays1 = vec4(1.0) *\n rayStrength(rayPos, finalRayDir, coord, 36.2214, 21.11349,\n 1.5 * raysSpeed);\n vec4 rays2 = vec4(1.0) *\n rayStrength(rayPos, finalRayDir, coord, 22.3991, 18.0234,\n 1.1 * raysSpeed);\n\n fragColor = rays1 * 0.5 + rays2 * 0.4;\n\n if (noiseAmount > 0.0) {\n float n = noise(coord * 0.01 + iTime * 0.1);\n fragColor.rgb *= (1.0 - noiseAmount + noiseAmount * n);\n }\n\n float brightness = 1.0 - (coord.y / iResolution.y);\n fragColor.x *= 0.1 + brightness * 0.8;\n fragColor.y *= 0.3 + brightness * 0.6;\n fragColor.z *= 0.5 + brightness * 0.5;\n\n if (saturation != 1.0) {\n float gray = dot(fragColor.rgb, vec3(0.299, 0.587, 0.114));\n fragColor.rgb = mix(vec3(gray), fragColor.rgb, saturation);\n }\n\n fragColor.rgb *= raysColor;\n}\n\nvoid main() {\n vec4 color;\n mainImage(color, gl_FragCoord.xy);\n gl_FragColor = color;\n}`;\n\nconst initializeWebGL = async (): Promise<void> => {\n if (!containerRef.value) return;\n\n await nextTick();\n\n if (!containerRef.value) return;\n\n try {\n const renderer = new Renderer({\n dpr: devicePixelRatio.value,\n alpha: true,\n antialias: false,\n powerPreference: 'high-performance'\n });\n rendererRef.value = renderer;\n\n const gl = renderer.gl;\n gl.canvas.style.width = '100%';\n gl.canvas.style.height = '100%';\n\n while (containerRef.value.firstChild) {\n containerRef.value.removeChild(containerRef.value.firstChild);\n }\n containerRef.value.appendChild(gl.canvas);\n\n const uniforms: WebGLUniforms = {\n iTime: { value: 0 },\n iResolution: { value: [1, 1] },\n rayPos: { value: [0, 0] },\n rayDir: { value: [0, 1] },\n raysColor: { value: rgbColor.value },\n raysSpeed: { value: props.raysSpeed },\n lightSpread: { value: props.lightSpread },\n rayLength: { value: props.rayLength },\n pulsating: { value: pulsatingValue.value },\n fadeDistance: { value: props.fadeDistance },\n saturation: { value: props.saturation },\n mousePos: { value: [0.5, 0.5] },\n mouseInfluence: { value: props.mouseInfluence },\n noiseAmount: { value: props.noiseAmount },\n distortion: { value: props.distortion }\n };\n uniformsRef.value = uniforms;\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex: vertexShader,\n fragment: fragmentShader,\n uniforms\n });\n const mesh = new Mesh(gl, { geometry, program });\n meshRef.value = mesh;\n\n const updatePlacement = (): void => {\n if (!containerRef.value || !renderer) return;\n\n renderer.dpr = devicePixelRatio.value;\n\n const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.value;\n renderer.setSize(wCSS, hCSS);\n\n const dpr = renderer.dpr;\n const w = wCSS * dpr;\n const h = hCSS * dpr;\n\n uniforms.iResolution.value = [w, h];\n\n const { anchor, dir } = getAnchorAndDir(props.raysOrigin, w, h);\n uniforms.rayPos.value = anchor;\n uniforms.rayDir.value = dir;\n };\n\n const loop = (t: number): void => {\n if (!rendererRef.value || !uniformsRef.value || !meshRef.value || !isVisible.value) {\n return;\n }\n\n uniforms.iTime.value = t * 0.001;\n\n if (props.followMouse && props.mouseInfluence > 0.0) {\n const smoothing = 0.92;\n\n smoothMouseRef.value.x = smoothMouseRef.value.x * smoothing + mouseRef.value.x * (1 - smoothing);\n smoothMouseRef.value.y = smoothMouseRef.value.y * smoothing + mouseRef.value.y * (1 - smoothing);\n\n uniforms.mousePos.value = [smoothMouseRef.value.x, smoothMouseRef.value.y];\n }\n\n try {\n renderer.render({ scene: mesh });\n animationIdRef.value = requestAnimationFrame(loop);\n } catch (error) {\n console.warn('WebGL rendering error:', error);\n return;\n }\n };\n\n const handleResize = (): void => {\n debouncedUpdatePlacement(updatePlacement);\n };\n\n window.addEventListener('resize', handleResize, { passive: true });\n updatePlacement();\n animationIdRef.value = requestAnimationFrame(loop);\n\n cleanupFunctionRef.value = (): void => {\n if (animationIdRef.value) {\n cancelAnimationFrame(animationIdRef.value);\n animationIdRef.value = null;\n }\n\n window.removeEventListener('resize', handleResize);\n\n if (resizeTimeoutRef.value) {\n clearTimeout(resizeTimeoutRef.value);\n resizeTimeoutRef.value = null;\n }\n\n if (renderer) {\n try {\n const canvas = renderer.gl.canvas;\n const loseContextExt = renderer.gl.getExtension('WEBGL_lose_context');\n if (loseContextExt) {\n loseContextExt.loseContext();\n }\n\n if (canvas && canvas.parentNode) {\n canvas.parentNode.removeChild(canvas);\n }\n } catch (error) {\n console.warn('Error during WebGL cleanup:', error);\n }\n }\n\n rendererRef.value = null;\n uniformsRef.value = null;\n meshRef.value = null;\n };\n } catch (error) {\n console.error('Failed to initialize WebGL:', error);\n }\n};\n\nlet mouseThrottleId: number | null = null;\nconst handleMouseMove = (e: MouseEvent): void => {\n if (!containerRef.value || !rendererRef.value) return;\n\n if (mouseThrottleId) return;\n\n mouseThrottleId = requestAnimationFrame(() => {\n if (!containerRef.value) return;\n\n const rect = containerRef.value.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = (e.clientY - rect.top) / rect.height;\n mouseRef.value = { x, y };\n mouseThrottleId = null;\n });\n};\n\nonMounted((): void => {\n if (!containerRef.value) return;\n\n observerRef.value = new IntersectionObserver(\n (entries: IntersectionObserverEntry[]): void => {\n const entry = entries[0];\n isVisible.value = entry.isIntersecting;\n },\n {\n threshold: 0.1,\n rootMargin: '50px'\n }\n );\n\n observerRef.value.observe(containerRef.value);\n});\n\nwatch(isVisible, (newVisible: boolean): void => {\n if (newVisible && containerRef.value) {\n if (cleanupFunctionRef.value) {\n cleanupFunctionRef.value();\n cleanupFunctionRef.value = null;\n }\n initializeWebGL();\n } else if (!newVisible && cleanupFunctionRef.value) {\n if (animationIdRef.value) {\n cancelAnimationFrame(animationIdRef.value);\n animationIdRef.value = null;\n }\n }\n});\n\nwatch(\n [\n () => props.raysColor,\n () => props.raysSpeed,\n () => props.lightSpread,\n () => props.raysOrigin,\n () => props.rayLength,\n () => props.pulsating,\n () => props.fadeDistance,\n () => props.saturation,\n () => props.mouseInfluence,\n () => props.noiseAmount,\n () => props.distortion\n ],\n (): void => {\n if (!uniformsRef.value || !containerRef.value || !rendererRef.value) return;\n\n const u = uniformsRef.value;\n const renderer = rendererRef.value;\n\n u.raysColor.value = rgbColor.value;\n u.raysSpeed.value = props.raysSpeed;\n u.lightSpread.value = props.lightSpread;\n u.rayLength.value = props.rayLength;\n u.pulsating.value = pulsatingValue.value;\n u.fadeDistance.value = props.fadeDistance;\n u.saturation.value = props.saturation;\n u.mouseInfluence.value = props.mouseInfluence;\n u.noiseAmount.value = props.noiseAmount;\n u.distortion.value = props.distortion;\n\n const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.value;\n const dpr = renderer.dpr;\n const { anchor, dir } = getAnchorAndDir(props.raysOrigin, wCSS * dpr, hCSS * dpr);\n u.rayPos.value = anchor;\n u.rayDir.value = dir;\n },\n { flush: 'post' }\n);\n\nwatch(\n () => props.followMouse,\n (newFollowMouse: boolean): void => {\n if (newFollowMouse) {\n window.addEventListener('mousemove', handleMouseMove, { passive: true });\n } else {\n window.removeEventListener('mousemove', handleMouseMove);\n if (mouseThrottleId) {\n cancelAnimationFrame(mouseThrottleId);\n mouseThrottleId = null;\n }\n }\n },\n { immediate: true }\n);\n\nonUnmounted((): void => {\n if (observerRef.value) {\n observerRef.value.disconnect();\n observerRef.value = null;\n }\n\n if (cleanupFunctionRef.value) {\n cleanupFunctionRef.value();\n cleanupFunctionRef.value = null;\n }\n\n if (mouseThrottleId) {\n cancelAnimationFrame(mouseThrottleId);\n mouseThrottleId = null;\n }\n\n window.removeEventListener('mousemove', handleMouseMove);\n});\n</script>\n","path":"LightRays/LightRays.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"ogl","version":"^1.0.11"}],"devDependencies":[],"categories":["Backgrounds"]} |