app
Circuit Trace Generators
Public Made by Adomby adom
Two dependency-free, seeded circuit-background generators: a seamless torus tile with ambient or progressive reveal, and the v9 continuous field with obstacles, scroll reveal, and five ambient systems.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
// Timing audit of the reveal schedule, offline. Replicates buildSchedule's math
// against the real generated tile, then simulates the release loop at a fixed
// frame clock and asks the only question that matters: how many frames does
// each route spend PARTIALLY drawn? One frame or fewer == it popped.
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 enginePath = join(tmpdir(), `reveal-tile-${process.pid}.mjs`);
copyFileSync(join(here, "..", "circuit-background-reveal.js"), enginePath);
const { generateCircuitTile, DEFAULTS } = await import(enginePath);
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
function hashSeed(input) {
let h = 2166136261;
for (let i = 0; i < input.length; i += 1) {
h ^= input.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return h >>> 0;
}
function mulberry32(seed) {
let a = seed >>> 0;
return () => {
a = (a + 0x6d2b79f5) >>> 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const REVEAL_JITTER = 70;
const REVEAL_CATCHUP = 40;
const cfg = { ...DEFAULTS, reveal: "flood", seed: "nitrogen" };
const tile = generateCircuitTile(cfg);
const scale = cfg.scale;
const viewWidth = 1400;
const viewHeight = 900;
// buildRevealGeometry
const geometry = tile.traces.map((trace) => {
const points = trace.points.map(([x, y]) => [x * scale, y * scale]);
let len = 0;
for (let i = 1; i < points.length; i += 1) {
len += Math.hypot(points[i][0] - points[i - 1][0], points[i][1] - points[i - 1][1]);
}
const anchor = trace.points[0] || trace.vias[0] || [0, 0];
return {
length: len,
drawable: points.length >= 2,
vias: trace.vias,
anchor: [anchor[0] * scale, anchor[1] * scale],
};
});
const tileWidth = tile.width * scale;
const tileHeight = tile.height * scale;
const tilesAcross = Math.ceil(viewWidth / tileWidth);
const tilesDown = Math.ceil(viewHeight / tileHeight);
const topOffset = -tile.height / 2;
const focusX = viewWidth / 2;
const focusY = viewHeight / 2;
const revealRandom = mulberry32(hashSeed(`${cfg.seed}:reveal:${cfg.reveal}`));
// flood depths are unavailable offline; distance-only ordering ("bloom"-ish).
// Ordering shifts which run sits where, not the size of the numbers below.
const runs = [];
for (let row = -1; row <= tilesDown; row += 1) {
for (let column = -1; column <= tilesAcross; column += 1) {
const x = column * tileWidth;
const y = row * tileHeight + topOffset * scale;
if (x > viewWidth || y > viewHeight) continue;
if (x + tileWidth < 0 || y + tileHeight < 0) continue;
geometry.forEach((entry) => {
const ax = clamp(x + entry.anchor[0], 0, viewWidth);
const ay = clamp(y + entry.anchor[1], 0, viewHeight);
runs.push({
geometry: entry,
metric: Math.hypot(ax - focusX, ay - focusY),
jitter: revealRandom(),
nudge: revealRandom(),
});
});
}
}
runs.sort((a, b) => a.metric - b.metric || a.jitter - b.jitter);
const speed = (cfg.revealSpeed * scale) / 1000;
let longest = 0;
for (const run of runs) longest = Math.max(longest, run.geometry.length / speed);
const lengths = geometry.map((e) => e.length).sort((a, b) => a - b);
const median = Math.max(1, lengths[Math.floor(lengths.length / 2)] || 1);
const padWeight = median * 0.15;
let total = 0;
for (const run of runs) total += run.geometry.length + padWeight;
const window_ = Math.max(0, cfg.revealDuration - longest);
let cursor = 0;
for (const run of runs) {
run.duration = run.geometry.length / speed;
run.delay = Math.max(0, (cursor / total) * window_ + (run.nudge - 0.5) * REVEAL_JITTER);
cursor += run.geometry.length + padWeight;
}
let stretch = Infinity;
for (const run of runs) {
if (run.delay > 0) stretch = Math.min(stretch, (cfg.revealDuration - run.duration) / run.delay);
}
if (Number.isFinite(stretch) && stretch > 0) for (const run of runs) run.delay *= stretch;
const instances = runs.filter((r) => r.geometry.drawable).sort((a, b) => a.delay - b.delay);
const pct = (arr, p) => arr[Math.min(arr.length - 1, Math.floor(arr.length * p))];
const durs = instances.map((r) => r.duration).sort((a, b) => a - b);
console.log(`runs total ${runs.length}, drawable ${instances.length}, stretch ${stretch.toFixed(3)}`);
console.log(`longest route ${longest.toFixed(0)}ms, window ${window_.toFixed(0)}ms, padWeight ${padWeight.toFixed(1)}px`);
console.log(
`route duration ms: min ${durs[0].toFixed(1)} p10 ${pct(durs, 0.1).toFixed(1)} ` +
`median ${pct(durs, 0.5).toFixed(1)} p90 ${pct(durs, 0.9).toFixed(1)} max ${durs[durs.length - 1].toFixed(1)}`,
);
for (const f of [16.7, 33, 50]) {
const n = durs.filter((d) => d <= f).length;
console.log(` routes finishing within one ${f}ms frame: ${n} (${((100 * n) / durs.length).toFixed(1)}%)`);
}
// how far out of delay-order was the release queue BEFORE the sort?
let inversions = 0;
let worstBacklog = 0;
let peak = -Infinity;
const unsorted = runs.filter((r) => r.geometry.drawable);
for (let i = 0; i < unsorted.length; i += 1) {
if (unsorted[i].delay < peak) {
inversions += 1;
worstBacklog = Math.max(worstBacklog, peak - unsorted[i].delay);
}
peak = Math.max(peak, unsorted[i].delay);
}
console.log(
`delay-order before sorting: ${inversions}/${unsorted.length} out of order (${((100 * inversions) / unsorted.length).toFixed(1)}%), ` +
`worst run held back ${worstBacklog.toFixed(0)}ms behind the queue head`,
);
const steps = [];
for (let i = 1; i < instances.length; i += 1) steps.push(instances[i].delay - instances[i - 1].delay);
steps.sort((a, b) => a - b);
console.log(`consecutive delay step: median ${pct(steps, 0.5).toFixed(2)}ms vs jitter amplitude ${(REVEAL_JITTER * stretch).toFixed(0)}ms`);
// --- simulate the real release + growth loop at a fixed frame clock ---
function simulate(frameMs, label) {
for (const r of instances) {
r.drawn = 0;
r.startedAt = undefined;
r.frames = 0;
r.bornAt = 0;
}
const live = new Set();
let next = 0;
let time = 0;
const bursts = [];
const end = cfg.revealDuration + longest + 1000;
while (time < end) {
let released = 0;
while (next < instances.length && instances[next].delay <= time) {
const e = instances[next];
e.startedAt = Math.max(e.delay, time - REVEAL_CATCHUP);
live.add(e);
next += 1;
released += 1;
}
bursts.push(released);
for (const run of [...live]) {
const grown = Math.min(run.geometry.length, (time - run.startedAt) * speed);
if (run.drawn === 0) run.bornAt = grown;
if (grown > 0 && grown < run.geometry.length) run.frames += 1;
run.drawn = grown;
if (grown >= run.geometry.length) live.delete(run);
}
time += frameMs;
}
const popped = instances.filter((r) => r.frames <= 1);
const bornWhole = instances.filter((r) => r.bornAt >= r.geometry.length - 0.01);
bursts.sort((a, b) => b - a);
const fr = instances.map((r) => r.frames).sort((a, b) => a - b);
console.log(
`\n@${frameMs}ms frames (${label}): partially-drawn frames per route — ` +
`min ${fr[0]} p10 ${pct(fr, 0.1)} median ${pct(fr, 0.5)} p90 ${pct(fr, 0.9)}`,
);
console.log(
` routes animated in <=1 frame: ${popped.length}/${instances.length} ` +
`(${((100 * popped.length) / instances.length).toFixed(1)}%) born fully grown: ${bornWhole.length}`,
);
console.log(` biggest single-frame release bursts: ${bursts.slice(0, 6).join(", ")}`);
}
simulate(16.7, "60fps");
simulate(33, "30fps");
simulate(60, "hitching");