"use client"; /** * Iridescence — generative WebGL backdrop. * * A living interpretation of the brand wave: domain-warped fractal noise mapped * onto the brand spectrum (violet -> blue -> rose -> emerald) glowing out of a * near-black field, with a faint rainbow refraction band and animated film * grain. Drifts on its own and leans toward the cursor. Falls back to a static * dark 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.204, 0.827, 0.600); // #34d399 emerald const vec3 BASE = vec3(0.031, 0.031, 0.047); // near-black #08080c // -- 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); // the brand spectrum, glowing ADDITIVELY out of a near-black base vec3 glow = vec3(0.0); glow += VIOLET * smoothstep(0.35, 0.95, f); glow += BLUE * smoothstep(0.45, 1.0, r.x) * 0.9; glow += ROSE * smoothstep(0.62, 1.05, q.y) * 0.5; glow += MINT * smoothstep(0.66, 1.0, r.y) * 0.55; // concentrate the light into a soft diagonal wave band (echoes the asset) float band = smoothstep(0.62, 0.0, abs((uv.x - uv.y) * 1.25 - 0.05 + 0.06 * sin(t * 2.0))); float field = smoothstep(0.2, 0.85, f); float energy = (0.35 + 0.65 * band) * field; vec3 col = BASE + glow * energy * (0.55 + 0.45 * uIntensity); // faint rainbow refraction along the band 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.05 * (0.4 + uIntensity); // darken edges so content stays the focus float vig = smoothstep(1.3, 0.2, distance(uv, vec2(0.5))); col *= 0.55 + 0.45 * vig; // keep the base from washing out; clamp the glow softly col = max(col, BASE * 0.6); // animated grain to kill banding + add texture float g = grain(uv, floor(uTime * 12.0) * 0.5); col += (g - 0.5) * 0.03; 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.031, 0.031, 0.047, 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 ( ); }