// Browser glue: turns a canvas and a window into a Platform. Impure by nature; not part of the exercises' logic. /** @import { Point, Platform } from './types' */ const PREVENT = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' ']) // m10: where the best score survives a reload. const BEST_KEY = 'pure-snake-best' /** * (m13, given) Where a mouse event hit the canvas, in canvas pixels, as whole numbers. * The page may show the canvas at another size than its `width` x `height` (CSS scaling, zoom) and draws a border * around it, so the event's window coordinates (`clientX`, `clientY`) are first made relative to the drawing area * (`getBoundingClientRect` minus the border, `clientLeft`/`clientTop`) and then scaled by the canvas size over its * shown size (`clientWidth`/`clientHeight`). * @type {(canvas: HTMLCanvasElement, clientX: number, clientY: number) => Point} */ export const canvasPoint = (canvas, clientX, clientY) => { const r = canvas.getBoundingClientRect() return { x: Math.floor(((clientX - r.left - canvas.clientLeft) * canvas.width) / canvas.clientWidth), y: Math.floor(((clientY - r.top - canvas.clientTop) * canvas.height) / canvas.clientHeight), } } /** @type {(canvas: HTMLCanvasElement, win: Window) => Platform} */ export const browserPlatform = (canvas, win) => { const ctx = /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d')) // Keys that scroll the page should only steer the game. const onKey = (cb) => (e) => { if (PREVENT.has(e.key)) e.preventDefault() cb(e.key) } return { fillRect: (x, y, w, h, color) => { ctx.fillStyle = color ctx.fillRect(x, y, w, h) }, fillText: (x, y, text, color, size, align) => { ctx.fillStyle = color ctx.font = `${size}px monospace` ctx.textAlign = align ctx.fillText(text, x, y) }, randomSeed: () => crypto.getRandomValues(new Uint32Array(1))[0], // Storage can be missing, blocked (privacy settings) or hold anything: all of that reads as 0, and nothing throws. loadBest: () => { try { // Only what saveBest writes counts: decimal digits, at most 2147483647 (so all three tracks read alike). const raw = win.localStorage.getItem(BEST_KEY) return typeof raw === 'string' && /^[0-9]{1,10}$/.test(raw) && Number(raw) <= 2147483647 ? Number(raw) : 0 } catch { return 0 } }, // A record that cannot be stored is lost; the game goes on. saveBest: (score) => { try { win.localStorage.setItem(BEST_KEY, String(score)) } catch { // ignored on purpose } }, // t is the number of milliseconds since the first frame. startClock: (onTime) => { const frame = (start) => (now) => { const t0 = start ?? now onTime(now - t0) win.requestAnimationFrame(frame(t0)) } win.requestAnimationFrame(frame(undefined)) }, onKeyDown: (cb) => win.addEventListener('keydown', onKey(cb)), onKeyUp: (cb) => win.addEventListener('keyup', onKey(cb)), // m13: clicks on the canvas (not the window), converted to canvas pixels. onClick: (cb) => canvas.addEventListener('click', (e) => { const p = canvasPoint(canvas, e.clientX, e.clientY) cb(p.x, p.y) }), } }