import React, { useRef, useEffect, useState } from 'react';
import * as THREE from 'three';
interface LightPillarProps {
topColor?: string;
bottomColor?: string;
intensity?: number;
rotationSpeed?: number;
interactive?: boolean;
className?: string;
glowAmount?: number;
pillarWidth?: number;
pillarHeight?: number;
noiseIntensity?: number;
mixBlendMode?: React.CSSProperties['mixBlendMode'];
pillarRotation?: number;
}
const LightPillar: React.FC<LightPillarProps> = ({
topColor = '#5227FF',
bottomColor = '#FF9FFC',
intensity = 1.0,
rotationSpeed = 0.3,
interactive = false,
className = '',
glowAmount = 0.005,
pillarWidth = 3.0,
pillarHeight = 0.4,
noiseIntensity = 0.5,
mixBlendMode = 'screen',
pillarRotation = 0
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const rafRef = useRef<number | null>(null);
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
const materialRef = useRef<THREE.ShaderMaterial | null>(null);
const sceneRef = useRef<THREE.Scene | null>(null);
const cameraRef = useRef<THREE.OrthographicCamera | null>(null);
const geometryRef = useRef<THREE.PlaneGeometry | null>(null);
const mouseRef = useRef<THREE.Vector2>(new THREE.Vector2(0, 0));
const timeRef = useRef<number>(0);
const [webGLSupported, setWebGLSupported] = useState<boolean>(true);
// Check WebGL support
useEffect(() => {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) {
setWebGLSupported(false);
console.warn('WebGL is not supported in this browser');
}
}, []);
useEffect(() => {
if (!containerRef.current || !webGLSupported) return;
const container = containerRef.current;
const width = container.clientWidth;
const height = container.clientHeight;
// Scene setup
const scene = new THREE.Scene();
sceneRef.current = scene;
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
cameraRef.current = camera;
let renderer: THREE.WebGLRenderer;
try {
renderer = new THREE.WebGLRenderer({
antialias: false,
alpha: true,
powerPreference: 'high-performance',
precision: 'lowp',
stencil: false,
depth: false
});
} catch (error) {
console.error('Failed to create WebGL renderer:', error);
setWebGLSupported(false);
return;
}
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
rendererRef.current = renderer;
// Convert hex colors to RGB
const parseColor = (hex: string): THREE.Vector3 => {
const color = new THREE.Color(hex);
return new THREE.Vector3(color.r, color.g, color.b);
};
// Shader material
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform float uTime;
uniform vec2 uResolution;
uniform vec2 uMouse;
uniform vec3 uTopColor;
uniform vec3 uBottomColor;
uniform float uIntensity;
uniform bool uInteractive;
uniform float uGlowAmount;
uniform float uPillarWidth;
uniform float uPillarHeight;
uniform float uNoiseIntensity;
uniform float uPillarRotation;
varying vec2 vUv;
const float PI = 3.141592653589793;
const float EPSILON = 0.001;
const float E = 2.71828182845904523536;
const float HALF = 0.5;
mat2 rot(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat2(c, -s, s, c);
}
// Procedural noise function
float noise(vec2 coord) {
float G = E;
vec2 r = (G * sin(G * coord));
return fract(r.x * r.y * (1.0 + coord.x));
}
// Apply layered wave deformation to position
vec3 applyWaveDeformation(vec3 pos, float timeOffset) {
float frequency = 1.0;
float amplitude = 1.0;
vec3 deformed = pos;
for(float i = 0.0; i < 4.0; i++) {
deformed.xz *= rot(0.4);
float phase = timeOffset * i * 2.0;
vec3 oscillation = cos(deformed.zxy * frequency - phase);
deformed += oscillation * amplitude;
frequency *= 2.0;
amplitude *= HALF;
}
return deformed;
}
// Polynomial smooth blending between two values
float blendMin(float a, float b, float k) {
float scaledK = k * 4.0;
float h = max(scaledK - abs(a - b), 0.0);
return min(a, b) - h * h * 0.25 / scaledK;
}
float blendMax(float a, float b, float k) {
return -blendMin(-a, -b, k);
}
void main() {
vec2 fragCoord = vUv * uResolution;
vec2 uv = (fragCoord * 2.0 - uResolution) / uResolution.y;
// Apply 2D rotation to UV coordinates
float rotAngle = uPillarRotation * PI / 180.0;
uv *= rot(rotAngle);
vec3 origin = vec3(0.0, 0.0, -10.0);
vec3 direction = normalize(vec3(uv, 1.0));
float maxDepth = 50.0;
float depth = 0.1;
mat2 rotX = rot(uTime * 0.3);
if(uInteractive && length(uMouse) > 0.0) {
rotX = rot(uMouse.x * PI * 2.0);
}
vec3 color = vec3(0.0);
for(float i = 0.0; i < 100.0; i++) {
vec3 pos = origin + direction * depth;
pos.xz *= rotX;
// Apply vertical scaling and wave deformation
vec3 deformed = pos;
deformed.y *= uPillarHeight;
deformed = applyWaveDeformation(deformed + vec3(0.0, uTime, 0.0), uTime);
// Calculate distance field using cosine pattern
vec2 cosinePair = cos(deformed.xz);
float fieldDistance = length(cosinePair) - 0.2;
// Radial boundary constraint
float radialBound = length(pos.xz) - uPillarWidth;
fieldDistance = blendMax(radialBound, fieldDistance, 1.0);
fieldDistance = abs(fieldDistance) * 0.15 + 0.01;
vec3 gradient = mix(uBottomColor, uTopColor, smoothstep(15.0, -15.0, pos.y));
color += gradient * pow(1.0 / fieldDistance, 1.0);
if(fieldDistance < EPSILON || depth > maxDepth) break;
depth += fieldDistance;
}
// Normalize by pillar width to maintain consistent glow regardless of size
float widthNormalization = uPillarWidth / 3.0;
color = tanh(color * uGlowAmount / widthNormalization);
// Add noise postprocessing
float rnd = noise(gl_FragCoord.xy);
color -= rnd / 15.0 * uNoiseIntensity;
gl_FragColor = vec4(color * uIntensity, 1.0);
}
`;
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTime: { value: 0 },
uResolution: { value: new THREE.Vector2(width, height) },
uMouse: { value: mouseRef.current },
uTopColor: { value: parseColor(topColor) },
uBottomColor: { value: parseColor(bottomColor) },
uIntensity: { value: intensity },
uInteractive: { value: interactive },
uGlowAmount: { value: glowAmount },
uPillarWidth: { value: pillarWidth },
uPillarHeight: { value: pillarHeight },
uNoiseIntensity: { value: noiseIntensity },
uPillarRotation: { value: pillarRotation }
},
transparent: true,
depthWrite: false,
depthTest: false
});
materialRef.current = material;
const geometry = new THREE.PlaneGeometry(2, 2);
geometryRef.current = geometry;
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// Mouse interaction - throttled for performance
let mouseMoveTimeout: number | null = null;
const handleMouseMove = (event: MouseEvent) => {
if (!interactive) return;
if (mouseMoveTimeout) return;
mouseMoveTimeout = window.setTimeout(() => {
mouseMoveTimeout = null;
}, 16); // ~60fps throttle
const rect = container.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
const y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
mouseRef.current.set(x, y);
};
if (interactive) {
container.addEventListener('mousemove', handleMouseMove, { passive: true });
}
// Animation loop with fixed timestep
let lastTime = performance.now();
const targetFPS = 60;
const frameTime = 1000 / targetFPS;
const animate = (currentTime: number) => {
if (!materialRef.current || !rendererRef.current || !sceneRef.current || !cameraRef.current) return;
const deltaTime = currentTime - lastTime;
if (deltaTime >= frameTime) {
timeRef.current += 0.016 * rotationSpeed;
materialRef.current.uniforms.uTime.value = timeRef.current;
rendererRef.current.render(sceneRef.current, cameraRef.current);
lastTime = currentTime - (deltaTime % frameTime);
}
rafRef.current = requestAnimationFrame(animate);
};
rafRef.current = requestAnimationFrame(animate);
// Handle resize with debouncing
let resizeTimeout: number | null = null;
const handleResize = () => {
if (resizeTimeout) {
clearTimeout(resizeTimeout);
}
resizeTimeout = window.setTimeout(() => {
if (!rendererRef.current || !materialRef.current || !containerRef.current) return;
const newWidth = containerRef.current.clientWidth;
const newHeight = containerRef.current.clientHeight;
rendererRef.current.setSize(newWidth, newHeight);
materialRef.current.uniforms.uResolution.value.set(newWidth, newHeight);
}, 150);
};
window.addEventListener('resize', handleResize, { passive: true });
// Cleanup
return () => {
window.removeEventListener('resize', handleResize);
if (interactive) {
container.removeEventListener('mousemove', handleMouseMove);
}
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
if (rendererRef.current) {
rendererRef.current.dispose();
rendererRef.current.forceContextLoss();
if (container.contains(rendererRef.current.domElement)) {
container.removeChild(rendererRef.current.domElement);
}
}
if (materialRef.current) {
materialRef.current.dispose();
}
if (geometryRef.current) {
geometryRef.current.dispose();
}
rendererRef.current = null;
materialRef.current = null;
sceneRef.current = null;
cameraRef.current = null;
geometryRef.current = null;
rafRef.current = null;
};
}, [
topColor,
bottomColor,
intensity,
rotationSpeed,
interactive,
glowAmount,
pillarWidth,
pillarHeight,
noiseIntensity,
pillarRotation,
webGLSupported
]);
if (!webGLSupported) {
return (
<div
className={`w-full h-full absolute top-0 left-0 flex items-center justify-center bg-black/10 text-gray-500 text-sm ${className}`}
style={{ mixBlendMode }}
>
WebGL not supported
</div>
);
}
return (
<div ref={containerRef} className={`w-full h-full absolute top-0 left-0 ${className}`} style={{ mixBlendMode }} />
);
};
export default LightPillar;