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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
const SVG_NS = "http://www.w3.org/2000/svg";
const DEFAULTS = Object.freeze({
seed: "nitrogen",
tileWidth: 900,
tileHeight: 450,
pitch: 7.895,
scale: 1000 / 900,
density: 1,
traceColor: "#151b25",
traceWidth: 1.58,
viaOuterRadius: 3.34,
viaInnerRadius: 1.76,
signals: true,
interactive: true,
signalColors: [
["#00e6dc", 0.6],
["#64abff", 0.25],
["#c5b3ff", 0.15],
],
signalInterval: 520,
signalSpeed: 800,
signalLength: 150,
maxSignals: 8,
});
let instanceCount = 0;
function hashSeed(value) {
let hash = 2166136261;
for (const char of String(value)) {
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 integer(random, min, max) {
return Math.floor(random() * (max - min + 1)) + min;
}
function choose(random, values) {
return values[Math.floor(random() * values.length)];
}
function weighted(random, entries) {
let roll = random();
for (const [value, weight] of entries) {
roll -= weight;
if (roll <= 0) return value;
}
return entries[0][0];
}
function shuffle(random, values) {
const result = [...values];
for (let index = result.length - 1; index > 0; index -= 1) {
const swap = integer(random, 0, index);
[result[index], result[swap]] = [result[swap], result[index]];
}
return result;
}
function partition(random, total, count, minimum) {
const values = Array(count).fill(Math.floor(total / count));
for (let index = 0; index < total % count; index += 1) values[index] += 1;
for (let turn = 0; turn < 10; turn += 1) {
const from = integer(random, 0, count - 1);
let to = integer(random, 0, count - 1);
if (to === from) to = (to + 1) % count;
if (values[from] > minimum) {
values[from] -= 1;
values[to] += 1;
}
}
return values;
}
function lengthOf(points) {
return points.slice(1).reduce((total, point, index) => {
const previous = points[index];
return total + Math.hypot(point[0] - previous[0], point[1] - previous[1]);
}, 0);
}
function compact(points) {
const clean = [];
for (const point of points) {
const previous = clean.at(-1);
if (!previous || point[0] !== previous[0] || point[1] !== previous[1]) clean.push(point);
}
return clean;
}
function addTrace(target, points, viaStart = true, viaEnd = true) {
const clean = compact(points);
target.push({ points: clean, length: lengthOf(clean), viaStart, viaEnd });
}
function horizontalBundle(target, box, count, random) {
const firstBend = box.x + Math.max(3, Math.floor(box.w * 0.25));
const secondBend = box.x + Math.min(box.w - 3, Math.floor(box.w * 0.75));
const maximumDiagonal = Math.max(1, Math.floor((secondBend - firstBend - 1) / 2));
const diagonal = Math.min(integer(random, 2, 3), maximumDiagonal);
const shift = diagonal * (random() < 0.5 ? -1 : 1);
const available = box.h - 4 - diagonal;
const lanes = Math.max(3, Math.min(count, available));
const yStart = shift > 0 ? box.y + 2 : box.y + box.h - 2 - lanes;
for (let lane = 0; lane < lanes; lane += 1) {
const y = yStart + lane;
addTrace(target, [
[box.x, y],
[firstBend, y],
[firstBend + diagonal, y + shift],
[secondBend - diagonal, y + shift],
[secondBend, y],
[box.x + box.w, y],
], random() < 0.72, random() < 0.72);
}
}
function verticalBundle(target, box, count, random) {
const swapped = [];
horizontalBundle(swapped, { x: box.y, y: box.x, w: box.h, h: box.w }, count, random);
for (const trace of swapped) {
addTrace(target, trace.points.map(([x, y]) => [y, x]), trace.viaStart, trace.viaEnd);
}
}
function cornerBundle(target, box, count, random) {
const lanes = Math.max(4, Math.min(count, box.w - 5, box.h - 5));
const orientation = integer(random, 0, 3);
for (let lane = 0; lane < lanes; lane += 1) {
let points;
if (orientation === 0) {
const x = box.x + 2 + lane;
const y = box.y + box.h - 2 - lane;
points = [[x, box.y], [x, y - 1], [x + 1, y], [box.x + box.w, y]];
} else if (orientation === 1) {
const x = box.x + box.w - 2 - lane;
const y = box.y + box.h - 2 - lane;
points = [[x, box.y], [x, y - 1], [x - 1, y], [box.x, y]];
} else if (orientation === 2) {
const x = box.x + 2 + lane;
const y = box.y + 2 + lane;
points = [[x, box.y + box.h], [x, y + 1], [x + 1, y], [box.x + box.w, y]];
} else {
const x = box.x + box.w - 2 - lane;
const y = box.y + 2 + lane;
points = [[x, box.y + box.h], [x, y + 1], [x - 1, y], [box.x, y]];
}
addTrace(target, points, random() < 0.76, random() < 0.76);
}
}
function hairpinBundle(target, box, count, random) {
const loops = Math.max(3, Math.min(Math.floor(count * 0.62), Math.floor((box.h - 3) / 2), box.w - 6));
const opensLeft = random() < 0.5;
for (let lane = 0; lane < loops; lane += 1) {
const top = box.y + 1 + lane;
const bottom = box.y + box.h - 1 - lane;
if (opensLeft) {
const end = box.x + box.w - 1 - lane;
addTrace(target, [
[box.x, top],
[end - 1, top],
[end, top + 1],
[end, bottom - 1],
[end - 1, bottom],
[box.x, bottom],
]);
} else {
const end = box.x + 1 + lane;
addTrace(target, [
[box.x + box.w, top],
[end + 1, top],
[end, top + 1],
[end, bottom - 1],
[end + 1, bottom],
[box.x + box.w, bottom],
]);
}
}
}
function scaleTrace(trace, pitch) {
const points = trace.points.map(([x, y]) => [x * pitch, y * pitch]);
return { ...trace, points, length: lengthOf(points) };
}
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 rowHeights = partition(random, rows, 4, 12);
const patterns = shuffle(random, [
"horizontal", "corner", "hairpin", "vertical", "corner",
"vertical", "horizontal", "corner", "hairpin", "horizontal",
"corner", "vertical", "horizontal", "corner", "hairpin",
"horizontal", "corner", "vertical", "hairpin", "horizontal",
]);
const traces = [];
const cells = [];
let y = 0;
let patternIndex = 0;
for (const rowHeight of rowHeights) {
const widths = partition(random, columns, 5, 18);
let x = 0;
for (const width of widths) {
const box = { x: x + 1, y: y + 1, w: width - 2, h: rowHeight - 2 };
const pattern = patterns[patternIndex++];
const baseLanes = integer(random, 5, 8);
const laneCount = Math.max(3, Math.round(baseLanes * config.density));
if (pattern === "horizontal") horizontalBundle(traces, box, laneCount, random);
if (pattern === "vertical") verticalBundle(traces, box, laneCount, random);
if (pattern === "corner") cornerBundle(traces, box, laneCount, random);
if (pattern === "hairpin") hairpinBundle(traces, box, laneCount, random);
cells.push({ ...box, pattern });
x += width;
}
y += rowHeight;
}
const scaled = traces.map((trace) => scaleTrace(trace, config.pitch));
return {
width: columns * config.pitch,
height: rows * config.pitch,
pitch: config.pitch,
traces: scaled,
cells,
};
}
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("");
}
function svgElement(name, attributes = {}) {
const element = document.createElementNS(SVG_NS, name);
for (const [key, value] of Object.entries(attributes)) element.setAttribute(key, String(value));
return element;
}
export function createCircuitBackground(mount, options = {}) {
if (!(mount instanceof Element)) throw new TypeError("mount must be a DOM element");
const id = `circuit-v2-${++instanceCount}`;
let config = { ...DEFAULTS, ...options };
let tile;
let svg;
let uses;
let signals;
let resizeObserver;
let timer;
let activeSignals = 0;
let tilesAcross = 1;
let tilesDown = 1;
let pointer = null;
let signalRandom = mulberry32(hashSeed(`${config.seed}:signals`));
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 via(parent, point) {
parent.append(
svgElement("circle", { cx: point[0], cy: point[1], r: config.viaOuterRadius, fill: config.traceColor }),
svgElement("circle", { cx: point[0], cy: point[1], r: config.viaInnerRadius, fill: "var(--circuit-hole, #0d1117)" }),
);
}
function layout() {
const width = Math.max(1, mount.clientWidth);
const height = Math.max(1, mount.clientHeight);
tilesAcross = Math.max(1, Math.ceil(width / (tile.width * config.scale)));
tilesDown = Math.max(2, Math.ceil(height / (tile.height * config.scale)) + 1);
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 ${tile.height / 2} ${viewWidth} ${viewHeight}`);
uses.replaceChildren();
for (let row = 0; row < tilesDown; row += 1) {
for (let column = 0; column < tilesAcross; column += 1) {
uses.appendChild(svgElement("use", {
href: `#${id}-tile`,
transform: `translate(${column * tile.width}, ${row * tile.height})`,
}));
}
}
}
function build() {
svg?.remove();
tile = generateCircuitTile(config);
signalRandom = mulberry32(hashSeed(`${config.seed}:signals`));
svg = svgElement("svg", { "aria-hidden": true, focusable: false, xmlns: SVG_NS });
Object.assign(svg.style, {
position: "absolute", left: "0", top: "0", display: "block",
maxWidth: "none", pointerEvents: "none", overflow: "visible",
});
const defs = svgElement("defs");
const tileGroup = svgElement("g", { id: `${id}-tile` });
for (const trace of tile.traces) {
tileGroup.appendChild(svgElement("path", {
d: pathData(trace.points), fill: "none", stroke: config.traceColor,
"stroke-width": config.traceWidth, "stroke-linecap": "butt", "stroke-linejoin": "miter",
}));
if (trace.viaStart) via(tileGroup, trace.points[0]);
if (trace.viaEnd) via(tileGroup, trace.points.at(-1));
}
defs.appendChild(tileGroup);
svg.appendChild(defs);
uses = svgElement("g");
signals = svgElement("g", { "data-signals": "", fill: "none" });
svg.append(uses, signals);
mount.prepend(svg);
layout();
}
function nearestTrace() {
if (!pointer) return null;
let result = null;
let best = Infinity;
for (let attempt = 0; attempt < 40; attempt += 1) {
const trace = tile.traces[integer(signalRandom, 0, tile.traces.length - 1)];
const point = trace.points[Math.floor(trace.points.length / 2)];
const column = Math.floor(pointer.x / tile.width);
const row = Math.floor(pointer.y / tile.height);
const distance = Math.hypot(point[0] + column * tile.width - pointer.x, point[1] + row * tile.height - pointer.y);
if (distance < best) {
best = distance;
result = { trace, column, row };
}
}
return result;
}
function spawnSignal({ nearPointer = false } = {}) {
if (!config.signals || reducedMotion || activeSignals >= config.maxSignals || !tile.traces.length) return;
const selected = nearPointer ? nearestTrace() : null;
const trace = selected?.trace || tile.traces[integer(signalRandom, 0, tile.traces.length - 1)];
const column = selected?.column ?? integer(signalRandom, 0, tilesAcross - 1);
const row = selected?.row ?? integer(signalRandom, 0, tilesDown - 1);
if (column < 0 || column >= tilesAcross || row < 0 || row >= tilesDown) return;
const points = signalRandom() < 0.5 ? [...trace.points].reverse() : trace.points;
const d = pathData(points, column * tile.width, row * tile.height);
const color = weighted(signalRandom, config.signalColors);
const duration = ((trace.length + config.signalLength) / config.signalSpeed) * 1000;
const group = svgElement("g");
const pulse = svgElement("path", {
d, stroke: color, "stroke-width": 2.4, "stroke-linecap": "round",
"stroke-dasharray": `${config.signalLength} ${trace.length + config.signalLength}`,
"stroke-dashoffset": config.signalLength, "stroke-opacity": 0.62,
});
pulse.style.filter = `drop-shadow(0 0 3px ${color})`;
group.appendChild(pulse);
signals.appendChild(group);
activeSignals += 1;
pulse.animate(
[{ strokeDashoffset: config.signalLength }, { strokeDashoffset: -trace.length }],
{ duration, easing: "linear", fill: "forwards" },
);
window.setTimeout(() => {
group.remove();
activeSignals = Math.max(0, activeSignals - 1);
}, duration + 150);
}
function onPointerMove(event) {
const rect = mount.getBoundingClientRect();
pointer = {
x: (event.clientX - rect.left) / config.scale,
y: (event.clientY - rect.top) / config.scale + tile.height / 2,
at: performance.now(),
};
}
function onClick() {
for (let index = 0; index < 5; index += 1) {
window.setTimeout(() => spawnSignal({ nearPointer: true }), index * 55);
}
}
function startSignals() {
window.clearInterval(timer);
if (!config.signals || reducedMotion) return;
timer = window.setInterval(() => {
if (!document.hidden) spawnSignal({ nearPointer: pointer && performance.now() - pointer.at < 4000 });
}, config.signalInterval);
}
function regenerate(nextOptions = {}) {
config = { ...config, ...nextOptions };
activeSignals = 0;
build();
startSignals();
return api;
}
function exportSvg() {
const clone = svg.cloneNode(true);
clone.querySelector("[data-signals]")?.replaceChildren();
clone.removeAttribute("style");
return `<?xml version="1.0" encoding="UTF-8"?>\n${clone.outerHTML}`;
}
function destroy() {
window.clearInterval(timer);
resizeObserver?.disconnect();
interactionTarget.removeEventListener("pointermove", onPointerMove);
interactionTarget.removeEventListener("click", onClick);
svg?.remove();
if (computedPosition === "static") mount.style.position = originalPosition;
}
const api = { regenerate, spawnSignal, exportSvg, destroy, get tile() { return tile; } };
build();
resizeObserver = new ResizeObserver(layout);
resizeObserver.observe(mount);
if (config.interactive) {
interactionTarget.addEventListener("pointermove", onPointerMove, { passive: true });
interactionTarget.addEventListener("click", onClick);
}
startSignals();
return api;
}
export { DEFAULTS };