let gameState = { running: false, lastReported: null, frameTimes: [], frames: 0, width: 0, height: 0, canvas: null, gl: null, shader: null, gameExports: null, } const initialMessage = "Click the board above to start simulation" export async function setup(params, wasmModule) { const { canvas: canvasSelector, fragmentSrc, vertexSrc } = params; gameState.gameExports = wasmModule.exports const canvas = gameState.canvas = document.querySelector(canvasSelector) const gl = gameState.gl = gameState.canvas.getContext("webgl2") const vertexShader = gl.createShader(gl.VERTEX_SHADER) gl.shaderSource(vertexShader, await fetch(vertexSrc.path).then(res => res.text())) gl.compileShader(vertexShader) const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER) gl.shaderSource(fragmentShader, await fetch(fragmentSrc.path).then(res => res.text())) gl.compileShader(fragmentShader) const program = gameState.shader = gl.createProgram() gl.attachShader(program, vertexShader) gl.attachShader(program, fragmentShader) gl.linkProgram(program) gl.useProgram(program) const renderQuad = new Float32Array([-1, -1, -1, 1, 1, 1, -1, -1, 1, 1, 1, -1]) const vertexBuffer = gl.createBuffer() gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer) gl.bufferData(gl.ARRAY_BUFFER, renderQuad, gl.STATIC_DRAW) const positionLocation = gl.getAttribLocation(program, "position") gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0) gl.enableVertexAttribArray(positionLocation); const pixelSize = parseInt(canvas.getAttribute('data-pixelsize') || '2') gameState.width = Math.floor(parseInt(canvas.width) / pixelSize) gameState.height = Math.floor(parseInt(canvas.height) / pixelSize) const texture = gl.createTexture() gl.activeTexture(gl.TEXTURE0) gl.bindTexture(gl.TEXTURE_2D, texture) gl.pixelStorei( gl.UNPACK_ALIGNMENT, 1 ) 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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); initialize() onThemeChange() drawBoard() const frameTimesElem = document.getElementById('frameTimes') const resetButton = document.getElementById('reset') frameTimesElem.innerText = initialMessage gameState.canvas.addEventListener('click', () => { if (!gameState.running) { if (frameTimesElem.innerText === initialMessage) frameTimesElem.innerText = 'Starting...' gameState.running = true frameLoop() } else { gameState.running = false; gameState.frameTimes = [] gameState.lastReported = null } }) resetButton.addEventListener('click', () => { gameState.running = false gameState.frameTimes = [] gameState.lastReported = null frameTimesElem.innerText = initialMessage initialize() drawBoard() }) } export async function onThemeChange() { const {gl, shader} = gameState const drawColorString = getComputedStyle(gameState.canvas).getPropertyValue('color') const drawColor = parseColorString(drawColorString) const backgroundColorString = getComputedStyle(gameState.canvas).getPropertyValue('--background') const backgroundColor = parseColorString(backgroundColorString) const drawColorLocation = gl.getUniformLocation(shader, "drawColor") gl.uniform3fv(drawColorLocation, drawColor) const backgroundColorLocation = gl.getUniformLocation(shader, "backgroundColor") gl.uniform3fv(backgroundColorLocation, backgroundColor) drawBoard() } export async function cleanup() { gameState.running = false } function initialize() { const { gameExports, width, height } = gameState gameExports.initializeBoard(width, height) for (let row = 0; row < height; row++) { for (let column = 0; column < width; column++) { const filled = Math.random() > .5; gameExports.setValueAtPosition(row, column, filled ? 1 : 0) } } } function frameLoop() { const { gameExports, running, frameTimes } = gameState if (!gameState.lastReported) gameState.lastReported = performance.now() if (!running) return const beforeTick = performance.now() gameExports.tick() const afterTick = performance.now() drawBoard() const afterDraw = performance.now() // Push raw frame time frameTimes.push(afterDraw - beforeTick) if (frameTimes.length >= 10) { const averageFrame = (frameTimes.reduce((a, b) => a + b) / frameTimes.length).toFixed(2); const lastReported = gameState.lastReported const current = gameState.lastReported = performance.now() const tenFrameTime = (current - lastReported) / 1000 const fps = Math.round(10 / tenFrameTime).toFixed(0) gameState.frameTimes = [] const pagesUsed = gameExports.getPagesUsed() document.getElementById('frameTimes').innerHTML = `Frames per second: ${fps}
Milliseconds per frame: ${averageFrame}
Memory allocated: ${pagesUsed} pages (${pagesUsed * 64} KiB)` } const tickMS = (afterTick - beforeTick).toFixed(3).padStart(7, ' ') const drawMS = (afterDraw - afterTick).toFixed(3).padStart(7, ' ') console.log(`tick took ${tickMS}ms, draw took ${drawMS}ms`) requestAnimationFrame(frameLoop) } function drawBoard() { const { gameExports, width, height, gl } = gameState const wasmBuffer = gameExports.shared_memory.buffer const boardPointer = gameExports.getBoardPointer() const boardLength = gameExports.getBoardLength() const boardData = (new Uint8Array(wasmBuffer)).slice(boardPointer, boardPointer + boardLength) gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, width, height, 0, gl.ALPHA, gl.UNSIGNED_BYTE, boardData) gl.drawArrays(gl.TRIANGLES, 0, 6) } function parseColorString(string) { const rgbMatch = /rgb\(([0-9]+), ?([0-9]+), ?([0-9]+)\)/i.exec(string) if (rgbMatch) return rgbMatch.slice(1).map(n => parseInt(n, 10) / 255) const hexMatch = /^\#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(string) if (hexMatch) return hexMatch.slice(1).map(n => parseInt(n, 16) / 255) const shortHexMatch = /^\#([0-9a-f]){3}$/i.exec(string) if (shortHexMatch) return shortHexMatch.slice(1).map(n => parseInt(n, 16) / 16) throw new Error(`Cannot parse color string "${string}"`) }