import React, { useEffect, useRef } from 'react'; import { Renderer, Program, Mesh, Triangle } from 'ogl'; export interface GradientBlindsProps { className?: string; dpr?: number; paused?: boolean; gradientColors?: string[]; gradientColorsSecondary?: 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?: string; } const MAX_COLORS = 8; const hexToRGB = (hex: string): [number, number, number] => { const c = hex.replace('#', '').padEnd(6, '0'); const r = parseInt(c.slice(0, 2), 16) / 255; const g = parseInt(c.slice(2, 4), 16) / 255; const b = parseInt(c.slice(4, 6), 16) / 255; return [r, g, b]; }; const prepStops = (stops?: string[]) => { const base = (stops && stops.length ? stops : ['#FF9FFC', '#5227FF']).slice(0, MAX_COLORS); if (base.length === 1) base.push(base[0]); while (base.length < MAX_COLORS) base.push(base[base.length - 1]); const arr: [number, number, number][] = []; for (let i = 0; i < MAX_COLORS; i++) arr.push(hexToRGB(base[i])); const count = Math.max(2, Math.min(MAX_COLORS, stops?.length ?? 2)); return { arr, count }; }; const GradientBlinds: React.FC = ({ className, dpr, paused = false, gradientColors, gradientColorsSecondary, 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' }) => { const containerRef = useRef(null); const rafRef = useRef(null); const programRef = useRef(null); const meshRef = useRef | null>(null); const geometryRef = useRef(null); const rendererRef = useRef(null); const mouseTargetRef = useRef<[number, number]>([0, 0]); const lastTimeRef = useRef(0); const firstResizeRef = useRef(true); useEffect(() => { const container = containerRef.current; if (!container) return; let renderer: Renderer; try { renderer = new Renderer({ dpr: dpr ?? (typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1), alpha: true, antialias: true }); } catch { return; } rendererRef.current = renderer; const gl = renderer.gl; const canvas = gl.canvas as HTMLCanvasElement; canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.display = 'block'; container.appendChild(canvas); const vertex = ` attribute vec2 position; attribute vec2 uv; varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 0.0, 1.0); } `; const fragment = ` #ifdef GL_ES precision mediump float; #endif uniform vec3 iResolution; uniform vec2 iMouse; uniform float iTime; uniform float uAngle; uniform float uNoise; uniform float uBlindCount; uniform float uSpotlightRadius; uniform float uSpotlightSoftness; uniform float uSpotlightOpacity; uniform float uMirror; uniform float uDistort; uniform float uShineFlip; uniform vec3 uColor0; uniform vec3 uColor1; uniform vec3 uColor2; uniform vec3 uColor3; uniform vec3 uColor4; uniform vec3 uColor5; uniform vec3 uColor6; uniform vec3 uColor7; uniform int uColorCount; uniform vec3 uColorS0; uniform vec3 uColorS1; uniform vec3 uColorS2; uniform vec3 uColorS3; uniform vec3 uColorS4; uniform vec3 uColorS5; uniform vec3 uColorS6; uniform vec3 uColorS7; uniform int uColorCountS; varying vec2 vUv; float rand(vec2 co){ return fract(sin(dot(co, vec2(12.9898,78.233))) * 43758.5453); } vec2 rotate2D(vec2 p, float a){ float c = cos(a); float s = sin(a); return mat2(c, -s, s, c) * p; } vec3 getGradientColor(float t){ float tt = clamp(t, 0.0, 1.0); int count = uColorCount; if (count < 2) count = 2; float scaled = tt * float(count - 1); float seg = floor(scaled); float f = fract(scaled); if (seg < 1.0) return mix(uColor0, uColor1, f); if (seg < 2.0 && count > 2) return mix(uColor1, uColor2, f); if (seg < 3.0 && count > 3) return mix(uColor2, uColor3, f); if (seg < 4.0 && count > 4) return mix(uColor3, uColor4, f); if (seg < 5.0 && count > 5) return mix(uColor4, uColor5, f); if (seg < 6.0 && count > 6) return mix(uColor5, uColor6, f); if (seg < 7.0 && count > 7) return mix(uColor6, uColor7, f); if (count > 7) return uColor7; if (count > 6) return uColor6; if (count > 5) return uColor5; if (count > 4) return uColor4; if (count > 3) return uColor3; if (count > 2) return uColor2; return uColor1; } vec3 getGradientColorS(float t){ float tt = clamp(t, 0.0, 1.0); int count = uColorCountS; if (count < 2) count = 2; float scaled = tt * float(count - 1); float seg = floor(scaled); float f = fract(scaled); if (seg < 1.0) return mix(uColorS0, uColorS1, f); if (seg < 2.0 && count > 2) return mix(uColorS1, uColorS2, f); if (seg < 3.0 && count > 3) return mix(uColorS2, uColorS3, f); if (seg < 4.0 && count > 4) return mix(uColorS3, uColorS4, f); if (seg < 5.0 && count > 5) return mix(uColorS4, uColorS5, f); if (seg < 6.0 && count > 6) return mix(uColorS5, uColorS6, f); if (seg < 7.0 && count > 7) return mix(uColorS6, uColorS7, f); if (count > 7) return uColorS7; if (count > 6) return uColorS6; if (count > 5) return uColorS5; if (count > 4) return uColorS4; if (count > 3) return uColorS3; if (count > 2) return uColorS2; return uColorS1; } void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 uvraw = fragCoord.xy / iResolution.xy; float aspect = iResolution.x / iResolution.y; vec2 p = uvraw * 2.0 - 1.0; p.x *= aspect; vec2 pr = rotate2D(p, uAngle); pr.x /= aspect; vec2 uv = pr * 0.5 + 0.5; // t: 0 at top, 1 at bottom float t = uv.y; float t_h = uv.x; if (uMirror > 0.5) { t_h = 1.0 - abs(1.0 - 2.0 * fract(t_h)); } // Base color from vertical gradient mixed with secondary horizontal gradient vec3 base = mix(getGradientColor(t), getGradientColorS(t), t_h); // Column logic float grid = uv.x * uBlindCount; float stripe = fract(grid); if (uShineFlip > 0.5) stripe = 1.0 - stripe; // Gentle "stepped" V-shape top boundary float colID = floor(grid); float colCenter = (colID + 0.5) / uBlindCount; float dx = colCenter - 0.5; float vHeight = 0.48 + (dx * dx) * 1.8; // Quadratic curve for gentler center float hJitter = (rand(vec2(colID, 13.37)) - 0.5) * 0.1; // Add subtle organic unevenness float topClip = 1.0 - smoothstep(vHeight + hJitter - 0.01, vHeight + hJitter + 0.01, uvraw.y); // Pillar shape with "Crease" (折痕) effect // We use a sharper transition at 0.5 of the stripe float crease = stripe < 0.5 ? mix(0.8, 1.0, stripe * 2.0) : mix(1.0, 0.7, (stripe - 0.5) * 2.0); float pillar = smoothstep(0.0, 0.05, stripe) * smoothstep(1.0, 0.95, stripe) * crease; // Random brightness per column float pRand = rand(vec2(colID, 42.0)); float brightness = 0.7 + 0.5 * pRand; // Base Pillar Color vec3 col = base * pillar * brightness; // Mouse/Spotlight Glow vec2 mouseUV = iMouse.xy / iResolution.xy; float dist = length(uvraw - mouseUV); float spot = pow(max(0.0, 1.0 - dist / uSpotlightRadius), uSpotlightSoftness) * uSpotlightOpacity; col += base * spot * 0.5; // Additive glow using base color // Apply the V-shape clip col *= topClip; // Grain - very important for this look col += (rand(uvraw + iTime * 0.01) - 0.5) * uNoise; fragColor = vec4(max(vec3(0.0), col), 1.0); } void main() { vec4 color; mainImage(color, vUv * iResolution.xy); gl_FragColor = color; } `; const { arr: colorArr, count: colorCount } = prepStops(gradientColors); const { arr: colorArrS, count: colorCountS } = prepStops(gradientColorsSecondary || gradientColors); const uniforms: { iResolution: { value: [number, number, number] }; iMouse: { value: [number, number] }; iTime: { value: number }; uAngle: { value: number }; uNoise: { value: number }; uBlindCount: { value: number }; uSpotlightRadius: { value: number }; uSpotlightSoftness: { value: number }; uSpotlightOpacity: { value: number }; uMirror: { value: number }; uDistort: { value: number }; uShineFlip: { value: number }; uColor0: { value: [number, number, number] }; uColor1: { value: [number, number, number] }; uColor2: { value: [number, number, number] }; uColor3: { value: [number, number, number] }; uColor4: { value: [number, number, number] }; uColor5: { value: [number, number, number] }; uColor6: { value: [number, number, number] }; uColor7: { value: [number, number, number] }; uColorCount: { value: number }; uColorS0: { value: [number, number, number] }; uColorS1: { value: [number, number, number] }; uColorS2: { value: [number, number, number] }; uColorS3: { value: [number, number, number] }; uColorS4: { value: [number, number, number] }; uColorS5: { value: [number, number, number] }; uColorS6: { value: [number, number, number] }; uColorS7: { value: [number, number, number] }; uColorCountS: { value: number }; } = { iResolution: { value: [gl.drawingBufferWidth, gl.drawingBufferHeight, 1] }, iMouse: { value: [0, 0] }, iTime: { value: 0 }, uAngle: { value: (angle * Math.PI) / 180 }, uNoise: { value: noise }, uBlindCount: { value: Math.max(1, blindCount) }, uSpotlightRadius: { value: spotlightRadius }, uSpotlightSoftness: { value: spotlightSoftness }, uSpotlightOpacity: { value: spotlightOpacity }, uMirror: { value: mirrorGradient ? 1 : 0 }, uDistort: { value: distortAmount }, uShineFlip: { value: shineDirection === 'right' ? 1 : 0 }, uColor0: { value: colorArr[0] }, uColor1: { value: colorArr[1] }, uColor2: { value: colorArr[2] }, uColor3: { value: colorArr[3] }, uColor4: { value: colorArr[4] }, uColor5: { value: colorArr[5] }, uColor6: { value: colorArr[6] }, uColor7: { value: colorArr[7] }, uColorCount: { value: colorCount }, uColorS0: { value: colorArrS[0] }, uColorS1: { value: colorArrS[1] }, uColorS2: { value: colorArrS[2] }, uColorS3: { value: colorArrS[3] }, uColorS4: { value: colorArrS[4] }, uColorS5: { value: colorArrS[5] }, uColorS6: { value: colorArrS[6] }, uColorS7: { value: colorArrS[7] }, uColorCountS: { value: colorCountS } }; const program = new Program(gl, { vertex, fragment, uniforms }); programRef.current = program; const geometry = new Triangle(gl); geometryRef.current = geometry; const mesh = new Mesh(gl, { geometry, program }); meshRef.current = mesh; const resize = () => { const rect = container.getBoundingClientRect(); renderer.setSize(rect.width, rect.height); uniforms.iResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight, 1]; if (blindMinWidth && blindMinWidth > 0) { const maxByMinWidth = Math.max(1, Math.floor(rect.width / blindMinWidth)); const effective = blindCount ? Math.min(blindCount, maxByMinWidth) : maxByMinWidth; uniforms.uBlindCount.value = Math.max(1, effective); } else { uniforms.uBlindCount.value = Math.max(1, blindCount); } if (firstResizeRef.current) { firstResizeRef.current = false; const cx = gl.drawingBufferWidth / 2; const cy = gl.drawingBufferHeight / 2; uniforms.iMouse.value = [cx, cy]; mouseTargetRef.current = [cx, cy]; } }; resize(); const ro = new ResizeObserver(resize); ro.observe(container); const onPointerMove = (e: PointerEvent) => { const rect = canvas.getBoundingClientRect(); const scale = (renderer as unknown as { dpr?: number }).dpr || 1; const x = (e.clientX - rect.left) * scale; const y = (rect.height - (e.clientY - rect.top)) * scale; mouseTargetRef.current = [x, y]; if (mouseDampening <= 0) { uniforms.iMouse.value = [x, y]; } }; canvas.addEventListener('pointermove', onPointerMove); const loop = (t: number) => { rafRef.current = requestAnimationFrame(loop); uniforms.iTime.value = t * 0.001; if (mouseDampening > 0) { if (!lastTimeRef.current) lastTimeRef.current = t; const dt = (t - lastTimeRef.current) / 1000; lastTimeRef.current = t; const tau = Math.max(1e-4, mouseDampening); let factor = 1 - Math.exp(-dt / tau); if (factor > 1) factor = 1; const target = mouseTargetRef.current; const cur = uniforms.iMouse.value; cur[0] += (target[0] - cur[0]) * factor; cur[1] += (target[1] - cur[1]) * factor; } else { lastTimeRef.current = t; } if (!paused && programRef.current && meshRef.current) { try { renderer.render({ scene: meshRef.current }); } catch (e) { console.error(e); } } }; rafRef.current = requestAnimationFrame(loop); return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); canvas.removeEventListener('pointermove', onPointerMove); ro.disconnect(); if (canvas.parentElement === container) { container.removeChild(canvas); } const callIfFn = (obj: T | null, key: K) => { if (obj && typeof obj[key] === 'function') { (obj[key] as unknown as () => void).call(obj); } }; callIfFn(programRef.current, 'remove'); callIfFn(geometryRef.current, 'remove'); callIfFn(meshRef.current as unknown as { remove?: () => void }, 'remove'); callIfFn(rendererRef.current as unknown as { destroy?: () => void }, 'destroy'); programRef.current = null; geometryRef.current = null; meshRef.current = null; rendererRef.current = null; }; }, [ dpr, paused, gradientColors, gradientColorsSecondary, angle, noise, blindCount, blindMinWidth, mouseDampening, mirrorGradient, spotlightRadius, spotlightSoftness, spotlightOpacity, distortAmount, shineDirection ]); return (
); }; export default GradientBlinds;