const CELL_SIZE = 10;
const HUE_PERIOD_MS = 3000;
const BRUSH_RADIUS = 2;
const MAX_DPR = 2;
const PANEL_LIMIT = 3;
const TAU = Math.PI * 2;
const PANEL_BORDER_INSET_PX = 10;
type TextureTarget = {
  texture: WebGLTexture;
  framebuffer: WebGLFramebuffer;
  width: number;
  height: number;
};

type ProgramBundle = {
  program: WebGLProgram;
  uniforms: Record<string, WebGLUniformLocation | null>;
};

type PointerState = {
  active: boolean;
  col: number;
  row: number;
};

type StackTuning = {
  blurStrength: number;
  blurRadius: number;
  smallestBlock: number;
  largestBlock: number;
  levels: number;
  detailThreshold: number;
  ditherStrength: number;
  edgeBias: number;
  hueDrift: number;
  streakStrength: number;
};

type TuningState = {
  inside: StackTuning;
  outside: StackTuning;
  outsideGlow: {
    threshold: number;
    softness: number;
    boost: number;
  };
  darkness: number;
  imageEmit: number;
  ansiEmit: number;
  linkEmit: number;
};

const fullscreenVertexSource = `#version 300 es
precision highp float;

out vec2 v_uv;

void main() {
  vec2 position;
  if (gl_VertexID == 0) {
    position = vec2(-1.0, -1.0);
  } else if (gl_VertexID == 1) {
    position = vec2(3.0, -1.0);
  } else {
    position = vec2(-1.0, 3.0);
  }

  v_uv = position * 0.5 + 0.5;
  gl_Position = vec4(position, 0.0, 1.0);
}`;

const simulationFragmentSource = `#version 300 es
precision highp float;
precision highp sampler2D;

uniform sampler2D u_state;
uniform ivec2 u_gridSize;

out vec4 outColor;

int readState(ivec2 cell) {
  ivec2 wrapped = ivec2(
    int(mod(float(cell.x + u_gridSize.x), float(u_gridSize.x))),
    int(mod(float(cell.y + u_gridSize.y), float(u_gridSize.y)))
  );
  return int(round(texelFetch(u_state, wrapped, 0).r * 255.0));
}

float hueAngle(int hue) {
  return float(hue) * ${TAU.toFixed(8)} / 256.0;
}

void main() {
  ivec2 cell = ivec2(gl_FragCoord.xy);
  int current = readState(cell);
  int count = 0;
  float sumSin = 0.0;
  float sumCos = 0.0;

  for (int dy = -1; dy <= 1; dy++) {
    for (int dx = -1; dx <= 1; dx++) {
      if (dx == 0 && dy == 0) {
        continue;
      }

      int neighbor = readState(cell + ivec2(dx, dy));
      if (neighbor != 0) {
        count += 1;
        float angle = hueAngle(neighbor);
        sumSin += sin(angle);
        sumCos += cos(angle);
      }
    }
  }

  int nextState = 0;

  if (current != 0) {
    if (count == 2 || count == 3) {
      nextState = current;
    }
  } else if (count == 3) {
    float angle = atan(sumSin, sumCos);
    if (angle < 0.0) {
      angle += ${TAU.toFixed(8)};
    }
    nextState = int(floor(angle * 256.0 / ${TAU.toFixed(8)}));
    if (nextState == 0) {
      nextState = 1;
    }
  }

  outColor = vec4(float(nextState) / 255.0, 0.0, 0.0, 1.0);
}`;

const stampFragmentSource = `#version 300 es
precision highp float;
precision highp sampler2D;

uniform sampler2D u_state;
uniform ivec2 u_gridSize;
uniform vec2 u_center;
uniform float u_radius;
uniform float u_hue;
uniform bool u_active;

out vec4 outColor;

void main() {
  ivec2 cell = ivec2(gl_FragCoord.xy);
  vec4 state = texelFetch(u_state, cell, 0);

  if (!u_active) {
    outColor = state;
    return;
  }

  vec2 position = vec2(cell) + 0.5;
  vec2 delta = abs(position - u_center);
  delta = min(delta, vec2(u_gridSize) - delta);

  if (dot(delta, delta) <= u_radius * u_radius) {
    outColor = vec4(u_hue, 0.0, 0.0, 1.0);
    return;
  }

  outColor = state;
}`;

const injectFragmentSource = `#version 300 es
precision highp float;
precision highp sampler2D;

uniform sampler2D u_state;
uniform sampler2D u_emitter;
uniform ivec2 u_gridSize;

out vec4 outColor;

float hash12(vec2 point) {
  vec3 p3 = fract(vec3(point.xyx) * 0.1031);
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.x + p3.y) * p3.z);
}

vec3 rgbToHsv(vec3 color) {
  vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
  vec4 p = mix(vec4(color.bg, K.wz), vec4(color.gb, K.xy), step(color.b, color.g));
  vec4 q = mix(vec4(p.xyw, color.r), vec4(color.r, p.yzx), step(p.x, color.r));
  float d = q.x - min(q.w, q.y);
  float e = 1.0e-10;
  return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

void main() {
  ivec2 cell = ivec2(gl_FragCoord.xy);
  vec4 state = texelFetch(u_state, cell, 0);
  vec4 emitter = texelFetch(u_emitter, cell, 0);
  float strength = clamp(emitter.a, 0.0, 1.0);

  if (strength <= 0.0) {
    outColor = state;
    return;
  }

  float noise = hash12(vec2(cell) + emitter.rg * 255.0);
  if (noise > strength) {
    outColor = state;
    return;
  }

  vec3 hsv = rgbToHsv(emitter.rgb);
  int hue = int(floor(hsv.x * 254.0)) + 1;
  outColor = vec4(float(hue) / 255.0, 0.0, 0.0, 1.0);
}`;

const colorFragmentSource = `#version 300 es
precision highp float;
precision highp sampler2D;

uniform sampler2D u_state;
uniform sampler2D u_palette;
uniform ivec2 u_gridSize;
uniform vec2 u_resolution;

out vec4 outColor;

void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution;
  ivec2 cell = ivec2(floor(uv * vec2(u_gridSize)));
  cell = clamp(cell, ivec2(0), u_gridSize - 1);
  float index = texelFetch(u_state, cell, 0).r;
  outColor = texture(u_palette, vec2(index, 0.5));
}`;

const copyFragmentSource = `#version 300 es
precision highp float;
precision highp sampler2D;

in vec2 v_uv;

uniform sampler2D u_image;

out vec4 outColor;

void main() {
  outColor = texture(u_image, v_uv);
}`;

const blurFragmentSource = `#version 300 es
precision highp float;
precision highp sampler2D;

in vec2 v_uv;

uniform sampler2D u_image;
uniform vec2 u_direction;
uniform vec2 u_resolution;
uniform float u_radius;

out vec4 outColor;

void main() {
  vec2 texel = (u_direction / u_resolution) * u_radius;

  vec4 color = texture(u_image, v_uv) * 0.15957691;
  color += texture(u_image, v_uv + texel * 1.0) * 0.14730806;
  color += texture(u_image, v_uv - texel * 1.0) * 0.14730806;
  color += texture(u_image, v_uv + texel * 2.0) * 0.11587662;
  color += texture(u_image, v_uv - texel * 2.0) * 0.11587662;
  color += texture(u_image, v_uv + texel * 3.0) * 0.07767442;
  color += texture(u_image, v_uv - texel * 3.0) * 0.07767442;
  color += texture(u_image, v_uv + texel * 4.0) * 0.04436833;
  color += texture(u_image, v_uv - texel * 4.0) * 0.04436833;
  color += texture(u_image, v_uv + texel * 5.0) * 0.02159639;
  color += texture(u_image, v_uv - texel * 5.0) * 0.02159639;
  color += texture(u_image, v_uv + texel * 6.0) * 0.00895781;
  color += texture(u_image, v_uv - texel * 6.0) * 0.00895781;

  outColor = color;
}`;

const compositeFragmentSource = `#version 300 es
precision highp float;
precision highp sampler2D;

uniform sampler2D u_sharp;
uniform sampler2D u_inBlur;
uniform sampler2D u_outBlur;
uniform sampler2D u_outBlur2;
uniform sampler2D u_palette;
uniform vec2 u_resolution;
uniform vec4 u_panelRects[${PANEL_LIMIT}];
uniform int u_panelCount;
uniform float u_inMinBlockSize;
uniform float u_inMaxBlockSize;
uniform int u_inLevels;
uniform float u_inDetailThreshold;
uniform float u_inDitherStrength;
uniform float u_inEdgeBias;
uniform float u_inHueDrift;
uniform float u_inStreakStrength;
uniform float u_outMinBlockSize;
uniform float u_outMaxBlockSize;
uniform int u_outLevels;
uniform float u_outDetailThreshold;
uniform float u_outDitherStrength;
uniform float u_outEdgeBias;
uniform float u_outHueDrift;
uniform float u_outStreakStrength;
uniform float u_outGlowStrength;
uniform float u_outGlowThreshold;
uniform float u_outGlowSoftness;
uniform float u_outGlowBoost;
out vec4 outColor;

bool inRect(vec2 point, vec4 rect) {
  return point.x >= rect.x &&
    point.y >= rect.y &&
    point.x < rect.x + rect.z &&
    point.y < rect.y + rect.w;
}

float rectSignedDistance(vec2 point, vec4 rect) {
  vec2 center = rect.xy + rect.zw * 0.5;
  vec2 halfSize = rect.zw * 0.5;
  vec2 delta = abs(point - center) - halfSize;
  float outside = length(max(delta, 0.0));
  float inside = min(max(delta.x, delta.y), 0.0);
  return outside + inside;
}

float bayer4(vec2 point) {
  vec2 cell = mod(floor(point), 4.0);
  if (cell.y < 1.0) {
    if (cell.x < 1.0) return 0.0 / 16.0;
    if (cell.x < 2.0) return 8.0 / 16.0;
    if (cell.x < 3.0) return 2.0 / 16.0;
    return 10.0 / 16.0;
  }
  if (cell.y < 2.0) {
    if (cell.x < 1.0) return 12.0 / 16.0;
    if (cell.x < 2.0) return 4.0 / 16.0;
    if (cell.x < 3.0) return 14.0 / 16.0;
    return 6.0 / 16.0;
  }
  if (cell.y < 3.0) {
    if (cell.x < 1.0) return 3.0 / 16.0;
    if (cell.x < 2.0) return 11.0 / 16.0;
    if (cell.x < 3.0) return 1.0 / 16.0;
    return 9.0 / 16.0;
  }
  if (cell.x < 1.0) return 15.0 / 16.0;
  if (cell.x < 2.0) return 7.0 / 16.0;
  if (cell.x < 3.0) return 13.0 / 16.0;
  return 5.0 / 16.0;
}

vec3 rgbToHsv(vec3 color) {
  vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
  vec4 p = mix(vec4(color.bg, K.wz), vec4(color.gb, K.xy), step(color.b, color.g));
  vec4 q = mix(vec4(p.xyw, color.r), vec4(color.r, p.yzx), step(p.x, color.r));
  float d = q.x - min(q.w, q.y);
  float e = 1.0e-10;
  return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

float luma(vec3 color) {
  return dot(color, vec3(0.299, 0.587, 0.114));
}

float hash12(vec2 point) {
  vec3 p3 = fract(vec3(point.xyx) * 0.1031);
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.x + p3.y) * p3.z);
}

vec3 sampleBlock(vec2 point, float blockSize, float streakStrength, sampler2D blurTex) {
  vec2 blockCoord = floor(point / blockSize);
  vec2 samplePoint = (blockCoord + 0.5) * blockSize;
  float streak = (hash12(vec2(blockCoord.y, blockSize)) - 0.5) * blockSize * streakStrength;
  samplePoint.x += streak;
  samplePoint.x = clamp(samplePoint.x, 0.5, u_resolution.x - 0.5);
  vec2 sampleUv = samplePoint / u_resolution;
  return texture(blurTex, sampleUv).rgb;
}

float blockDetail(vec2 point, float blockSize, float edgeBias, float streakStrength, sampler2D blurTex) {
  vec2 offset = vec2(blockSize * 0.35);
  vec3 center = sampleBlock(point, blockSize, streakStrength, blurTex);
  vec3 right = sampleBlock(point + vec2(offset.x, 0.0), blockSize, streakStrength, blurTex);
  vec3 left = sampleBlock(point + vec2(-offset.x, 0.0), blockSize, streakStrength, blurTex);
  vec3 up = sampleBlock(point + vec2(0.0, offset.y), blockSize, streakStrength, blurTex);
  vec3 down = sampleBlock(point + vec2(0.0, -offset.y), blockSize, streakStrength, blurTex);

  float edgeX = abs(luma(right) - luma(left));
  float edgeY = abs(luma(up) - luma(down));
  float edgeDetail = max(edgeX, edgeY);

  float chromaShift = max(
    max(length(right - center), length(left - center)),
    max(length(up - center), length(down - center))
  );

  return edgeDetail * edgeBias + chromaShift * 0.2;
}

float chooseBlockSize(vec2 point, float minBlockSize, float maxBlockSize, int levels, float detailThreshold, float edgeBias, float streakStrength, sampler2D blurTex) {
  float minSize = max(minBlockSize, 0.25);
  float maxSize = max(minSize, maxBlockSize);
  int sections = clamp(levels, 1, 10);
  int sizeCount = sections + 1;
  float chosen = maxSize;
  float steps = float(max(sizeCount - 1, 1));

  for (int i = 0; i < 11; i++) {
    if (i >= sizeCount) {
      break;
    }
    float t = float(i) / steps;
    float candidate = exp(mix(log(maxSize), log(minSize), t));
    chosen = candidate;
    if (blockDetail(point, candidate, edgeBias, streakStrength, blurTex) <= detailThreshold) {
      break;
    }
  }

  return chosen;
}

vec4 stylizeBlur(vec2 frag, sampler2D blurTex, float minBlockSize, float maxBlockSize, int levels, float detailThreshold, float ditherStrength, float edgeBias, float hueDrift, float streakStrength) {
  float pixelSize = chooseBlockSize(frag, minBlockSize, maxBlockSize, levels, detailThreshold, edgeBias, streakStrength, blurTex);
  vec2 blockCoord = floor(frag / pixelSize);
  vec3 blurred = sampleBlock(frag, pixelSize, streakStrength, blurTex);
  vec3 hsv = rgbToHsv(blurred);
  float drift = (hash12(blockCoord + vec2(pixelSize, 17.0)) - 0.5) * hueDrift;
  float hueIndex = floor(fract(hsv.x + drift) * 254.0) + 1.0;
  vec3 hueColor = texture(u_palette, vec2(hueIndex / 255.0, 0.5)).rgb;
  float coverage = clamp(hsv.z * mix(0.28, 1.0, hsv.y), 0.0, 1.0);
  float threshold = mix(0.5, bayer4(frag), clamp(ditherStrength, 0.0, 1.0));
  return coverage > threshold ? vec4(hueColor, 1.0) : vec4(0.0, 0.0, 0.0, 1.0);
}

vec3 screenBlend(vec3 base, vec3 blend) {
  return 1.0 - (1.0 - base) * (1.0 - blend);
}

vec4 glowOutside(vec2 frag) {
  vec2 uv = frag / u_resolution;
  vec3 sharp = texture(u_sharp, uv).rgb;
  vec3 blur = texture(u_outBlur, uv).rgb;
  vec3 bloom = max(blur - vec3(u_outGlowThreshold), vec3(0.0));
  bloom = pow(bloom, vec3(1.0 / max(u_outGlowSoftness, 0.01)));
  vec3 glow = bloom * u_outGlowBoost * u_outGlowStrength;
  vec3 color = screenBlend(sharp, glow);

  return vec4(color, 1.0);
}

void main() {
  vec2 frag = gl_FragCoord.xy;
  bool inside = false;
  for (int i = 0; i < ${PANEL_LIMIT}; i++) {
    if (i >= u_panelCount) {
      break;
    }
    if (inRect(frag, u_panelRects[i])) {
      inside = true;
      break;
    }
  }
  outColor = inside
    ? stylizeBlur(frag, u_inBlur, u_inMinBlockSize, u_inMaxBlockSize, u_inLevels, u_inDetailThreshold, u_inDitherStrength, u_inEdgeBias, u_inHueDrift, u_inStreakStrength)
    : glowOutside(frag);
}`;

function compileShader(
  gl: WebGL2RenderingContext,
  type: number,
  source: string,
) {
  const shader = gl.createShader(type);
  if (!shader) {
    throw new Error("Unable to create WebGL shader");
  }

  gl.shaderSource(shader, source);
  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const message = gl.getShaderInfoLog(shader) ?? "Unknown shader error";
    gl.deleteShader(shader);
    throw new Error(message);
  }

  return shader;
}

function createProgram(
  gl: WebGL2RenderingContext,
  vertexSource: string,
  fragmentSource: string,
  uniformNames: string[],
): ProgramBundle {
  const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexSource);
  const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
  const program = gl.createProgram();

  if (!program) {
    throw new Error("Unable to create WebGL program");
  }

  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  gl.deleteShader(vertexShader);
  gl.deleteShader(fragmentShader);

  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    const message = gl.getProgramInfoLog(program) ?? "Unknown program error";
    gl.deleteProgram(program);
    throw new Error(message);
  }

  const uniforms = Object.fromEntries(
    uniformNames.map((name) => [name, gl.getUniformLocation(program, name)]),
  );

  return { program, uniforms };
}

function createTextureTarget(
  gl: WebGL2RenderingContext,
  width: number,
  height: number,
  filter: number,
) {
  const texture = gl.createTexture();
  const framebuffer = gl.createFramebuffer();

  if (!texture || !framebuffer) {
    throw new Error("Unable to allocate WebGL texture target");
  }

  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texImage2D(
    gl.TEXTURE_2D,
    0,
    gl.RGBA8,
    width,
    height,
    0,
    gl.RGBA,
    gl.UNSIGNED_BYTE,
    null,
  );

  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
  gl.framebufferTexture2D(
    gl.FRAMEBUFFER,
    gl.COLOR_ATTACHMENT0,
    gl.TEXTURE_2D,
    texture,
    0,
  );

  if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
    throw new Error("Incomplete WebGL framebuffer");
  }

  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  gl.bindTexture(gl.TEXTURE_2D, null);

  return { texture, framebuffer, width, height };
}

function deleteTextureTarget(
  gl: WebGL2RenderingContext,
  target: TextureTarget | null,
) {
  if (!target) return;
  gl.deleteFramebuffer(target.framebuffer);
  gl.deleteTexture(target.texture);
}

function createPaletteTexture(gl: WebGL2RenderingContext) {
  const texture = gl.createTexture();
  if (!texture) {
    throw new Error("Unable to create palette texture");
  }

  const data = new Uint8Array(256 * 4);
  data[3] = 255;

  for (let i = 1; i < 256; i++) {
    const h = (i / 255) * 6;
    const x = 1 - Math.abs((h % 2) - 1);
    let r = 0;
    let g = 0;
    let b = 0;

    if (h < 1) {
      r = 1;
      g = x;
    } else if (h < 2) {
      r = x;
      g = 1;
    } else if (h < 3) {
      g = 1;
      b = x;
    } else if (h < 4) {
      g = x;
      b = 1;
    } else if (h < 5) {
      r = x;
      b = 1;
    } else {
      r = 1;
      b = x;
    }

    const offset = i * 4;
    data[offset] = Math.floor(r * 255);
    data[offset + 1] = Math.floor(g * 255);
    data[offset + 2] = Math.floor(b * 255);
    data[offset + 3] = 255;
  }

  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texImage2D(
    gl.TEXTURE_2D,
    0,
    gl.RGBA8,
    256,
    1,
    0,
    gl.RGBA,
    gl.UNSIGNED_BYTE,
    data,
  );
  gl.bindTexture(gl.TEXTURE_2D, null);

  return texture;
}

function createInitialState(width: number, height: number) {
  const data = new Uint8Array(width * height * 4);
  const seedBuffer = new Uint32Array(1);
  globalThis.crypto.getRandomValues(seedBuffer);
  let rng = seedBuffer[0] ?? 0x4a455450;

  const next = () => {
    rng ^= rng << 13;
    rng ^= rng >>> 17;
    rng ^= rng << 5;
    return rng >>> 0;
  };

  for (let i = 0; i < width * height; i++) {
    const alive = (next() & 1) === 0;
    data[i * 4] = alive ? next() & 0xff || 1 : 0;
    data[i * 4 + 3] = 255;
  }

  return data;
}

function setTextureUnit(
  gl: WebGL2RenderingContext,
  unit: number,
  texture: WebGLTexture,
) {
  gl.activeTexture(gl.TEXTURE0 + unit);
  gl.bindTexture(gl.TEXTURE_2D, texture);
}

function uniform(
  bundle: ProgramBundle,
  name: string,
): WebGLUniformLocation | null {
  return bundle.uniforms[name] ?? null;
}

export function initWebGLBackground() {
  const canvas = document.getElementById("canvas") as HTMLCanvasElement | null;
  if (!canvas) {
    return null;
  }

  const gl = canvas.getContext("webgl2", {
    alpha: true,
    antialias: false,
    premultipliedAlpha: true,
    depth: false,
    stencil: false,
    preserveDrawingBuffer: false,
  });

  if (!gl) {
    document.body.dataset.backgroundMode = "failed";
    canvas.hidden = true;
    return null;
  }

  const vao = gl.createVertexArray();
  if (!vao) {
    throw new Error("Unable to create WebGL vertex array");
  }
  gl.bindVertexArray(vao);

  const simulationProgram = createProgram(
    gl,
    fullscreenVertexSource,
    simulationFragmentSource,
    ["u_state", "u_gridSize"],
  );
  const stampProgram = createProgram(
    gl,
    fullscreenVertexSource,
    stampFragmentSource,
    ["u_state", "u_gridSize", "u_center", "u_radius", "u_hue", "u_active"],
  );
  const injectProgram = createProgram(
    gl,
    fullscreenVertexSource,
    injectFragmentSource,
    ["u_state", "u_emitter", "u_gridSize"],
  );
  const colorProgram = createProgram(
    gl,
    fullscreenVertexSource,
    colorFragmentSource,
    ["u_state", "u_palette", "u_gridSize", "u_resolution"],
  );
  const copyProgram = createProgram(
    gl,
    fullscreenVertexSource,
    copyFragmentSource,
    ["u_image"],
  );
  const blurProgram = createProgram(
    gl,
    fullscreenVertexSource,
    blurFragmentSource,
    ["u_image", "u_direction", "u_resolution", "u_radius"],
  );
  const compositeProgram = createProgram(
    gl,
    fullscreenVertexSource,
    compositeFragmentSource,
    [
      "u_sharp",
      "u_inBlur",
      "u_outBlur",
      "u_outBlur2",
      "u_palette",
      "u_resolution",
      "u_panelRects",
      "u_panelCount",
      "u_inMinBlockSize",
      "u_inMaxBlockSize",
      "u_inLevels",
      "u_inDetailThreshold",
      "u_inDitherStrength",
      "u_inEdgeBias",
      "u_inHueDrift",
      "u_inStreakStrength",
      "u_outMinBlockSize",
      "u_outMaxBlockSize",
      "u_outLevels",
      "u_outDetailThreshold",
      "u_outDitherStrength",
      "u_outEdgeBias",
      "u_outHueDrift",
      "u_outStreakStrength",
      "u_outGlowStrength",
      "u_outGlowThreshold",
      "u_outGlowSoftness",
      "u_outGlowBoost",
    ],
  );

  const paletteTexture = createPaletteTexture(gl);
  const pointer: PointerState = { active: false, col: 0, row: 0 };
  const tuning: TuningState = {
    inside: {
      blurStrength: 1,
      blurRadius: 20,
      smallestBlock: 0.25,
      largestBlock: 63,
      levels: 5,
      detailThreshold: 0.103,
      ditherStrength: 0.79,
      edgeBias: 3,
      hueDrift: 0.04,
      streakStrength: 1,
    },
    outside: {
      blurStrength: 1,
      blurRadius: 20,
      smallestBlock: 1,
      largestBlock: 20,
      levels: 3,
      detailThreshold: 0.04,
      ditherStrength: 0.75,
      edgeBias: 1.35,
      hueDrift: 0.08,
      streakStrength: 0.35,
    },
    outsideGlow: {
      threshold: 0,
      softness: 1,
      boost: 1,
    },
    darkness: 0.68,
    imageEmit: 1,
    ansiEmit: 1,
    linkEmit: 1,
  };
  const panelRects = new Float32Array(PANEL_LIMIT * 4);

  let stateTargets: [TextureTarget, TextureTarget] | null = null;
  let colorTarget: TextureTarget | null = null;
  let sharpTarget: TextureTarget | null = null;
  let smoothTarget: TextureTarget | null = null;
  let insideBlurTargets: [TextureTarget, TextureTarget] | null = null;
  let outsideBlurTargets: [TextureTarget, TextureTarget] | null = null;
  let emitterTarget: TextureTarget | null = null;
  let stateIndex = 0;
  let rafId = 0;
  let cssWidth = 0;
  let cssHeight = 0;
  let canvasWidth = 0;
  let canvasHeight = 0;
  let gridWidth = 0;
  let gridHeight = 0;
  const emitterCanvas = document.createElement("canvas");
  const emitterCtx = emitterCanvas.getContext("2d");

  const renderPass = (
    target: TextureTarget | null,
    width: number,
    height: number,
    program: WebGLProgram,
  ) => {
    gl.bindFramebuffer(gl.FRAMEBUFFER, target?.framebuffer ?? null);
    gl.viewport(0, 0, width, height);
    gl.useProgram(program);
    gl.drawArrays(gl.TRIANGLES, 0, 3);
  };

  const currentState = () => {
    if (!stateTargets) {
      throw new Error("State textures not initialized");
    }
    return stateTargets[stateIndex]!;
  };

  const nextState = () => {
    if (!stateTargets) {
      throw new Error("State textures not initialized");
    }
    return stateTargets[1 - stateIndex]!;
  };

  const swapState = () => {
    stateIndex = 1 - stateIndex;
  };

  const resizeTargets = () => {
    const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);
    cssWidth = Math.max(1, Math.round(window.innerWidth));
    cssHeight = Math.max(1, Math.round(window.innerHeight));
    canvasWidth = Math.max(1, Math.round(cssWidth * dpr));
    canvasHeight = Math.max(1, Math.round(cssHeight * dpr));
    gridWidth = Math.max(1, Math.floor(cssWidth / CELL_SIZE));
    gridHeight = Math.max(1, Math.floor(cssHeight / CELL_SIZE));

    canvas.width = canvasWidth;
    canvas.height = canvasHeight;
    canvas.hidden = false;
    document.body.dataset.backgroundMode = "animated";
    document.documentElement.style.setProperty(
      "--panel-bg-alpha",
      tuning.darkness.toFixed(2),
    );

    deleteTextureTarget(gl, stateTargets?.[0] ?? null);
    deleteTextureTarget(gl, stateTargets?.[1] ?? null);
    deleteTextureTarget(gl, colorTarget);
    deleteTextureTarget(gl, sharpTarget);
    deleteTextureTarget(gl, smoothTarget);
    deleteTextureTarget(gl, insideBlurTargets?.[0] ?? null);
    deleteTextureTarget(gl, insideBlurTargets?.[1] ?? null);
    deleteTextureTarget(gl, outsideBlurTargets?.[0] ?? null);
    deleteTextureTarget(gl, outsideBlurTargets?.[1] ?? null);
    deleteTextureTarget(gl, emitterTarget);

    stateTargets = [
      createTextureTarget(gl, gridWidth, gridHeight, gl.NEAREST),
      createTextureTarget(gl, gridWidth, gridHeight, gl.NEAREST),
    ];
    colorTarget = createTextureTarget(gl, gridWidth, gridHeight, gl.LINEAR);
    sharpTarget = createTextureTarget(gl, canvasWidth, canvasHeight, gl.LINEAR);
    smoothTarget = createTextureTarget(
      gl,
      canvasWidth,
      canvasHeight,
      gl.LINEAR,
    );
    insideBlurTargets = [
      createTextureTarget(gl, canvasWidth, canvasHeight, gl.LINEAR),
      createTextureTarget(gl, canvasWidth, canvasHeight, gl.LINEAR),
    ];
    outsideBlurTargets = [
      createTextureTarget(gl, canvasWidth, canvasHeight, gl.LINEAR),
      createTextureTarget(gl, canvasWidth, canvasHeight, gl.LINEAR),
    ];
    emitterTarget = createTextureTarget(gl, gridWidth, gridHeight, gl.LINEAR);
    stateIndex = 0;
    emitterCanvas.width = gridWidth;
    emitterCanvas.height = gridHeight;

    const initialState = createInitialState(gridWidth, gridHeight);
    setTextureUnit(gl, 0, stateTargets[0].texture);
    gl.texSubImage2D(
      gl.TEXTURE_2D,
      0,
      0,
      0,
      gridWidth,
      gridHeight,
      gl.RGBA,
      gl.UNSIGNED_BYTE,
      initialState,
    );
    setTextureUnit(gl, 0, stateTargets[1].texture);
    gl.texSubImage2D(
      gl.TEXTURE_2D,
      0,
      0,
      0,
      gridWidth,
      gridHeight,
      gl.RGBA,
      gl.UNSIGNED_BYTE,
      initialState,
    );
    gl.bindTexture(gl.TEXTURE_2D, null);
  };

  const updatePanelRects = () => {
    panelRects.fill(0);
    const dpr = canvasWidth / cssWidth;
    const nodes = Array.from(
      document.querySelectorAll<HTMLElement>(".site-panel-frame"),
    ).slice(0, PANEL_LIMIT);

    nodes.forEach((node, index) => {
      const rect = node.getBoundingClientRect();
      const offset = index * 4;
      const inset = PANEL_BORDER_INSET_PX * dpr;
      panelRects[offset] = rect.left * dpr + inset;
      panelRects[offset + 1] = (cssHeight - rect.bottom) * dpr + inset;
      panelRects[offset + 2] = Math.max(1, rect.width * dpr - inset * 2);
      panelRects[offset + 3] = Math.max(1, rect.height * dpr - inset * 2);
    });

    return nodes.length;
  };

  const updatePointer = (clientX: number, clientY: number) => {
    pointer.col = Math.max(
      0,
      Math.min(gridWidth - 1, Math.floor(clientX / CELL_SIZE)),
    );
    pointer.row = Math.max(
      0,
      Math.min(gridHeight - 1, Math.floor((cssHeight - clientY) / CELL_SIZE)),
    );
    pointer.active = true;
  };

  const renderEmitters = () => {
    if (!emitterCtx || !emitterTarget) {
      return;
    }

    emitterCtx.clearRect(0, 0, gridWidth, gridHeight);
    emitterCtx.imageSmoothingEnabled = true;

    const scaleX = gridWidth / cssWidth;
    const scaleY = gridHeight / cssHeight;
    const intersectRect = (a: DOMRect, b: DOMRect) => {
      const left = Math.max(a.left, b.left);
      const top = Math.max(a.top, b.top);
      const right = Math.min(a.right, b.right);
      const bottom = Math.min(a.bottom, b.bottom);
      if (right <= left || bottom <= top) return null;
      return new DOMRect(left, top, right - left, bottom - top);
    };
    const getVisibleRect = (element: HTMLElement) => {
      let visible: DOMRect | null = intersectRect(
        element.getBoundingClientRect(),
        new DOMRect(0, 0, cssWidth, cssHeight),
      );
      if (!visible) return null;

      let current = element.parentElement;
      while (current && current !== document.body) {
        const style = getComputedStyle(current);
        const clipsX = /(auto|scroll|hidden|clip)/.test(style.overflowX);
        const clipsY = /(auto|scroll|hidden|clip)/.test(style.overflowY);
        if (clipsX || clipsY) {
          visible = intersectRect(visible, current.getBoundingClientRect());
          if (!visible) return null;
        }
        current = current.parentElement;
      }

      return visible;
    };
    const drawRect = (rect: DOMRect, color: string, alpha: number) => {
      if (alpha <= 0 || rect.width <= 0 || rect.height <= 0) return;
      emitterCtx.globalAlpha = alpha;
      emitterCtx.fillStyle = color;
      emitterCtx.fillRect(
        rect.left * scaleX,
        rect.top * scaleY,
        rect.width * scaleX,
        rect.height * scaleY,
      );
    };

    const drawText = (element: HTMLElement, alpha: number) => {
      const text = element.textContent?.trim();
      if (!text || alpha <= 0) return;
      const rect = getVisibleRect(element);
      if (!rect) return;
      if (rect.width <= 0 || rect.height <= 0) return;
      const style = getComputedStyle(element);
      if (style.visibility === "hidden" || style.display === "none") return;
      emitterCtx.save();
      emitterCtx.beginPath();
      emitterCtx.rect(
        rect.left * scaleX,
        rect.top * scaleY,
        rect.width * scaleX,
        rect.height * scaleY,
      );
      emitterCtx.clip();
      emitterCtx.globalAlpha = alpha;
      emitterCtx.fillStyle = style.color;
      emitterCtx.textBaseline = "top";
      emitterCtx.font = `${style.fontStyle} ${style.fontWeight} ${Math.max(8, parseFloat(style.fontSize) * scaleY)}px ${style.fontFamily}`;
      const x = rect.left * scaleX;
      const y = rect.top * scaleY;
      const lines = text
        .split(/\n+/)
        .map((line) => line.trim())
        .filter(Boolean);
      const lineHeight = Math.max(
        8,
        parseFloat(style.lineHeight || style.fontSize) * scaleY ||
          parseFloat(style.fontSize) * scaleY,
      );
      lines.forEach((line, index) => {
        emitterCtx.fillText(
          line,
          x,
          y + index * lineHeight,
          Math.max(1, rect.width * scaleX),
        );
      });
      emitterCtx.restore();
    };

    if (tuning.imageEmit > 0) {
      document
        .querySelectorAll<HTMLImageElement>("img[data-emitter-image]")
        .forEach((image) => {
          const rect = getVisibleRect(image);
          if (!rect) return;
          if (rect.width <= 0 || rect.height <= 0 || !image.complete) return;
          emitterCtx.save();
          emitterCtx.beginPath();
          emitterCtx.rect(
            rect.left * scaleX,
            rect.top * scaleY,
            rect.width * scaleX,
            rect.height * scaleY,
          );
          emitterCtx.clip();
          emitterCtx.globalAlpha = tuning.imageEmit;
          emitterCtx.drawImage(
            image,
            rect.left * scaleX,
            rect.top * scaleY,
            rect.width * scaleX,
            rect.height * scaleY,
          );
          emitterCtx.restore();
        });
    }

    if (tuning.ansiEmit > 0) {
      document
        .querySelectorAll<HTMLElement>("[data-emitter-ansi] span")
        .forEach((span) => {
          const rect = getVisibleRect(span);
          if (!rect) return;
          const color = getComputedStyle(span).color;
          drawRect(rect, color, tuning.ansiEmit);
        });
    }

    if (tuning.linkEmit > 0) {
      document.querySelectorAll<HTMLElement>("a").forEach((link) => {
        drawText(link, tuning.linkEmit);
      });
    }

    const textEmitStrength = Math.max(tuning.linkEmit, tuning.ansiEmit * 0.8);
    if (textEmitStrength > 0) {
      document
        .querySelectorAll<HTMLElement>(
          "button, p, span, legend, label, [role='button'], .qa-meta, .qa-button, .qa-inline-action",
        )
        .forEach((element) => {
          if (element.closest("[data-emitter-ansi]")) {
            return;
          }
          drawText(element, textEmitStrength);
        });

      document
        .querySelectorAll<HTMLElement>("button, .qa-button, .qa-inline-action")
        .forEach((element) => {
          const rect = getVisibleRect(element);
          if (!rect) return;
          const color = getComputedStyle(element).color;
          drawRect(rect, color, textEmitStrength * 0.2);
        });

      document
        .querySelectorAll<HTMLElement>(
          "#char-count, #qa-status, #copy-email-status",
        )
        .forEach((element) => {
          if (element.id === "char-count") {
            const color = getComputedStyle(element).color.replace(/\s+/g, "");
            if (color !== "rgb(255,85,85)") {
              return;
            }
          }
          drawText(element, Math.max(textEmitStrength, 0.75));
        });
    }

    emitterCtx.globalAlpha = 1;
    setTextureUnit(gl, 3, emitterTarget.texture);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
    gl.texSubImage2D(
      gl.TEXTURE_2D,
      0,
      0,
      0,
      gl.RGBA,
      gl.UNSIGNED_BYTE,
      emitterCanvas,
    );
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0);
  };

  const injectEmitters = () => {
    if (!emitterTarget) return;

    const source = currentState();
    const target = nextState();
    setTextureUnit(gl, 0, source.texture);
    setTextureUnit(gl, 3, emitterTarget.texture);
    gl.useProgram(injectProgram.program);
    gl.uniform1i(uniform(injectProgram, "u_state"), 0);
    gl.uniform1i(uniform(injectProgram, "u_emitter"), 3);
    gl.uniform2i(uniform(injectProgram, "u_gridSize"), gridWidth, gridHeight);
    renderPass(target, gridWidth, gridHeight, injectProgram.program);
    swapState();
  };

  const simulate = () => {
    const source = currentState();
    const target = nextState();

    setTextureUnit(gl, 0, source.texture);
    gl.useProgram(simulationProgram.program);
    gl.uniform1i(uniform(simulationProgram, "u_state"), 0);
    gl.uniform2i(
      uniform(simulationProgram, "u_gridSize"),
      gridWidth,
      gridHeight,
    );
    renderPass(target, gridWidth, gridHeight, simulationProgram.program);
    swapState();
  };

  const stamp = (now: number) => {
    const source = currentState();
    const target = nextState();

    setTextureUnit(gl, 0, source.texture);
    gl.useProgram(stampProgram.program);
    gl.uniform1i(uniform(stampProgram, "u_state"), 0);
    gl.uniform2i(uniform(stampProgram, "u_gridSize"), gridWidth, gridHeight);
    gl.uniform2f(
      uniform(stampProgram, "u_center"),
      pointer.col + 0.5,
      pointer.row + 0.5,
    );
    gl.uniform1f(uniform(stampProgram, "u_radius"), BRUSH_RADIUS);
    gl.uniform1f(
      uniform(stampProgram, "u_hue"),
      (((now % HUE_PERIOD_MS) / HUE_PERIOD_MS) * 255 || 1) / 255,
    );
    gl.uniform1i(uniform(stampProgram, "u_active"), 1);
    renderPass(target, gridWidth, gridHeight, stampProgram.program);
    swapState();
  };

  const colorize = () => {
    if (!colorTarget || !sharpTarget || !smoothTarget) return;

    setTextureUnit(gl, 0, currentState().texture);
    setTextureUnit(gl, 1, paletteTexture);
    gl.useProgram(colorProgram.program);
    gl.uniform1i(uniform(colorProgram, "u_state"), 0);
    gl.uniform1i(uniform(colorProgram, "u_palette"), 1);
    gl.uniform2i(uniform(colorProgram, "u_gridSize"), gridWidth, gridHeight);
    gl.uniform2f(uniform(colorProgram, "u_resolution"), gridWidth, gridHeight);
    renderPass(colorTarget, gridWidth, gridHeight, colorProgram.program);

    gl.uniform2f(
      uniform(colorProgram, "u_resolution"),
      canvasWidth,
      canvasHeight,
    );
    renderPass(sharpTarget, canvasWidth, canvasHeight, colorProgram.program);

    setTextureUnit(gl, 0, colorTarget.texture);
    gl.useProgram(copyProgram.program);
    gl.uniform1i(uniform(copyProgram, "u_image"), 0);
    renderPass(smoothTarget, canvasWidth, canvasHeight, copyProgram.program);
  };

  const blur = () => {
    if (!smoothTarget || !insideBlurTargets || !outsideBlurTargets) return;

    const runBlurStack = (
      stack: StackTuning,
      targets: [TextureTarget, TextureTarget],
    ) => {
      const iterations = Math.max(0, Math.floor(stack.blurStrength));
      if (iterations === 0) {
        setTextureUnit(gl, 0, smoothTarget!.texture);
        gl.useProgram(copyProgram.program);
        gl.uniform1i(uniform(copyProgram, "u_image"), 0);
        renderPass(targets[1], canvasWidth, canvasHeight, copyProgram.program);
        return;
      }

      gl.useProgram(blurProgram.program);
      gl.uniform1i(uniform(blurProgram, "u_image"), 0);
      gl.uniform1f(uniform(blurProgram, "u_radius"), stack.blurRadius);
      gl.uniform2f(
        uniform(blurProgram, "u_resolution"),
        canvasWidth,
        canvasHeight,
      );

      let sourceTexture = smoothTarget!.texture;
      for (let index = 0; index < iterations; index++) {
        setTextureUnit(gl, 0, sourceTexture);
        gl.uniform2f(uniform(blurProgram, "u_direction"), 1, 0);
        renderPass(targets[0], canvasWidth, canvasHeight, blurProgram.program);

        setTextureUnit(gl, 0, targets[0].texture);
        gl.uniform2f(uniform(blurProgram, "u_direction"), 0, 1);
        renderPass(targets[1], canvasWidth, canvasHeight, blurProgram.program);
        sourceTexture = targets[1].texture;
      }
    };

    runBlurStack(tuning.inside, insideBlurTargets);
    runBlurStack(tuning.outside, outsideBlurTargets);
  };

  const composite = () => {
    if (!insideBlurTargets || !outsideBlurTargets) return;

    const panelCount = updatePanelRects();
    setTextureUnit(gl, 0, sharpTarget!.texture);
    setTextureUnit(gl, 1, insideBlurTargets[1].texture);
    setTextureUnit(gl, 2, outsideBlurTargets[1].texture);
    setTextureUnit(gl, 3, paletteTexture);
    gl.useProgram(compositeProgram.program);
    gl.uniform1i(uniform(compositeProgram, "u_sharp"), 0);
    gl.uniform1i(uniform(compositeProgram, "u_inBlur"), 1);
    gl.uniform1i(uniform(compositeProgram, "u_outBlur"), 2);
    gl.uniform1i(uniform(compositeProgram, "u_outBlur2"), 2);
    gl.uniform1i(uniform(compositeProgram, "u_palette"), 3);
    gl.uniform2f(
      uniform(compositeProgram, "u_resolution"),
      canvasWidth,
      canvasHeight,
    );
    const deviceScale = canvasWidth / cssWidth;
    gl.uniform1f(
      uniform(compositeProgram, "u_inMinBlockSize"),
      Math.max(0.25, tuning.inside.smallestBlock) * deviceScale,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_inMaxBlockSize"),
      Math.max(tuning.inside.largestBlock, tuning.inside.smallestBlock) *
        deviceScale,
    );
    gl.uniform1i(uniform(compositeProgram, "u_inLevels"), tuning.inside.levels);
    gl.uniform1f(
      uniform(compositeProgram, "u_inDetailThreshold"),
      tuning.inside.detailThreshold,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_inDitherStrength"),
      tuning.inside.ditherStrength,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_inEdgeBias"),
      tuning.inside.edgeBias,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_inHueDrift"),
      tuning.inside.hueDrift,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_inStreakStrength"),
      tuning.inside.streakStrength,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_outMinBlockSize"),
      Math.max(0.25, tuning.outside.smallestBlock) * deviceScale,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_outMaxBlockSize"),
      Math.max(tuning.outside.largestBlock, tuning.outside.smallestBlock) *
        deviceScale,
    );
    gl.uniform1i(
      uniform(compositeProgram, "u_outLevels"),
      tuning.outside.levels,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_outDetailThreshold"),
      tuning.outside.detailThreshold,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_outDitherStrength"),
      tuning.outside.ditherStrength,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_outEdgeBias"),
      tuning.outside.edgeBias,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_outHueDrift"),
      tuning.outside.hueDrift,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_outStreakStrength"),
      tuning.outside.streakStrength,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_outGlowStrength"),
      tuning.outside.blurStrength,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_outGlowThreshold"),
      tuning.outsideGlow.threshold,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_outGlowSoftness"),
      tuning.outsideGlow.softness,
    );
    gl.uniform1f(
      uniform(compositeProgram, "u_outGlowBoost"),
      tuning.outsideGlow.boost,
    );
    gl.uniform4fv(uniform(compositeProgram, "u_panelRects"), panelRects);
    gl.uniform1i(uniform(compositeProgram, "u_panelCount"), panelCount);
    renderPass(null, canvasWidth, canvasHeight, compositeProgram.program);
  };

  const frame = (now: number) => {
    rafId = window.requestAnimationFrame(frame);

    if (document.visibilityState === "hidden") {
      return;
    }

    if (
      canvasWidth !==
        Math.round(
          cssWidth * Math.min(window.devicePixelRatio || 1, MAX_DPR),
        ) ||
      canvasHeight !==
        Math.round(
          cssHeight * Math.min(window.devicePixelRatio || 1, MAX_DPR),
        ) ||
      cssWidth !== Math.round(window.innerWidth) ||
      cssHeight !== Math.round(window.innerHeight)
    ) {
      resizeTargets();
    }

    simulate();

    if (pointer.active) {
      stamp(now);
    }

    renderEmitters();
    injectEmitters();

    colorize();
    blur();
    composite();
  };

  document.addEventListener("mousemove", (event) => {
    updatePointer(event.clientX, event.clientY);
  });

  document.addEventListener("mouseleave", () => {
    pointer.active = false;
  });

  document.addEventListener(
    "touchstart",
    (event) => {
      event.preventDefault();
      const touch = event.touches.item(0);
      if (touch) {
        updatePointer(touch.clientX, touch.clientY);
      }
    },
    { passive: false },
  );

  document.addEventListener(
    "touchmove",
    (event) => {
      event.preventDefault();
      const touch = event.touches.item(0);
      if (touch) {
        updatePointer(touch.clientX, touch.clientY);
      }
    },
    { passive: false },
  );

  document.addEventListener("touchend", () => {
    pointer.active = false;
  });

  window.addEventListener("blur", () => {
    pointer.active = false;
  });

  gl.disable(gl.DEPTH_TEST);
  gl.disable(gl.BLEND);
  gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);

  resizeTargets();
  rafId = window.requestAnimationFrame(frame);

  return {
    destroy() {
      window.cancelAnimationFrame(rafId);
      deleteTextureTarget(gl, stateTargets?.[0] ?? null);
      deleteTextureTarget(gl, stateTargets?.[1] ?? null);
      deleteTextureTarget(gl, colorTarget);
      deleteTextureTarget(gl, sharpTarget);
      deleteTextureTarget(gl, smoothTarget);
      deleteTextureTarget(gl, insideBlurTargets?.[0] ?? null);
      deleteTextureTarget(gl, insideBlurTargets?.[1] ?? null);
      deleteTextureTarget(gl, outsideBlurTargets?.[0] ?? null);
      deleteTextureTarget(gl, outsideBlurTargets?.[1] ?? null);
      deleteTextureTarget(gl, emitterTarget);
      gl.deleteTexture(paletteTexture);
      gl.deleteVertexArray(vao);
      gl.deleteProgram(simulationProgram.program);
      gl.deleteProgram(stampProgram.program);
      gl.deleteProgram(injectProgram.program);
      gl.deleteProgram(colorProgram.program);
      gl.deleteProgram(copyProgram.program);
      gl.deleteProgram(blurProgram.program);
      gl.deleteProgram(compositeProgram.program);
    },
  };
}
