Back to Freeze Frame Response
SOURCE / PINNED RELEASE

Made of little things.

Freeze Frame Response

Release
3e45de6048a3…
Author-recorded commit
d04885dad630…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-ba52e30d5f2

Archive hash verified: e747ab4f9e35a11f…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/main.ts
import './napplet-settings.js';
import { storage } from '@napplet/sdk';
import { runtimeHasDomain } from './domain-availability.js';
import { DEVICE_LABEL, IncidentEngine, type Stats } from './physics.js';
import { GROUND_Y, LEVELS, WORLD_WIDTH, type DeviceType } from './levels.js';
import { renderScene, worldFromClient, type SceneMode } from './render.js';
import './styles.css';

const RECORD_KEY = 'ffr-service-record-v1';
const PAD = (value: number): string => String(value).padStart(2, '0');

interface Point {
  x: number;
  y: number;
}

interface DragState {
  x: number;
  y: number;
  currentX: number;
  currentY: number;
  moved: boolean;
}

interface SavedRecord {
  damage: number;
  perfect: boolean;
}

const elements = {
  caseNo: requireElement<HTMLElement>('#caseNo'),
  caseName: requireElement<HTMLElement>('#caseName'),
  pedStat: requireElement<HTMLElement>('#pedStat'),
  debrisStat: requireElement<HTMLElement>('#debrisStat'),
  kitStat: requireElement<HTMLElement>('#kitStat'),
  canvas: requireElement<HTMLCanvasElement>('#scene'),
  freezeBadge: requireElement<HTMLElement>('#freezeBadge'),
  freezeLabel: requireElement<HTMLElement>('#freezeLabel'),
  freezeClock: requireElement<HTMLElement>('#freezeClock'),
  toast: requireElement<HTMLElement>('#toast'),
  overlay: requireElement<HTMLElement>('#overlay'),
  briefButton: requireElement<HTMLButtonElement>('#briefButton'),
  retryButton: requireElement<HTMLButtonElement>('#retryButton'),
  tiltButton: requireElement<HTMLButtonElement>('#tiltButton'),
  tiltValue: requireElement<HTMLElement>('#tiltValue'),
  undoButton: requireElement<HTMLButtonElement>('#undoButton'),
  resumeButton: requireElement<HTMLButtonElement>('#resumeButton'),
  live: requireElement<HTMLElement>('#live'),
};

function getContext2d(): CanvasRenderingContext2D {
  const ctx = elements.canvas.getContext('2d');
  if (!ctx) throw new Error('Canvas is unavailable');
  return ctx;
}

const context = getContext2d();

let engine: IncidentEngine;
let levelIndex = 0;
let phase: SceneMode = 'frozen';
let selected: DeviceType = 'trampoline';
let angle = 0;
let hover: Point | null = null;
let drag: DragState | null = null;
let keyboardMode = false;
let keyCursor: Point = { x: 480, y: 380 };
let overlayOpen = false;
let reportShown = false;
let pixelRatio = 1;
let lastFrame = performance.now();
let lastHud = 0;
let toastTimer = 0;
let records: Record<string, SavedRecord> = {};
let hasPlayed = false;
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');

function requireElement<T extends HTMLElement>(selector: string): T {
  const element = document.querySelector<T>(selector);
  if (!element) throw new Error(`Missing required element: ${selector}`);
  return element;
}

function createEngine(index: number): IncidentEngine {
  const next = new IncidentEngine(LEVELS[index]);
  next.preSim();
  return next;
}

function remaining(type: DeviceType): number {
  return Math.max(0, LEVELS[levelIndex].kit[type] - engine.countDevices(type));
}

function firstAvailable(): DeviceType {
  const order: DeviceType[] = ['trampoline', 'funnel', 'cushion'];
  return order.find((type) => remaining(type) > 0) ?? 'trampoline';
}

function snap(value: number): number {
  const step = Math.PI / 12;
  return Math.round(value / step) * step;
}

function degrees(): string {
  const value = Math.round((angle * 180) / Math.PI);
  const normal = ((value % 360) + 540) % 360 - 180;
  return `${normal}°`;
}

function insideWorld(point: Point): boolean {
  return (
    point.x > 4 &&
    point.x < WORLD_WIDTH - 4 &&
    point.y > 8 &&
    point.y < GROUND_Y - 4
  );
}

function toast(message: string): void {
  elements.toast.textContent = message;
  elements.toast.classList.add('show');
  elements.live.textContent = message;
  window.clearTimeout(toastTimer);
  toastTimer = window.setTimeout(() => elements.toast.classList.remove('show'), 2400);
}

function syncTools(): void {
  const order: DeviceType[] = ['trampoline', 'funnel', 'cushion'];
  for (const type of order) {
    const button = requireElement<HTMLButtonElement>(`.tool[data-tool="${type}"]`);
    const count = requireElement<HTMLElement>(`[data-count="${type}"]`);
    const left = remaining(type);
    count.textContent = String(left);
    button.disabled = phase !== 'frozen' || left <= 0;
    button.setAttribute('aria-pressed', String(type === selected));
    button.classList.toggle('selected', type === selected);
  }
  const canEdit = phase === 'frozen';
  elements.tiltButton.disabled = !canEdit;
  elements.undoButton.disabled = !canEdit || engine.devices.length === 0;
  elements.resumeButton.disabled = phase !== 'frozen';
  elements.tiltValue.textContent = degrees();
  elements.canvas.classList.toggle('placing', canEdit && remaining(selected) > 0);
}

function updateHud(): void {
  const stats = engine.getStats();
  const level = LEVELS[levelIndex];
  elements.caseNo.textContent = `CASE ${PAD(levelIndex + 1)} / ${PAD(LEVELS.length)}`;
  elements.caseName.textContent = level.name;
  elements.pedStat.textContent = `${stats.pedsSaved}/${stats.pedsTotal}`;
  elements.debrisStat.textContent = `${stats.debrisSorted}/${stats.debrisTotal}`;
  const kit = remaining('trampoline') + remaining('funnel') + remaining('cushion');
  elements.kitStat.textContent = String(kit);
}

function syncPhase(): void {
  if (phase === 'frozen') {
    elements.freezeBadge.className = 'freeze-badge frozen';
    elements.freezeLabel.textContent = 'TIME SUSPENDED';
    elements.freezeBadge.hidden = false;
  } else if (phase === 'running') {
    elements.freezeBadge.className = 'freeze-badge running';
    elements.freezeLabel.textContent = 'TIME RUNNING';
    elements.freezeBadge.hidden = false;
  } else {
    elements.freezeBadge.className = 'freeze-badge settled';
    elements.freezeLabel.textContent = 'INCIDENT SETTLED';
    elements.freezeBadge.hidden = false;
  }
  syncTools();
  updateHud();
}

function selectTool(type: DeviceType, announce = false): void {
  if (remaining(type) <= 0) {
    toast(`${DEVICE_LABEL[type]} stock is empty. Undo a placement or try another item.`);
    return;
  }
  selected = type;
  if (announce) toast(`${DEVICE_LABEL[type]} selected. ${remaining(type)} in the van.`);
  syncTools();
}

function placeDevice(type: DeviceType, x: number, y: number): boolean {
  if (phase !== 'frozen') {
    toast('Time is already moving. Retry the case to re-plan.');
    return false;
  }
  if (remaining(type) <= 0) {
    toast(`${DEVICE_LABEL[type]} stock is empty.`);
    return false;
  }
  if (!insideWorld({ x, y })) {
    toast('Outside the frozen frame. The paperwork does not cover that.');
    return false;
  }
  engine.placeDevice(type, x, y, angle);
  const left = remaining(type);
  toast(`${DEVICE_LABEL[type]} placed at ${degrees()}. ${left} left.`);
  if (left <= 0) selected = firstAvailable();
  syncTools();
  updateHud();
  return true;
}

function tilt(): void {
  if (phase !== 'frozen') return;
  angle = snap(angle + Math.PI / 12);
  elements.tiltValue.textContent = degrees();
  elements.live.textContent = `Placement tilt ${degrees()}.`;
}

function undo(): void {
  if (phase !== 'frozen') return;
  const removed = engine.removeLastDevice();
  if (!removed) {
    toast('Nothing to recover. The van is still full.');
    return;
  }
  selected = removed.type;
  toast(`${DEVICE_LABEL[removed.type]} recovered.`);
  syncTools();
  updateHud();
}

function resume(): void {
  if (phase !== 'frozen') return;
  drag = null;
  hover = null;
  phase = 'running';
  hasPlayed = true;
  engine.beginLive();
  syncPhase();
  toast('Time resumed. Please do not cheer.');
}

function startLevel(index: number, withBrief = false): void {
  levelIndex = ((index % LEVELS.length) + LEVELS.length) % LEVELS.length;
  engine = createEngine(levelIndex);
  phase = 'frozen';
  reportShown = false;
  drag = null;
  hover = null;
  keyboardMode = false;
  keyCursor = { x: 480, y: 380 };
  angle = 0;
  selected = firstAvailable();
  closeOverlay();
  syncPhase();
  toast(`Case ${PAD(levelIndex + 1)}: ${LEVELS[levelIndex].name}. Time is suspended.`);
  if (withBrief) showBrief();
}

function briefHtml(): string {
  const level = LEVELS[levelIndex];
  const closed = Object.values(records).filter((record) => record.perfect).length;
  const recordText =
    closed > 0
      ? `Service record: ${closed} of ${LEVELS.length} cases closed.`
      : 'No closed cases on file. The shredder is ready.';
  return `
    <article class="card brief-card">
      <p class="card-kicker">FROZEN ASSETS DIVISION · CASE ${PAD(levelIndex + 1)} / ${PAD(LEVELS.length)}</p>
      <h2>${level.name}</h2>
      <p class="tagline">${level.tagline}</p>
      <p class="brief-copy">${level.briefing}</p>
      <div class="legend">
        <span><i class="swatch mattress"></i>Mattress · pedestrians</span>
        <span><i class="swatch masonry"></i>Masonry bin</span>
        <span><i class="swatch furniture"></i>Furniture bin</span>
        <span><i class="swatch glass"></i>Glass bin</span>
      </div>
      <div class="howto"><b>Frozen protocol</b> Pick a kit item. Tap the scene to place it, drag from the anchor to aim, or steer the cursor with the arrow keys and press <kbd>Enter</kbd>. <kbd>R</kbd> tilts, <kbd>Z</kbd> undoes, <kbd>Space</kbd> resumes. Dotted lines show where objects are already going.</div>
      <p class="hint">${level.hint}</p>
      <div class="card-actions">
        <span class="service-record">${recordText}</span>
        <button type="button" class="primary-btn" data-action="begin">${hasPlayed ? 'Back to the scene' : 'Begin inspection'}</button>
      </div>
    </article>`;
}

function showBrief(): void {
  if (phase === 'running') {
    toast('The incident is already in motion. Let the report finish.');
    return;
  }
  if (phase === 'report') return;
  elements.overlay.innerHTML = briefHtml();
  elements.overlay.hidden = false;
  overlayOpen = true;
  const begin = elements.overlay.querySelector<HTMLButtonElement>('[data-action="begin"]');
  begin?.addEventListener('click', () => {
    closeOverlay();
    toast('Frame held. Place kit, then resume time.');
  });
  begin?.focus();
}

function closeOverlay(): void {
  elements.overlay.hidden = true;
  elements.overlay.innerHTML = '';
  overlayOpen = false;
}

function damageEstimate(stats: Stats): number {
  const raw =
    stats.debrisMissed * 1850 +
    stats.debrisContaminated * 3200 +
    stats.pedsLost * 14500 +
    stats.volatileBlasts * 6800 +
    stats.devicesUsed * 120;
  return Math.max(250, Math.round(raw / 50) * 50);
}

function reportNotes(stats: Stats, perfect: boolean): string[] {
  const notes: string[] = [];
  if (stats.pedsLost === 0) {
    notes.push('All pedestrians delivered to soft furnishings. Nobody signed the waiver.');
  } else {
    notes.push(
      `${stats.pedsLost} pedestrian(s) remain at large and are listed as "taking a walk".`,
    );
  }
  if (stats.debrisMissed === 0 && stats.debrisContaminated === 0) {
    notes.push('Recycling targets met. The bins have been wiped down with pride.');
  } else {
    if (stats.debrisMissed > 0) {
      notes.push(`${stats.debrisMissed} item(s) missed the bins entirely.`);
    }
    if (stats.debrisContaminated > 0) {
      notes.push(`${stats.debrisContaminated} item(s) contaminated a material stream.`);
    }
  }
  if (stats.volatileBlasts > 0) {
    notes.push(
      `${stats.volatileBlasts} gas canister(s) joined the meeting. Chain-reaction forms are in triplicate.`,
    );
  } else if (stats.volatiles > 0) {
    notes.push(`All ${stats.volatiles} gas canister(s) remained unopinionated.`);
  }
  if (stats.devicesUsed === 0) {
    notes.push('No equipment was deployed. The department respects the confidence.');
  }
  if (perfect) {
    notes.push('Commendation filed directly into the shredder.');
  } else if (stats.pedsSaved > 0) {
    notes.push('At least one mattress justified its budget.');
  }
  return notes;
}

function reportHtml(stats: Stats, perfect: boolean, damage: number): string {
  const level = LEVELS[levelIndex];
  const status = perfect
    ? 'CASE CLOSED'
    : stats.pedsSaved === stats.pedsTotal
      ? 'ACCEPTABLE LOSS'
      : 'REVIEW REQUIRED';
  const stampClass = perfect ? 'closed' : stats.pedsSaved === stats.pedsTotal ? 'partial' : 'review';
  const notes = reportNotes(stats, perfect)
    .map((note) => `<li>${note}</li>`)
    .join('');
  const money = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
    maximumFractionDigits: 0,
  }).format(damage);
  const nextIsRestart = levelIndex + 1 >= LEVELS.length;
  return `
    <article class="card report-card">
      <p class="card-kicker">INCIDENT REPORT · CASE ${PAD(levelIndex + 1)} / ${PAD(LEVELS.length)}</p>
      <div class="report-head">
        <h2>${level.name}</h2>
        <span class="stamp ${stampClass}">${status}</span>
      </div>
      <div class="report-grid">
        <span>Pedestrians delivered</span><b>${stats.pedsSaved} / ${stats.pedsTotal}</b>
        <span>Debris processed</span><b>${stats.debrisSorted} / ${stats.debrisTotal}</b>
        <span>Contaminated streams</span><b>${stats.debrisContaminated}</b>
        <span>Pedestrians unaccounted</span><b>${stats.pedsLost}</b>
        <span>Canister detonations</span><b>${stats.volatileBlasts}</b>
        <span>Equipment deployed</span><b>${stats.devicesUsed}</b>
      </div>
      <div class="damage">
        <span>Estimated damage</span>
        <strong>${money}</strong>
      </div>
      <ul class="report-notes">${notes}</ul>
      <p id="reportSaveNote" class="save-note">Filing result with the local service record…</p>
      <div class="card-actions">
        <button type="button" class="small-btn" data-action="retry">Retry case</button>
        <button type="button" class="primary-btn" data-action="next">${nextIsRestart ? 'Restart case load' : 'Next case file'}</button>
      </div>
    </article>`;
}

function finalizeReport(): void {
  if (reportShown) return;
  reportShown = true;
  phase = 'report';
  syncPhase();
  const stats = engine.getStats();
  const perfect =
    stats.pedsSaved === stats.pedsTotal &&
    stats.debrisSorted === stats.debrisTotal &&
    stats.debrisContaminated === 0;
  const damage = damageEstimate(stats);
  elements.overlay.innerHTML = reportHtml(stats, perfect, damage);
  elements.overlay.hidden = false;
  overlayOpen = true;

  const retry = elements.overlay.querySelector<HTMLButtonElement>('[data-action="retry"]');
  const next = elements.overlay.querySelector<HTMLButtonElement>('[data-action="next"]');
  retry?.addEventListener('click', () => startLevel(levelIndex));
  next?.addEventListener('click', () => startLevel(levelIndex + 1, true));
  next?.focus();

  void fileResult(levelIndex, damage, perfect).then((note) => {
    if (phase !== 'report') return;
    const target = elements.overlay.querySelector<HTMLElement>('#reportSaveNote');
    if (target) target.textContent = note;
  });
}

async function fileResult(index: number, damage: number, perfect: boolean): Promise<string> {
  if (!runtimeHasDomain('storage')) {
    return 'Local service record unavailable in this runtime. The result exists only in this form.';
  }
  try {
    const raw = await storage.getItem(RECORD_KEY);
    let stored: Record<string, SavedRecord> = {};
    if (raw) {
      const parsed: unknown = JSON.parse(raw);
      if (parsed && typeof parsed === 'object') stored = parsed as Record<string, SavedRecord>;
    }
    const key = LEVELS[index].id;
    const existing = stored[key];
    const better = !existing || damage < existing.damage || (perfect && !existing.perfect);
    if (better) {
      stored[key] = { damage, perfect };
      await storage.setItem(RECORD_KEY, JSON.stringify(stored));
      records = stored;
      return 'Result filed. The service record has been updated.';
    }
    records = stored;
    return 'Previous attempt had lower damage. Kept the original form.';
  } catch {
    return 'Record could not be filed. The form remains optimistic.';
  }
}

async function loadRecords(): Promise<void> {
  if (!runtimeHasDomain('storage')) return;
  try {
    const raw = await storage.getItem(RECORD_KEY);
    if (!raw) return;
    const parsed: unknown = JSON.parse(raw);
    if (parsed && typeof parsed === 'object') {
      records = parsed as Record<string, SavedRecord>;
    }
  } catch {
    records = {};
  }
}

function pointFromEvent(event: PointerEvent): Point | null {
  const rect = elements.canvas.getBoundingClientRect();
  if (rect.width === 0 || rect.height === 0) return null;
  return worldFromClient(
    event.clientX - rect.left,
    event.clientY - rect.top,
    rect.width,
    rect.height,
  );
}

elements.canvas.addEventListener('pointerdown', (event) => {
  if (phase !== 'frozen' || overlayOpen) return;
  if (event.pointerType === 'mouse' && event.button !== 0) return;
  const point = pointFromEvent(event);
  if (!point || !insideWorld(point)) return;
  if (remaining(selected) <= 0) {
    toast(`No ${DEVICE_LABEL[selected]} left. Undo a placement or choose another item.`);
    return;
  }
  event.preventDefault();
  keyboardMode = false;
  drag = {
    x: point.x,
    y: point.y,
    currentX: point.x,
    currentY: point.y,
    moved: false,
  };
  hover = point;
  elements.canvas.setPointerCapture(event.pointerId);
});

elements.canvas.addEventListener('pointermove', (event) => {
  const point = pointFromEvent(event);
  hover = point && insideWorld(point) ? point : null;
  if (point) keyboardMode = false;
  if (!drag || !point) return;
  drag.currentX = point.x;
  drag.currentY = point.y;
  const dx = point.x - drag.x;
  const dy = point.y - drag.y;
  if (Math.hypot(dx, dy) > 16) {
    drag.moved = true;
    angle = snap(Math.atan2(dy, dx));
    elements.tiltValue.textContent = degrees();
  }
  event.preventDefault();
});

function finishPointer(event: PointerEvent): void {
  if (!drag) return;
  const point = pointFromEvent(event);
  const anchor = drag.moved ? { x: drag.x, y: drag.y } : point;
  if (anchor) placeDevice(selected, anchor.x, anchor.y);
  drag = null;
  elements.canvas.classList.remove('placing');
  if (elements.canvas.hasPointerCapture(event.pointerId)) {
    elements.canvas.releasePointerCapture(event.pointerId);
  }
  syncTools();
}

elements.canvas.addEventListener('pointerup', finishPointer);
elements.canvas.addEventListener('pointercancel', () => {
  drag = null;
  elements.canvas.classList.remove('placing');
});

document.querySelectorAll<HTMLButtonElement>('.tool').forEach((button) => {
  button.addEventListener('click', () => {
    const type = button.dataset.tool as DeviceType | undefined;
    if (!type) return;
    selectTool(type, true);
  });
});

elements.tiltButton.addEventListener('click', tilt);
elements.undoButton.addEventListener('click', undo);
elements.resumeButton.addEventListener('click', resume);
elements.retryButton.addEventListener('click', () => startLevel(levelIndex));
elements.briefButton.addEventListener('click', showBrief);

window.addEventListener('keydown', (event) => {
  if (event.metaKey || event.ctrlKey || event.altKey) return;
  const key = event.key.toLowerCase();
  if (overlayOpen) {
    if (key === 'escape' && phase !== 'report') closeOverlay();
    return;
  }
  if (phase === 'report') return;
  if (key.startsWith('arrow')) {
    event.preventDefault();
    keyboardMode = true;
    hover = null;
    const step = event.shiftKey ? 6 : 20;
    if (key === 'arrowleft') keyCursor.x -= step;
    else if (key === 'arrowright') keyCursor.x += step;
    else if (key === 'arrowup') keyCursor.y -= step;
    else if (key === 'arrowdown') keyCursor.y += step;
    keyCursor.x = Math.min(WORLD_WIDTH - 10, Math.max(10, keyCursor.x));
    keyCursor.y = Math.min(GROUND_Y - 10, Math.max(20, keyCursor.y));
    elements.live.textContent = `Placement cursor at ${Math.round(keyCursor.x)}, ${Math.round(keyCursor.y)}.`;
    return;
  }
  if (key === '1') selectTool('trampoline');
  else if (key === '2') selectTool('funnel');
  else if (key === '3') selectTool('cushion');
  else if (key === 'r') tilt();
  else if (key === 'z') undo();
  else if (key === 'enter') {
    event.preventDefault();
    if (phase === 'frozen') {
      keyboardMode = true;
      placeDevice(selected, keyCursor.x, keyCursor.y);
    }
  } else if (key === ' ') {
    event.preventDefault();
    resume();
  } else if (key === 'escape') {
    keyboardMode = false;
  }
});

function resizeCanvas(): void {
  const rect = elements.canvas.getBoundingClientRect();
  const dpr = Math.min(2, window.devicePixelRatio || 1);
  const width = Math.max(1, Math.round(rect.width * dpr));
  const height = Math.max(1, Math.round(rect.height * dpr));
  if (elements.canvas.width !== width || elements.canvas.height !== height) {
    elements.canvas.width = width;
    elements.canvas.height = height;
  }
  pixelRatio = dpr;
}

function draw(now: number): void {
  const rect = elements.canvas.getBoundingClientRect();
  if (rect.width === 0 || rect.height === 0) return;
  const ghostPoint =
    phase === 'frozen' && !overlayOpen
      ? drag
        ? { x: drag.x, y: drag.y }
        : keyboardMode
          ? keyCursor
          : hover
      : null;
  renderScene(context, engine, rect.width, rect.height, pixelRatio, {
    mode: phase,
    selected,
    angle,
    ghost: ghostPoint && remaining(selected) > 0 ? ghostPoint : null,
    showHints: document.documentElement.dataset.trajectoryHints !== 'false',
    now: motionQuery.matches ? 0 : now,
  });
}

function frame(now: number): void {
  const dt = Math.min(0.05, Math.max(0, (now - lastFrame) / 1000));
  lastFrame = now;
  if (phase === 'running') {
    engine.advance(dt);
    const stats = engine.getStats();
    if (!reportShown && stats.allResolved && engine.liveElapsed > 0.95) {
      finalizeReport();
    }
  }
  if (now - lastHud > 120) {
    updateHud();
    lastHud = now;
  }
  if (phase === 'frozen') {
    elements.freezeClock.textContent = `T+${engine.elapsed.toFixed(2)}`;
  } else if (phase === 'running') {
    elements.freezeClock.textContent = `T+${engine.liveElapsed.toFixed(2)}`;
  } else {
    elements.freezeClock.textContent = 'FILED';
  }
  draw(now);
  window.requestAnimationFrame(frame);
}

elements.canvas.tabIndex = 0;
engine = createEngine(0);
selected = firstAvailable();
const observer = new ResizeObserver(() => resizeCanvas());
observer.observe(elements.canvas);
resizeCanvas();
void loadRecords().then(() => {
  const service = elements.overlay.querySelector<HTMLElement>('.service-record');
  if (service) {
    const closed = Object.values(records).filter((record) => record.perfect).length;
    service.textContent =
      closed > 0
        ? `Service record: ${closed} of ${LEVELS.length} cases closed.`
        : 'No closed cases on file. The shredder is ready.';
  }
});
syncPhase();
showBrief();
window.addEventListener('resize', resizeCanvas);
window.addEventListener('beforeunload', () => observer.disconnect());
window.requestAnimationFrame(frame);