website/src/lib/webgl-background.ts

1539 lines
43 KiB
TypeScript

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 TOUCH_DRAG_THRESHOLD = 8;
let touchDragActive = false;
let touchStartX = 0;
let touchStartY = 0;
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) => {
const touch = event.touches.item(0);
if (touch) {
touchDragActive = false;
touchStartX = touch.clientX;
touchStartY = touch.clientY;
updatePointer(touch.clientX, touch.clientY);
}
});
document.addEventListener(
"touchmove",
(event) => {
const touch = event.touches.item(0);
if (!touch) {
return;
}
if (!touchDragActive) {
const distance = Math.hypot(
touch.clientX - touchStartX,
touch.clientY - touchStartY,
);
if (distance < TOUCH_DRAG_THRESHOLD) {
return;
}
touchDragActive = true;
}
event.preventDefault();
updatePointer(touch.clientX, touch.clientY);
},
{ passive: false },
);
document.addEventListener("touchend", () => {
touchDragActive = false;
pointer.active = false;
});
document.addEventListener("touchcancel", () => {
touchDragActive = false;
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);
},
};
}