"use client"; /** * Iridescence — generative WebGL backdrop. * * A living interpretation of the brand cloudscape: domain-warped fractal noise * mapped onto an iridescent pastel palette (lavender -> blue -> rose -> mint), * with a faint rainbow refraction band and animated film grain. Drifts on its * own and leans toward the cursor. Falls back to a static CSS gradient when * WebGL is unavailable or the user prefers reduced motion. * * Renders behind everything (fixed, -z) and is purely decorative (aria-hidden). */ import { useEffect, useRef } from "react"; import { Renderer, Program, Mesh, Triangle, Vec2 } from "ogl"; const VERT = /* glsl */ ` attribute vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); } `; const FRAG = /* glsl */ ` precision highp float; uniform float uTime; uniform vec2 uResolution; uniform vec2 uMouse; // 0..1, smoothed uniform float uIntensity; // global motion amount (0 for reduced motion) // Brand palette anchors const vec3 VIOLET = vec3(0.545, 0.361, 0.965); // #8b5cf6 const vec3 BLUE = vec3(0.231, 0.510, 0.965); // #3b82f6 const vec3 ROSE = vec3(0.957, 0.475, 0.851); // soft pink const vec3 MINT = vec3(0.063, 0.725, 0.506); // #10b981 softened const vec3 HAZE = vec3(0.945, 0.949, 0.992); // near-white lavender // -- hash / value noise -------------------------------------------------- vec2 hash22(vec2 p) { p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3))); return fract(sin(p) * 43758.5453123); } float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f); float a = dot(hash22(i + vec2(0.0, 0.0)) - 0.5, f - vec2(0.0, 0.0)); float b = dot(hash22(i + vec2(1.0, 0.0)) - 0.5, f - vec2(1.0, 0.0)); float c = dot(hash22(i + vec2(0.0, 1.0)) - 0.5, f - vec2(0.0, 1.0)); float d = dot(hash22(i + vec2(1.0, 1.0)) - 0.5, f - vec2(1.0, 1.0)); return 0.5 + mix(mix(a, b, u.x), mix(c, d, u.x), u.y); } float fbm(vec2 p) { float v = 0.0; float amp = 0.55; mat2 rot = mat2(0.8, -0.6, 0.6, 0.8); for (int i = 0; i < 5; i++) { v += amp * noise(p); p = rot * p * 2.0; amp *= 0.5; } return v; } // film grain float grain(vec2 uv, float t) { return fract(sin(dot(uv * (t + 1.0), vec2(12.9898, 78.233))) * 43758.5453); } void main() { vec2 uv = gl_FragCoord.xy / uResolution.xy; float aspect = uResolution.x / uResolution.y; vec2 p = uv; p.x *= aspect; float t = uTime * 0.045 * (0.25 + uIntensity); // mouse parallax (gentle) vec2 m = (uMouse - 0.5); p += m * 0.12 * uIntensity; // domain warping for that liquid cloud feel vec2 q = vec2(fbm(p + vec2(0.0, t)), fbm(p + vec2(5.2, -t))); vec2 r = vec2( fbm(p + 1.7 * q + vec2(8.3, 2.8) + 0.15 * t), fbm(p + 1.7 * q + vec2(1.2, 6.5) - 0.12 * t) ); float f = fbm(p + 2.0 * r); // build the iridescent gradient from the field vec3 col = HAZE; col = mix(col, VIOLET, smoothstep(0.15, 0.85, f)); col = mix(col, BLUE, smoothstep(0.30, 0.95, r.x)); col = mix(col, ROSE, smoothstep(0.55, 1.05, q.y) * 0.65); col = mix(col, MINT, smoothstep(0.62, 1.0, r.y) * 0.30); // central radiant bloom (echoes the cloudscape light source up high) vec2 center = vec2(0.5 * aspect, 0.86); float d = distance(p, center); float bloom = smoothstep(0.9, 0.0, d); col = mix(col, HAZE, bloom * 0.55); // faint rainbow refraction band, lower-left like the asset float band = smoothstep(0.5, 0.0, abs((uv.x - uv.y) * 1.3 - 0.15 + 0.05 * sin(t * 2.0))); vec3 spectrum = 0.5 + 0.5 * cos(6.2831 * (vec3(0.0, 0.33, 0.67) + (uv.x + uv.y) * 0.7)); col += spectrum * band * 0.06 * (0.4 + uIntensity); // lift overall to keep it airy / pastel and protect text legibility col = mix(col, vec3(1.0), 0.18); // soft vignette to anchor content readability float vig = smoothstep(1.25, 0.25, distance(uv, vec2(0.5))); col *= 0.82 + 0.18 * vig; // animated grain to kill banding + add texture float g = grain(uv, floor(uTime * 12.0) * 0.5); col += (g - 0.5) * 0.035; gl_FragColor = vec4(col, 1.0); } `; export default function Iridescence() { const ref = useRef(null); useEffect(() => { const canvas = ref.current; if (!canvas) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; let renderer: Renderer; try { renderer = new Renderer({ canvas, dpr: Math.min(window.devicePixelRatio, 1.75), alpha: false, antialias: false, }); } catch { // WebGL unavailable — CSS fallback (set on the host) remains visible. canvas.style.display = "none"; return; } const gl = renderer.gl; gl.clearColor(0.95, 0.95, 0.99, 1); const uniforms = { uTime: { value: 0 }, uResolution: { value: new Vec2(1, 1) }, uMouse: { value: new Vec2(0.5, 0.5) }, uIntensity: { value: reduce ? 0 : 1 }, }; const program = new Program(gl, { vertex: VERT, fragment: FRAG, uniforms, }); const mesh = new Mesh(gl, { geometry: new Triangle(gl), program }); const resize = () => { const w = window.innerWidth; const h = window.innerHeight; renderer.setSize(w, h); uniforms.uResolution.value.set(gl.drawingBufferWidth, gl.drawingBufferHeight); }; resize(); window.addEventListener("resize", resize); // smoothed pointer const target = { x: 0.5, y: 0.5 }; const onMove = (e: PointerEvent) => { target.x = e.clientX / window.innerWidth; target.y = 1 - e.clientY / window.innerHeight; }; window.addEventListener("pointermove", onMove, { passive: true }); let raf = 0; let running = true; const start = performance.now(); const loop = (now: number) => { if (!running) return; raf = requestAnimationFrame(loop); uniforms.uTime.value = (now - start) / 1000; // ease pointer const m = uniforms.uMouse.value; m.x += (target.x - m.x) * 0.04; m.y += (target.y - m.y) * 0.04; renderer.render({ scene: mesh }); }; if (reduce) { // render a single frame, then idle uniforms.uTime.value = 18; renderer.render({ scene: mesh }); } else { raf = requestAnimationFrame(loop); } // pause when tab hidden (perf + battery) const onVis = () => { if (document.hidden) { running = false; cancelAnimationFrame(raf); } else if (!reduce) { running = true; raf = requestAnimationFrame(loop); } }; document.addEventListener("visibilitychange", onVis); return () => { running = false; cancelAnimationFrame(raf); window.removeEventListener("resize", resize); window.removeEventListener("pointermove", onMove); document.removeEventListener("visibilitychange", onVis); const ext = gl.getExtension("WEBGL_lose_context"); ext?.loseContext(); }; }, []); return ( ); }