import Inspector from './inspector.mjs'; // simulation speed let TICKS_PER_SECOND = 15; let MS_PER_TICK = 1_000 / TICKS_PER_SECOND; // one per page const CANVAS_SELECTOR = '#arena'; const TICK_BUTTON_SELECTOR = '#tick'; const TICK_RATE_SELECTOR = '#tick-rate-select'; const RUN_BUTTON_SELECTOR = '#run'; const FPS_SELECTOR = '#fps'; function clamp(radius, x, y, width, height) { const xx = Math.min(Math.max(radius, x), width-radius); const yy = Math.min(Math.max(radius, y), height-radius); return [xx, yy]; } function renderRobo(ctx, x, y) { ctx.fillStyle = 'rgb(200 0 0)'; ctx.beginPath(); //ctx.arc(Math.floor(x), Math.floor(y), 25, 0, 2 * Math.PI); ctx.arc(x, y, 25, 0, 2 * Math.PI); ctx.fill(); } function renderArena(robos, delta=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); robos.forEach(robo => { let [x, y] = [robo.x, robo.y]; if (delta > 0) { const [velx, vely] = [ robo.speedx, robo.speedy ].map(x => timeScale * x); [x, y] = clamp(25, robo.x + velx, robo.y + vely, canvas.width, canvas.height); } renderRobo(ctx, x, y); }); } async function loaded() { const canvas = document.querySelector(CANVAS_SELECTOR); const roboWorker = new Worker('robo.mjs', { type: 'module' }); const robo = { worker: roboWorker, lastTick: undefined, x: 132, y: 25, heading: 0, speed: 0, speedx: 0, speedy: 0, }; roboWorker.onmessage = e => { const { kind, res, trans } = e.data; switch (kind) { case 'compile': if (res) { renderArena([robo]); document.querySelector('x-inspector').render(trans, true); } break; case 'tick': robo.lastTick = document.timeline.currentTime; const [x, y] = clamp(25, robo.x + robo.speedx, robo.y + robo.speedy, canvas.width, canvas.height); robo.x = x; robo.y = y; renderArena([robo]); robo.heading = trans.vars.heading; robo.speed = trans.vars.speed; const [speedx, speedy] = [ Math.cos(2 * Math.PI * robo.heading / 360), Math.sin(2 * Math.PI * robo.heading / 360) ].map(x => robo.speed * x); robo.speedx = speedx; robo.speedy = speedy; document.querySelector('x-inspector').render(trans); break; default: console.error('invalid message from robo worker', e.data); } }; roboWorker.onerror = e => { console.error('error in roboWorker', e); }; document.querySelector(TICK_BUTTON_SELECTOR).onclick = e => { console.debug('tick clicked', e); roboWorker.postMessage({ kind: 'tick' }); }; document.querySelector(TICK_RATE_SELECTOR).onchange = e => { console.debug('tick rate changed', e, Number(e.target.value)); TICKS_PER_SECOND = e.target.value; MS_PER_TICK = 1_000 / TICKS_PER_SECOND; } document.querySelector(TICK_RATE_SELECTOR).onchange({target: { value: TICKS_PER_SECOND }}); let blinkenRun = false; let lastTime = 0; function renderFrame(t) { if (!blinkenRun) { return; } self.requestAnimationFrame(renderFrame); // we often get called with the same time stamp many times // in a row due to fingerprint-reduction tech. const ms = t - lastTime; if (ms > 0) { lastTime = t; const delta = robo.lastTick ? t - robo.lastTick : 0; renderArena([robo], delta); const fps = document.querySelector(FPS_SELECTOR); fps.textContent = ms > 0 ? Math.floor(1_000 / ms) : '0'; } } function timeout() { if (!blinkenRun) { return } self.setTimeout(timeout, MS_PER_TICK); robo.worker.postMessage({ kind: 'tick' }); } document.querySelector(RUN_BUTTON_SELECTOR).onclick = e => { console.debug('blinken clicked', e); blinkenRun = !blinkenRun; if (blinkenRun) { e.currentTarget.classList.remove('halten'); e.currentTarget.classList.add('blinken'); e.currentTarget.querySelector('title').textContent = 'halten'; setTimeout(timeout); renderFrame(document.timeline.currentTime); } else { e.currentTarget.classList.remove('blinken'); e.currentTarget.classList.add('halten'); e.currentTarget.querySelector('title').textContent = 'blinken'; document.querySelector(FPS_SELECTOR).textContent = 'n/a'; } } Inspector.register(); document.querySelectorAll(Inspector.name).forEach(elt => { elt.addEventListener(Inspector.compileRequest, e => { console.debug('compiling', e.detail.text); roboWorker.postMessage({ kind: 'compile', text: e.detail.text }); }); }); } document.addEventListener('DOMContentLoaded', loaded);