ANIMATION: shd-plasma
NAME: Plasma
CATEGORY: Shader

DESCRIPTION: Lava-lamp interference patterns using layered sine waves. Multiple oscillating functions combine to produce organic, flowing color fields that continuously morph and blend into each other.

IMPLEMENTATION RULES:
- Native WebGL only — no external libraries (no Three.js)
- Use useRef + useEffect with proper cleanup (cancelAnimationFrame, removeEventListener)
- Always check if (!gl) return for graceful fallback when WebGL unavailable
- Use precision mediump float — highp causes issues on some mobile GPUs
- Pass u_time, u_resolution, u_mouse uniforms
- Background effects: position fixed, zIndex 0, pointerEvents none
- Canvas size via JS attributes with devicePixelRatio

CODE EXAMPLE:
function PlasmaShader() {
  const canvasRef = React.useRef(null);
  React.useEffect(() => {
    const canvas = canvasRef.current;
    const gl = canvas.getContext("webgl");
    if (!gl) return;
    let animId;
    function resize() {
      canvas.width = window.innerWidth * devicePixelRatio;
      canvas.height = window.innerHeight * devicePixelRatio;
      gl.viewport(0, 0, canvas.width, canvas.height);
    }
    resize();
    window.addEventListener("resize", resize);
    const vs = "attribute vec2 p; void main(){ gl_Position=vec4(p,0,1); }";
    const fs = `precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
void main(){
  vec2 uv = gl_FragCoord.xy / u_resolution;
  float v = sin(uv.x*10.0+u_time)+sin(uv.y*10.0+u_time);
  v += sin((uv.x+uv.y)*10.0+u_time*0.5);
  v += sin(length(uv-0.5)*15.0-u_time*2.0);
  v = v * 0.25 + 0.5;
  vec3 col = vec3(v, v*0.6, 1.0-v);
  gl_FragColor = vec4(col, 1.0);
}`;
    function compile(src, type) {
      const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); return s;
    }
    const prog = gl.createProgram();
    gl.attachShader(prog, compile(vs, gl.VERTEX_SHADER));
    gl.attachShader(prog, compile(fs, gl.FRAGMENT_SHADER));
    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 uTime = gl.getUniformLocation(prog, "u_time");
    const uRes = gl.getUniformLocation(prog, "u_resolution");
    function render(t) {
      gl.uniform1f(uTime, t * 0.001);
      gl.uniform2f(uRes, canvas.width, canvas.height);
      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
      animId = requestAnimationFrame(render);
    }
    render(0);
    return () => { cancelAnimationFrame(animId); window.removeEventListener("resize", resize); gl.deleteProgram(prog); gl.deleteBuffer(buf); gl.getExtension('WEBGL_lose_context')?.loseContext(); };
  }, []);
  return <canvas ref={canvasRef} style={{ position: "fixed", inset: 0, zIndex: 0, pointerEvents: "none", width: "100vw", height: "100vh" }} />;
}
