// ui_kits/site/ShaderField.jsx — animated webgl field driven by p3 brand colors. // Falls back to a CSS gradient if WebGL is unavailable. const ShaderField = ({ mode = 'lava', scrollVel = 0 }) => { const canvasRef = React.useRef(null); const stateRef = React.useRef({ vel: 0, t: 0 }); React.useEffect(() => { stateRef.current.vel = scrollVel; }, [scrollVel]); React.useEffect(() => { const canvas = canvasRef.current; const gl = canvas.getContext('webgl', { antialias: true, premultipliedAlpha: false }); if (!gl) { canvas.classList.add('fallback'); return; } const vs = `attribute vec2 p; void main(){ gl_Position = vec4(p, 0.0, 1.0); }`; const fs = ` precision highp float; uniform vec2 u_res; uniform float u_time; uniform float u_vel; uniform vec3 u_c1; uniform vec3 u_c2; uniform vec3 u_c3; // cheap value noise float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); } float noise(vec2 p) { vec2 i = floor(p), f = fract(p); float a = hash(i), b = hash(i + vec2(1.0, 0.0)); float c = hash(i + vec2(0.0, 1.0)), d = hash(i + vec2(1.0, 1.0)); vec2 u = f * f * (3.0 - 2.0 * f); return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); } float fbm(vec2 p) { float v = 0.0, a = 0.5; for (int i = 0; i < 4; i++) { v += a * noise(p); p *= 2.03; a *= 0.5; } return v; } void main(){ vec2 uv = (gl_FragCoord.xy - 0.5 * u_res) / u_res.y; float t = u_time * 0.07; vec2 q = vec2(fbm(uv * 1.3 + t), fbm(uv * 1.3 - t + 7.0)); vec2 r = vec2(fbm(uv * 2.0 + q + vec2(1.7, 9.2) + u_vel * 0.3), fbm(uv * 2.0 + q + vec2(8.3, 2.8) - u_vel * 0.3)); float f = fbm(uv * 1.7 + r); vec3 col = mix(u_c1, u_c2, smoothstep(0.0, 1.0, f)); col = mix(col, u_c3, smoothstep(0.5, 0.95, length(r))); // subtle vignette float d = length(uv); col *= 1.0 - smoothstep(0.4, 1.2, d) * 0.4; gl_FragColor = vec4(col, 1.0); } `; const compile = (type, src) => { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); return s; }; const prog = gl.createProgram(); gl.attachShader(prog, compile(gl.VERTEX_SHADER, vs)); gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fs)); gl.linkProgram(prog); gl.useProgram(prog); const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW); const loc = gl.getAttribLocation(prog, 'p'); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); const uRes = gl.getUniformLocation(prog, 'u_res'); const uTime = gl.getUniformLocation(prog, 'u_time'); const uVel = gl.getUniformLocation(prog, 'u_vel'); const uC1 = gl.getUniformLocation(prog, 'u_c1'); const uC2 = gl.getUniformLocation(prog, 'u_c2'); const uC3 = gl.getUniformLocation(prog, 'u_c3'); const palettes = { lava: [[0.96,0.93,0.86], [0.95,0.46,0.18], [0.30,0.14,0.08]], abyss: [[0.02,0.06,0.10], [0.05,0.32,0.42], [0.30,0.85,0.95]], plasma: [[0.10,0.04,0.18], [0.85,0.18,0.78], [0.70,0.95,0.20]], }; const resize = () => { const dpr = Math.min(devicePixelRatio || 1, 2); canvas.width = canvas.clientWidth * dpr; canvas.height = canvas.clientHeight * dpr; gl.viewport(0, 0, canvas.width, canvas.height); gl.uniform2f(uRes, canvas.width, canvas.height); }; resize(); addEventListener('resize', resize); const start = performance.now(); let raf; const tick = () => { const p = palettes[mode] || palettes.lava; gl.uniform3f(uC1, p[0][0], p[0][1], p[0][2]); gl.uniform3f(uC2, p[1][0], p[1][1], p[1][2]); gl.uniform3f(uC3, p[2][0], p[2][1], p[2][2]); gl.uniform1f(uTime, (performance.now() - start) / 1000); gl.uniform1f(uVel, stateRef.current.vel); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); raf = requestAnimationFrame(tick); }; tick(); return () => { cancelAnimationFrame(raf); removeEventListener('resize', resize); }; }, [mode]); return ( <>