import type { RgbaColor, ScreenRect, WebGlRendererContext } from "./types"; const darkChecker: RgbaColor = [0.82, 0.82, 0.86, 1]; const lightChecker: RgbaColor = [0.94, 0.94, 0.97, 1]; export type CheckerboardRenderer = { render(rect: ScreenRect, squareSize: number): void; dispose(): void; }; export function createCheckerboardRenderer(context: WebGlRendererContext): CheckerboardRenderer { const { gl } = context; const program = createProgram(gl); const positionLocation = gl.getAttribLocation(program, "a_position"); const canvasSizeLocation = gl.getUniformLocation(program, "u_canvasSize"); const originLocation = gl.getUniformLocation(program, "u_origin"); const squareSizeLocation = gl.getUniformLocation(program, "u_squareSize"); const darkColorLocation = gl.getUniformLocation(program, "u_darkColor"); const lightColorLocation = gl.getUniformLocation(program, "u_lightColor"); const positionBuffer = gl.createBuffer(); if ( positionLocation < 0 || !canvasSizeLocation || !originLocation || !squareSizeLocation || !darkColorLocation || !lightColorLocation || !positionBuffer ) { if (positionBuffer) gl.deleteBuffer(positionBuffer); gl.deleteProgram(program); throw new Error("Failed to create checkerboard renderer"); } return { render(rect, squareSize) { const drawRect = intersectScreenRectWithCanvas(rect, context.canvas); if (!drawRect) return; gl.enable(gl.SCISSOR_TEST); gl.scissor(drawRect.x, context.canvas.height - drawRect.y - drawRect.h, drawRect.w, drawRect.h); gl.disable(gl.BLEND); gl.useProgram(program); gl.uniform2f(canvasSizeLocation, context.canvas.width, context.canvas.height); gl.uniform2f(originLocation, rect.x, rect.y); gl.uniform1f(squareSizeLocation, Math.max(1, squareSize)); gl.uniform4fv(darkColorLocation, darkChecker); gl.uniform4fv(lightColorLocation, lightChecker); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, screenRectVertices(drawRect), gl.DYNAMIC_DRAW); gl.enableVertexAttribArray(positionLocation); gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.TRIANGLES, 0, 6); gl.disableVertexAttribArray(positionLocation); }, dispose() { gl.deleteBuffer(positionBuffer); gl.deleteProgram(program); }, }; } function intersectScreenRectWithCanvas(rect: ScreenRect, canvas: HTMLCanvasElement): ScreenRect | undefined { const x1 = Math.max(0, rect.x); const y1 = Math.max(0, rect.y); const x2 = Math.min(canvas.width, rect.x + rect.w); const y2 = Math.min(canvas.height, rect.y + rect.h); if (x2 <= x1 || y2 <= y1) return undefined; return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 }; } function screenRectVertices(rect: ScreenRect) { const x1 = rect.x; const x2 = rect.x + rect.w; const y1 = rect.y; const y2 = rect.y + rect.h; return new Float32Array([x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]); } function createProgram(gl: WebGL2RenderingContext) { const vertexShader = compileShader( gl, gl.VERTEX_SHADER, `#version 300 es in vec2 a_position; uniform vec2 u_canvasSize; out vec2 v_screenPosition; void main() { vec2 clip = vec2((a_position.x / u_canvasSize.x) * 2.0 - 1.0, 1.0 - (a_position.y / u_canvasSize.y) * 2.0); gl_Position = vec4(clip, 0.0, 1.0); v_screenPosition = a_position; }`, ); const fragmentShader = compileShader( gl, gl.FRAGMENT_SHADER, `#version 300 es precision highp float; uniform vec2 u_origin; uniform float u_squareSize; uniform vec4 u_darkColor; uniform vec4 u_lightColor; in vec2 v_screenPosition; out vec4 outColor; void main() { vec2 cell = floor((v_screenPosition - u_origin) / max(1.0, u_squareSize)); float parity = mod(cell.x + cell.y, 2.0); outColor = mix(u_darkColor, u_lightColor, parity); }`, ); const program = gl.createProgram(); if (!program) throw new Error("Failed to create checkerboard shader 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 checkerboard program link error"; gl.deleteProgram(program); throw new Error(message); } return program; } function compileShader(gl: WebGL2RenderingContext, type: number, source: string) { const shader = gl.createShader(type); if (!shader) throw new Error("Failed to create shader"); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { const message = gl.getShaderInfoLog(shader) ?? "Unknown shader compile error"; gl.deleteShader(shader); throw new Error(message); } return shader; }