{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "LightPillar-JS-CSS", "title": "LightPillar", "description": "Vertical pillar of light with glow effects.", "type": "registry:component", "files": [ { "type": "registry:component", "path": "LightPillar/LightPillar.css", "content": ".light-pillar-fallback {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: rgba(0, 0, 0, 0.1);\n color: #888;\n font-size: 14px;\n}\n\n.light-pillar-container {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n}\n" }, { "type": "registry:component", "path": "LightPillar/LightPillar.jsx", "content": "import { useRef, useEffect, useState } from 'react';\nimport * as THREE from 'three';\nimport './LightPillar.css';\n\nconst LightPillar = ({\n topColor = '#5227FF',\n bottomColor = '#FF9FFC',\n intensity = 1.0,\n rotationSpeed = 0.3,\n interactive = false,\n className = '',\n glowAmount = 0.005,\n pillarWidth = 3.0,\n pillarHeight = 0.4,\n noiseIntensity = 0.5,\n mixBlendMode = 'screen',\n pillarRotation = 0,\n quality = 'high'\n}) => {\n const containerRef = useRef(null);\n const rafRef = useRef(null);\n const rendererRef = useRef(null);\n const materialRef = useRef(null);\n const sceneRef = useRef(null);\n const cameraRef = useRef(null);\n const geometryRef = useRef(null);\n const mouseRef = useRef(new THREE.Vector2(0, 0));\n const timeRef = useRef(0);\n const rotationSpeedRef = useRef(rotationSpeed);\n const [webGLSupported, setWebGLSupported] = useState(true);\n\n useEffect(() => {\n const canvas = document.createElement('canvas');\n const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');\n if (!gl) {\n setWebGLSupported(false);\n }\n }, []);\n\n useEffect(() => {\n if (!containerRef.current || !webGLSupported) return;\n\n const container = containerRef.current;\n const width = container.clientWidth;\n const height = container.clientHeight;\n\n const scene = new THREE.Scene();\n sceneRef.current = scene;\n const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n cameraRef.current = camera;\n\n const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);\n const isLowEndDevice = isMobile || (navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4);\n\n let effectiveQuality = quality;\n if (isLowEndDevice && quality === 'high') effectiveQuality = 'medium';\n if (isMobile && quality !== 'low') effectiveQuality = 'low';\n\n const qualitySettings = {\n low: { iterations: 24, waveIterations: 1, pixelRatio: 0.5, precision: 'mediump', stepMultiplier: 1.5 },\n medium: { iterations: 40, waveIterations: 2, pixelRatio: 0.65, precision: 'mediump', stepMultiplier: 1.2 },\n high: {\n iterations: 80,\n waveIterations: 4,\n pixelRatio: Math.min(window.devicePixelRatio, 2),\n precision: 'highp',\n stepMultiplier: 1.0\n }\n };\n\n const settings = qualitySettings[effectiveQuality] || qualitySettings.medium;\n\n let renderer;\n try {\n renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: true,\n powerPreference: effectiveQuality === 'high' ? 'high-performance' : 'low-power',\n precision: settings.precision,\n stencil: false,\n depth: false\n });\n } catch (error) {\n setWebGLSupported(false);\n return;\n }\n\n renderer.setSize(width, height);\n renderer.setPixelRatio(settings.pixelRatio);\n container.appendChild(renderer.domElement);\n rendererRef.current = renderer;\n\n const parseColor = hex => {\n const color = new THREE.Color(hex);\n return new THREE.Vector3(color.r, color.g, color.b);\n };\n\n const vertexShader = `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = vec4(position, 1.0);\n }\n `;\n\n const fragmentShader = `\n precision ${settings.precision} float;\n\n uniform float uTime;\n uniform vec2 uResolution;\n uniform vec2 uMouse;\n uniform vec3 uTopColor;\n uniform vec3 uBottomColor;\n uniform float uIntensity;\n uniform bool uInteractive;\n uniform float uGlowAmount;\n uniform float uPillarWidth;\n uniform float uPillarHeight;\n uniform float uNoiseIntensity;\n uniform float uRotCos;\n uniform float uRotSin;\n uniform float uPillarRotCos;\n uniform float uPillarRotSin;\n uniform float uWaveSin;\n uniform float uWaveCos;\n varying vec2 vUv;\n\n const float STEP_MULT = ${settings.stepMultiplier.toFixed(1)};\n const int MAX_ITER = ${settings.iterations};\n const int WAVE_ITER = ${settings.waveIterations};\n\n void main() {\n vec2 uv = (vUv * 2.0 - 1.0) * vec2(uResolution.x / uResolution.y, 1.0);\n uv = vec2(uPillarRotCos * uv.x - uPillarRotSin * uv.y, uPillarRotSin * uv.x + uPillarRotCos * uv.y);\n\n vec3 ro = vec3(0.0, 0.0, -10.0);\n vec3 rd = normalize(vec3(uv, 1.0));\n\n float rotC = uRotCos;\n float rotS = uRotSin;\n if(uInteractive && (uMouse.x != 0.0 || uMouse.y != 0.0)) {\n float a = uMouse.x * 6.283185;\n rotC = cos(a);\n rotS = sin(a);\n }\n\n vec3 col = vec3(0.0);\n float t = 0.1;\n \n for(int i = 0; i < MAX_ITER; i++) {\n vec3 p = ro + rd * t;\n p.xz = vec2(rotC * p.x - rotS * p.z, rotS * p.x + rotC * p.z);\n\n vec3 q = p;\n q.y = p.y * uPillarHeight + uTime;\n \n float freq = 1.0;\n float amp = 1.0;\n for(int j = 0; j < WAVE_ITER; j++) {\n q.xz = vec2(uWaveCos * q.x - uWaveSin * q.z, uWaveSin * q.x + uWaveCos * q.z);\n q += cos(q.zxy * freq - uTime * float(j) * 2.0) * amp;\n freq *= 2.0;\n amp *= 0.5;\n }\n \n float d = length(cos(q.xz)) - 0.2;\n float bound = length(p.xz) - uPillarWidth;\n float k = 4.0;\n float h = max(k - abs(d - bound), 0.0);\n d = max(d, bound) + h * h * 0.0625 / k;\n d = abs(d) * 0.15 + 0.01;\n\n float grad = clamp((15.0 - p.y) / 30.0, 0.0, 1.0);\n col += mix(uBottomColor, uTopColor, grad) / d;\n\n t += d * STEP_MULT;\n if(t > 50.0) break;\n }\n\n float widthNorm = uPillarWidth / 3.0;\n col = tanh(col * uGlowAmount / widthNorm);\n \n col -= fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) / 15.0 * uNoiseIntensity;\n \n gl_FragColor = vec4(col * uIntensity, 1.0);\n }\n `;\n\n const pillarRotRad = (pillarRotation * Math.PI) / 180;\n const waveSin = Math.sin(0.4);\n const waveCos = Math.cos(0.4);\n\n const material = new THREE.ShaderMaterial({\n vertexShader,\n fragmentShader,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: new THREE.Vector2(width, height) },\n uMouse: { value: mouseRef.current },\n uTopColor: { value: parseColor(topColor) },\n uBottomColor: { value: parseColor(bottomColor) },\n uIntensity: { value: intensity },\n uInteractive: { value: interactive },\n uGlowAmount: { value: glowAmount },\n uPillarWidth: { value: pillarWidth },\n uPillarHeight: { value: pillarHeight },\n uNoiseIntensity: { value: noiseIntensity },\n uRotCos: { value: 1.0 },\n uRotSin: { value: 0.0 },\n uPillarRotCos: { value: Math.cos(pillarRotRad) },\n uPillarRotSin: { value: Math.sin(pillarRotRad) },\n uWaveSin: { value: waveSin },\n uWaveCos: { value: waveCos }\n },\n transparent: true,\n depthWrite: false,\n depthTest: false\n });\n materialRef.current = material;\n\n const geometry = new THREE.PlaneGeometry(2, 2);\n geometryRef.current = geometry;\n const mesh = new THREE.Mesh(geometry, material);\n scene.add(mesh);\n\n let mouseMoveTimeout = null;\n const handleMouseMove = event => {\n if (!interactive) return;\n if (mouseMoveTimeout) return;\n mouseMoveTimeout = window.setTimeout(() => {\n mouseMoveTimeout = null;\n }, 16);\n const rect = container.getBoundingClientRect();\n const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;\n const y = -((event.clientY - rect.top) / rect.height) * 2 + 1;\n mouseRef.current.set(x, y);\n };\n\n if (interactive) {\n container.addEventListener('mousemove', handleMouseMove, { passive: true });\n }\n\n let lastTime = performance.now();\n const targetFPS = effectiveQuality === 'low' ? 30 : 60;\n const frameTime = 1000 / targetFPS;\n\n const animate = currentTime => {\n if (!materialRef.current || !rendererRef.current || !sceneRef.current || !cameraRef.current) return;\n\n const deltaTime = currentTime - lastTime;\n\n if (deltaTime >= frameTime) {\n timeRef.current += 0.016 * rotationSpeedRef.current;\n const t = timeRef.current;\n materialRef.current.uniforms.uTime.value = t;\n materialRef.current.uniforms.uRotCos.value = Math.cos(t * 0.3);\n materialRef.current.uniforms.uRotSin.value = Math.sin(t * 0.3);\n rendererRef.current.render(sceneRef.current, cameraRef.current);\n lastTime = currentTime - (deltaTime % frameTime);\n }\n\n rafRef.current = requestAnimationFrame(animate);\n };\n rafRef.current = requestAnimationFrame(animate);\n\n let resizeTimeout = null;\n const handleResize = () => {\n if (resizeTimeout) {\n clearTimeout(resizeTimeout);\n }\n\n resizeTimeout = window.setTimeout(() => {\n if (!rendererRef.current || !materialRef.current || !containerRef.current) return;\n const newWidth = containerRef.current.clientWidth;\n const newHeight = containerRef.current.clientHeight;\n rendererRef.current.setSize(newWidth, newHeight);\n materialRef.current.uniforms.uResolution.value.set(newWidth, newHeight);\n }, 150);\n };\n\n window.addEventListener('resize', handleResize, { passive: true });\n\n return () => {\n window.removeEventListener('resize', handleResize);\n if (interactive) {\n container.removeEventListener('mousemove', handleMouseMove);\n }\n if (rafRef.current) {\n cancelAnimationFrame(rafRef.current);\n }\n if (rendererRef.current) {\n rendererRef.current.dispose();\n rendererRef.current.forceContextLoss();\n if (container.contains(rendererRef.current.domElement)) {\n container.removeChild(rendererRef.current.domElement);\n }\n }\n if (materialRef.current) materialRef.current.dispose();\n if (geometryRef.current) geometryRef.current.dispose();\n\n rendererRef.current = null;\n materialRef.current = null;\n sceneRef.current = null;\n cameraRef.current = null;\n geometryRef.current = null;\n rafRef.current = null;\n };\n }, [webGLSupported, quality]);\n\n useEffect(() => {\n rotationSpeedRef.current = rotationSpeed;\n }, [rotationSpeed]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n const parseColor = hex => {\n const color = new THREE.Color(hex);\n return new THREE.Vector3(color.r, color.g, color.b);\n };\n materialRef.current.uniforms.uTopColor.value = parseColor(topColor);\n }, [topColor]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n const parseColor = hex => {\n const color = new THREE.Color(hex);\n return new THREE.Vector3(color.r, color.g, color.b);\n };\n materialRef.current.uniforms.uBottomColor.value = parseColor(bottomColor);\n }, [bottomColor]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uIntensity.value = intensity;\n }, [intensity]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uInteractive.value = interactive;\n }, [interactive]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uGlowAmount.value = glowAmount;\n }, [glowAmount]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uPillarWidth.value = pillarWidth;\n }, [pillarWidth]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uPillarHeight.value = pillarHeight;\n }, [pillarHeight]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n materialRef.current.uniforms.uNoiseIntensity.value = noiseIntensity;\n }, [noiseIntensity]);\n\n useEffect(() => {\n if (!materialRef.current) return;\n const pillarRotRad = (pillarRotation * Math.PI) / 180;\n materialRef.current.uniforms.uPillarRotCos.value = Math.cos(pillarRotRad);\n materialRef.current.uniforms.uPillarRotSin.value = Math.sin(pillarRotRad);\n }, [pillarRotation]);\n\n if (!webGLSupported) {\n return (\n