338 lines
11 KiBLFS
TypeScript
338 lines
11 KiBLFS
TypeScript
"use client";
|
|
import { Renderer, Program, Mesh, Triangle } from 'ogl';
|
|
import { useEffect, useRef, CSSProperties } from 'react';
|
|
|
|
interface GradientBlindsProps {
|
|
gradientColors?: string[];
|
|
angle?: number;
|
|
noise?: number;
|
|
blindCount?: number;
|
|
blindMinWidth?: number;
|
|
mouseDampening?: number;
|
|
mirrorGradient?: boolean;
|
|
spotlightRadius?: number;
|
|
spotlightSoftness?: number;
|
|
spotlightOpacity?: number;
|
|
distortAmount?: number;
|
|
shineDirection?: 'left' | 'right';
|
|
mixBlendMode?: CSSProperties['mixBlendMode'];
|
|
paused?: boolean;
|
|
dpr?: number;
|
|
className?: string;
|
|
}
|
|
|
|
export default function GradientBlinds({
|
|
gradientColors = ['#FF9FFC', '#5227FF'],
|
|
angle = 0,
|
|
noise = 0.3,
|
|
blindCount = 16,
|
|
blindMinWidth = 60,
|
|
mouseDampening = 0.15,
|
|
mirrorGradient = false,
|
|
spotlightRadius = 0.5,
|
|
spotlightSoftness = 1,
|
|
spotlightOpacity = 1,
|
|
distortAmount = 0,
|
|
shineDirection = 'left',
|
|
mixBlendMode = 'lighten',
|
|
paused = false,
|
|
dpr,
|
|
className = '',
|
|
}: GradientBlindsProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const programRef = useRef<Program>(null);
|
|
const rendererRef = useRef<Renderer>(null);
|
|
const animationIdRef = useRef<number>(0);
|
|
|
|
// Helper to convert hex colors
|
|
const hexToRgb = (hex: string) => {
|
|
const bigint = parseInt(hex.replace('#', ''), 16);
|
|
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255].map(
|
|
(c) => c / 255
|
|
);
|
|
};
|
|
|
|
const propsRef = useRef({ paused, mouseDampening });
|
|
useEffect(() => {
|
|
propsRef.current = { paused, mouseDampening };
|
|
}, [paused, mouseDampening]);
|
|
|
|
// 1) Initialize Renderer & Program (Run once)
|
|
useEffect(() => {
|
|
if (!containerRef.current) return;
|
|
|
|
const container = containerRef.current;
|
|
const renderer = new Renderer({
|
|
alpha: true,
|
|
dpr: dpr || window.devicePixelRatio,
|
|
});
|
|
rendererRef.current = renderer;
|
|
const gl = renderer.gl;
|
|
container.appendChild(gl.canvas);
|
|
|
|
const geometry = new Triangle(gl);
|
|
|
|
// Initial uniforms
|
|
const uColors = new Float32Array(8 * 3);
|
|
// (We'll update these in the other effect, but need initial data for compilation)
|
|
|
|
const program = new Program(gl, {
|
|
vertex: `
|
|
attribute vec2 uv;
|
|
attribute vec2 position;
|
|
varying vec2 vUv;
|
|
void main() {
|
|
vUv = uv;
|
|
gl_Position = vec4(position, 0, 1);
|
|
}
|
|
`,
|
|
fragment: `
|
|
precision highp float;
|
|
varying vec2 vUv;
|
|
|
|
uniform float uTime;
|
|
uniform vec2 uResolution;
|
|
uniform vec2 uMouse;
|
|
uniform vec3 uColors[8];
|
|
uniform int uColorCount;
|
|
uniform float uAngle;
|
|
uniform float uNoise;
|
|
uniform float uBlindCount;
|
|
uniform float uSpotlightRadius;
|
|
uniform float uSpotlightSoftness;
|
|
uniform float uSpotlightOpacity;
|
|
uniform float uDistortAmount;
|
|
uniform int uMirrorGradient;
|
|
uniform int uShineDirection;
|
|
|
|
// Random function
|
|
float random(vec2 st) {
|
|
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
|
|
}
|
|
|
|
// Rotate 2D vector
|
|
vec2 rotate(vec2 v, float a) {
|
|
float s = sin(a);
|
|
float c = cos(a);
|
|
mat2 m = mat2(c, -s, s, c);
|
|
return m * v;
|
|
}
|
|
|
|
void main() {
|
|
vec2 st = vUv;
|
|
vec2 aspect = vec2(uResolution.x / uResolution.y, 1.0);
|
|
vec2 uvCorrected = (st - 0.5) * aspect + 0.5;
|
|
|
|
// Rotation
|
|
float rad = radians(uAngle);
|
|
vec2 rotatedUv = rotate(st - 0.5, rad) + 0.5;
|
|
|
|
// Blinds pattern
|
|
float blindWidth = 1.0 / uBlindCount;
|
|
float blindIndex = floor(rotatedUv.x * uBlindCount);
|
|
float blindProgress = fract(rotatedUv.x * uBlindCount);
|
|
|
|
// Distortion
|
|
float distort = sin(rotatedUv.y * 10.0 + uTime * 0.5) * uDistortAmount * 0.05;
|
|
blindProgress += distort;
|
|
|
|
// Gradient Logic
|
|
float gradientPos = (blindIndex + 0.5) / uBlindCount;
|
|
if (uMirrorGradient == 1) {
|
|
gradientPos = abs(gradientPos * 2.0 - 1.0);
|
|
}
|
|
|
|
// Color interpolation
|
|
vec3 finalColor = vec3(0.0);
|
|
float stepSize = 1.0 / float(uColorCount - 1);
|
|
|
|
for (int i = 0; i < 8; i++) {
|
|
if (i >= uColorCount - 1) break;
|
|
float stop1 = float(i) * stepSize;
|
|
float stop2 = float(i + 1) * stepSize;
|
|
|
|
if (gradientPos >= stop1 && gradientPos <= stop2) {
|
|
float t = (gradientPos - stop1) / (stop2 - stop1);
|
|
finalColor = mix(uColors[i], uColors[i+1], t);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Shine / Lighting on blinds
|
|
float shine = 0.0;
|
|
if (uShineDirection == 0) { // Left
|
|
shine = smoothstep(0.0, 1.0, 1.0 - blindProgress);
|
|
} else { // Right
|
|
shine = smoothstep(0.0, 1.0, blindProgress);
|
|
}
|
|
finalColor += shine * 0.1;
|
|
|
|
// Mouse Spotlight
|
|
vec2 mousePos = uMouse;
|
|
mousePos.y = 1.0 - mousePos.y; // Flip Y
|
|
|
|
// Adjust mouse pos for aspect ratio
|
|
vec2 distVec = (st - mousePos);
|
|
distVec.x *= (uResolution.x / uResolution.y);
|
|
|
|
float dist = length(distVec);
|
|
float spot = smoothstep(uSpotlightRadius, uSpotlightRadius * (1.0 - uSpotlightSoftness * 0.5), dist);
|
|
|
|
finalColor = mix(finalColor, finalColor + vec3(0.2), spot * uSpotlightOpacity);
|
|
|
|
// Noise
|
|
float n = random(st * uTime);
|
|
finalColor += (n - 0.5) * uNoise;
|
|
|
|
gl_FragColor = vec4(finalColor, 1.0);
|
|
}
|
|
`,
|
|
uniforms: {
|
|
uTime: { value: 0 },
|
|
uResolution: { value: new Float32Array([gl.canvas.width, gl.canvas.height]) },
|
|
uMouse: { value: new Float32Array([0.5, 0.5]) },
|
|
uColors: { value: uColors },
|
|
uColorCount: { value: 2 },
|
|
uAngle: { value: 0 },
|
|
uNoise: { value: 0.3 },
|
|
uBlindCount: { value: 16 },
|
|
uSpotlightRadius: { value: 0.5 },
|
|
uSpotlightSoftness: { value: 1 },
|
|
uSpotlightOpacity: { value: 1 },
|
|
uDistortAmount: { value: 0 },
|
|
uMirrorGradient: { value: 0 },
|
|
uShineDirection: { value: 0 },
|
|
},
|
|
});
|
|
programRef.current = program;
|
|
|
|
const mesh = new Mesh(gl, { geometry, program });
|
|
|
|
let mouseTarget = { x: 0.5, y: 0.5 };
|
|
const mouseCurrent = { x: 0.5, y: 0.5 };
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
const rect = container.getBoundingClientRect();
|
|
const x = (e.clientX - rect.left) / rect.width;
|
|
const y = (e.clientY - rect.top) / rect.height;
|
|
mouseTarget = { x, y };
|
|
};
|
|
|
|
window.addEventListener('mousemove', handleMouseMove);
|
|
|
|
const resize = () => {
|
|
renderer.setSize(container.offsetWidth, container.offsetHeight);
|
|
program.uniforms.uResolution.value = new Float32Array([
|
|
gl.canvas.width,
|
|
gl.canvas.height,
|
|
]);
|
|
};
|
|
window.addEventListener('resize', resize);
|
|
resize();
|
|
|
|
const update = (t: number) => {
|
|
const { paused, mouseDampening } = propsRef.current;
|
|
if (paused) return;
|
|
|
|
const time = t * 0.001;
|
|
program.uniforms.uTime.value = time;
|
|
|
|
// Mouse smoothing
|
|
mouseCurrent.x += (mouseTarget.x - mouseCurrent.x) * (1 / (mouseDampening * 60 + 1));
|
|
mouseCurrent.y += (mouseTarget.y - mouseCurrent.y) * (1 / (mouseDampening * 60 + 1));
|
|
|
|
program.uniforms.uMouse.value = new Float32Array([mouseCurrent.x, mouseCurrent.y]);
|
|
|
|
renderer.render({ scene: mesh });
|
|
animationIdRef.current = requestAnimationFrame(update);
|
|
};
|
|
animationIdRef.current = requestAnimationFrame(update);
|
|
|
|
return () => {
|
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
window.removeEventListener('resize', resize);
|
|
cancelAnimationFrame(animationIdRef.current);
|
|
if (container && gl.canvas && container.contains(gl.canvas)) {
|
|
container.removeChild(gl.canvas);
|
|
}
|
|
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
|
};
|
|
}, [dpr]); // Only run once (or if dpr changes)
|
|
|
|
// 2) Update uniforms when props change
|
|
useEffect(() => {
|
|
const program = programRef.current;
|
|
if (!program) return;
|
|
|
|
// Update Colors
|
|
const colors = gradientColors.map(hexToRgb);
|
|
if (colors.length < 2) colors.push(colors[0]);
|
|
|
|
const uColors = new Float32Array(8 * 3);
|
|
colors.forEach((c, i) => {
|
|
if (i < 8) {
|
|
uColors[i * 3] = c[0];
|
|
uColors[i * 3 + 1] = c[1];
|
|
uColors[i * 3 + 2] = c[2];
|
|
}
|
|
});
|
|
|
|
program.uniforms.uColors.value = uColors;
|
|
program.uniforms.uColorCount.value = colors.length;
|
|
program.uniforms.uAngle.value = angle;
|
|
program.uniforms.uNoise.value = noise;
|
|
program.uniforms.uBlindCount.value = blindCount;
|
|
program.uniforms.uSpotlightRadius.value = spotlightRadius;
|
|
program.uniforms.uSpotlightSoftness.value = spotlightSoftness;
|
|
program.uniforms.uSpotlightOpacity.value = spotlightOpacity;
|
|
program.uniforms.uDistortAmount.value = distortAmount;
|
|
program.uniforms.uMirrorGradient.value = mirrorGradient ? 1 : 0;
|
|
program.uniforms.uShineDirection.value = shineDirection === 'left' ? 0 : 1;
|
|
|
|
// Note: mouseDampening and paused are handled in the loop, which reads closed-over props if we are not careful.
|
|
// Actually, the loop closes over the initial render's variables.
|
|
// We should probably use refs for mouseDampening if we want it to be dynamic,
|
|
// but for now, we'll assume it doesn't change often or we can accept it being stale.
|
|
// However, `paused` IS used in the loop.
|
|
// To support `paused` changing, we should check a ref in the loop.
|
|
|
|
}, [
|
|
gradientColors,
|
|
angle,
|
|
noise,
|
|
blindCount,
|
|
blindMinWidth,
|
|
mirrorGradient,
|
|
spotlightRadius,
|
|
spotlightSoftness,
|
|
spotlightOpacity,
|
|
distortAmount,
|
|
shineDirection,
|
|
// mouseDampening - see note below
|
|
// paused - see note below
|
|
]);
|
|
|
|
// We need to handle `paused` and `mouseDampening` dynamically in the loop.
|
|
// The loop is defined in the first effect and closes over the initial props.
|
|
// We can use refs to bridge this.
|
|
// const propsRef = useRef({ paused, mouseDampening }); <--- Duplicate removed
|
|
// useEffect(() => {
|
|
// propsRef.current = { paused, mouseDampening };
|
|
// }, [paused, mouseDampening]);
|
|
|
|
// We need to modify the loop in the first effect to read from propsRef.
|
|
// Since I can't easily edit the first effect without rewriting it,
|
|
// I'll assume the user won't change mouseDampening often.
|
|
// But `paused` might be important.
|
|
// Let's rely on the user not pausing it for now, or use a separate ref approach if needed.
|
|
// Actually, to make the loop read the latest `paused`, we can use a ref for it.
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className={`${className} absolute inset-0 w-full h-full`}
|
|
style={{ mixBlendMode }}
|
|
/>
|
|
);
|
|
}
|