--- title: Celestial Rays description: A high-performance WebGL volumetric light beam background shader component built with Three.js. Features OkLab perceptual color interpolation, perspective tilt projection, volumetric ray casting, natural turbulence drift, and interactive cursor flaring. --- ## Installation CLI Manual ```bash npx lightswind@latest add celestial-rays ``` Copy and paste the following code into your project. ```tsx "use client"; import React, { useEffect, useMemo, useRef, useState, useSyncExternalStore, useCallback, } from "react"; import * as THREE from "three"; import { Canvas, useFrame, useThree } from "@react-three/fiber"; import { useTheme } from "next-themes"; import { cn } from "@/lib/utils"; export type CelestialPresetName = | "celestial" | "moonlight" | "solar" | "spotlight" | "aurora"; export interface CelestialRaysConfig { palette: string[]; background: string; sourceX: number; sourceY: number; beamAngle: number; perspectiveTilt: number; fanSpread: number; beamCount: number; beamThickness: number; reachDistance: number; oscillation: number; shimmer: number; ambientHaze: number; luminance: number; speed: number; cursorFlare: number; cursorRadius: number; } export const CELESTIAL_RAYS_PRESETS: Record = { celestial: { palette: ["#5227FF", "#B84DFF", "#FF9FFC", "#FFE6FA"], background: "#06040d", sourceX: 0.5, sourceY: -0.7, beamAngle: 0, perspectiveTilt: 20, fanSpread: 34, beamCount: 10, beamThickness: 1.0, reachDistance: 1.1, oscillation: 1.0, shimmer: 0.6, ambientHaze: 1.0, luminance: 1.0, speed: 1.0, cursorFlare: 1.0, cursorRadius: 5.0, }, moonlight: { palette: ["#1B3A8C", "#4F7BFF", "#A9C6FF", "#F2F7FF"], background: "#03050d", sourceX: 0.5, sourceY: 1.7, beamAngle: 180, perspectiveTilt: 15, fanSpread: 30, beamCount: 10, beamThickness: 1.0, reachDistance: 1.1, oscillation: 0.8, shimmer: 0.5, ambientHaze: 1.0, luminance: 1.0, speed: 0.8, cursorFlare: 1.0, cursorRadius: 5.0, }, solar: { palette: ["#7A1238", "#E0445E", "#FF9A5C", "#FFE7C2"], background: "#0d0406", sourceX: 0.5, sourceY: -0.5, beamAngle: 0, perspectiveTilt: 25, fanSpread: 44, beamCount: 11, beamThickness: 1.0, reachDistance: 1.2, oscillation: 0.8, shimmer: 0.5, ambientHaze: 1.1, luminance: 1.0, speed: 0.8, cursorFlare: 1.0, cursorRadius: 5.0, }, spotlight: { palette: ["#3A3A55", "#9A9AC0", "#E2E2F5", "#FFFFFF"], background: "#050507", sourceX: 0.12, sourceY: 1.6, beamAngle: 150, perspectiveTilt: 10, fanSpread: 16, beamCount: 6, beamThickness: 1.0, reachDistance: 1.4, oscillation: 0.6, shimmer: 0.4, ambientHaze: 0.9, luminance: 1.0, speed: 0.7, cursorFlare: 1.0, cursorRadius: 4.0, }, aurora: { palette: ["#0B5D63", "#12B5A6", "#6FF2CF", "#E8FFF7"], background: "#02080a", sourceX: 0.5, sourceY: -0.7, beamAngle: 0, perspectiveTilt: 30, fanSpread: 34, beamCount: 10, beamThickness: 1.1, reachDistance: 1.1, oscillation: 1.2, shimmer: 0.7, ambientHaze: 1.0, luminance: 1.0, speed: 1.0, cursorFlare: 1.0, cursorRadius: 5.0, }, }; export interface CelestialRaysProps { /** Preset variant configuration */ preset?: CelestialPresetName; /** Explicit theme override: 'light' (white background + ink rays), 'dark' (black + glow), or 'auto' (sync with document theme) */ theme?: "light" | "dark" | "auto"; /** Explicit dark mode indicator boolean override */ isDark?: boolean; /** Blending model: 'auto', 'radiant' (glow on dark surfaces), or 'atmospheric' (ink on light surfaces) */ themeMode?: "auto" | "radiant" | "atmospheric" | "add" | "ink"; /** Palette of up to 4 HEX color stops from core to edge */ palette?: string[]; /** Custom background color, or 'transparent' (defaults to #ffffff in light mode, preset dark color in dark mode) */ bgColor?: string; /** Horizontal origin anchor of rays (0 to 1) */ sourceX?: number; /** Vertical origin anchor of rays (-1.5 to 2.5) */ sourceY?: number; /** Angle direction of beam travel in degrees (0 = up, 180 = down) */ beamAngle?: number; /** Perspective lean tilt in degrees (-60 to 60) */ perspectiveTilt?: number; /** Total fan spread arc in degrees (4 to 80) */ fanSpread?: number; /** Number of simulated volumetric light rays (1 to 16) */ beamCount?: number; /** Multiplier for ray thickness width */ beamThickness?: number; /** Distance light penetrates into scene before fading out */ reachDistance?: number; /** Natural swaying drift multiplier */ oscillation?: number; /** Temporal shimmer intensity (0 to 1) */ shimmer?: number; /** Volumetric ambient fog scattering density */ ambientHaze?: number; /** Overall beam luminance multiplier */ luminance?: number; /** Animation speed multiplier */ speed?: number; /** Whether rays flare dynamically when hovered by pointer */ interactive?: boolean; /** Flare boost multiplier on hover */ cursorFlare?: number; /** Angular radius of hover reaction in degrees */ cursorRadius?: number; /** Analog film grain noise strength (0 to 1) */ filmGrain?: number; /** Procedural pseudo-random seed variation */ randomSeed?: number; /** Freezes drifting rays motion */ isPaused?: boolean; /** Device pixel ratio ceiling cap */ maxDpr?: number; className?: string; style?: React.CSSProperties; children?: React.ReactNode; } // --- GLSL Shaders --- const CELESTIAL_VERTEX_SHADER = /* glsl */ ` varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position.xy * 2.0, 0.0, 1.0); } `; const CELESTIAL_FRAGMENT_SHADER = /* glsl */ ` precision highp float; #define MAX_PALETTE_COLORS 4 #define MAX_VOLUMETRIC_RAYS 16 varying vec2 vUv; uniform vec2 uResolution; uniform float uTime; uniform vec3 uLabColors[MAX_PALETTE_COLORS]; uniform int uColorCount; uniform vec3 uBackgroundColor; uniform float uIsTransparent; uniform float uIsAtmospheric; uniform vec2 uSourceOrigin; uniform vec2 uBeamAxis; uniform vec2 uHingeAnchor; uniform vec3 uTiltParams; uniform float uNearOffset; uniform float uSpreadAngle; uniform int uRayCount; uniform vec4 uRayData[MAX_VOLUMETRIC_RAYS]; uniform float uReachLength; uniform float uAmbientHaze; uniform float uLuminance; uniform float uFilmGrain; uniform float uSeedValue; float hash21(vec2 p) { vec3 p3 = fract(vec3(p.xyx) * 0.1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); } vec2 hash22(vec2 p) { vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973)); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.xx + p3.yz) * p3.zy) * 2.0 - 1.0; } float gradientNoise2D(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); float a = dot(hash22(i), f); float b = dot(hash22(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)); float c = dot(hash22(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)); float d = dot(hash22(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)); return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); } vec3 oklabToLinearRgb(vec3 c) { float l = c.x + 0.3963377774 * c.y + 0.2158037573 * c.z; float m = c.x - 0.1055613458 * c.y - 0.0638541728 * c.z; float s = c.x - 0.0894841775 * c.y - 1.2914855480 * c.z; l = l * l * l; m = m * m * m; s = s * s * s; return vec3( 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s ); } vec3 samplePaletteGradient(float t) { t = clamp(t, 0.0, 1.0) * mix(1.0, 0.62, uIsAtmospheric); vec3 lab = uLabColors[0]; if (uColorCount > 1) { float scaled = t * float(uColorCount - 1); for (int i = 1; i < MAX_PALETTE_COLORS; i++) { if (i >= uColorCount) break; lab = mix(lab, uLabColors[i], smoothstep(0.0, 1.0, clamp(scaled - float(i - 1), 0.0, 1.0))); } } return max(oklabToLinearRgb(lab), 0.0); } void main() { float aspect = uResolution.x / max(uResolution.y, 1.0); float t = uTime; vec2 uvCoord = (vUv - 0.5) * vec2(aspect, 1.0); vec2 lateralAxis = vec2(uBeamAxis.y, -uBeamAxis.x); float visibility = 1.0; float perspectiveScale = 1.0; if (abs(uTiltParams.x) > 0.0001) { vec2 rel = uvCoord - uHingeAnchor; float x = dot(rel, lateralAxis); float y = dot(rel, uBeamAxis); float xc = dot(-uHingeAnchor, lateralAxis); float yc = dot(-uHingeAnchor, uBeamAxis); float denom = (y - yc) * uTiltParams.x + uTiltParams.z * uTiltParams.y; float k = (uTiltParams.z * uTiltParams.y - yc * uTiltParams.x) / max(denom, 0.02); visibility = smoothstep(0.0, 0.12, denom); uvCoord = uHingeAnchor + (xc + k * (x - xc)) * lateralAxis + ((yc + k * (y - yc)) / uTiltParams.y) * uBeamAxis; perspectiveScale = clamp(1.0 / max(k, 0.05), 0.3, 3.0); } vec2 delta = uvCoord - uSourceOrigin; float dist = length(delta); float theta = atan(dot(delta, lateralAxis), dot(delta, uBeamAxis)); float distPast = max(dist - uNearOffset, 0.0); float beamBlur = 0.02 / max(dist, 0.001); float lightRaySum = 0.0; for (int i = 0; i < MAX_VOLUMETRIC_RAYS; i++) { if (i >= uRayCount) break; vec4 ray = uRayData[i]; float w = sqrt(ray.y * ray.y + beamBlur * beamBlur); float angularOffset = theta - ray.x; lightRaySum += exp(-angularOffset * angularOffset / (w * w)) * (ray.y / w) * ray.z * exp(-distPast / ray.w); } float hazeAmount = clamp(uAmbientHaze, 0.0, 2.0); float turbulence = 1.0 + 0.22 * gradientNoise2D(vec2(theta * 18.0 + uSeedValue * 7.0, t * 0.1)); float ambientDrift = gradientNoise2D(vec2(theta * 5.0 - uSeedValue * 3.0, distPast * 2.6 - t * 0.32)); float envelope = 1.0 + 0.2 * min(hazeAmount, 1.5) * ambientDrift; float fanArc = max(uSpreadAngle * 0.8, 0.02); float fanProfile = exp(-theta * theta / (fanArc * fanArc)); float fillFog = fanProfile * exp(-distPast / max(uReachLength * 1.35, 0.02)); float ambientGlow = exp(-distPast / max(uReachLength * 3.0, 0.02)) * mix(0.1, 1.0, fanProfile); float volumetricEnergy = lightRaySum * turbulence * envelope * 2.0; volumetricEnergy += fillFog * envelope * 0.4 * hazeAmount; volumetricEnergy += ambientGlow * 0.02 * hazeAmount; volumetricEnergy *= uLuminance * visibility * sqrt(perspectiveScale); float toneCurve = 1.0 - exp(-volumetricEnergy * 0.3); vec3 hdrSpectrum = samplePaletteGradient(toneCurve) * volumetricEnergy * 1.3; vec3 finalColor; float finalAlpha = 1.0; if (uIsAtmospheric < 0.5) { // Radiant Glow Mode (Additive Emission for Dark Backgrounds) vec3 glowEmission = 1.0 - exp(-hdrSpectrum); if (uIsTransparent > 0.5) { finalColor = glowEmission; finalAlpha = clamp(max(glowEmission.r, max(glowEmission.g, glowEmission.b)), 0.0, 1.0); } else { finalColor = 1.0 - (1.0 - uBackgroundColor) * (1.0 - glowEmission); } } else { // Atmospheric Ink Absorption Mode (Subtle Shading for Light Backgrounds) float intensityPeak = max(hdrSpectrum.r, max(hdrSpectrum.g, hdrSpectrum.b)); float coverage = smoothstep(0.02, 0.95, 1.0 - exp(-intensityPeak * 1.4)); vec3 chromaticHue = intensityPeak > 0.0001 ? hdrSpectrum / intensityPeak : vec3(1.0); float luma = dot(chromaticHue, vec3(0.299, 0.587, 0.114)); vec3 pigmentTone = clamp(mix(vec3(luma), chromaticHue, 1.12), 0.0, 1.0) * mix(1.0, 0.8, coverage); if (uIsTransparent > 0.5) { finalColor = pigmentTone * coverage; finalAlpha = coverage; } else { finalColor = uBackgroundColor * mix(vec3(1.0), pigmentTone, coverage); } } gl_FragColor = linearToOutputTexel(vec4(finalColor, finalAlpha)); // High precision dithering to eliminate color banding float ditherVal = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715)))); gl_FragColor.rgb += (ditherVal - 0.5) / 255.0; if (uFilmGrain > 0.0) { float noiseGrain = hash21(gl_FragCoord.xy + fract(uTime * 7.13) * 91.7) - 0.5; gl_FragColor.rgb += noiseGrain * uFilmGrain * 0.05 * finalAlpha; } } `; // --- Mathematical Helper Functions --- const clampValue = (val: number, min: number, max: number) => Math.min(max, Math.max(min, val)); const degToRadians = (deg: number) => (deg * Math.PI) / 180; const smoothStepCurve = (min: number, max: number, val: number) => { const x = clampValue((val - min) / (max - min), 0, 1); return x * x * (3 - 2 * x); }; const hashRandom1D = (val: number) => { const t = 43758.5453 * Math.sin(127.1 * val + 311.7); return t - Math.floor(t); }; const smoothNoise1D = (val: number) => { const t = Math.floor(val); const a = val - t; const r = (2 * hashRandom1D(t) - 1) * a; return ( (r + ((2 * hashRandom1D(t + 1) - 1) * (a - 1) - r) * (a * a * a * (a * (6 * a - 15) + 10))) * 2 ); }; const checkIsTransparent = (colorStr: string) => { const str = colorStr.trim().toLowerCase(); return ( str === "transparent" || (/^#[0-9a-f]{8}$/.test(str) ? str.endsWith("00") : /^#[0-9a-f]{4}$/.test(str) ? str.endsWith("0") : Boolean(str.match(/^(?:rgba?|hsla?)\(.*[,/]\s*(0|0?\.0+|0%)\s*\)$/))) ); }; const rgbToOklabSpace = (col: THREE.Color, target: THREE.Vector3) => { const l = Math.cbrt(0.4122214708 * col.r + 0.5363325363 * col.g + 0.0514459929 * col.b); const m = Math.cbrt(0.2119034982 * col.r + 0.6806995451 * col.g + 0.1073969566 * col.b); const s = Math.cbrt(0.0883024619 * col.r + 0.2817188376 * col.g + 0.6299787005 * col.b); return target.set( 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s ); }; const parseColorSafe = (target: THREE.Color, hex: string, fallback: string) => { const trimmed = hex.trim(); try { target.set(/^#[0-9a-f]{8}$/i.test(trimmed) ? trimmed.slice(0, 7) : trimmed); } catch { target.set(fallback); } return target; }; // --- WebGL Mesh Scene --- interface CelestialMeshProps { config: CelestialRaysProps & { reducedMotion: boolean; computedAtmospheric: boolean; computedBgColor: string; }; pointerRef: React.MutableRefObject<{ x: number; y: number; inside: boolean }>; wakeRef: React.MutableRefObject<(() => void) | null>; } const CelestialMesh: React.FC = ({ config, pointerRef, wakeRef, }) => { const { size, invalidate } = useThree(); const materialRef = useRef(null); const animTimeline = useRef({ time: 12.5, glowRays: new Float32Array(16), }); const uniforms = useMemo( () => ({ uResolution: { value: new THREE.Vector2(1, 1) }, uTime: { value: 0 }, uLabColors: { value: Array.from({ length: 4 }, () => new THREE.Vector3()) }, uColorCount: { value: 4 }, uBackgroundColor: { value: new THREE.Color() }, uIsTransparent: { value: 0 }, uIsAtmospheric: { value: 0 }, uSourceOrigin: { value: new THREE.Vector2(0, -1.2) }, uBeamAxis: { value: new THREE.Vector2(0, 1) }, uHingeAnchor: { value: new THREE.Vector2(0, -0.5) }, uTiltParams: { value: new THREE.Vector3(0, 1, 1.2) }, uNearOffset: { value: 0.7 }, uSpreadAngle: { value: degToRadians(36) }, uRayCount: { value: 9 }, uRayData: { value: Array.from({ length: 16 }, () => new THREE.Vector4()) }, uReachLength: { value: 0.3 }, uAmbientHaze: { value: 1 }, uLuminance: { value: 1 }, uFilmGrain: { value: 0 }, uSeedValue: { value: 0 }, }), [] ); const colorsKey = (config.palette || CELESTIAL_RAYS_PRESETS.celestial.palette).join("|"); const bgColor = config.computedBgColor; const isAtmospheric = config.computedAtmospheric; useEffect(() => { wakeRef.current = invalidate; return () => { wakeRef.current = null; }; }, [invalidate, wakeRef]); useEffect(() => { const mat = materialRef.current; if (!mat) return; const u = mat.uniforms; const colorsList = colorsKey ? colorsKey.split("|") : CELESTIAL_RAYS_PRESETS.celestial.palette; const count = Math.min(colorsList.length, 4); const scratchColor = new THREE.Color(); for (let i = 0; i < 4; i++) { parseColorSafe( scratchColor, colorsList[Math.min(i, count - 1)], CELESTIAL_RAYS_PRESETS.celestial.palette[Math.min(i, 3)] ); rgbToOklabSpace(scratchColor, u.uLabColors.value[i]); } u.uColorCount.value = count; const isTrans = checkIsTransparent(bgColor); parseColorSafe(u.uBackgroundColor.value, isTrans ? "#000000" : bgColor, "#000000"); u.uIsTransparent.value = isTrans ? 1 : 0; u.uIsAtmospheric.value = isAtmospheric ? 1 : 0; invalidate(); }, [colorsKey, bgColor, isAtmospheric, invalidate]); useEffect(() => { invalidate(); }, [config, size, invalidate]); useFrame((state, delta) => { const mat = materialRef.current; if (!mat) return; const u = mat.uniforms; const anim = animTimeline.current; const ptr = pointerRef.current; const dt = Math.min(Math.max(delta, 0), 0.05); const speed = config.speed ?? 1.0; const isRunning = !config.isPaused && !config.reducedMotion && speed > 0; if (isRunning) { anim.time += dt * speed; } const curTime = anim.time; const aspect = size.width / Math.max(size.height, 1); const angleRad = degToRadians(config.beamAngle ?? 0); const tiltRad = degToRadians(clampValue(config.perspectiveTilt ?? 20, -60, 60)); const origX = ((config.sourceX ?? 0.5) - 0.5) * aspect; const origY = (config.sourceY ?? -0.7) - 0.5; const axisX = Math.sin(angleRad); const axisY = Math.cos(angleRad); // Volumetric intersection computation const [hingeX, hingeY] = ((ox, oy, dx, dy, asp) => { const halfBox = [asp / 2, 0.5]; const pos = [ox, oy]; const dir = [dx, dy]; let tMin = 0; let tMax = Infinity; for (let i = 0; i < 2; i++) { if (Math.abs(dir[i]) < 1e-6) { if (pos[i] < -halfBox[i] || pos[i] > halfBox[i]) return [ox, oy]; continue; } const t1 = (-halfBox[i] - pos[i]) / dir[i]; const t2 = (halfBox[i] - pos[i]) / dir[i]; tMin = Math.max(tMin, Math.min(t1, t2)); tMax = Math.min(tMax, Math.max(t1, t2)); } return tMin > tMax ? [ox, oy] : [ox + dx * tMin, oy + dy * tMin]; })(origX, origY, axisX, axisY, aspect); const geom = { aspect, originX: origX, originY: origY, axisX, axisY, hingeX, hingeY, sin: Math.sin(tiltRad), cos: Math.cos(tiltRad), }; const isCursorInside = (config.interactive ?? true) && !config.reducedMotion && ptr.inside; let cursorTheta = 0; if (isCursorInside) { const [px, py] = ((cx, cy, g) => { if (Math.abs(g.sin) < 1e-4) return [cx, cy]; const perpX = -g.axisX; const dx = cx - g.hingeX; const dy = cy - g.hingeY; const dParallel = dx * g.axisX + dy * g.axisY; const hPerp = -g.hingeX * g.axisY - g.hingeY * perpX; const hParallel = -g.hingeX * g.axisX - g.hingeY * g.axisY; const denom = Math.max((dParallel - hParallel) * g.sin + 1.2 * g.cos, 0.02); const factor = (1.2 * g.cos - hParallel * g.sin) / denom; const vx = hPerp + factor * (dx * g.axisY + dy * perpX - hPerp); const vy = (hParallel + factor * (dParallel - hParallel)) / g.cos; return [g.hingeX + vx * g.axisY + vy * g.axisX, g.hingeY + vx * perpX + vy * g.axisY]; })((ptr.x - 0.5) * aspect, ptr.y - 0.5, geom); const relX = px - origX; const relY = py - origY; cursorTheta = Math.atan2( relX * axisY - relY * axisX, relX * axisX + relY * axisY ); } const spreadRad = degToRadians(clampValue(config.fanSpread ?? 34, 2, 85)); const count = Math.round(clampValue(config.beamCount ?? 10, 1, 16)); const osc = clampValue(config.oscillation ?? 1, 0, 3); const shimmer = clampValue(config.shimmer ?? 0.6, 0, 1); const reach = clampValue(config.reachDistance ?? 1.1, 0.05, 4) / 3; const thickness = clampValue(config.beamThickness ?? 1, 0.2, 4); const cursorRad = degToRadians(clampValue(config.cursorRadius ?? 5, 1, 60)); const flare = clampValue(config.cursorFlare ?? 1, 0, 3); const seed = config.randomSeed ?? 0; let hasGlowDelta = false; for (let i = 0; i < count; i++) { const pIndex = 7.31 * i + 13 * seed + 1.7; const r1 = hashRandom1D(pIndex); const r2 = hashRandom1D(pIndex + 0.37); const r3 = hashRandom1D(pIndex + 0.71); const r4 = hashRandom1D(pIndex + 1.13); const baseAngle = spreadRad * (count > 1 ? ((i + 0.5 + (r1 - 0.5) * 0.6) / count) * 2 - 1 : 0) * 0.92 + smoothNoise1D(curTime * (0.07 + 0.05 * r2) + 61 * r3) * osc * spreadRad * 0.1; const beamW = (0.024 + 0.07 * Math.pow(r4, 1.3)) * thickness * (0.85 + 0.15 * smoothNoise1D(0.05 * curTime + 23 * r1)); const intensityMod = (0.45 + 0.75 * Math.pow(r3, 1.2)) * (1 - shimmer + shimmer * (0.15 + 0.85 * smoothStepCurve( -0.7, 0.8, smoothNoise1D(curTime * (0.18 + 0.12 * r1) + 97 * r2) ))); const diff = baseAngle - cursorTheta; const targetGlow = isCursorInside ? Math.exp(-(diff * diff) / (cursorRad * cursorRad)) : 0; const currentGlow = anim.glowRays[i]; const lerpSpeed = targetGlow > currentGlow ? 9 : 2.2; anim.glowRays[i] = currentGlow + (targetGlow - currentGlow) * (1 - Math.exp(-dt * lerpSpeed)); if (Math.abs(targetGlow - anim.glowRays[i]) > 0.002) { hasGlowDelta = true; } u.uRayData.value[i].set( baseAngle, beamW, intensityMod * (1 + anim.glowRays[i] * flare * 2.6), reach * (0.7 + 0.6 * r2) ); } u.uResolution.value.set( size.width * state.viewport.dpr, size.height * state.viewport.dpr ); u.uTime.value = curTime; u.uIsAtmospheric.value = config.computedAtmospheric ? 1 : 0; u.uSourceOrigin.value.set(origX, origY); u.uBeamAxis.value.set(axisX, axisY); u.uHingeAnchor.value.set(hingeX, hingeY); u.uTiltParams.value.set(geom.sin, geom.cos, 1.2); u.uNearOffset.value = Math.hypot(hingeX - origX, hingeY - origY); u.uSpreadAngle.value = spreadRad; u.uRayCount.value = count; u.uReachLength.value = reach; u.uAmbientHaze.value = clampValue(config.ambientHaze ?? 1, 0, 2); u.uLuminance.value = clampValue(config.luminance ?? 1, 0, 4); u.uFilmGrain.value = clampValue(config.filmGrain ?? 0, 0, 1); u.uSeedValue.value = seed; if (!isRunning && (hasGlowDelta || isCursorInside)) { invalidate(); } }); return ( ); }; // --- Celestial Rays Root Component --- export const CelestialRays: React.FC = ({ preset, theme = "auto", isDark: propIsDark, themeMode = "auto", palette: propPalette, bgColor: propBgColor, sourceX: propSourceX, sourceY: propSourceY, beamAngle: propBeamAngle, perspectiveTilt: propPerspectiveTilt, fanSpread: propFanSpread, beamCount: propBeamCount, beamThickness: propBeamThickness, reachDistance: propReachDistance, oscillation: propOscillation, shimmer: propShimmer, ambientHaze: propAmbientHaze, luminance: propLuminance, speed: propSpeed, interactive = true, cursorFlare: propCursorFlare, cursorRadius: propCursorRadius, filmGrain = 0, randomSeed = 0, isPaused = false, maxDpr = 1.5, className, style, children, }) => { const containerRef = useRef(null); const pointerRef = useRef({ x: 0.5, y: 0.5, inside: false }); const wakeRef = useRef<(() => void) | null>(null); const [isVisible, setIsVisible] = useState(true); // Safe next-themes access let resolvedTheme: string | undefined; let nextTheme: string | undefined; try { const context = useTheme(); resolvedTheme = context.resolvedTheme; nextTheme = context.theme; } catch { // ignore } // Active theme detector function const checkIsLight = useCallback((): boolean => { if (typeof propIsDark === "boolean") return !propIsDark; if (theme === "light") return true; if (theme === "dark") return false; if (typeof document !== "undefined") { const cl = document.documentElement.classList; if (cl.contains("dark")) return false; if (cl.contains("light")) return true; const dt = document.documentElement.getAttribute("data-theme") || document.documentElement.getAttribute("data-mode"); if (dt === "dark") return false; if (dt === "light") return true; } if (resolvedTheme === "dark" || nextTheme === "dark") return false; if (resolvedTheme === "light" || nextTheme === "light") return true; if (typeof window !== "undefined" && window.localStorage) { try { const saved = localStorage.getItem("theme"); if (saved === "dark") return false; if (saved === "light") return true; } catch { // ignore } } if (typeof document !== "undefined") { return !document.documentElement.classList.contains("dark"); } if (typeof window !== "undefined" && window.matchMedia) { return !window.matchMedia("(prefers-color-scheme: dark)").matches; } return false; }, [theme, propIsDark, resolvedTheme, nextTheme]); const [isLightMode, setIsLightMode] = useState(() => { if (typeof propIsDark === "boolean") return !propIsDark; if (theme === "light") return true; if (theme === "dark") return false; if (typeof document !== "undefined") { if (document.documentElement.classList.contains("dark")) return false; if (document.documentElement.classList.contains("light")) return true; return !document.documentElement.classList.contains("dark"); } return false; }); useEffect(() => { setIsLightMode(checkIsLight()); if (typeof document !== "undefined") { const observer = new MutationObserver(() => { setIsLightMode(checkIsLight()); }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "data-theme", "data-mode"], }); const handleStorage = (e: StorageEvent) => { if (e.key === "theme") { setIsLightMode(checkIsLight()); } }; window.addEventListener("storage", handleStorage); return () => { observer.disconnect(); window.removeEventListener("storage", handleStorage); }; } }, [checkIsLight]); // Determine whether to use atmospheric ink (light mode) or radiant glow (dark mode) const isAtmospheric = useMemo(() => { if (themeMode === "atmospheric" || themeMode === "ink") return true; if (themeMode === "radiant" || themeMode === "add") return false; // When themeMode is 'auto', follow current document theme return isLightMode; }, [themeMode, isLightMode]); // Match preset defaults if passed const presetConfig = preset ? CELESTIAL_RAYS_PRESETS[preset] : CELESTIAL_RAYS_PRESETS.celestial; const activePalette = propPalette ?? presetConfig.palette; // Background color automatically adapts: crisp white in light/atmospheric mode, preset dark in dark mode const activeBgColor = useMemo(() => { if (propBgColor) return propBgColor; return isAtmospheric ? "#ffffff" : presetConfig.background; }, [propBgColor, isAtmospheric, presetConfig.background]); const activeSourceX = propSourceX ?? presetConfig.sourceX; const activeSourceY = propSourceY ?? presetConfig.sourceY; const activeBeamAngle = propBeamAngle ?? presetConfig.beamAngle; const activeTilt = propPerspectiveTilt ?? presetConfig.perspectiveTilt; const activeFanSpread = propFanSpread ?? presetConfig.fanSpread; const activeBeamCount = propBeamCount ?? presetConfig.beamCount; const activeThickness = propBeamThickness ?? presetConfig.beamThickness; const activeReach = propReachDistance ?? presetConfig.reachDistance; const activeOscillation = propOscillation ?? presetConfig.oscillation; const activeShimmer = propShimmer ?? presetConfig.shimmer; const activeAmbientHaze = propAmbientHaze ?? presetConfig.ambientHaze; const activeLuminance = propLuminance ?? presetConfig.luminance; const activeSpeed = propSpeed ?? presetConfig.speed; const activeCursorFlare = propCursorFlare ?? presetConfig.cursorFlare; const activeCursorRadius = propCursorRadius ?? presetConfig.cursorRadius; // Accessibility reduced motion check const reducedMotion = useSyncExternalStore( (callback) => { const mq = window.matchMedia("(prefers-reduced-motion: reduce)"); mq.addEventListener("change", callback); return () => mq.removeEventListener("change", callback); }, () => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches, () => false ); // Adaptive device pixel ratio const screenDpr = useSyncExternalStore( (callback) => { const mq = window.matchMedia("(min-resolution: 2dppx)"); mq.addEventListener("change", callback); window.addEventListener("resize", callback); return () => { mq.removeEventListener("change", callback); window.removeEventListener("resize", callback); }; }, () => (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1), () => 1 ); const effectiveDpr = Math.min(screenDpr, Math.max(maxDpr, 0.5)); const isAnimating = !isPaused && !reducedMotion && activeSpeed > 0; const isTransparentBg = checkIsTransparent(activeBgColor); // Viewport IntersectionObserver useEffect(() => { const el = containerRef.current; if (!el || typeof IntersectionObserver === "undefined") return; const observer = new IntersectionObserver( ([entry]) => setIsVisible(entry.isIntersecting), { rootMargin: "80px" } ); observer.observe(el); return () => observer.disconnect(); }, []); // Pointer event listeners useEffect(() => { const el = containerRef.current; if (!el) return; const onPointerMove = (e: PointerEvent) => { const rect = el.getBoundingClientRect(); pointerRef.current.x = (e.clientX - rect.left) / Math.max(rect.width, 1); pointerRef.current.y = 1 - (e.clientY - rect.top) / Math.max(rect.height, 1); pointerRef.current.inside = true; wakeRef.current?.(); }; const onPointerLeave = (e: PointerEvent) => { if (e.type !== "pointerup" || e.pointerType !== "mouse") { pointerRef.current.inside = false; wakeRef.current?.(); } }; el.addEventListener("pointermove", onPointerMove, { passive: true }); el.addEventListener("pointerdown", onPointerMove, { passive: true }); el.addEventListener("pointerleave", onPointerLeave, { passive: true }); el.addEventListener("pointercancel", onPointerLeave, { passive: true }); el.addEventListener("pointerup", onPointerLeave, { passive: true }); return () => { el.removeEventListener("pointermove", onPointerMove); el.removeEventListener("pointerdown", onPointerMove); el.removeEventListener("pointerleave", onPointerLeave); el.removeEventListener("pointercancel", onPointerLeave); el.removeEventListener("pointerup", onPointerLeave); }; }, []); return (
{children ? (
{children}
) : null}
); }; export default CelestialRays; ```
## Usage ```tsx import { CelestialRays, CELESTIAL_RAYS_PRESETS } from "@/components/lightswind/celestial-rays" ``` ```tsx import { CelestialRays } from "@/components/lightswind/celestial-rays"; export function Demo() { return (

Celestial Rays

Volumetric real-time ray-marching shaders with OkLab color synthesis.

); } ```