23 lines
14 KiB
JSON
23 lines
14 KiB
JSON
{
|
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
"name": "LightRays-TS-CSS",
|
|
"title": "LightRays",
|
|
"description": "Volumetric light rays/beams with customizable direction.",
|
|
"type": "registry:component",
|
|
"files": [
|
|
{
|
|
"type": "registry:component",
|
|
"path": "LightRays/LightRays.css",
|
|
"content": ".light-rays-container {\n width: 100%;\n height: 100%;\n position: relative;\n pointer-events: none;\n z-index: 3;\n overflow: hidden;\n}\n"
|
|
},
|
|
{
|
|
"type": "registry:component",
|
|
"path": "LightRays/LightRays.tsx",
|
|
"content": "import { useRef, useEffect, useState } from 'react';\nimport { Renderer, Program, Triangle, Mesh } from 'ogl';\nimport './LightRays.css';\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\nconst DEFAULT_COLOR = '#ffffff';\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 = (\n origin: RaysOrigin,\n w: number,\n h: number\n): { anchor: [number, number]; dir: [number, number] } => {\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: // \"top-center\"\n return { anchor: [0.5 * w, -outside * h], dir: [0, 1] };\n }\n};\n\ntype Vec2 = [number, number];\ntype Vec3 = [number, number, number];\n\ninterface Uniforms {\n iTime: { value: number };\n iResolution: { value: Vec2 };\n rayPos: { value: Vec2 };\n rayDir: { value: Vec2 };\n raysColor: { value: Vec3 };\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: Vec2 };\n mouseInfluence: { value: number };\n noiseAmount: { value: number };\n distortion: { value: number };\n}\n\nconst LightRays: React.FC<LightRaysProps> = ({\n raysOrigin = 'top-center',\n raysColor = DEFAULT_COLOR,\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 const containerRef = useRef<HTMLDivElement>(null);\n const uniformsRef = useRef<Uniforms | null>(null);\n const rendererRef = useRef<Renderer | null>(null);\n const mouseRef = useRef({ x: 0.5, y: 0.5 });\n const smoothMouseRef = useRef({ x: 0.5, y: 0.5 });\n const animationIdRef = useRef<number | null>(null);\n const meshRef = useRef<Mesh | null>(null);\n const cleanupFunctionRef = useRef<(() => void) | null>(null);\n const [isVisible, setIsVisible] = useState(false);\n const observerRef = useRef<IntersectionObserver | null>(null);\n\n useEffect(() => {\n if (!containerRef.current) return;\n\n observerRef.current = new IntersectionObserver(\n entries => {\n const entry = entries[0];\n setIsVisible(entry.isIntersecting);\n },\n { threshold: 0.1 }\n );\n\n observerRef.current.observe(containerRef.current);\n\n return () => {\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n };\n }, []);\n\n useEffect(() => {\n if (!isVisible || !containerRef.current) return;\n\n if (cleanupFunctionRef.current) {\n cleanupFunctionRef.current();\n cleanupFunctionRef.current = null;\n }\n\n const initializeWebGL = async () => {\n if (!containerRef.current) return;\n\n await new Promise(resolve => setTimeout(resolve, 10));\n\n if (!containerRef.current) return;\n\n const renderer = new Renderer({\n dpr: Math.min(window.devicePixelRatio, 2),\n alpha: true\n });\n rendererRef.current = renderer;\n\n const gl = renderer.gl;\n gl.canvas.style.width = '100%';\n gl.canvas.style.height = '100%';\n\n while (containerRef.current.firstChild) {\n containerRef.current.removeChild(containerRef.current.firstChild);\n }\n containerRef.current.appendChild(gl.canvas);\n\n const vert = `\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\n const frag = `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\n const uniforms: Uniforms = {\n iTime: { value: 0 },\n iResolution: { value: [1, 1] },\n\n rayPos: { value: [0, 0] },\n rayDir: { value: [0, 1] },\n\n raysColor: { value: hexToRgb(raysColor) },\n raysSpeed: { value: raysSpeed },\n lightSpread: { value: lightSpread },\n rayLength: { value: rayLength },\n pulsating: { value: pulsating ? 1.0 : 0.0 },\n fadeDistance: { value: fadeDistance },\n saturation: { value: saturation },\n mousePos: { value: [0.5, 0.5] },\n mouseInfluence: { value: mouseInfluence },\n noiseAmount: { value: noiseAmount },\n distortion: { value: distortion }\n };\n uniformsRef.current = uniforms;\n\n const geometry = new Triangle(gl);\n const program = new Program(gl, {\n vertex: vert,\n fragment: frag,\n uniforms\n });\n const mesh = new Mesh(gl, { geometry, program });\n meshRef.current = mesh;\n\n const updatePlacement = () => {\n if (!containerRef.current || !renderer) return;\n\n renderer.dpr = Math.min(window.devicePixelRatio, 2);\n\n const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current;\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(raysOrigin, w, h);\n uniforms.rayPos.value = anchor;\n uniforms.rayDir.value = dir;\n };\n\n const loop = (t: number) => {\n if (!rendererRef.current || !uniformsRef.current || !meshRef.current) {\n return;\n }\n\n uniforms.iTime.value = t * 0.001;\n\n if (followMouse && mouseInfluence > 0.0) {\n const smoothing = 0.92;\n\n smoothMouseRef.current.x = smoothMouseRef.current.x * smoothing + mouseRef.current.x * (1 - smoothing);\n smoothMouseRef.current.y = smoothMouseRef.current.y * smoothing + mouseRef.current.y * (1 - smoothing);\n\n uniforms.mousePos.value = [smoothMouseRef.current.x, smoothMouseRef.current.y];\n }\n\n try {\n renderer.render({ scene: mesh });\n animationIdRef.current = requestAnimationFrame(loop);\n } catch (error) {\n console.warn('WebGL rendering error:', error);\n return;\n }\n };\n\n window.addEventListener('resize', updatePlacement);\n updatePlacement();\n animationIdRef.current = requestAnimationFrame(loop);\n\n cleanupFunctionRef.current = () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current);\n animationIdRef.current = null;\n }\n\n window.removeEventListener('resize', updatePlacement);\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.current = null;\n uniformsRef.current = null;\n meshRef.current = null;\n };\n };\n\n initializeWebGL();\n\n return () => {\n if (cleanupFunctionRef.current) {\n cleanupFunctionRef.current();\n cleanupFunctionRef.current = null;\n }\n };\n }, [\n isVisible,\n raysOrigin,\n raysColor,\n raysSpeed,\n lightSpread,\n rayLength,\n pulsating,\n fadeDistance,\n saturation,\n followMouse,\n mouseInfluence,\n noiseAmount,\n distortion\n ]);\n\n useEffect(() => {\n if (!uniformsRef.current || !containerRef.current || !rendererRef.current) return;\n\n const u = uniformsRef.current;\n const renderer = rendererRef.current;\n\n u.raysColor.value = hexToRgb(raysColor);\n u.raysSpeed.value = raysSpeed;\n u.lightSpread.value = lightSpread;\n u.rayLength.value = rayLength;\n u.pulsating.value = pulsating ? 1.0 : 0.0;\n u.fadeDistance.value = fadeDistance;\n u.saturation.value = saturation;\n u.mouseInfluence.value = mouseInfluence;\n u.noiseAmount.value = noiseAmount;\n u.distortion.value = distortion;\n\n const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current;\n const dpr = renderer.dpr;\n const { anchor, dir } = getAnchorAndDir(raysOrigin, wCSS * dpr, hCSS * dpr);\n u.rayPos.value = anchor;\n u.rayDir.value = dir;\n }, [\n raysColor,\n raysSpeed,\n lightSpread,\n raysOrigin,\n rayLength,\n pulsating,\n fadeDistance,\n saturation,\n mouseInfluence,\n noiseAmount,\n distortion\n ]);\n\n useEffect(() => {\n const handleMouseMove = (e: MouseEvent) => {\n if (!containerRef.current || !rendererRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = (e.clientY - rect.top) / rect.height;\n mouseRef.current = { x, y };\n };\n\n if (followMouse) {\n window.addEventListener('mousemove', handleMouseMove);\n return () => window.removeEventListener('mousemove', handleMouseMove);\n }\n }, [followMouse]);\n\n return <div ref={containerRef} className={`light-rays-container ${className}`.trim()} />;\n};\n\nexport default LightRays;\n"
|
|
}
|
|
],
|
|
"registryDependencies": [],
|
|
"dependencies": [
|
|
"ogl@^1.0.11"
|
|
]
|
|
} |