frame.js

3.8 kB · javascript · 124 lines

1import { palette } from '../palette.js';2import { dark as NIGHT, light as DAY } from '../theme.js';34/* RANDOM */56export function rng(seed) {7  if (seed === undefined || seed === null) return Math.random;8  let s = (seed >>> 0) || 0x9e3779b9;9  return () => {10    s ^= s << 13;11    s ^= s >>> 17;12    s ^= s << 5;13    s >>>= 0;14    return s / 4294967296;15  };16}1718export function pick(rand, list) {19  return list[Math.floor(rand() * list.length)];20}2122/* COLOUR */2324export function rgb(value) {25  const text = String(value).trim();26  if (text.startsWith('#')) {27    const hex = text.length < 7 ? [...text.slice(1, 4)].map((c) => c + c).join('') : text.slice(1, 7);28    const n = Number.parseInt(hex, 16);29    return [(n >> 16) & 255, (n >> 8) & 255, n & 255];30  }31  const parts = text.match(/-?\d+(\.\d+)?/g);32  return parts ? parts.slice(0, 3).map(Number) : [0, 0, 0];33}3435export function veil(color, alpha) {36  const [r, g, b] = rgb(color);37  return `rgba(${r}, ${g}, ${b}, ${alpha})`;38}3940export function paints(canvas) {41  const night = typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches;42  const base = night ? NIGHT : DAY;43  const css = typeof getComputedStyle === 'function' && canvas ? getComputedStyle(canvas) : null;44  const read = (name, fallback) => (css && css.getPropertyValue(name).trim()) || fallback;45  const paper = read('--art', read('--ground', base.ground ?? palette.white));46  const accent = read('--accent', base.accent ?? palette.blue);47  return { paper, accent };48}4950/* LIFE */5152export function run(canvas, make, opts = {}) {53  const rand = rng(opts.seed);54  const still = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;55  let skin = paints(canvas);56  const view = { rand, look: () => skin, still, w: 1, h: 1, dpr: 1 };57  const size = () => {58    const dpr = Math.min(globalThis.devicePixelRatio || 1, 2);59    const w = Math.max(1, Math.round((canvas.clientWidth || 300) * dpr));60    const h = Math.max(1, Math.round((canvas.clientHeight || 150) * dpr));61    if (w === view.w && h === view.h && dpr === view.dpr) return false;62    view.dpr = dpr;63    view.w = w;64    view.h = h;65    canvas.width = w;66    canvas.height = h;67    return true;68  };69  size();70  const saver = make(canvas, view, opts);71  const watched = typeof IntersectionObserver === 'function';72  let timer = 0;73  let frame = 0;74  let seen = !watched;75  const loop = () => {76    saver.draw();77    frame = requestAnimationFrame(loop);78  };79  const play = () => {80    if (still || timer || frame) return;81    if (saver.every) timer = setInterval(() => saver.draw(), saver.every);82    else frame = requestAnimationFrame(loop);83  };84  const halt = () => {85    if (timer) clearInterval(timer);86    if (frame) cancelAnimationFrame(frame);87    timer = 0;88    frame = 0;89  };90  const wake = () => (seen && !document.hidden ? play() : halt());91  const paint = () => {92    skin = paints(canvas);93    saver.theme?.();94    if (still) saver.draw();95  };96  const grow = () => {97    if (!size()) return;98    saver.size?.();99    if (still) saver.draw();100  };101  const spy = ([entry]) => {102    seen = entry.isIntersecting;103    wake();104  };105  const eye = watched ? new IntersectionObserver(spy) : null;106  const tape = typeof ResizeObserver === 'function' ? new ResizeObserver(grow) : null;107  const shade = typeof matchMedia === 'function' ? matchMedia('(prefers-color-scheme: dark)') : null;108  eye?.observe(canvas);109  tape?.observe(canvas);110  shade?.addEventListener('change', paint);111  window.addEventListener('theme', paint);112  document.addEventListener('visibilitychange', wake);113  if (still) saver.draw();114  else wake();115  return () => {116    halt();117    eye?.disconnect();118    tape?.disconnect();119    shade?.removeEventListener('change', paint);120    window.removeEventListener('theme', paint);121    document.removeEventListener('visibilitychange', wake);122    saver.stop?.();123  };124}