master
AdityaAngajala Publish 0.3.0 6d39e2e 2mo ago
// Runs the REAL engine against a recording canvas on a synthetic clock, and
// asks the only question that matters for "popping": on how many frames is a
// route visible while it is still incomplete? A route that only ever shows up
// at full length popped, however good its schedule was.
//
// usage: node checks/frames.mjs [engine.js]      (default: the reveal engine)
import { copyFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const here = dirname(fileURLToPath(import.meta.url));
const source = process.argv[2] || join(here, "..", "circuit-background-reveal.js");
// Node reads .js as CommonJS without a package.json, so import a .mjs copy.
const enginePath = join(tmpdir(), `reveal-engine-${process.pid}.mjs`);
copyFileSync(source, enginePath);

const layers = new Map(); // canvas -> name
let nextId = 0;

// Every stroke/fill is recorded against the canvas it landed on. Path lengths
// are accumulated so we can compare a partial route with a complete route.
function makeContext(canvas) {
  const ctx = {
    canvas,
    _path: [],
    _len: 0,
    globalAlpha: 1,
    globalCompositeOperation: "source-over",
    strokeStyle: "#000",
    fillStyle: "#000",
    lineWidth: 1,
    lineCap: "butt",
    lineJoin: "miter",
    setTransform() {},
    save() {},
    restore() {},
    clearRect() {
      canvas.ops.push({ op: "clear" });
    },
    fillRect() {},
    beginPath() {
      ctx._path = [];
      ctx._len = 0;
    },
    moveTo(x, y) {
      ctx._path = [[x, y]];
    },
    lineTo(x, y) {
      const p = ctx._path[ctx._path.length - 1];
      if (p) ctx._len += Math.hypot(x - p[0], y - p[1]);
      ctx._path.push([x, y]);
    },
    arc() {
      ctx._path.push(["arc"]);
    },
    stroke() {
      if (ctx._len > 0) canvas.ops.push({ op: "stroke", len: ctx._len, alpha: ctx.globalAlpha });
    },
    fill() {
      canvas.ops.push({ op: "fill", alpha: ctx.globalAlpha });
    },
    drawImage(src) {
      canvas.ops.push({ op: "draw", from: layers.get(src) || "?", alpha: ctx.globalAlpha });
    },
  };
  return ctx;
}

function makeCanvas() {
  const canvas = {
    _id: nextId++,
    width: 0,
    height: 0,
    style: {},
    ops: [],
    getContext: () => canvas._ctx,
  };
  canvas._ctx = makeContext(canvas);
  return canvas;
}

const noopEl = () => {
  const el = {
    style: {},
    children: [],
    dataset: {},
    setAttribute() {},
    getAttribute: () => null,
    removeAttribute() {},
    appendChild(c) {
      el.children.push(c);
      return c;
    },
    append(...c) {
      el.children.push(...c);
    },
    insertBefore(c) {
      el.children.unshift(c);
      return c;
    },
    get firstChild() {
      return el.children[0] || null;
    },
    prepend(c) {
      el.children.unshift(c);
      return c;
    },
    removeChild() {},
    remove() {},
    replaceChildren() {
      el.children = [];
    },
    addEventListener() {},
    removeEventListener() {},
    querySelector: () => null,
    querySelectorAll: () => [],
    getBoundingClientRect: () => ({ left: 0, top: 0, width: 1400, height: 900 }),
    clientWidth: 1400,
    clientHeight: 900,
    cloneNode: () => noopEl(),
    contains: () => false,
  };
  return el;
};

let clock = 0;
const rafQueue = [];
globalThis.performance = { now: () => clock };
globalThis.ResizeObserver = class {
  observe() {}
  disconnect() {}
};
globalThis.document = {
  hidden: false,
  createElement(tag) {
    if (tag === "canvas") {
      const c = makeCanvas();
      layers.set(c, `canvas#${c._id}`);
      return c;
    }
    return noopEl();
  },
  createElementNS: () => noopEl(),
  addEventListener() {},
  removeEventListener() {},
};
globalThis.window = {
  devicePixelRatio: 1,
  requestAnimationFrame(fn) {
    rafQueue.push(fn);
    return rafQueue.length;
  },
  cancelAnimationFrame() {},
  setTimeout: () => 0,
  clearTimeout() {},
  setInterval: () => 0,
  clearInterval() {},
  matchMedia: () => ({ matches: false, addEventListener() {}, removeEventListener() {} }),
  getComputedStyle: () => ({ getPropertyValue: () => "#0d1117" }),
};
globalThis.getComputedStyle = globalThis.window.getComputedStyle;
globalThis.devicePixelRatio = 1;
globalThis.Element = class Element {};
globalThis.SVGElement = class SVGElement extends Element {};

const { createCircuitBackground } = await import(enginePath);

const mount = Object.assign(new globalThis.Element(), noopEl());
const circuit = createCircuitBackground(mount, { seed: "nitrogen", reveal: "flood", animate: false, ...(process.env.COOL ? { revealCool: Number(process.env.COOL) } : {}) });

// The visible canvas is the one the engine prepends to the mount.
const visible = mount.children.find((c) => c.ops);
layers.set(visible, "VISIBLE");

// Name the offscreen layers by the order the engine creates them:
// settled, heat A, heat B, tip  (see ensureRevealCanvas)
const offscreen = [...layers.keys()].filter((c) => c !== visible && c.ops);
const names = ["settled", "heatA", "heatB", "tip"];
offscreen.slice(-4).forEach((c, i) => layers.set(c, names[i] || `extra${i}`));

const FRAME = 16.7;
const seen = new Map(); // route -> {partial, whole}
let frames = 0;
let compositedFrom = new Set();

while (rafQueue.length && frames < 700) {
  const batch = rafQueue.splice(0, rafQueue.length);
  clock += FRAME;
  for (const c of layers.keys()) c.ops.length = 0;
  for (const fn of batch) fn(clock);
  frames += 1;

  // which layers actually reached the screen this frame?
  const drawn = new Set(visible.ops.filter((o) => o.op === "draw" && o.alpha > 0.001).map((o) => o.from));
  drawn.forEach((d) => compositedFrom.add(d));

  // stroke length that reached the screen, split by layer
  for (const [canvas, name] of layers) {
    if (canvas === visible || !drawn.has(name)) continue;
    const len = canvas.ops.filter((o) => o.op === "stroke").reduce((a, o) => a + o.len, 0);
    const rec = seen.get(name) || { frames: 0, len: 0 };
    if (len > 0) rec.frames += 1;
    rec.len += len;
    seen.set(name, rec);
  }
}

console.log(`engine: ${source}`);
console.log(`frames simulated: ${frames}`);
console.log(`layers ever composited to the visible canvas: ${[...compositedFrom].sort().join(", ") || "(none)"}`);
const never = names.filter((n) => !compositedFrom.has(n));
console.log(`layers drawn into but NEVER composited: ${never.join(", ") || "(none)"}`);
for (const [name, rec] of seen) {
  console.log(`  ${name}: painted on ${rec.frames} frames, ${Math.round(rec.len)}px of stroke reached the screen`);
}