figure.js

2.5 kB · javascript · 92 lines

1import { canvas, ink } from 'mrlyjs/view';2import { dark, light } from '../kit/theme/theme.js';34const SIZE = [1024, 1024];5const scheme = matchMedia('(prefers-color-scheme: dark)');6const doors = new Map();78/* THEME */910function tint() {11  const set = document.documentElement.dataset.theme;12  return ink((set ? set === 'dark' : scheme.matches) ? dark : light);13}1415/* UNITS */1617function open(name, door, at) {18  if (!doors.has(name)) doors.set(name, door.default({ module_or_path: at(name) }));19  return doors.get(name);20}2122/* PLAY */2324export async function play(host, figure, at) {25  await Promise.all(Object.entries(figure.units ?? {}).map(([name, door]) => open(name, door, at)));26  const [width, height] = figure.size ?? SIZE;27  const span = figure.loop * 1000;28  const frames = figure.frames ?? 0;29  const start = figure.still ?? 0;30  const node = document.createElement('canvas');31  node.width = width;32  node.height = height;33  node.setAttribute('aria-hidden', 'true');34  const ctx = node.getContext('2d');35  let colors = tint();36  let seen = true;37  let live = false;38  let origin = 0;39  let spent = 0;40  let timer = 0;41  let frame = 0;42  const phase = () => ((live ? performance.now() - origin : spent) / span + start) % 1;43  const paint = () => {44    const now = phase();45    const step = Math.floor(now * frames);46    figure.default(canvas(ctx, width, height, colors.ground), colors, frames ? step / frames : now);47    return frames ? ((step + 1) / frames - now) * span : 0;48  };49  const halt = () => {50    if (!live) return;51    spent = performance.now() - origin;52    live = false;53    clearTimeout(timer);54    cancelAnimationFrame(frame);55  };56  const guard = (work) => {57    try {58      work();59    } catch {60      halt();61      node.remove();62    }63  };64  const tick = () =>65    guard(() => {66      const wait = paint();67      if (frames) timer = setTimeout(tick, Math.max(wait, 16));68      else frame = requestAnimationFrame(tick);69    });70  const wake = () => {71    if (!seen || document.hidden) return halt();72    if (live || !node.isConnected) return;73    live = true;74    origin = performance.now() - spent;75    tick();76  };77  const shade = () =>78    guard(() => {79      colors = tint();80      paint();81    });82  paint();83  host.append(node);84  new IntersectionObserver(([entry]) => {85    seen = entry.isIntersecting;86    wake();87  }).observe(host);88  scheme.addEventListener('change', shade);89  window.addEventListener('theme', shade);90  document.addEventListener('visibilitychange', wake);91  wake();92}