From 3014a14b3a86681be15288f2495de6ae01196b42 Mon Sep 17 00:00:00 2001 From: Brian Cully Date: Tue, 16 Dec 2025 18:59:39 -0500 Subject: html: animate independently of tick rate, reorg a lot of html i should have done this in more commits, but i'm not going to go back and fix it now. --- site/main.mjs | 97 +++++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 67 insertions(+), 30 deletions(-) (limited to 'site/main.mjs') diff --git a/site/main.mjs b/site/main.mjs index e049dea..f4e1989 100644 --- a/site/main.mjs +++ b/site/main.mjs @@ -1,5 +1,29 @@ import init, { make_vm } from './wasm/automathon.js'; +// simulation speed +const TICKS_PER_SECOND = 12; +const MS_PER_TICK = 1_000 / TICKS_PER_SECOND; + +// one per page +const CANVAS_SELECTOR = '#arena canvas'; +const TICK_BUTTON_SELECTOR = '#tick'; +const BENCH_BUTTON_SELECTOR = '#bench'; +const BLINKEN_BUTTON_SELECTOR = '#blinken'; + +// one per bot +const SRC_SELECT_SELECTOR = '#src-select'; +const COMPILE_BUTTON_SELECTOR = '#compile'; +const WORDLIST_SELECTOR = '#wordlist'; +const STACK_SELECTOR = '#stack'; +const CALLSTACK_SELECTOR = '#callstack'; +const VARS_SELECTOR = '#vars'; +const SRC_SELECTOR = '#src'; +const IP_SELECTOR = '#wordlist .ip'; + +function selectorForIP(word, offset) { + return `#wordlist x-bytecode[x-index='${word}'] x-op[x-index='${offset}']`; +} + function wordlistElts(wordlist) { return wordlist.map((bc, i) => { const bcElt = document.createElement('x-bytecode'); @@ -14,10 +38,6 @@ function wordlistElts(wordlist) { }) } -function selectorForIP(word, offset) { - return `#wordlist x-bytecode[x-index='${word}'] x-op[x-index='${offset}']`; -} - function initWordlist() { const sel = selectorForIP(0, 0); document.querySelectorAll(sel).forEach(e => { @@ -26,7 +46,7 @@ function initWordlist() { } function renderStack(vm) { - document.querySelectorAll('#stack').forEach(e => { + document.querySelectorAll(STACK_SELECTOR).forEach(e => { while (e.lastChild) { e.removeChild(e.lastChild); } @@ -41,7 +61,7 @@ function renderStack(vm) { } function renderCallStack(vm) { - document.querySelectorAll('#callstack').forEach(e => { + document.querySelectorAll(CALLSTACK_SELECTOR).forEach(e => { while (e.lastChild) { e.removeChild(e.lastChild); } @@ -56,7 +76,7 @@ function renderCallStack(vm) { } function renderVars(vm) { - document.querySelectorAll('#vars').forEach(e => { + document.querySelectorAll(VARS_SELECTOR).forEach(e => { while (e.lastChild) { e.removeChild(e.lastChild); } @@ -80,13 +100,14 @@ function renderRobo(ctx, x, y) { ctx.fill(); } -function renderArena(robos) { - const canvas = document.querySelector('#arena'); +function renderArena(robos, delta=0.0) { + // interpolation factor for smoother movement independent of tick + // rate, but never tween more than 1 tick. + const timeScale = Math.min(delta / MS_PER_TICK, 1.0); + const canvas = document.querySelector(CANVAS_SELECTOR); const ctx = canvas.getContext('2d'); - //ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = 'rgb(200 200 200 / 5%)'; - ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.clearRect(0, 0, canvas.width, canvas.height); robos.forEach(robo => { let heading = robo.vm.heading(); @@ -94,7 +115,7 @@ function renderArena(robos) { let [velx, vely] = [ Math.cos(2 * Math.PI * heading / 360), Math.sin(2 * Math.PI * heading / 360) - ].map(x => speed * x); + ].map(x => timeScale * speed * x); renderRobo(ctx, robo.x, robo.y); robo.x += velx; @@ -109,7 +130,7 @@ CSS.highlights.set('exec', highlight); function renderTextHighlight(vm) { const ip = vm.ip(); const anno = vm.annotation_at(ip) - const src = document.querySelector('#src'); + const src = document.querySelector(SRC_SELECTOR); // this assumes the text node is the first child, maybe it isn't? highRange.setStart(src.childNodes[0], anno.start); @@ -122,7 +143,7 @@ function tick(robo) { } const { word, offset } = robo.vm.ip(); - document.querySelectorAll('#wordlist .ip').forEach(e => e.classList.remove('ip')); + document.querySelectorAll(IP_SELECTOR).forEach(e => e.classList.remove('ip')); const sel = selectorForIP(word, offset); document.querySelectorAll(sel).forEach(e => { e.classList.add('ip'); @@ -131,7 +152,6 @@ function tick(robo) { renderCallStack(robo.vm); renderVars(robo.vm); renderTextHighlight(robo.vm); - renderArena([robo]); } function loadForth(taintedPath) { @@ -148,7 +168,7 @@ function loadForth(taintedPath) { return resp.text() }) .then(text => { - document.querySelector('#src').textContent = text; + document.querySelector(SRC_SELECTOR).textContent = text; }) .catch(e => { console.error(`couldn't fetch ‘${path}’`, e); @@ -165,22 +185,22 @@ async function loaded() { vm: make_vm() }; - document.querySelectorAll('#src-select').forEach(async sel => { + document.querySelectorAll(SRC_SELECT_SELECTOR).forEach(async sel => { sel.onchange = _ => loadForth(sel.value); loadForth(sel.value); }); - document.querySelector('#compile').onclick = e => { + document.querySelector(COMPILE_BUTTON_SELECTOR).onclick = e => { console.debug('compile clicked', e); - let wordlistContainer = document.querySelector('#wordlist'); + let wordlistContainer = document.querySelector(WORDLIST_SELECTOR); while (wordlistContainer.lastChild) { console.debug('removing child', wordlistContainer.lastChild) wordlistContainer.removeChild(wordlistContainer.lastChild); } // always add a newline until i decide what to do with the parser. - const text = document.querySelector('#src').textContent + '\n'; + const text = document.querySelector(SRC_SELECTOR).textContent + '\n'; console.debug('compiling', text); const start = performance.now(); const res = robo.vm.compile(text); @@ -197,11 +217,8 @@ async function loaded() { renderArena([robo]); } }; - document.querySelector('#tick').onclick = e => { - console.debug('tick clicked', e); - tick(robo); - }; - document.querySelector('#bench').onclick = e => { + + document.querySelector(BENCH_BUTTON_SELECTOR).onclick = e => { console.debug('bench clicked', e); const start = performance.now(); let tickCount = 0; @@ -213,21 +230,41 @@ async function loaded() { console.info('result', robo.vm.stack()); }; + document.querySelector(TICK_BUTTON_SELECTOR).onclick = e => { + console.debug('tick clicked', e); + tick(robo); + renderArena([robo], MS_PER_TICK); + }; + let blinkenRun = false; - document.querySelector('#blinken').onclick = e => { + document.querySelector(BLINKEN_BUTTON_SELECTOR).onclick = e => { console.debug('blinken clicked', e); + + let lastTime; + function r(t, manual=false) { + if (blinkenRun && !manual) { + window.requestAnimationFrame(r); + } + const delta = (lastTime === undefined) ? 0 : t - lastTime; + lastTime = t; + if (delta > 0) { + renderArena([robo], delta); + } + } + blinkenRun = !blinkenRun; if (blinkenRun) { e.target.textContent = 'haltenblinken'; + r(document.timeline.currentTime); } else { e.target.textContent = 'blinken'; } - const frameRate = 30; const onTimeout = _ => { if (blinkenRun) { - tick(robo); - setTimeout(onTimeout, 1_000 / frameRate); + setTimeout(onTimeout, MS_PER_TICK); } + tick(robo); + r(document.timeline.currentTime, true); } setTimeout(onTimeout); } -- cgit v1.3