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}