You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

195 lines
6.2 KiB
JavaScript

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.floor(10 / tenFrameTime).toFixed(0)
gameState.frameTimes = []
const pagesUsed = gameExports.getPagesUsed()
document.getElementById('frameTimes').innerHTML =
`Frames per second: ${fps}<br/>
Milliseconds per frame: ${averageFrame}<br/>
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}"`)
}