summaryrefslogtreecommitdiffstats
path: root/site
diff options
context:
space:
mode:
authorbrian cully <bjc@spork.org>2025-12-23 21:47:59 -0500
committerbrian cully <bjc@spork.org>2025-12-23 21:47:59 -0500
commitfdd715f4e610ae68dcb5310c37cebfbf383b1a4c (patch)
treeced540d95376ba015703eff141c1219fa66c8711 /site
parentd1bc29694219a3d3d34e6dcc696ce249c7540fcd (diff)
downloadautomathon-fdd715f4e610ae68dcb5310c37cebfbf383b1a4c.tar.gz
automathon-fdd715f4e610ae68dcb5310c37cebfbf383b1a4c.zip
js: move arena stuff to module
Diffstat (limited to 'site')
-rw-r--r--site/arena.mjs79
-rw-r--r--site/index.html10
-rw-r--r--site/main.css2
-rw-r--r--site/main.mjs78
4 files changed, 97 insertions, 72 deletions
diff --git a/site/arena.mjs b/site/arena.mjs
new file mode 100644
index 0000000..c76f7dc
--- /dev/null
+++ b/site/arena.mjs
@@ -0,0 +1,79 @@
+const CANVAS_SELECTOR = 'canvas';
+const FPS_SELECTOR = '#fps';
+
+export default class extends HTMLElement {
+ static name = 'x-arena';
+ static register() {
+ console.debug('registering custom element', this.name, this);
+ self.customElements.define(this.name, this);
+ }
+ static radius = 25;
+
+ #canvas;
+ #ctx;
+ #lastTime;
+ tickMS;
+
+ connectedCallback() {
+ console.debug('connectedCallback', this);
+ this.#canvas = this.querySelector(CANVAS_SELECTOR);
+ this.#ctx = this.#canvas.getContext('2d');
+ }
+
+ invalidateFPS() {
+ document.querySelector(FPS_SELECTOR).textContent = 'n/a';
+ }
+
+ clamp(radius, x, y) {
+ const xx = Math.min(Math.max(radius, x), this.#canvas.width-radius);
+ const yy = Math.min(Math.max(radius, y), this.#canvas.height-radius);
+ return [xx, yy];
+ }
+
+ randStart(robo) {
+ const [x, y] = [this.#canvas.width, this.#canvas.height].map(len =>
+ this.constructor.radius + Math.floor(Math.random() * (len - 2*this.constructor.radius)));
+ return {...robo, x, y};
+ }
+
+ renderFPS(now) {
+ const delta = now - (this.#lastTime ?? now);
+ if (delta > 0) {
+ document.querySelector(FPS_SELECTOR).textContent = Math.floor(1_000 / delta);
+ }
+ }
+
+ render(robos, now=0) {
+ // we often get called with the same time stamp many times in
+ // a row due to fingerprint-reduction tech.
+ if (now === this.#lastTime) {
+ return;
+ }
+ this.renderFPS(now);
+ this.#lastTime = now;
+
+ this.#ctx.clearRect(0, 0, this.#canvas.width, this.#canvas.height);
+
+ robos.forEach(robo => {
+ // interpolation factor for smoother movement independent
+ // of tick rate, but never tween more than 1 tick.
+ const delta = robo.lastTick ? now - robo.lastTick : 0;
+ const timeScale = this.tickMS > 0 ? Math.min(delta / this.tickMS, 1.0) : 1;
+ let [x, y] = [robo.x, robo.y];
+ if (delta > 0) {
+ const [velx, vely] = [
+ robo.speedx, robo.speedy
+ ].map(x => timeScale * x);
+ [x, y] = this.clamp(this.constructor.radius, robo.x + velx, robo.y + vely);
+ }
+ this.#renderRobo(x, y);
+ });
+ }
+
+ #renderRobo(x, y) {
+ this.#ctx.fillStyle = 'rgb(200 0 0)';
+ this.#ctx.beginPath();
+ this.#ctx.arc(x, y, this.constructor.radius, 0, 2 * Math.PI);
+ this.#ctx.fill();
+ }
+}
diff --git a/site/index.html b/site/index.html
index 665411b..ace176c 100644
--- a/site/index.html
+++ b/site/index.html
@@ -13,9 +13,12 @@
<div class='flex-col'>
<section>
<h2>arena</h2>
- <canvas id='arena' width='300' height='300'>
- oh no! no canvas support!
- </canvas>
+ <x-arena>
+ <p>fps: <span id='fps'>n/a</span></p>
+ <canvas width='300' height='300'>
+ oh no! no canvas support!
+ </canvas>
+ </x-arena>
<div class='controls'>
<button id='tick'>
@@ -49,7 +52,6 @@
<option>60</option>
</select>
</div> <!-- controls -->
- fps: <span id='fps'>n/a</span>
</section> <!-- arena -->
<section>
diff --git a/site/main.css b/site/main.css
index d31bec3..7e7ed18 100644
--- a/site/main.css
+++ b/site/main.css
@@ -14,7 +14,7 @@ html {
flex: 1;
}
-#arena {
+x-arena canvas {
border: 1px solid orangered;
background-color: rgb(200 200 200);
}
diff --git a/site/main.mjs b/site/main.mjs
index fd9abb6..e2cdc3e 100644
--- a/site/main.mjs
+++ b/site/main.mjs
@@ -1,83 +1,38 @@
+import Arena from './arena.mjs';
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() {
+ Arena.register();
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,
- }];
+ }].map(r => document.querySelector(Arena.name).randStart(r));
robos.forEach((robo, i) => {
robo.worker.onmessage = e => {
const { kind, res, trans } = e.data;
switch (kind) {
case 'compile':
if (res) {
- renderArena(robos);
+ document.querySelector(Arena.name).render(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.x, robo.y] = document.querySelector(Arena.name).clamp(Arena.radius, robo.x + robo.speedx, robo.y + robo.speedy);
+ document.querySelector(Arena.name).render(robos, robo.lastTick);
robo.heading = trans.vars.heading;
robo.speed = trans.vars.speed;
@@ -111,35 +66,24 @@ async function loaded() {
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(Arena.name).tickMS = 1_000 / e.target.value;
}
- document.querySelector(TICK_RATE_SELECTOR).onchange({target: { value: TICKS_PER_SECOND }});
+ document.querySelectorAll(TICK_RATE_SELECTOR).forEach(elt => elt.onchange({ target: elt }));
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';
- }
+ document.querySelector(Arena.name).render(robos, t);
}
function timeout() {
if (!blinkenRun) {
return
}
- self.setTimeout(timeout, MS_PER_TICK);
+ self.setTimeout(timeout, document.querySelector(Arena.name).tickMS);
robos.forEach(robo => robo.worker.postMessage({ kind: 'tick' }));
}
@@ -158,7 +102,7 @@ async function loaded() {
e.currentTarget.classList.remove('blinken');
e.currentTarget.classList.add('halten');
e.currentTarget.querySelector('title').textContent = 'blinken';
- document.querySelector(FPS_SELECTOR).textContent = 'n/a';
+ document.querySelector(Arena.name).invalidateFPS();
}
}
}