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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830
const SVG_NS = "http://www.w3.org/2000/svg";
// Geometry constants measured from the reference artwork (adom.inc hero):
// a 114 x 57 cell routing grid with a 150/19 ~= 7.895 unit pitch, 1.58 unit
// traces, and 3.34 / 1.76 annular vias sitting exactly on grid nodes.
const DEFAULTS = Object.freeze({
seed: "traces",
tileWidth: 900,
tileHeight: 450,
pitch: 150 / 19,
scale: 1000 / 900,
density: 1.05,
traceColor: "#151b25",
traceWidth: 1.58,
viaOuterRadius: 3.34,
viaInnerRadius: 1.76,
// --- generation tuning -----------------------------------------------
formations: 96, // terminal groups (via ladders/rows) per tile
busWidth: 1, // scales pins per formation => bundle width
turnPenalty: 0.55, // higher = longer, calmer runs
weave: 0.22, // preference for 2-lane spacing between neighbors
cramp: 0.4, // aversion to 1-lane spacing outside bundles
horizontalBias: 0.6, // 0.5 = isotropic, higher = more horizontal flow
netReach: 42, // max cells a net may span
jogBias: 5, // preferred endpoint misalignment => staircase jogs
stubChance: 0, // leftover pins that get short escape stubs
inlineViaChance: 0.03, // per-step chance of a mid-trace via
combs: 8, // max tap-comb motifs per tile
signalColors: [
["#00e6dc", 0.6],
["#64abff", 0.25],
["#c5b3ff", 0.15],
],
signals: true,
interactive: true,
signalInterval: 240, // passive spawn cadence — a fresh signal appears near the cursor this often
maxSignals: 12, // fill target for the ambient fountain (maxBurstSignals is the hard ceiling)
spawnRadius: 110, // spawn scatter around the pointer (soft jitter, not a hard cutoff)
signalSpeed: 420,
signalLength: 120,
burstBoost: 7, // click: how many extra signals join at once
hopSurvival: 0.4, // per-hop survival for PASSIVE signals; low = short-lived, high = long chains
burstSurvival: 0.72, // per-hop survival for CLICKED signals — tuned independently of hopSurvival
hopRadius: 440, // a surviving signal hops to a via within this range
colorDrift: 0.18, // chance per hop that a chain crossfades to a new color
maxBurstSignals: 25,
});
// 8 directions, clockwise from east; even indices are cardinal.
const DIR8 = [
[1, 0],
[1, 1],
[0, 1],
[-1, 1],
[-1, 0],
[-1, -1],
[0, -1],
[1, -1],
];
// Walker start-direction weights derived from horizontalBias (0.5 = even).
function startDirectionWeights(horizontalBias) {
const ew = 0.48 * horizontalBias;
const ns = 0.24 * (1 - horizontalBias);
const diagonal = Math.max(0.005, (1 - 2 * ew - 2 * ns) / 4);
return [
[0, ew],
[4, ew],
[2, ns],
[6, ns],
[1, diagonal],
[3, diagonal],
[5, diagonal],
[7, diagonal],
];
}
let instanceCount = 0;
function svgEl(name, attrs = {}) {
const node = document.createElementNS(SVG_NS, name);
for (const [key, value] of Object.entries(attrs)) {
node.setAttribute(key, String(value));
}
return node;
}
function hashSeed(input) {
let hash = 2166136261;
for (const char of String(input)) {
hash ^= char.charCodeAt(0);
hash = Math.imul(hash, 16777619);
}
return hash >>> 0;
}
function mulberry32(seed) {
return function random() {
let value = (seed += 0x6d2b79f5);
value = Math.imul(value ^ (value >>> 15), value | 1);
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
};
}
function weightedChoice(random, entries) {
let roll = random();
for (const [value, weight] of entries) {
roll -= weight;
if (roll <= 0) return value;
}
return entries[0][0];
}
// --- routing engine ---------------------------------------------------------
//
// Occupancy lives on a doubled ("half step") grid wrapped on a torus: every
// cell step claims its end node and the midpoint it crosses, which blocks
// same-lane overlap and any crossing (two diagonals share a midpoint, a
// diagonal crosses a cardinal only through a node). The torus wrap is what
// makes the finished tile seamless.
function createRouter(random, columns, rows) {
const wide = columns * 2;
const tall = rows * 2;
// Occupancy as a flat typed array — this sits in the A* hot path.
const occupied = new Uint8Array(wide * tall);
const viaNodes = new Uint8Array(wide * tall);
const halfKey = (hx, hy) => (((hy % tall) + tall) % tall) * wide + (((hx % wide) + wide) % wide);
const nodeKey = (x, y) => halfKey(x * 2, y * 2);
function claim(trace, key) {
occupied[key] = 1;
trace.claims.push(key);
trace.ownKeys.add(key);
}
return {
columns,
rows,
nodeKey,
nodeFree: (x, y) => !occupied[nodeKey(x, y)],
crowding(x, y) {
let count = 0;
for (let dy = -4; dy <= 4; dy += 2) {
for (let dx = -4; dx <= 4; dx += 2) {
if (occupied[nodeKey(x + dx, y + dy)]) count += 1;
}
}
return count;
},
stepFree(x, y, direction) {
const [dx, dy] = DIR8[direction];
return (
!occupied[halfKey(x * 2 + dx, y * 2 + dy)] &&
!occupied[halfKey(x * 2 + dx * 2, y * 2 + dy * 2)]
);
},
// Like stepFree, but the destination node may be the (already-claimed)
// goal pin the route is allowed to land on.
routeStepFree(x, y, direction, goalKey) {
const [dx, dy] = DIR8[direction];
if (occupied[halfKey(x * 2 + dx, y * 2 + dy)]) return false;
const node = halfKey(x * 2 + dx * 2, y * 2 + dy * 2);
return node === goalKey || !occupied[node];
},
// How close the nearest parallel neighbor sits, perpendicular to travel:
// 1 = one lane away (cramped), 2 = two lanes away (the reference's even
// weave), 0 = open field. The A* cost prefers 2 and avoids 1.
laneNeighbors(x, y, direction) {
const [dx, dy] = DIR8[direction];
if (occupied[nodeKey(x - dy, y + dx)] || occupied[nodeKey(x + dy, y - dx)]) return 1;
if (occupied[nodeKey(x - dy * 2, y + dx * 2)] || occupied[nodeKey(x + dy * 2, y - dx * 2)]) return 2;
return 0;
},
claimNode(trace, x, y) {
claim(trace, nodeKey(x, y));
},
claimStep(trace, x, y, direction) {
const [dx, dy] = DIR8[direction];
claim(trace, halfKey(x * 2 + dx, y * 2 + dy));
let count = 1;
const node = halfKey(x * 2 + dx * 2, y * 2 + dy * 2);
if (!occupied[node]) {
claim(trace, node);
count = 2;
}
trace.stepClaims.push(count);
},
// Remove the record's most recent step and exactly the claims it made.
// Only valid while nothing later than that step has claimed (sealEnd
// never trims past the record's own last via).
unclaimLastStep(record) {
const count = record.stepClaims.pop() || 0;
for (let index = 0; index < count; index += 1) {
const key = record.claims.pop();
occupied[key] = 0;
record.ownKeys.delete(key);
}
const step = record.steps.pop();
record.end = [record.end[0] - DIR8[step][0], record.end[1] - DIR8[step][1]];
},
// A via may not sit on or cardinally beside another via (their annular
// rings would overlap at one-pitch spacing), and the four diagonal
// midpoints around it must not carry someone else's trace.
viaAllowed(trace, x, y) {
for (const [dx, dy] of [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]) {
if (viaNodes[nodeKey(x + dx, y + dy)]) return false;
}
for (const [dx, dy] of [[1, 1], [1, -1], [-1, 1], [-1, -1]]) {
const key = halfKey(x * 2 + dx, y * 2 + dy);
if (occupied[key] && !trace.ownKeys.has(key)) return false;
}
return true;
},
placeVia(trace, x, y) {
viaNodes[nodeKey(x, y)] = 1;
trace.viaCells.push([x, y]);
trace.viaSteps.add(trace.steps.length);
for (const [dx, dy] of [[1, 1], [1, -1], [-1, 1], [-1, -1]]) {
const key = halfKey(x * 2 + dx, y * 2 + dy);
if (!occupied[key]) claim(trace, key);
}
},
rollback(trace) {
for (const key of trace.claims) occupied[key] = 0;
for (const [x, y] of trace.viaCells) viaNodes[nodeKey(x, y)] = 0;
},
};
}
function newTraceRecord(x, y) {
return {
start: [x, y],
end: [x, y],
steps: [],
claims: [],
stepClaims: [], // claims added per step, so ends can be trimmed precisely
ownKeys: new Set(),
viaCells: [],
viaSteps: new Set(),
};
}
// Every trace must end on a via. If the end position has no clearance, trim
// the trace back one step at a time (never past its own last via) until a
// via fits. Returns false when no compliant ending exists.
function sealEnd(router, record, minSteps) {
for (;;) {
if (record.viaSteps.has(record.steps.length)) return true;
const [x, y] = record.end;
if (router.viaAllowed(record, x, y)) {
router.placeVia(record, x, y);
return true;
}
const floor = Math.max(minSteps, record.viaSteps.size ? Math.max(...record.viaSteps) : 0);
if (record.steps.length <= floor) return false;
router.unclaimLastStep(record);
}
}
// --- A* net router ----------------------------------------------------------
//
// Routes run between terminal formations the way a human lays out a board:
// they escape a pin, join a corridor beside existing traces (hug bonus), pay
// for every 45 degree turn (long calm runs), pay a small surcharge for
// sustained diagonals (the reference uses short 45 degree jogs), and finish
// into the far pin. States are (node, incoming direction) so turn costs are
// exact. Everything wraps on the torus.
class MinHeap {
constructor() {
this.costs = [];
this.values = [];
}
get size() {
return this.costs.length;
}
push(cost, value) {
const { costs, values } = this;
let index = costs.length;
costs.push(cost);
values.push(value);
while (index > 0) {
const parent = (index - 1) >> 1;
if (costs[parent] <= costs[index]) break;
[costs[parent], costs[index]] = [costs[index], costs[parent]];
[values[parent], values[index]] = [values[index], values[parent]];
index = parent;
}
}
pop() {
const { costs, values } = this;
const top = values[0];
const lastCost = costs.pop();
const lastValue = values.pop();
if (costs.length) {
costs[0] = lastCost;
values[0] = lastValue;
let index = 0;
for (;;) {
const left = index * 2 + 1;
const right = left + 1;
let smallest = index;
if (left < costs.length && costs[left] < costs[smallest]) smallest = left;
if (right < costs.length && costs[right] < costs[smallest]) smallest = right;
if (smallest === index) break;
[costs[smallest], costs[index]] = [costs[index], costs[smallest]];
[values[smallest], values[index]] = [values[index], values[smallest]];
index = smallest;
}
}
return top;
}
}
function createPathfinder(router, columns, rows, config) {
const turnPenalty = config.turnPenalty;
const crampPenalty = config.cramp;
const weaveBonus = config.weave;
const stateCount = columns * rows * 8;
const bestCost = new Float64Array(stateCount);
const seen = new Int32Array(stateCount);
const closed = new Int32Array(stateCount);
const parent = new Int32Array(stateCount);
let generation = 0;
const wrapX = (value) => ((value % columns) + columns) % columns;
const wrapY = (value) => ((value % rows) + rows) % rows;
// start: {x, y, direction}; goal: {x, y, arrivals: Set<direction>}
// bundleMode flips the lane preference: followers rejoining their bundle
// WANT to ride one lane off their neighbor.
return function route(random, start, goal, why = {}, bundleMode = false) {
generation += 1;
const heap = new MinHeap();
const goalX = wrapX(goal.x);
const goalY = wrapY(goal.y);
const goalKey = router.nodeKey(goalX, goalY);
const heuristic = (x, y) => {
let dx = Math.abs(x - goalX);
dx = Math.min(dx, columns - dx);
let dy = Math.abs(y - goalY);
dy = Math.min(dy, rows - dy);
return Math.max(dx, dy) + 0.42 * Math.min(dx, dy);
};
const startX = wrapX(start.x);
const startY = wrapY(start.y);
// Seed the facing direction free, and the two perpendiculars at a small
// cost — a pin whose lane is congested can still hook out sideways.
const h0 = heuristic(startX, startY);
for (const [turn, cost] of [[0, 0], [2, 0.9], [6, 0.9]]) {
const startState = (startY * columns + startX) * 8 + ((start.direction + turn) % 8);
seen[startState] = generation;
bestCost[startState] = cost;
parent[startState] = -1;
heap.push(cost + h0, startState);
}
const straightestFirst = heuristic(startX, startY);
let goalState = -1;
let expansions = 0;
const TURNS = [0, 1, 7];
// Weighted A* (heuristic scaled below): meaningfully faster, and the mild
// suboptimality reads as organic hand-routing rather than error.
while (heap.size && expansions < 9000) {
expansions += 1;
const state = heap.pop();
if (closed[state] === generation) continue;
closed[state] = generation;
const direction = state % 8;
const nodeIndex = (state - direction) / 8;
const x = nodeIndex % columns;
const y = (nodeIndex - x) / columns;
if (x === goalX && y === goalY) {
if (goal.arrivals.has(direction)) {
goalState = state;
break;
}
continue;
}
const cost = bestCost[state];
for (const turn of TURNS) {
const next = (direction + turn) % 8;
if (!router.routeStepFree(x, y, next, goalKey)) continue;
const nx = wrapX(x + DIR8[next][0]);
const ny = wrapY(y + DIR8[next][1]);
let stepCost = next % 2 === 0 ? 1 : 1.414;
if (turn !== 0) stepCost += turnPenalty;
const lane = router.laneNeighbors(nx, ny, next);
if (bundleMode) {
if (lane === 1) stepCost -= 0.22;
} else if (lane === 1) stepCost += crampPenalty;
else if (lane === 2) stepCost -= weaveBonus;
stepCost += random() * 0.05;
const nextState = (ny * columns + nx) * 8 + next;
const total = cost + stepCost;
if (seen[nextState] === generation && bestCost[nextState] <= total) continue;
seen[nextState] = generation;
bestCost[nextState] = total;
parent[nextState] = state;
heap.push(total + heuristic(nx, ny) * 1.2, nextState);
}
}
if (goalState < 0) {
why.reason = heap.size ? "expansions" : "exhausted";
return null;
}
const directions = [];
let cursor = goalState;
while (parent[cursor] >= 0) {
directions.push(cursor % 8);
cursor = parent[cursor];
}
directions.reverse();
// Reject absurd detours (torus wrap can produce them when hemmed in).
if (directions.length > straightestFirst * 2.4 + 12) {
why.reason = "detour";
return null;
}
return directions;
};
}
// --- terminal formations ----------------------------------------------------
//
// The reference pattern's vias are not sprinkled at random: they sit in small
// deliberate formations — diagonal ladders (staggered bus fanouts), rows, and
// singles — and traces escape them on a consistent side. Formations are placed
// with best-candidate sampling so the tile keeps even coverage and breathing
// room between groups.
function placeFormations(random, router, columns, rows, count, config) {
const formations = [];
const centers = [];
const busWidth = config.busWidth;
const horizontalEscape = Math.min(0.95, config.horizontalBias + 0.02);
const rowSplit = 0.52 + 0.26 * config.horizontalBias;
const wrapDist = (a, b, m) => {
const d = Math.abs(a - b) % m;
return Math.min(d, m - d);
};
for (let index = 0; index < count; index += 1) {
let best = null;
let bestClearance = -1;
for (let candidate = 0; candidate < 8; candidate += 1) {
const x = Math.floor(random() * columns);
const y = Math.floor(random() * rows);
let clearance = Infinity;
for (const [cx, cy] of centers) {
clearance = Math.min(
clearance,
Math.hypot(wrapDist(x, cx, columns), wrapDist(y, cy, rows)),
);
}
if (clearance > bestClearance) {
bestClearance = clearance;
best = [x, y];
}
}
const [x, y] = best;
const roll = random();
const pins = [];
let signature;
if (roll < 0.52) {
// Diagonal via ladder — the staggered bus-fanout motif.
const length = Math.max(2, Math.min(8, Math.round((3 + random() * 4) * busWidth)));
const slope = random() < 0.5 ? -1 : 1;
const escape = random() < horizontalEscape ? 0 : 4;
signature = `ladder${slope}`;
for (let k = 0; k < length; k += 1) pins.push({ x: x + k, y: y + k * slope, escape });
} else if (roll < rowSplit) {
// Vertical via row (2-cell spacing), horizontal escapes.
const length = Math.max(2, Math.min(6, Math.round((2 + random() * 2) * busWidth)));
const escape = random() < 0.5 ? 0 : 4;
signature = "vrow";
for (let k = 0; k < length; k += 1) pins.push({ x, y: y + k * 2, escape });
} else if (roll < 0.78) {
// Horizontal via row, vertical escapes.
const length = Math.max(2, Math.min(6, Math.round((2 + random() * 2) * busWidth)));
const escape = random() < 0.5 ? 2 : 6;
signature = "hrow";
for (let k = 0; k < length; k += 1) pins.push({ x: x + k * 2, y, escape });
} else {
signature = "solo";
pins.push({ x, y, escape: [0, 4, 2, 6][Math.floor(random() * 4)] });
}
// Reserve the pin nodes now; the via graphics are only placed when a net
// or stub actually connects, so a formation that stays unconnected leaves
// no orphaned via row behind.
const record = newTraceRecord(pins[0].x, pins[0].y);
let placeable = true;
for (const pin of pins) {
if (!router.nodeFree(pin.x, pin.y)) {
placeable = false;
break;
}
router.claimNode(record, pin.x, pin.y);
}
if (!placeable) {
router.rollback(record);
continue;
}
centers.push([x, y]);
formations.push({ pins: pins.map((pin) => ({ ...pin, used: false })), record, signature });
}
return formations;
}
// Start a trace record at a pin with its mandatory via already in place.
// Returns null when the pin has no via clearance (the pin is then skipped —
// every trace must begin on a via).
function startAtPin(router, pin) {
const record = newTraceRecord(pin.x, pin.y);
if (!router.viaAllowed(record, pin.x, pin.y)) return null;
router.placeVia(record, pin.x, pin.y);
return record;
}
function signedWrap(delta, modulus) {
let d = ((delta % modulus) + modulus) % modulus;
if (d > modulus / 2) d -= modulus;
return d;
}
// A route may enter a pin from the facing side or hook in from up to 90
// degrees off — only a straight-through backside entry is forbidden.
const ARRIVALS = DIR8.map((_, escape) => {
const reverse = (escape + 4) % 8;
return new Set([
(reverse + 6) % 8,
(reverse + 7) % 8,
reverse,
(reverse + 1) % 8,
(reverse + 2) % 8,
]);
});
// Route bundles of nets between compatible formations. Only the FIRST pin
// pair is routed with A*; the remaining pins get pure translated copies of
// the leader. Translation is what the reference does: neighbors that bend at
// the same places, producing nested chevrons and group staircases. Followers
// that hit an obstruction stop early and drop their via there — a natural
// fanout.
function routeNets(random, router, route, formations, addRecord, stats, config) {
const majorFor = (escape) => (escape === 0 || escape === 4 ? 0 : 1);
const sameNode = (x1, y1, x2, y2) =>
router.nodeKey(x1, y1) === router.nodeKey(x2, y2);
for (const formation of formations) {
const minePins = formation.pins.filter((pin) => !pin.used);
if (!minePins.length) continue;
const escape = formation.pins[0].escape;
const axis = majorFor(escape);
// Find a partner formation whose pins face back toward this one; a
// partner with the same shape nests perfectly under translation.
let partner = null;
let partnerScore = Infinity;
for (const other of formations) {
if (other === formation) continue;
const otherFree = other.pins.filter((pin) => !pin.used);
if (!otherFree.length) continue;
if (other.pins[0].escape !== (escape + 4) % 8) continue;
const dx = signedWrap(other.pins[0].x - formation.pins[0].x, router.columns);
const dy = signedWrap(other.pins[0].y - formation.pins[0].y, router.rows);
const major = axis === 0 ? dx : dy;
const minor = axis === 0 ? dy : dx;
const forward = escape === 0 || escape === 2 ? major : -major;
if (forward < 8 || forward > config.netReach) continue;
if (Math.abs(minor) > 12) continue;
// Prefer a few cells of misalignment: it forces the staircase jogs the
// reference is full of. Perfect alignment yields sterile laser lines.
let score =
Math.abs(Math.abs(minor) - config.jogBias) * 0.8 +
Math.abs(forward - 22) * 0.3 +
random() * 5;
if (other.signature === formation.signature) score -= 4;
if (score < partnerScore) {
partnerScore = score;
partner = other;
}
}
if (!partner) continue;
const minorOf = (pin) => (axis === 0 ? pin.y : pin.x);
const mine = minePins.sort((a, b) => minorOf(a) - minorOf(b));
const theirs = partner.pins
.filter((pin) => !pin.used)
.sort((a, b) => minorOf(a) - minorOf(b));
const pairCount = Math.min(mine.length, theirs.length);
if (!pairCount) continue;
// Leader. Both pin vias go in before routing — every trace begins and
// ends on a via, so a net is only viable if both terminals have
// clearance.
const leadFrom = mine[0];
const leadTo = theirs[0];
const leadRecord = startAtPin(router, leadFrom);
if (!leadRecord) continue;
if (!router.viaAllowed(leadRecord, leadTo.x, leadTo.y)) {
router.rollback(leadRecord);
continue;
}
router.placeVia(leadRecord, leadTo.x, leadTo.y);
const why = {};
const directions = route(
random,
{ x: leadFrom.x, y: leadFrom.y, direction: leadFrom.escape },
{ x: leadTo.x, y: leadTo.y, arrivals: ARRIVALS[leadTo.escape] },
why,
);
if (!directions) {
router.rollback(leadRecord);
stats.leaderFails += 1;
stats[why.reason] = (stats[why.reason] || 0) + 1;
continue;
}
stats.leaders += 1;
stats.followerTries += pairCount - 1;
{
let x = leadFrom.x;
let y = leadFrom.y;
for (const step of directions) {
router.claimStep(leadRecord, x, y, step);
x += DIR8[step][0];
y += DIR8[step][1];
leadRecord.steps.push(step);
}
leadRecord.end = [x, y];
leadFrom.used = true;
leadTo.used = true;
addRecord(leadRecord);
}
// Followers: translated copies pin-by-pin. A follower whose translated
// corridor is blocked early gets rerouted with A* in bundle mode so it
// wraps the obstacle and rejoins the group. Every follower starts on its
// pin via and seals its far end with a via (trimming back if needed).
for (let index = 1; index < pairCount; index += 1) {
const from = mine[index];
const to = theirs[index];
const goalKey = router.nodeKey(to.x, to.y);
const record = startAtPin(router, from);
if (!record) {
stats.followerFails += 1;
continue;
}
const clearSteps = () => {
while (record.steps.length) router.unclaimLastStep(record);
};
const layTranslation = () => {
let x = from.x;
let y = from.y;
for (const step of directions) {
if (!router.routeStepFree(x, y, step, goalKey)) break;
router.claimStep(record, x, y, step);
x += DIR8[step][0];
y += DIR8[step][1];
record.steps.push(step);
if (sameNode(x, y, to.x, to.y)) break;
}
record.end = [x, y];
};
layTranslation();
let reached = sameNode(record.end[0], record.end[1], to.x, to.y);
if (!reached && record.steps.length < Math.max(6, Math.floor(directions.length * 0.6))) {
clearSteps();
const alt = route(
random,
{ x: from.x, y: from.y, direction: from.escape },
{ x: to.x, y: to.y, arrivals: ARRIVALS[to.escape] },
{},
true,
);
if (alt) {
let x = from.x;
let y = from.y;
for (const step of alt) {
router.claimStep(record, x, y, step);
x += DIR8[step][0];
y += DIR8[step][1];
record.steps.push(step);
}
record.end = [x, y];
reached = true;
stats.followersRerouted += 1;
} else {
layTranslation();
reached = sameNode(record.end[0], record.end[1], to.x, to.y);
}
}
if (record.steps.length < 4 || !sealEnd(router, record, 4)) {
router.rollback(record);
stats.followerFails += 1;
continue;
}
from.used = true;
if (sameNode(record.end[0], record.end[1], to.x, to.y)) {
to.used = true;
stats.followersFull += 1;
} else {
stats.followersPartial += 1;
}
addRecord(record);
}
}
}
// Tap combs: short perpendicular stubs dropping off a long run at a regular
// pitch, each ending in a via — the power-rail tap motif that punctuates the
// reference artwork.
function tapCombs(random, router, records, addRecord, config) {
const runs = [];
for (const record of records) {
let x = record.start[0];
let y = record.start[1];
let runX = x;
let runY = y;
let runDir = -1;
let runLen = 0;
const flush = () => {
if (runDir >= 0 && runDir % 2 === 0 && runLen >= 8) {
runs.push({ x: runX, y: runY, dir: runDir, len: runLen });
}
};
for (const step of record.steps) {
if (step === runDir) runLen += 1;
else {
flush();
runDir = step;
runX = x;
runY = y;
runLen = 1;
}
x += DIR8[step][0];
y += DIR8[step][1];
}
flush();
}
let combs = 0;
for (const run of runs) {
if (combs >= config.combs || random() >= 0.28) continue;
const [dx, dy] = DIR8[run.dir];
const side = random() < 0.5 ? 1 : -1;
const px = -dy * side;
const py = dx * side;
const tapDir = DIR8.findIndex(([tx, ty]) => tx === px && ty === py);
const teeth = 2 + Math.floor(random() * 3);
// Depth 2-3: the junction via and tip via need a two-cell gap to clear
// each other's annular rings.
const depth = 2 + (random() < 0.35 ? 1 : 0);
const span = teeth * 2;
if (run.len < span + 2) continue;
const offset = 1 + Math.floor(random() * (run.len - span - 1));
const made = [];
for (let tooth = 0; tooth < teeth; tooth += 1) {
const bx = run.x + dx * (offset + tooth * 2);
const by = run.y + dy * (offset + tooth * 2);
// The tap starts ON the parent trace (a T junction, marked with its
// own via) and steps outward to a tip via.
const record = newTraceRecord(bx, by);
if (!router.viaAllowed(record, bx, by)) continue;
router.placeVia(record, bx, by);
let cx = bx;
let cy = by;
for (let d = 0; d < depth; d += 1) {
if (!router.stepFree(cx, cy, tapDir)) break;
router.claimStep(record, cx, cy, tapDir);
cx += px;
cy += py;
record.steps.push(tapDir);
}
record.end = [cx, cy];
if (record.steps.length < 2 || !sealEnd(router, record, 2)) {
router.rollback(record);
continue;
}
made.push(record);
}
if (made.length >= 2) {
for (const record of made) addRecord(record);
combs += 1;
} else {
for (const record of made) router.rollback(record);
}
}
}
// Short bare-ended escape stubs for pins that never got a net — the reference
// is full of these little launches.
function routeStubs(random, router, formations, addRecord, config) {
for (const formation of formations) {
for (const pin of formation.pins) {
if (pin.used || random() >= config.stubChance) continue;
const record = startAtPin(router, pin);
if (!record) continue;
let x = pin.x;
let y = pin.y;
let direction = pin.escape;
const length = 2 + Math.floor(random() * 5);
for (let step = 0; step < length; step += 1) {
if (step > 0 && random() < 0.2) {
const jog = (direction + (random() < 0.5 ? 1 : 7)) % 8;
if (router.stepFree(x, y, jog)) direction = jog;
}
if (!router.stepFree(x, y, direction)) break;
router.claimStep(record, x, y, direction);
x += DIR8[direction][0];
y += DIR8[direction][1];
record.steps.push(direction);
}
record.end = [x, y];
if (record.steps.length < 2 || !sealEnd(router, record, 2)) {
router.rollback(record);
continue;
}
pin.used = true;
addRecord(record);
}
}
}
// Translated companions for a filler walk — same idea as the net followers:
// staggered trims and via ladders at the ends, bending wherever the base
// bends. In the reference almost nothing travels alone.
function replicateWalk(random, router, base, addRecord) {
if (random() >= 0.55 || base.steps.length < 8) return;
let horizontalSteps = 0;
let verticalSteps = 0;
for (const step of base.steps) {
if (step === 0 || step === 4) horizontalSteps += 1;
else if (step === 2 || step === 6) verticalSteps += 1;
}
const horizontal = horizontalSteps >= verticalSteps;
const spacing = random() < 0.7 ? 1 : 2;
const side = random() < 0.5 ? 1 : -1;
const shift = horizontal ? [0, spacing * side] : [spacing * side, 0];
const copies = 1 + Math.floor(random() * 3);
const trimStartUnit = random() < 0.5 ? 1 : 0;
const trimEndUnit = random() < 0.8 ? 1 : 2;
for (let copy = 1; copy <= copies; copy += 1) {
const trimStart = copy * trimStartUnit;
const trimEnd = copy * trimEndUnit;
if (base.steps.length - trimStart - trimEnd < 5) break;
let x = base.start[0] + shift[0] * copy;
let y = base.start[1] + shift[1] * copy;
for (let index = 0; index < trimStart; index += 1) {
x += DIR8[base.steps[index]][0];
y += DIR8[base.steps[index]][1];
}
if (!router.nodeFree(x, y) || !router.viaAllowed(SCRATCH_RECORD, x, y)) continue;
const record = newTraceRecord(x, y);
router.claimNode(record, x, y);
router.placeVia(record, x, y); // every trace starts on a via
for (let index = trimStart; index < base.steps.length - trimEnd; index += 1) {
const step = base.steps[index];
if (!router.stepFree(x, y, step)) break;
router.claimStep(record, x, y, step);
x += DIR8[step][0];
y += DIR8[step][1];
record.steps.push(step);
}
record.end = [x, y];
if (record.steps.length < 5 || !sealEnd(router, record, 5)) {
router.rollback(record);
continue;
}
addRecord(record);
}
}
// --- filler walker ----------------------------------------------------------
//
// A small amount of free-roaming texture between the routed bundles, using the
// same collision rules. Kept from the earlier engine but used sparingly.
const SCRATCH_RECORD = { ownKeys: new Set() };
function walkTrace(random, router, config, startWeights) {
let sx = -1;
let sy = -1;
let bestCrowding = Infinity;
for (let attempt = 0, found = 0; attempt < 24 && found < 3; attempt += 1) {
const x = Math.floor(random() * router.columns);
const y = Math.floor(random() * router.rows);
if (!router.nodeFree(x, y) || !router.viaAllowed(SCRATCH_RECORD, x, y)) continue;
found += 1;
const crowding = router.crowding(x, y);
if (crowding < bestCrowding) {
bestCrowding = crowding;
sx = x;
sy = y;
}
}
if (sx < 0) return null;
const trace = newTraceRecord(sx, sy);
router.claimNode(trace, sx, sy);
router.placeVia(trace, sx, sy); // every trace starts on a via
let direction = weightedChoice(random, startWeights);
let lastCardinal = direction % 2 === 0 ? direction : (direction + (random() < 0.5 ? 1 : 7)) % 8;
const budget = 14 + Math.floor(random() * 22);
let x = sx;
let y = sy;
let sinceVia = 4;
let turnStreak = 0; // consecutive same-side turns; capped to avoid closed loops
while (trace.steps.length < budget) {
const cardinal = direction % 2 === 0;
const runLength = cardinal
? 3 + Math.floor(random() * random() * 9)
: 1 + Math.floor(random() * 3);
for (let step = 0; step < runLength && trace.steps.length < budget; step += 1) {
if (!router.stepFree(x, y, direction)) break;
router.claimStep(trace, x, y, direction);
x += DIR8[direction][0];
y += DIR8[direction][1];
trace.steps.push(direction);
sinceVia += 1;
if (
cardinal &&
sinceVia > 4 &&
trace.steps.length < budget - 2 &&
random() < config.inlineViaChance &&
router.viaAllowed(trace, x, y)
) {
router.placeVia(trace, x, y);
sinceVia = 0;
}
}
if (trace.steps.length >= budget) break;
let next;
if (cardinal) {
// Curling the same way repeatedly closes octagon loops; alternate.
const side = Math.abs(turnStreak) >= 3 ? -Math.sign(turnStreak) : random() < 0.5 ? 1 : -1;
next = (direction + (side > 0 ? 1 : 7)) % 8;
} else {
const optionA = (direction + 1) % 8;
const optionB = (direction + 7) % 8;
const preferPrevious = (optionA === lastCardinal || optionB === lastCardinal) && random() < 0.7;
next = preferPrevious ? lastCardinal : random() < 0.5 ? optionA : optionB;
}
if (!router.stepFree(x, y, next)) {
const mirrored = (direction + (8 - ((next - direction + 8) % 8))) % 8;
if (!router.stepFree(x, y, mirrored)) break;
next = mirrored;
}
const turnSide = ((next - direction + 8) % 8) === 1 ? 1 : -1;
turnStreak = Math.sign(turnStreak) === turnSide ? turnStreak + turnSide : turnSide;
direction = next;
if (direction % 2 === 0) lastCardinal = direction;
}
trace.end = [x, y];
if (trace.steps.length < 7 || !sealEnd(router, trace, 7)) {
router.rollback(trace);
return null;
}
return trace;
}
function toTileTrace(record, pitch) {
const points = [[record.start[0], record.start[1]]];
let previous = -1;
let [x, y] = record.start;
for (const step of record.steps) {
x += DIR8[step][0];
y += DIR8[step][1];
if (step === previous) points[points.length - 1] = [x, y];
else points.push([x, y]);
previous = step;
}
const scaled = points.map(([px, py]) => [px * pitch, py * pitch]);
let length = 0;
for (let index = 1; index < scaled.length; index += 1) {
length += Math.hypot(
scaled[index][0] - scaled[index - 1][0],
scaled[index][1] - scaled[index - 1][1],
);
}
return {
points: scaled,
length,
vias: record.viaCells.map(([vx, vy]) => [vx * pitch, vy * pitch]),
};
}
export function generateCircuitTile(options = {}) {
const config = { ...DEFAULTS, ...options };
const random = mulberry32(hashSeed(config.seed));
const columns = Math.round(config.tileWidth / config.pitch);
const rows = Math.round(config.tileHeight / config.pitch);
const router = createRouter(random, columns, rows);
const route = createPathfinder(router, columns, rows, config);
const startWeights = startDirectionWeights(config.horizontalBias);
const records = [];
let routedSteps = 0;
const addRecord = (record) => {
records.push(record);
routedSteps += record.steps.length;
};
// 1. Terminal formations: via ladders, rows, singles.
const formations = placeFormations(
random,
router,
columns,
rows,
Math.round(config.formations * config.density),
config,
);
for (const formation of formations) records.push(formation.record);
// 2. Nets between formations — the flow. Two passes so pins left over from
// the first pairing find another partner.
const stats = {
formations: formations.length,
leaders: 0,
leaderFails: 0,
followerTries: 0,
followersFull: 0,
followersPartial: 0,
followersRerouted: 0,
followerFails: 0,
};
routeNets(random, router, route, formations, addRecord, stats, config);
routeNets(random, router, route, formations, addRecord, stats, config);
// 3. Escape stubs for leftover pins.
routeStubs(random, router, formations, addRecord, config);
// 4. A sprinkle of free-roaming filler until the reference ink density.
const targetSteps = Math.round(4800 * config.density);
let attempts = 0;
while (routedSteps < targetSteps && attempts < 3200) {
attempts += 1;
const trace = walkTrace(random, router, config, startWeights);
if (trace) {
addRecord(trace);
replicateWalk(random, router, trace, addRecord);
}
}
// 5. Tap combs hanging off the long runs (nets and filler alike).
if (config.combs > 0) tapCombs(random, router, [...records], addRecord, config);
return {
width: columns * config.pitch,
height: rows * config.pitch,
pitch: config.pitch,
stats,
traces: records.map((record) => toTileTrace(record, config.pitch)),
};
}
export function pathData(points, offsetX = 0, offsetY = 0) {
return points
.map(([x, y], index) => `${index ? "L" : "M"}${(x + offsetX).toFixed(2)},${(y + offsetY).toFixed(2)}`)
.join("");
}
// Standalone seamless tile: the geometry is drawn 3 x 3 around the viewBox and
// clipped, so traces that wrap the torus edge reappear on the opposite side.
export function tileSvg(tile, options = {}) {
const config = { ...DEFAULTS, ...options };
const { width, height } = tile;
const body = [];
for (const trace of tile.traces) {
if (trace.points.length < 2) continue;
body.push(
`<path d="${pathData(trace.points)}" fill="none" stroke="${config.traceColor}" ` +
`stroke-width="${config.traceWidth}" stroke-linecap="butt" stroke-linejoin="miter"/>`,
);
}
const vias = [];
for (const trace of tile.traces) {
for (const [cx, cy] of trace.vias) {
vias.push(
`<circle cx="${cx.toFixed(2)}" cy="${cy.toFixed(2)}" r="${config.viaOuterRadius}" fill="${config.traceColor}"/>` +
`<circle cx="${cx.toFixed(2)}" cy="${cy.toFixed(2)}" r="${config.viaInnerRadius}" fill="${config.holeColor || config.background || "#0d1117"}"/>`,
);
}
}
const uses = [];
for (let row = -1; row <= 1; row += 1) {
for (let column = -1; column <= 1; column += 1) {
uses.push(`<use href="#tile" transform="translate(${column * width}, ${row * height})"/>`);
}
}
const backgroundRect = config.background
? `<rect width="${width}" height="${height}" fill="${config.background}"/>`
: "";
return (
`<?xml version="1.0" encoding="UTF-8"?>\n` +
`<svg xmlns="${SVG_NS}" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">` +
`<defs><g id="tile">${body.join("")}${vias.join("")}</g>` +
`<clipPath id="edge"><rect width="${width}" height="${height}"/></clipPath></defs>` +
`${backgroundRect}<g clip-path="url(#edge)">${uses.join("")}</g></svg>`
);
}
// --- signal routing ---------------------------------------------------------
//
// Signals ride the real centerlines. Each trace is resampled to ~3.5 unit
// spacing and indexed in a spatial hash; when a pulse reaches the end of a
// trace it looks for a nearby, direction-aligned point on another trace (in
// this tile or a neighbor) and hops there, exactly like the reference site.
const SIGNAL_SAMPLE = 3.5;
const HOP_RADIUS = 3.5;
const HASH_CELL = 8;
const signalIndexCache = new WeakMap();
function ensureSignalIndex(tile) {
let index = signalIndexCache.get(tile);
if (index) return index;
const dense = tile.traces.map((trace) => {
if (trace.points.length < 2) return [];
const points = [];
for (let segment = 1; segment < trace.points.length; segment += 1) {
const [ax, ay] = trace.points[segment - 1];
const [bx, by] = trace.points[segment];
const steps = Math.max(1, Math.round(Math.hypot(bx - ax, by - ay) / SIGNAL_SAMPLE));
for (let step = segment === 1 ? 0 : 1; step <= steps; step += 1) {
points.push([ax + ((bx - ax) * step) / steps, ay + ((by - ay) * step) / steps]);
}
}
return points;
});
const buckets = new Map();
dense.forEach((points, traceIndex) => {
points.forEach((point, pointIndex) => {
const key = `${Math.floor(point[0] / HASH_CELL)}:${Math.floor(point[1] / HASH_CELL)}`;
let bucket = buckets.get(key);
if (!bucket) buckets.set(key, (bucket = []));
bucket.push([traceIndex, pointIndex]);
});
});
// Trace endpoints (all mandated vias) — the launch pads for via hopping.
const endpoints = [];
dense.forEach((points, traceIndex) => {
if (points.length < 6) return;
endpoints.push({ trace: traceIndex, point: 0, direction: 1, x: points[0][0], y: points[0][1] });
const last = points.length - 1;
endpoints.push({ trace: traceIndex, point: last, direction: -1, x: points[last][0], y: points[last][1] });
});
index = { dense, buckets, endpoints };
signalIndexCache.set(tile, index);
return index;
}
function findHop(tile, absX, absY, dirX, dirY, visited) {
const { dense, buckets } = ensureSignalIndex(tile);
const baseTileX = Math.floor(absX / tile.width);
const baseTileY = Math.floor(absY / tile.height);
let best = null;
for (let tileY = baseTileY - 2; tileY <= baseTileY + 2; tileY += 1) {
for (let tileX = baseTileX - 2; tileX <= baseTileX + 2; tileX += 1) {
const localX = absX - tileX * tile.width;
const localY = absY - tileY * tile.height;
const minCellX = Math.floor((localX - HOP_RADIUS) / HASH_CELL);
const maxCellX = Math.floor((localX + HOP_RADIUS) / HASH_CELL);
const minCellY = Math.floor((localY - HOP_RADIUS) / HASH_CELL);
const maxCellY = Math.floor((localY + HOP_RADIUS) / HASH_CELL);
for (let cellY = minCellY; cellY <= maxCellY; cellY += 1) {
for (let cellX = minCellX; cellX <= maxCellX; cellX += 1) {
const bucket = buckets.get(`${cellX}:${cellY}`);
if (!bucket) continue;
for (const [traceIndex, pointIndex] of bucket) {
if (visited.has(`${traceIndex}:${tileX}:${tileY}`)) continue;
const point = dense[traceIndex][pointIndex];
const distance = Math.hypot(point[0] - localX, point[1] - localY);
if (distance >= HOP_RADIUS) continue;
if (distance >= 1.2) {
const toward =
((point[0] - localX) * dirX + (point[1] - localY) * dirY) / (distance || 1);
if (toward < 0.35) continue;
}
for (const hopDirection of [1, -1]) {
const ahead = pointIndex + hopDirection * 3;
if (ahead < 0 || ahead >= dense[traceIndex].length) continue;
const segX = dense[traceIndex][ahead][0] - point[0];
const segY = dense[traceIndex][ahead][1] - point[1];
const segLength = Math.hypot(segX, segY) || 1;
const score = (segX * dirX + segY * dirY) / segLength - distance * 0.05;
if (!best || score > best.score) {
best = {
trace: traceIndex,
tileX,
tileY,
point: pointIndex,
direction: hopDirection,
score,
};
}
}
}
}
}
}
}
return best && best.score > 0.1 ? best : null;
}
function buildSignalPath(tile, start, budget, minLength) {
const { dense } = ensureSignalIndex(tile);
const visited = new Set();
const points = [];
let travelled = 0;
let cursor = { ...start };
// The walk always finishes at a trace end (a mandated via) — the budget
// only limits further mid-route hops, never cuts a pulse off mid-trace.
for (;;) {
const tracePoints = dense[cursor.trace];
visited.add(`${cursor.trace}:${cursor.tileX}:${cursor.tileY}`);
const offsetX = cursor.tileX * tile.width;
const offsetY = cursor.tileY * tile.height;
for (
let index = cursor.point;
index >= 0 && index < tracePoints.length;
index += cursor.direction
) {
const x = tracePoints[index][0] + offsetX;
const y = tracePoints[index][1] + offsetY;
if (points.length) {
const last = points[points.length - 1];
travelled += Math.hypot(x - last[0], y - last[1]);
}
points.push([x, y]);
}
if (points.length < 2 || travelled >= budget) break;
const last = points[points.length - 1];
const before = points[points.length - 2];
const stepLength = Math.hypot(last[0] - before[0], last[1] - before[1]) || 1;
const hop = findHop(
tile,
last[0],
last[1],
(last[0] - before[0]) / stepLength,
(last[1] - before[1]) / stepLength,
visited,
);
if (!hop) break;
cursor = hop;
}
return travelled >= minLength ? { points, length: travelled } : null;
}
// --- DOM mount ---------------------------------------------------------------
export function createCircuitBackground(mount, options = {}) {
if (!(mount instanceof Element)) throw new TypeError("mount must be a DOM element");
const id = `circuit-${++instanceCount}`;
let config = { ...DEFAULTS, ...options };
let random = mulberry32(hashSeed(`${config.seed}:signals`));
let tile = generateCircuitTile(config);
let svg;
let usesGroup;
let signalGroup;
let resizeObserver;
let activeSignals = 0;
let signalTimer;
let epoch = 0; // invalidates in-flight signal timeouts across regenerations
let tilesAcross = 1;
let tilesDown = 1;
let topOffset = -config.tileHeight / 2;
let pointer = null;
const interactionTarget = mount.parentElement || mount;
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const originalPosition = mount.style.position;
const computedPosition = getComputedStyle(mount).position;
if (computedPosition === "static") mount.style.position = "relative";
mount.style.overflow = "hidden";
function buildSvg() {
svg?.remove();
epoch += 1;
activeSignals = 0;
tile = generateCircuitTile(config);
random = mulberry32(hashSeed(`${config.seed}:signals`));
svg = svgEl("svg", {
"aria-hidden": "true",
focusable: "false",
xmlns: SVG_NS,
});
svg.dataset.circuitBackground = "";
Object.assign(svg.style, {
position: "absolute",
left: "0",
top: "0",
display: "block",
maxWidth: "none",
pointerEvents: "none",
overflow: "visible",
});
const defs = svgEl("defs");
const tileGroup = svgEl("g", { id: `${id}-tile` });
const traceGroup = svgEl("g", {
fill: "none",
stroke: config.traceColor,
"stroke-width": config.traceWidth,
"stroke-linecap": "butt",
"stroke-linejoin": "miter",
});
const viaGroup = svgEl("g");
for (const trace of tile.traces) {
if (trace.points.length >= 2) {
traceGroup.appendChild(svgEl("path", { d: pathData(trace.points) }));
}
for (const [cx, cy] of trace.vias) {
viaGroup.appendChild(
svgEl("circle", { cx, cy, r: config.viaOuterRadius, fill: config.traceColor }),
);
viaGroup.appendChild(
svgEl("circle", {
cx,
cy,
r: config.viaInnerRadius,
fill: "var(--circuit-hole, #0d1117)",
}),
);
}
}
tileGroup.append(traceGroup, viaGroup);
defs.appendChild(tileGroup);
svg.appendChild(defs);
usesGroup = svgEl("g");
signalGroup = svgEl("g", { fill: "none" });
svg.append(usesGroup, signalGroup);
mount.prepend(svg);
layout();
}
function layout() {
const width = Math.max(1, mount.clientWidth);
const height = Math.max(1, mount.clientHeight);
const tilePixelWidth = tile.width * config.scale;
const tilePixelHeight = tile.height * config.scale;
tilesAcross = Math.max(1, Math.ceil(width / tilePixelWidth));
tilesDown = Math.max(2, Math.ceil(height / tilePixelHeight) + 1);
topOffset = -tile.height / 2;
const viewWidth = tilesAcross * tile.width;
const viewHeight = tilesDown * tile.height;
svg.setAttribute("width", `${viewWidth * config.scale}`);
svg.setAttribute("height", `${viewHeight * config.scale}`);
svg.setAttribute("viewBox", `0 ${-topOffset} ${viewWidth} ${viewHeight}`);
usesGroup.replaceChildren();
// The extra ring of tiles paints the traces that wrap in from neighbors.
for (let row = -1; row <= tilesDown; row += 1) {
for (let column = -1; column <= tilesAcross; column += 1) {
usesGroup.appendChild(
svgEl("use", {
href: `#${id}-tile`,
transform: `translate(${column * tile.width}, ${row * tile.height})`,
}),
);
}
}
}
// Pointer-biased launch pad: candidates are scored by the distance from
// the ACTUAL launch point (a trace end, not the trace middle) to the
// cursor, plus spawnRadius worth of jitter — so spawns cluster within
// roughly spawnRadius of the pointer without a hard cutoff.
function pickStart() {
const { dense } = ensureSignalIndex(tile);
const nearPointer = pointer && performance.now() - pointer.at < 4000;
let choice = null;
let bestScore = Infinity;
const tries = nearPointer ? 40 : 4;
for (let attempt = 0; attempt < tries; attempt += 1) {
const trace = Math.floor(random() * dense.length);
const tileX = Math.floor(random() * tilesAcross);
const tileY = Math.floor(random() * tilesDown);
if (dense[trace].length < 6) continue;
const fromStart = random() < 0.5;
const candidate = {
trace,
tileX,
tileY,
point: fromStart ? 0 : dense[trace].length - 1,
direction: fromStart ? 1 : -1,
};
if (!nearPointer) return candidate;
const [padX, padY] = dense[trace][candidate.point];
const score =
Math.hypot(padX + tileX * tile.width - pointer.x, padY + tileY * tile.height - pointer.y) +
config.spawnRadius * random();
if (score < bestScore) {
bestScore = score;
choice = candidate;
}
}
return choice;
}
// Launch pads (trace-end vias) near an ended signal, closest first, so the
// replacement reads as the signal hopping vias onto a neighboring trace.
function nearbyEndpointStarts(absX, absY, excludeTrace) {
const { endpoints } = ensureSignalIndex(tile);
const baseTileX = Math.floor(absX / tile.width);
const baseTileY = Math.floor(absY / tile.height);
const candidates = [];
for (let tileY = baseTileY - 1; tileY <= baseTileY + 1; tileY += 1) {
for (let tileX = baseTileX - 1; tileX <= baseTileX + 1; tileX += 1) {
const localX = absX - tileX * tile.width;
const localY = absY - tileY * tile.height;
for (const endpoint of endpoints) {
if (endpoint.trace === excludeTrace) continue;
const distance = Math.hypot(endpoint.x - localX, endpoint.y - localY);
if (distance > 2 && distance < config.hopRadius) {
candidates.push({ endpoint, tileX, tileY, distance: distance + random() * 20 });
}
}
}
}
candidates.sort((a, b) => a.distance - b.distance);
return candidates.slice(0, 6).map((pick) => ({
trace: pick.endpoint.trace,
tileX: pick.tileX,
tileY: pick.tileY,
point: pick.endpoint.point,
direction: pick.endpoint.direction,
}));
}
// phase (0..1) fast-forwards the animation so boot signals are already
// mid-journey. A chain keeps its speedFactor, color AND survival rate across
// hops — the signal that entered blue exits blue, and a click-born chain
// keeps decaying at burstSurvival while ambient chains use hopSurvival.
// Occasionally (colorDrift) a leg crossfades to a new color over its whole
// run, so palettes shift gradually, never pop.
function launchSignal(path, phase = 0, speedFactor, chainColor, survival) {
const factor = speedFactor ?? 0.85 + random() * 0.3;
const surviveChance = survival ?? config.hopSurvival;
const colorFrom = chainColor ?? weightedChoice(random, config.signalColors);
const colorTo =
random() < config.colorDrift ? weightedChoice(random, config.signalColors) : colorFrom;
const d = pathData(path.points);
const speed = config.signalSpeed * factor;
const head = config.signalLength;
const tail = head * 2.2;
const length = path.length;
const total = length + tail;
const duration = (total / speed) * 1000;
const group = svgEl("g");
// Faint persistent wake along the whole route.
const wake = svgEl("path", { d, stroke: colorFrom, "stroke-width": 2, opacity: 0 });
// Soft glow under the head — a wide translucent stroke is far cheaper
// than a drop-shadow filter re-rasterizing every frame.
const glow = svgEl("path", {
d,
stroke: colorFrom,
"stroke-width": 7,
"stroke-linecap": "round",
"stroke-opacity": 0.1,
"stroke-dasharray": `${head} ${total}`,
"stroke-dashoffset": head,
});
// Long dim comet tail behind the head.
const comet = svgEl("path", {
d,
stroke: colorFrom,
"stroke-width": 2,
"stroke-linecap": "round",
"stroke-opacity": 0.16,
"stroke-dasharray": `${tail} ${total}`,
"stroke-dashoffset": tail,
});
// Bright head.
const pulse = svgEl("path", {
d,
stroke: colorFrom,
"stroke-width": 2.4,
"stroke-linecap": "round",
"stroke-dasharray": `${head} ${total}`,
"stroke-dashoffset": head,
"stroke-opacity": 0.72,
});
group.append(wake, glow, comet, pulse);
signalGroup.appendChild(group);
activeSignals += 1;
// All layers share the same leading edge; each runs from its own dash
// length down to -(total - itsDash) so they exit together. Color rides
// along in the same keyframes when this leg is a crossfade.
const timing = { duration, easing: "linear", fill: "forwards" };
const animations = [
pulse.animate(
[
{ strokeDashoffset: head, stroke: colorFrom },
{ strokeDashoffset: head - total, stroke: colorTo },
],
timing,
),
glow.animate(
[
{ strokeDashoffset: head, stroke: colorFrom },
{ strokeDashoffset: head - total, stroke: colorTo },
],
timing,
),
comet.animate(
[
{ strokeDashoffset: tail, stroke: colorFrom },
{ strokeDashoffset: tail - total, stroke: colorTo },
],
timing,
),
wake.animate(
[
{ opacity: 0, stroke: colorFrom },
{ opacity: 0.06 },
{ opacity: 0.06 },
{ opacity: 0, stroke: colorTo },
],
{ duration, fill: "forwards" },
),
];
const headArrival = (duration * length) / total;
const skip = Math.min(phase, 0.95) * headArrival;
if (skip > 0) for (const animation of animations) animation.currentTime = skip;
// The signal is logically done when its head reaches the final via (the
// tail after that is just residue): blip, release its population slot,
// and — in the same beat — launch its successor from a nearby via. The
// handoff is synchronous so motion never pauses.
const [endX, endY] = path.points[path.points.length - 1];
const myEpoch = epoch;
window.setTimeout(() => {
if (myEpoch !== epoch) return;
activeSignals -= 1;
const blip = svgEl("circle", {
cx: endX,
cy: endY,
r: 2,
fill: "none",
stroke: colorTo,
"stroke-width": 1.5,
"stroke-opacity": 0.6,
});
group.appendChild(blip);
blip.animate(
[
{ r: 2, strokeOpacity: 0.6 },
{ r: 8, strokeOpacity: 0 },
],
{ duration: 500, easing: "ease-out", fill: "forwards" },
);
// The successor inherits the color the chain ARRIVED with.
continueChain(endX, endY, path.startTrace, factor, colorTo, surviveChance);
}, headArrival - skip);
window.setTimeout(() => {
group.remove();
}, duration + 620 - skip);
}
// Every signal is mortal: on each via hop it rolls against its chain's
// survival rate (hopSurvival for ambient chains, burstSurvival for
// click-born ones — passed in so a chain keeps its own decay for life).
// Survivors continue synchronously from a nearby via, inheriting speed and
// color (arrive blue → depart blue); the rest just die with their arrival
// blip. Replenishment is NOT done here — the ambient spawn timer refills
// the population near the cursor, so a death simply lowers the census and
// the next tick fountains a fresh signal in. Low survival => most signals
// die after one leg => the fountain stays busy near the pointer.
function continueChain(endX, endY, excludeTrace, speedFactor, chainColor, survival) {
if (!config.signals || reducedMotion || document.hidden) return;
if (random() >= (survival ?? config.hopSurvival)) return;
if (activeSignals >= config.maxBurstSignals) return;
spawnSignal(nearbyEndpointStarts(endX, endY, excludeTrace), 0, speedFactor, chainColor, survival);
}
// preferred: array of nearby launch pads tried in order (short legs are
// fine for chain continuations); falls back to random picks elsewhere.
function spawnSignal(preferred, phase = 0, speedFactor, chainColor, survival) {
if (!config.signals || reducedMotion) return false;
if (activeSignals >= config.maxBurstSignals) return false;
const preferredCount = preferred ? preferred.length : 0;
const candidates = preferred ? [...preferred] : [];
for (let attempt = 0; attempt < 8; attempt += 1) {
const start = pickStart();
if (start) candidates.push(start);
}
for (let index = 0; index < candidates.length; index += 1) {
const minLength = index < preferredCount ? 60 : 140;
const path = buildSignalPath(tile, candidates[index], 450 + random() * 900, minLength);
if (path) {
path.startTrace = candidates[index].trace;
launchSignal(path, phase, speedFactor, chainColor, survival);
return true;
}
}
return false;
}
// Fill the population up to maxSignals with fast-forwarded signals, so a
// fresh board (or a tab returning to view after its chains drained) is
// already mid-journey instead of visibly cold-starting.
function prewarm() {
if (!config.signals || reducedMotion || document.hidden) return;
let guard = 0;
while (activeSignals < config.maxSignals && guard < config.maxSignals + 2) {
guard += 1;
if (!spawnSignal(null, random() * 0.85)) break;
}
}
// The passive fountain, like the reference site: a steady cadence of new
// signals near the cursor (pickStart biases toward the pointer when it moved
// recently, spreads across the board when idle), refilling toward
// maxSignals. Because signals are mortal, the census keeps dipping below the
// target and the timer keeps topping it up — so fresh signals keep appearing
// at the pointer. The balance the user wants is fast spawn + short life:
// a small signalInterval with a low hopSurvival gives a lively cursor
// fountain; long life lets the population sit at the cap and spawn rarely.
function startSignals() {
window.clearInterval(signalTimer);
if (!config.signals || reducedMotion) return;
prewarm();
signalTimer = window.setInterval(() => {
if (!document.hidden && activeSignals < config.maxSignals) spawnSignal(null);
}, config.signalInterval);
}
function burstAt(x, y) {
const { dense } = ensureSignalIndex(tile);
const candidates = [];
const baseTileX = Math.floor(x / tile.width);
const baseTileY = Math.floor(y / tile.height);
for (let tileY = baseTileY - 1; tileY <= baseTileY + 1; tileY += 1) {
for (let tileX = baseTileX - 1; tileX <= baseTileX + 1; tileX += 1) {
if (tileX < -1 || tileX > tilesAcross || tileY < -1 || tileY > tilesDown) continue;
const localX = x - tileX * tile.width;
const localY = y - tileY * tile.height;
for (let trace = 0; trace < dense.length; trace += 1) {
let bestPoint = -1;
let bestDistance = 40;
for (let point = 0; point < dense[trace].length; point += 1) {
const distance = Math.hypot(dense[trace][point][0] - localX, dense[trace][point][1] - localY);
if (distance < bestDistance) {
bestDistance = distance;
bestPoint = point;
}
}
if (bestPoint >= 0) candidates.push({ trace, tileX, tileY, point: bestPoint, distance: bestDistance });
}
}
}
candidates.sort((a, b) => a.distance - b.distance);
let spawned = 0;
for (const candidate of candidates.slice(0, Math.ceil(config.burstBoost / 2) + 1)) {
for (const direction of [1, -1]) {
if (activeSignals >= config.maxBurstSignals || spawned >= config.burstBoost) return;
const path = buildSignalPath(
tile,
{ trace: candidate.trace, tileX: candidate.tileX, tileY: candidate.tileY, point: candidate.point, direction },
260 + random() * 380,
50,
);
if (path) {
path.startTrace = candidate.trace;
// Clicked chains decay at burstSurvival, independent of the ambient
// hopSurvival, so a click can ripple long even when passive life is short.
launchSignal(path, 0, undefined, undefined, config.burstSurvival);
spawned += 1;
}
}
}
}
function onPointerMove(event) {
const rect = mount.getBoundingClientRect();
pointer = {
x: (event.clientX - rect.left) / config.scale,
y: (event.clientY - rect.top) / config.scale - topOffset,
at: performance.now(),
};
}
function onClick(event) {
if (!config.interactive || !config.signals || reducedMotion) return;
const rect = mount.getBoundingClientRect();
burstAt(
(event.clientX - rect.left) / config.scale,
(event.clientY - rect.top) / config.scale - topOffset,
);
}
function onVisibility() {
if (!document.hidden) prewarm();
}
function regenerate(nextOptions = {}) {
config = { ...config, ...nextOptions };
buildSvg();
startSignals();
return api;
}
function exportSvg() {
const clone = svg.cloneNode(true);
clone.querySelectorAll("g[fill='none'] > g").forEach((node) => node.remove());
clone.removeAttribute("style");
clone.setAttribute("width", String(tilesAcross * tile.width * config.scale));
clone.setAttribute("height", String(tilesDown * tile.height * config.scale));
return `<?xml version="1.0" encoding="UTF-8"?>\n${clone.outerHTML}`;
}
function exportTileSvg(tileOptions = {}) {
return tileSvg(tile, { ...config, ...tileOptions });
}
function destroy() {
epoch += 1;
window.clearInterval(signalTimer);
document.removeEventListener("visibilitychange", onVisibility);
resizeObserver?.disconnect();
interactionTarget.removeEventListener("pointermove", onPointerMove);
interactionTarget.removeEventListener("click", onClick);
svg?.remove();
if (computedPosition === "static") mount.style.position = originalPosition;
}
const api = {
regenerate,
spawnSignal,
exportSvg,
exportTileSvg,
destroy,
get tile() {
return tile;
},
};
buildSvg();
resizeObserver = new ResizeObserver(layout);
resizeObserver.observe(mount);
if (config.interactive) {
interactionTarget.addEventListener("pointermove", onPointerMove, { passive: true });
interactionTarget.addEventListener("click", onClick);
}
// Hidden tabs let the census drain (the timer no-ops); refill on return.
document.addEventListener("visibilitychange", onVisibility);
startSignals();
return api;
}
// Named animation presets. Each is a partial config merged over the current
// settings; only signal/animation knobs are touched so generation geometry is
// left alone. `signals: false` gives a fully static board.
const ANIMATION_PRESETS = Object.freeze({
"None": { signals: false },
"Fireflies": {
// A few long-lived signals drifting/bouncing across the whole screen.
signals: true,
signalInterval: 850,
maxSignals: 5,
spawnRadius: 200,
hopSurvival: 0.92,
burstSurvival: 0.9,
colorDrift: 0.12,
},
"Swarm": {
// Same persistent drift, but a dense population.
signals: true,
signalInterval: 360,
maxSignals: 18,
spawnRadius: 200,
hopSurvival: 0.9,
burstSurvival: 0.9,
colorDrift: 0.18,
},
"Cursor sparks": {
// Really short life + very fast spawning tight around the cursor.
signals: true,
signalInterval: 90,
maxSignals: 14,
spawnRadius: 35,
hopSurvival: 0.1,
burstSurvival: 0.6,
colorDrift: 0.15,
},
"Slow streams": {
// Longer-lived flowing chains at a relaxed cadence.
signals: true,
signalInterval: 560,
maxSignals: 8,
spawnRadius: 130,
hopSurvival: 0.72,
burstSurvival: 0.82,
colorDrift: 0.2,
},
});
export { DEFAULTS, ANIMATION_PRESETS };