Загрузка...
Анимированный WebGL фон с лучами света и реакцией на мышь. Минималистичный UI-паттерн для сайтов.
# Light Rays Background
You are given a task to integrate an existing React component in the codebase
~~~/README.md
# LightRays
A high-end, high-performance WebGL light ray background effect. It creates cinematic, atmospheric lighting that responds to mouse movement and animates organically.
## Features
- **WebGL Accelerated**: Smooth 60fps performance using OGL.
- **Interactive**: Rays can tilt and follow mouse movement.
- **Customizable**: Control origin, color, spread, speed, distortion, and more.
- **Lightweight**: Zero external heavy dependencies (uses OGL).
## Dependencies
- `ogl`: ^1.0.11
- `framer-motion`: latest
- `lucide-react`: latest
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `raysOrigin` | `RaysOrigin` | `'top-center'` | The starting point of the rays |
| `raysColor` | `string` | `'#ffffff'` | The hex color of the light rays |
| `raysSpeed` | `number` | `1` | Animation speed multiplier |
| `lightSpread` | `number` | `1` | Width of the light beam |
| `rayLength` | `number` | `2` | Length of rays relative to container |
| `pulsating` | `boolean` | `false` | Enable breathing intensity effect |
| `followMouse` | `boolean` | `true` | Whether rays tilt towards mouse |
| `mouseInfluence` | `number` | `0.1` | Intensity of mouse tracking |
| `noiseAmount` | `number` | `0.0` | Amount of film grain/noise |
| `distortion` | `number` | `0.0` | Wave distortion amount |
## Usage
```tsx
import { LightRays } from '@/sd-components/f0a75623-66d9-49e2-a9a9-aacf9ca7fe87';
function MyPage() {
return (
<div className="relative w-full h-[600px] bg-slate-950 overflow-hidden">
<LightRays
raysOrigin="top-center"
raysColor="#00ffff"
raysSpeed={1.5}
lightSpread={0.8}
rayLength={1.2}
followMouse={true}
/>
<div className="relative z-10">
<h1>Cinematic Lighting</h1>
</div>
</div>
);
}
```
~~~
~~~/src/App.tsx
import { LightRays } from './Component';
import { motion } from 'framer-motion';
import { ArrowLeft } from 'lucide-react';
/**
* Demo application for LightRays component.
* Showcases a high-end atmospheric background with a minimalist interface.
*/
export default function App() {
return (
<main className="relative w-full h-screen bg-[#1A1A1B] flex items-center justify-center overflow-hidden">
{/* The background effect */}
<LightRays
raysOrigin="top-center"
raysColor="#00ffff"
raysSpeed={1.5}
lightSpread={1.2}
rayLength={1.8}
followMouse={true}
mouseInfluence={0.3}
noiseAmount={0.03}
distortion={0.08}
/>
{/* Minimalist Overlay */}
<div className="relative z-10 flex flex-col items-center text-center px-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
className="space-y-6"
>
<h1 className="text-5xl md:text-7xl font-medium tracking-tight text-white/90">
LightRays
</h1>
<div className="h-px w-24 bg-gradient-to-r from-transparent via-[#00ffff]/50 to-transparent mx-auto" />
<p className="text-white/40 text-lg md:text-xl font-light max-w-md mx-auto">
High-end WebGL atmospheric lighting. Interactive, lightweight, and cinematic.
</p>
</motion.div>
</div>
{/* Required Reply Button as per instructions */}
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
onClick={() => window.history.back()}
className="fixed bottom-12 right-12 z-20 flex items-center gap-2 px-6 py-3 rounded-full bg-white/5 border border-white/10 text-white/70 hover:bg-white/10 hover:text-white transition-all backdrop-blur-md group"
>
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
<span className="text-sm font-medium">Reply</span>
</motion.button>
{/* Subtle floating shadow container as per style guide */}
<div className="fixed inset-24 pointer-events-none rounded-[40px] shadow-[0_0_80px_rgba(0,0,0,0.2)] border border-white/5" />
</main>
);
}
~~~
~~~/package.json
{
"name": "light-rays-showcase",
"description": "Atmospheric WebGL light rays background effect",
"dependencies": {
"ogl": "^1.0.11",
"lucide-react": "latest",
"framer-motion": "latest",
"clsx": "latest",
"tailwind-merge": "latest"
}
}
~~~
~~~/src/Component.tsx
import { useRef, useEffect, useState } from 'react';
import { Renderer, Program, Triangle, Mesh } from 'ogl';
/**
* LightRays Background Component
*
* A high-performance WebGL-based light ray effect that simulates atmospheric lighting.
* Supports various origins, mouse following, and organic movement.
*/
export type RaysOrigin =
| 'top-center'
| 'top-left'
| 'top-right'
| 'right'
| 'left'
| 'bottom-center'
| 'bottom-right'
| 'bottom-left';
interface LightRaysProps {
/** The starting point of the light rays */
raysOrigin?: RaysOrigin;
/** Primary color of the rays (hex) */
raysColor?: string;
/** Animation speed multiplier */
raysSpeed?: number;
/** Spread/width of the light beam */
lightSpread?: number;
/** Length of the rays relative to container size */
rayLength?: number;
/** Enable pulsing intensity */
pulsating?: boolean;
/** Distance before rays fade out */
fadeDistance?: number;
/** Color saturation (0 to 1) */
saturation?: number;
/** Whether rays should tilt towards the mouse */
followMouse?: boolean;
/** Intensity of mouse influence (0 to 1) */
mouseInfluence?: number;
/** Amount of grain/noise texture */
noiseAmount?: number;
/** Amount of wave distortion */
distortion?: number;
/** Additional CSS classes */
className?: string;
}
const DEFAULT_COLOR = '#ffffff';
const hexToRgb = (hex: string): [number, number, number] => {
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1];
};
const getAnchorAndDir = (
origin: RaysOrigin,
w: number,
h: number
): { anchor: [number, number]; dir: [number, number] } => {
const outside = 0.2;
switch (origin) {
case 'top-left':
return { anchor: [0, -outside * h], dir: [0.7, 0.7] };
case 'top-right':
return { anchor: [w, -outside * h], dir: [-0.7, 0.7] };
case 'left':
return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] };
case 'right':
return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] };
case 'bottom-left':
return { anchor: [0, (1 + outside) * h], dir: [0.7, -0.7] };
case 'bottom-center':
return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] };
case 'bottom-right':
return { anchor: [w, (1 + outside) * h], dir: [-0.7, -0.7] };
default: // "top-center"
return { anchor: [0.5 * w, -outside * h], dir: [0, 1] };
}
};
type Vec2 = [number, number];
type Vec3 = [number, number, number];
interface Uniforms {
iTime: { value: number };
iResolution: { value: Vec2 };
rayPos: { value: Vec2 };
rayDir: { value: Vec2 };
raysColor: { value: Vec3 };
raysSpeed: { value: number };
lightSpread: { value: number };
rayLength: { value: number };
pulsating: { value: number };
fadeDistance: { value: number };
saturation: { value: number };
mousePos: { value: Vec2 };
mouseInfluence: { value: number };
noiseAmount: { value: number };
distortion: { value: number };
}
export function LightRays({
raysOrigin = 'top-center',
raysColor = DEFAULT_COLOR,
raysSpeed = 1,
lightSpread = 1,
rayLength = 2,
pulsating = false,
fadeDistance = 1.0,
saturation = 1.0,
followMouse = true,
mouseInfluence = 0.1,
noiseAmount = 0.02,
distortion = 0.05,
className = ''
}: LightRaysProps) {
const containerRef = useRef<HTMLDivElement>(null);
const uniformsRef = useRef<Uniforms | null>(null);
const rendererRef = useRef<Renderer | null>(null);
const mouseRef = useRef({ x: 0.5, y: 0.5 });
const smoothMouseRef = useRef({ x: 0.5, y: 0.5 });
const animationIdRef = useRef<number | null>(null);
const meshRef = useRef<Mesh | null>(null);
const cleanupFunctionRef = useRef<(() => void) | null>(null);
const [isVisible, setIsVisible] = useState(false);
const observerRef = useRef<IntersectionObserver | null>(null);
useEffect(() => {
if (!containerRef.current) return;
observerRef.current = new IntersectionObserver(
entries => {
const entry = entries[0];
setIsVisible(entry.isIntersecting);
},
{ threshold: 0.1 }
);
observerRef.current.observe(containerRef.current);
return () => {
if (observerRef.current) {
observerRef.current.disconnect();
observerRef.current = null;
}
};
}, []);
useEffect(() => {
if (!isVisible || !containerRef.current) return;
if (cleanupFunctionRef.current) {
cleanupFunctionRef.current();
cleanupFunctionRef.current = null;
}
const initializeWebGL = async () => {
if (!containerRef.current) return;
await new Promise(resolve => setTimeout(resolve, 10));
if (!containerRef.current) return;
const renderer = new Renderer({
dpr: Math.min(window.devicePixelRatio, 2),
alpha: true
});
rendererRef.current = renderer;
const gl = renderer.gl;
gl.canvas.style.width = '100%';
gl.canvas.style.height = '100%';
gl.canvas.style.display = 'block';
while (containerRef.current.firstChild) {
containerRef.current.removeChild(containerRef.current.firstChild);
}
containerRef.current.appendChild(gl.canvas);
const vert = `
attribute vec2 position;
varying vec2 vUv;
void main() {
vUv = position * 0.5 + 0.5;
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const frag = `
precision highp float;
uniform float iTime;
uniform vec2 iResolution;
uniform vec2 rayPos;
uniform vec2 rayDir;
uniform vec3 raysColor;
uniform float raysSpeed;
uniform float lightSpread;
uniform float rayLength;
uniform float pulsating;
uniform float fadeDistance;
uniform float saturation;
uniform vec2 mousePos;
uniform float mouseInfluence;
uniform float noiseAmount;
uniform float distortion;
varying vec2 vUv;
float noise(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}
float rayStrength(vec2 raySource, vec2 rayRefDirection, vec2 coord,
float seedA, float seedB, float speed) {
vec2 sourceToCoord = coord - raySource;
vec2 dirNorm = normalize(sourceToCoord);
float cosAngle = dot(dirNorm, rayRefDirection);
float d = distortion * sin(iTime * 1.5 + length(sourceToCoord) * 0.005);
float distortedAngle = cosAngle + d;
float spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(lightSpread, 0.001));
float distance = length(sourceToCoord);
float maxDistance = max(iResolution.x, iResolution.y) * rayLength;
float lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0);
float fadeFactor = fadeDistance * max(iResolution.x, iResolution.y);
float fadeFalloff = clamp((fadeFactor - distance) / fadeFactor, 0.0, 1.0);
float pulse = pulsating > 0.5 ? (0.85 + 0.15 * sin(iTime * speed * 4.0)) : 1.0;
float baseStrength = clamp(
(0.5 + 0.2 * sin(distortedAngle * seedA + iTime * speed)) +
(0.3 + 0.2 * cos(-distortedAngle * seedB + iTime * speed * 0.8)),
0.0, 1.0
);
return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse;
}
void main() {
vec2 fragCoord = gl_FragCoord.xy;
vec2 coord = vec2(fragCoord.x, fragCoord.y);
vec2 finalRayDir = normalize(rayDir);
if (mouseInfluence > 0.0) {
vec2 mouseScreenPos = mousePos * iResolution.xy;
vec2 mouseDirection = normalize(mouseScreenPos - rayPos);
finalRayDir = normalize(mix(finalRayDir, mouseDirection, mouseInfluence));
}
float r1 = rayStrength(rayPos, finalRayDir, coord, 45.2, 31.4, 0.8 * raysSpeed);
float r2 = rayStrength(rayPos, finalRayDir, coord, 28.5, 19.8, 1.2 * raysSpeed);
float r3 = rayStrength(rayPos, finalRayDir, coord, 12.1, 56.2, 0.5 * raysSpeed);
float combined = (r1 * 0.4 + r2 * 0.4 + r3 * 0.2);
combined = pow(combined, 0.7); // Boost mid-tones for visibility
combined *= 1.5; // Overall intensity boost
vec3 finalColor = raysColor * combined;
if (noiseAmount > 0.0) {
float n = noise(coord * 0.01 + iTime * 0.05);
finalColor *= (1.0 - noiseAmount + noiseAmount * n);
}
if (saturation != 1.0) {
float gray = dot(finalColor, vec3(0.299, 0.587, 0.114));
finalColor = mix(vec3(gray), finalColor, saturation);
}
gl_FragColor = vec4(finalColor, combined);
}
`;
const uniforms: Uniforms = {
iTime: { value: 0 },
iResolution: { value: [1, 1] },
rayPos: { value: [0, 0] },
rayDir: { value: [0, 1] },
raysColor: { value: hexToRgb(raysColor) },
raysSpeed: { value: raysSpeed },
lightSpread: { value: lightSpread },
rayLength: { value: rayLength },
pulsating: { value: pulsating ? 1.0 : 0.0 },
fadeDistance: { value: fadeDistance },
saturation: { value: saturation },
mousePos: { value: [0.5, 0.5] },
mouseInfluence: { value: mouseInfluence },
noiseAmount: { value: noiseAmount },
distortion: { value: distortion }
};
uniformsRef.current = uniforms;
const geometry = new Triangle(gl);
const program = new Program(gl, {
vertex: vert,
fragment: frag,
uniforms,
transparent: true
});
const mesh = new Mesh(gl, { geometry, program });
meshRef.current = mesh;
const updatePlacement = () => {
if (!containerRef.current || !renderer) return;
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current;
renderer.setSize(wCSS, hCSS);
const dpr = renderer.dpr;
const w = wCSS * dpr;
const h = hCSS * dpr;
uniforms.iResolution.value = [w, h];
const { anchor, dir } = getAnchorAndDir(raysOrigin, w, h);
uniforms.rayPos.value = anchor;
uniforms.rayDir.value = dir;
};
const loop = (t: number) => {
if (!rendererRef.current || !uniformsRef.current || !meshRef.current) return;
uniforms.iTime.value = t * 0.001;
if (followMouse && mouseInfluence > 0.0) {
const smoothing = 0.95;
smoothMouseRef.current.x = smoothMouseRef.current.x * smoothing + mouseRef.current.x * (1 - smoothing);
smoothMouseRef.current.y = smoothMouseRef.current.y * smoothing + mouseRef.current.y * (1 - smoothing);
uniforms.mousePos.value = [smoothMouseRef.current.x, 1.0 - smoothMouseRef.current.y];
}
renderer.render({ scene: mesh });
animationIdRef.current = requestAnimationFrame(loop);
};
window.addEventListener('resize', updatePlacement);
updatePlacement();
animationIdRef.current = requestAnimationFrame(loop);
cleanupFunctionRef.current = () => {
if (animationIdRef.current) cancelAnimationFrame(animationIdRef.current);
window.removeEventListener('resize', updatePlacement);
if (renderer.gl.canvas.parentNode) {
renderer.gl.canvas.parentNode.removeChild(renderer.gl.canvas);
}
};
};
initializeWebGL();
return () => cleanupFunctionRef.current?.();
}, [isVisible, raysOrigin, raysColor, raysSpeed, lightSpread, rayLength, pulsating, fadeDistance, saturation, followMouse, mouseInfluence, noiseAmount, distortion]);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
mouseRef.current = {
x: (e.clientX - rect.left) / rect.width,
y: (e.clientY - rect.top) / rect.height
};
};
if (followMouse) {
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}
}, [followMouse]);
return (
<div
ref={containerRef}
className={`absolute inset-0 w-full h-full pointer-events-none overflow-hidden ${className}`}
/>
);
}
export default LightRays;
~~~
Implementation Guidelines
1. Analyze the component structure, styling, animation implementations
2. Review the component's arguments and state
3. Think through what is the best place to adopt this component/style into the design we are doing
4. Then adopt the component/design to our current system
Help me integrate this into my design