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(); } }