{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "LightPillar-TS-TW", "title": "LightPillar", "description": "Vertical pillar of light with glow effects.", "type": "registry:component", "files": [ { "type": "registry:component", "path": "LightPillar/LightPillar.tsx", "content": "import React, { useRef, useEffect, useState } from 'react';\nimport * as THREE from 'three';\n\ninterface LightPillarProps {\n topColor?: string;\n bottomColor?: string;\n intensity?: number;\n rotationSpeed?: number;\n interactive?: boolean;\n className?: string;\n glowAmount?: number;\n pillarWidth?: number;\n pillarHeight?: number;\n noiseIntensity?: number;\n mixBlendMode?: React.CSSProperties['mixBlendMode'];\n pillarRotation?: number;\n quality?: 'low' | 'medium' | 'high';\n}\n\nconst LightPillar: React.FC = ({\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 // Check WebGL support\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 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 // Scene setup\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 let renderer: THREE.WebGLRenderer;\n try {\n renderer = new THREE.WebGLRenderer({\n antialias: false,\n alpha: true,\n powerPreference: effectiveQuality === 'low' ? 'low-power' : 'high-performance',\n precision: settings.precision,\n stencil: false,\n depth: false\n });\n } catch (error) {\n console.error('Failed to create WebGL renderer:', 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 // Convert hex colors to RGB\n const parseColor = (hex: string): THREE.Vector3 => {\n const color = new THREE.Color(hex);\n return new THREE.Vector3(color.r, color.g, color.b);\n };\n\n // Shader material\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 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 uPillarRotation;\n uniform float uRotCos;\n uniform float uRotSin;\n uniform float uPillarRotCos;\n uniform float uPillarRotSin;\n uniform float uWaveSin[4];\n uniform float uWaveCos[4];\n varying vec2 vUv;\n\n const float PI = 3.141592653589793;\n const float EPSILON = 0.001;\n const float E = 2.71828182845904523536;\n\n float noise(vec2 coord) {\n vec2 r = (E * sin(E * coord));\n return fract(r.x * r.y * (1.0 + coord.x));\n }\n\n void main() {\n vec2 fragCoord = vUv * uResolution;\n vec2 uv = (fragCoord * 2.0 - uResolution) / uResolution.y;\n \n // Apply 2D rotation to UV coordinates using pre-computed values\n uv = vec2(\n uv.x * uPillarRotCos - uv.y * uPillarRotSin,\n uv.x * uPillarRotSin + uv.y * uPillarRotCos\n );\n\n vec3 origin = vec3(0.0, 0.0, -10.0);\n vec3 direction = normalize(vec3(uv, 1.0));\n\n float maxDepth = 50.0;\n float depth = 0.1;\n\n // Use pre-computed rotation values (or mouse-based)\n float rotCos = uRotCos;\n float rotSin = uRotSin;\n if(uInteractive && length(uMouse) > 0.0) {\n float mouseAngle = uMouse.x * PI * 2.0;\n rotCos = cos(mouseAngle);\n rotSin = sin(mouseAngle);\n }\n\n vec3 color = vec3(0.0);\n \n const int ITERATIONS = ${settings.iterations};\n const int WAVE_ITERATIONS = ${settings.waveIterations};\n const float STEP_MULT = ${settings.stepMultiplier.toFixed(1)};\n \n for(int i = 0; i < ITERATIONS; i++) {\n vec3 pos = origin + direction * depth;\n \n // Inline rotation: pos.xz *= rotMat\n float newX = pos.x * rotCos - pos.z * rotSin;\n float newZ = pos.x * rotSin + pos.z * rotCos;\n pos.x = newX;\n pos.z = newZ;\n\n // Apply vertical scaling and wave deformation\n vec3 deformed = pos;\n deformed.y *= uPillarHeight;\n deformed = deformed + vec3(0.0, uTime, 0.0);\n \n // Inlined wave deformation\n float frequency = 1.0;\n float amplitude = 1.0;\n for(int j = 0; j < WAVE_ITERATIONS; j++) {\n // Inline rotation: deformed.xz *= rot(0.4) using pre-computed\n float wx = deformed.x * uWaveCos[j] - deformed.z * uWaveSin[j];\n float wz = deformed.x * uWaveSin[j] + deformed.z * uWaveCos[j];\n deformed.x = wx;\n deformed.z = wz;\n \n float phase = uTime * float(j) * 2.0;\n vec3 oscillation = cos(deformed.zxy * frequency - phase);\n deformed += oscillation * amplitude;\n frequency *= 2.0;\n amplitude *= 0.5;\n }\n \n // Calculate distance field using cosine pattern\n vec2 cosinePair = cos(deformed.xz);\n float fieldDistance = length(cosinePair) - 0.2;\n \n // Radial boundary constraint (inlined blendMax)\n float radialBound = length(pos.xz) - uPillarWidth;\n float k = 4.0;\n float h = max(k - abs(-radialBound - (-fieldDistance)), 0.0);\n fieldDistance = -(min(-radialBound, -fieldDistance) - h * h * 0.25 / k);\n \n fieldDistance = abs(fieldDistance) * 0.15 + 0.01;\n\n vec3 gradient = mix(uBottomColor, uTopColor, smoothstep(15.0, -15.0, pos.y));\n color += gradient / fieldDistance;\n\n if(fieldDistance < EPSILON || depth > maxDepth) break;\n depth += fieldDistance * STEP_MULT;\n }\n\n // Normalize by pillar width to maintain consistent glow regardless of size\n float widthNormalization = uPillarWidth / 3.0;\n color = tanh(color * uGlowAmount / widthNormalization);\n \n // Add noise postprocessing\n float rnd = noise(gl_FragCoord.xy);\n color -= rnd / 15.0 * uNoiseIntensity;\n \n gl_FragColor = vec4(color * uIntensity, 1.0);\n }\n `;\n\n // Pre-compute wave rotation values\n const waveAngle = 0.4;\n const waveSinValues = new Float32Array(4);\n const waveCosValues = new Float32Array(4);\n for (let i = 0; i < 4; i++) {\n waveSinValues[i] = Math.sin(waveAngle);\n waveCosValues[i] = Math.cos(waveAngle);\n }\n\n // Pre-compute pillar rotation\n const pillarRotRad = (pillarRotation * Math.PI) / 180.0;\n const pillarRotCos = Math.cos(pillarRotRad);\n const pillarRotSin = Math.sin(pillarRotRad);\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 uPillarRotation: { value: pillarRotation },\n uRotCos: { value: 1.0 },\n uRotSin: { value: 0.0 },\n uPillarRotCos: { value: pillarRotCos },\n uPillarRotSin: { value: pillarRotSin },\n uWaveSin: { value: waveSinValues },\n uWaveCos: { value: waveCosValues }\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 // Mouse interaction - throttled for performance\n let mouseMoveTimeout: number | null = null;\n const handleMouseMove = (event: MouseEvent) => {\n if (!interactive) return;\n\n if (mouseMoveTimeout) return;\n\n mouseMoveTimeout = window.setTimeout(() => {\n mouseMoveTimeout = null;\n }, 16); // ~60fps throttle\n\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 // Animation loop with fixed timestep\n let lastTime = performance.now();\n const targetFPS = effectiveQuality === 'low' ? 30 : 60;\n const frameTime = 1000 / targetFPS;\n\n const animate = (currentTime: number) => {\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 materialRef.current.uniforms.uTime.value = timeRef.current;\n\n // Pre-compute rotation on CPU\n const rotAngle = timeRef.current * 0.3;\n materialRef.current.uniforms.uRotCos.value = Math.cos(rotAngle);\n materialRef.current.uniforms.uRotSin.value = Math.sin(rotAngle);\n\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 // Handle resize with debouncing\n let resizeTimeout: number | null = 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 // Cleanup\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) {\n materialRef.current.dispose();\n }\n if (geometryRef.current) {\n geometryRef.current.dispose();\n }\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: string) => {\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: string) => {\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 \n WebGL not supported\n \n );\n }\n\n return (\n
\n );\n};\n\nexport default LightPillar;\n" } ], "registryDependencies": [], "dependencies": [ "three@^0.167.1" ] }