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'; const ROBO_RADIUS = 25; 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, ROBO_RADIUS, 0, 2 * Math.PI); ctx.fill(); } function renderArena(robos, now=0) { // interpolation factor for smoother movement independent of tick // rate, but never tween more than 1 tick. const canvas = document.querySelector(CANVAS_SELECTOR); const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); robos.forEach(robo => { const delta = robo.lastTick ? now - robo.lastTick : 0; const timeScale = Math.min(delta / MS_PER_TICK, 1.0); let [x, y] = [robo.x, robo.y]; if (delta > 0) { const [velx, vely] = [ robo.speedx, robo.speedy ].map(x => timeScale * x); [x, y] = clamp(ROBO_RADIUS, robo.x + velx, robo.y + vely, canvas.width, canvas.height); } renderRobo(ctx, x, y); }); } async function loaded() { Inspector.register(); const canvas = document.querySelector(CANVAS_SELECTOR); const robos = [{ worker: new Worker('robo.mjs', { type: 'module' }), lastTick: undefined, x: ROBO_RADIUS + Math.floor(Math.random() * (canvas.width - ROBO_RADIUS)), y: ROBO_RADIUS + Math.floor(Math.random() * (canvas.height - ROBO_RADIUS)), heading: 0, speed: 0, speedx: 0, speedy: 0, }]; robos.forEach((robo, i) => { robo.worker.onmessage = e => { const { kind, res, trans } = e.data; switch (kind) { case 'compile': if (res) { renderArena(robos); document.querySelectorAll(Inspector.name)[i].render(trans, true); } break; case 'tick': robo.lastTick = document.timeline.currentTime; [robo.x, robo.y] = clamp(ROBO_RADIUS, robo.x + robo.speedx, robo.y + robo.speedy, canvas.width, canvas.height); renderArena(robos); robo.heading = trans.vars.heading; robo.speed = trans.vars.speed; [robo.speedx, robo.speedy] = [ Math.cos(2 * Math.PI * robo.heading / 360), Math.sin(2 * Math.PI * robo.heading / 360) ].map(x => robo.speed * x); document.querySelectorAll(Inspector.name)[i].render(trans); break; default: console.error('invalid message from robo worker', e.data); } }; robo.worker.onerror = e => { console.error('error in roboWorker', e); }; }); document.querySelectorAll(Inspector.name).forEach((elt, i) => { elt.addEventListener(Inspector.compileRequest, e => { console.debug('compiling', e.detail.text); robos[i].worker.postMessage({ kind: 'compile', text: e.detail.text }); }); }); document.querySelector(TICK_BUTTON_SELECTOR).onclick = e => { console.debug('tick clicked', e); robos.forEach(robo => robo.worker.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; renderArena(robos, t); 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); robos.forEach(robo => 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'; } } } document.addEventListener('DOMContentLoaded', loaded);