"use client"; import { useEffect, useRef } from "react"; import { Renderer, Program, Mesh, Triangle, Vec2 } from "ogl"; /** * Custom WebGL signature: a flowing "revenue field" — animated contour * lines that rise and surge like a growth chart / topographic gain map, * rendered as ink hairlines on paper with an acid-green crest threading * through. Reacts gently to the pointer. * * Constraints honored: * - "use client", all GL created in useEffect, full cleanup * - no window/document access during SSR/render * - pauses RAF when the tab is hidden or canvas is offscreen * - respects prefers-reduced-motion (renders one static frame, no loop) * - decorative only (aria-hidden); never the sole carrier of meaning */ export default function RevenueField() { const wrap = useRef(null); useEffect(() => { const host = wrap.current; if (!host) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const renderer = new Renderer({ alpha: true, antialias: true, dpr: Math.min(window.devicePixelRatio || 1, 2), }); const gl = renderer.gl; gl.clearColor(0, 0, 0, 0); host.appendChild(gl.canvas); gl.canvas.style.width = "100%"; gl.canvas.style.height = "100%"; gl.canvas.style.display = "block"; const geometry = new Triangle(gl); const program = new Program(gl, { uniforms: { uTime: { value: 0 }, uRes: { value: new Vec2(1, 1) }, uMouse: { value: new Vec2(0.5, 0.5) }, // ink + emerald crest (the hero sits on light paper, so the crest // uses the deeper "gain" green to stay perceptible, not invisible lime) uInk: { value: [0.078, 0.075, 0.059] }, uAcid: { value: [0.039, 0.431, 0.278] }, }, vertex: /* glsl */ ` attribute vec2 uv; attribute vec2 position; varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 0.0, 1.0); } `, fragment: /* glsl */ ` precision highp float; varying vec2 vUv; uniform float uTime; uniform vec2 uRes; uniform vec2 uMouse; uniform vec3 uInk; uniform vec3 uAcid; // cheap value noise float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } float noise(vec2 p){ vec2 i = floor(p); vec2 f = fract(p); vec2 u = f*f*(3.0-2.0*f); return mix(mix(hash(i), hash(i+vec2(1,0)), u.x), mix(hash(i+vec2(0,1)), hash(i+vec2(1,1)), u.x), u.y); } float fbm(vec2 p){ float v = 0.0; float a = 0.5; for(int i=0;i<5;i++){ v += a*noise(p); p *= 2.0; a *= 0.5; } return v; } void main() { vec2 uv = vUv; float aspect = uRes.x / max(uRes.y, 1.0); vec2 p = uv; p.x *= aspect; // mouse influence — a gentle lift around the pointer vec2 m = uMouse; m.x *= aspect; float md = distance(p, m); float lift = smoothstep(0.6, 0.0, md) * 0.18; float t = uTime * 0.06; // a rising surface: base height climbs left->right (growth) float climb = uv.x * 0.55; float field = fbm(p * 2.4 + vec2(t, t * 0.4)) + climb + lift; field += fbm(p * 5.0 - vec2(t * 0.7, 0.0)) * 0.25; // contour lines from the height field float lines = abs(fract(field * 9.0) - 0.5); float w = fwidth(field * 9.0) * 1.2; float contour = 1.0 - smoothstep(0.0, w, lines); // crest highlight: the topmost band glows acid (the "gain") float crest = smoothstep(0.62, 0.86, field) * smoothstep(0.96, 0.7, field); vec3 col = mix(uInk, uAcid, crest * 0.9); float alpha = contour * (0.20 + crest * 0.85); // soft right/top fade so type stays readable, lines vignette off edges alpha *= smoothstep(0.0, 0.18, uv.x); alpha *= smoothstep(0.0, 0.14, uv.y) * smoothstep(1.0, 0.82, uv.y); gl_FragColor = vec4(col, alpha); } `, }); const mesh = new Mesh(gl, { geometry, program }); const resize = () => { const w = host.clientWidth; const h = host.clientHeight; renderer.setSize(w, h); program.uniforms.uRes.value.set(w, h); }; resize(); const ro = new ResizeObserver(resize); ro.observe(host); // pointer (eased) const target = new Vec2(0.5, 0.5); const onPointer = (e: PointerEvent) => { const r = host.getBoundingClientRect(); target.set((e.clientX - r.left) / r.width, 1 - (e.clientY - r.top) / r.height); }; window.addEventListener("pointermove", onPointer); let raf = 0; let running = true; const start = performance.now(); const frame = (now: number) => { if (!running) return; const u = program.uniforms; u.uTime.value = (now - start) / 1000; (u.uMouse.value as Vec2).x += (target.x - (u.uMouse.value as Vec2).x) * 0.05; (u.uMouse.value as Vec2).y += (target.y - (u.uMouse.value as Vec2).y) * 0.05; renderer.render({ scene: mesh }); raf = requestAnimationFrame(frame); }; // visibility: pause GL when tab hidden or canvas scrolled offscreen const onVisibility = () => { if (document.hidden) { running = false; cancelAnimationFrame(raf); } else if (!reduce) { running = true; raf = requestAnimationFrame(frame); } }; document.addEventListener("visibilitychange", onVisibility); const io = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting && !document.hidden && !reduce) { if (!running) { running = true; raf = requestAnimationFrame(frame); } } else { running = false; cancelAnimationFrame(raf); } }, { threshold: 0 } ); io.observe(host); if (reduce) { // single static frame, no animation loop program.uniforms.uTime.value = 8; renderer.render({ scene: mesh }); } else { raf = requestAnimationFrame(frame); } return () => { running = false; cancelAnimationFrame(raf); window.removeEventListener("pointermove", onPointer); document.removeEventListener("visibilitychange", onVisibility); io.disconnect(); ro.disconnect(); const ext = gl.getExtension("WEBGL_lose_context"); if (ext) ext.loseContext(); if (gl.canvas.parentNode) gl.canvas.parentNode.removeChild(gl.canvas); }; }, []); return