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
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.round(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}"`)
|
|
}
|