// 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 (
<>
>
);
};
window.ShaderField = ShaderField;