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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Procedural Circuit Background</title>
<style>
:root {
color-scheme: dark;
--bg: #0d1117;
--surface: #161b22;
--border: #30363d;
--text: #e6edf3;
--muted: #8b949e;
--accent: #00b8b0;
--accent-bright: #00e6dc;
}
* { box-sizing: border-box; }
html, body { min-height: 100%; margin: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.hero {
--circuit-hole: var(--bg);
position: relative;
min-height: 100svh;
overflow: hidden;
display: grid;
place-items: center;
isolation: isolate;
}
.circuit-layer {
position: absolute;
inset: 0;
z-index: -2;
}
.glow {
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
background:
radial-gradient(circle at 52% 44%, rgba(0, 230, 220, .06), transparent 34%),
linear-gradient(to top, var(--bg) 0, transparent 12rem);
}
main {
width: min(760px, calc(100% - 40px));
text-align: center;
padding: 80px 0 180px;
}
.eyebrow {
margin: 0 0 20px;
color: var(--accent-bright);
font: 600 12px/1.3 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
letter-spacing: .18em;
text-transform: uppercase;
}
h1 {
margin: 0;
font-size: clamp(2.6rem, 8vw, 5.6rem);
line-height: .9;
letter-spacing: -.055em;
color: transparent;
background: linear-gradient(110deg, #e9ffff, #7bc9c9 62%, var(--accent-bright));
background-clip: text;
filter: drop-shadow(0 0 .4em rgba(255,255,255,.18));
}
.lede {
max-width: 570px;
margin: 28px auto 0;
color: #bac6d3;
font-size: clamp(1rem, 2vw, 1.25rem);
line-height: 1.6;
}
.hint {
margin-top: 24px;
color: var(--muted);
font: 500 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.controls {
position: fixed;
left: 50%;
bottom: 12px;
z-index: 5;
width: min(940px, calc(100% - 24px));
transform: translateX(-50%);
display: grid;
grid-template-columns: minmax(104px, .6fr) minmax(0, 3fr) auto auto auto;
align-items: end;
gap: 8px;
padding: 10px 12px;
border: 1px solid rgba(48, 54, 61, .9);
border-radius: 12px;
background: rgba(22, 27, 34, .88);
box-shadow: 0 18px 70px rgba(0,0,0,.38);
backdrop-filter: blur(18px);
}
label { display: grid; gap: 5px; color: var(--muted); font-size: 12px; }
input[type="text"] {
width: 100%;
height: 32px;
padding: 0 10px;
color: var(--text);
border: 1px solid var(--border);
border-radius: 7px;
outline: none;
background: #0d1117;
font: 500 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
input[type="text"]:focus { border-color: var(--accent); }
input[type="range"] { width: 100%; accent-color: var(--accent); }
button {
height: 32px;
padding: 0 12px;
color: var(--text);
border: 1px solid var(--border);
border-radius: 7px;
background: #21262d;
font: 700 13px system-ui, sans-serif;
cursor: pointer;
}
button:hover { border-color: #59636e; background: #282e36; }
button.primary { color: #062d2b; border-color: var(--accent); background: var(--accent); }
button.primary:hover { background: var(--accent-bright); }
/* Sits in the top row where Density + Live signals used to be, and is
labelled like the other fields ("Seed" above its input). */
.presets-field { display: grid; gap: 5px; min-width: 0; }
.presets-label { color: var(--muted); font-size: 12px; }
.presets {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
}
.presets button {
height: 32px;
padding: 0 9px;
font: 600 12px system-ui, sans-serif;
}
.presets button.active {
color: #062d2b;
border-color: var(--accent);
background: var(--accent);
}
.tuning {
grid-column: 1 / -1;
border-top: 1px solid rgba(48, 54, 61, .7);
padding-top: 8px;
}
.tuning summary {
cursor: pointer;
color: var(--muted);
font: 600 12px system-ui, sans-serif;
user-select: none;
}
.tuning summary:hover { color: var(--text); }
.tuning .knobs {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 7px 14px;
padding-top: 9px;
}
.knob span {
display: flex;
justify-content: space-between;
gap: 8px;
}
.knob output { color: var(--accent-bright); font-variant-numeric: tabular-nums; }
.tuning .reset-row { margin-top: 9px; display: flex; justify-content: flex-end; }
.tuning button { height: 27px; font-size: 12px; }
@media (max-width: 760px) {
.controls { grid-template-columns: 1fr 1fr; max-height: 60svh; overflow-y: auto; }
/* Direct children only — preset chips and panel resets keep their size. */
.controls > button { width: 100%; }
.presets-field { grid-column: 1 / -1; }
main { padding-bottom: 270px; }
}
</style>
</head>
<body>
<section class="hero" id="hero">
<div class="circuit-layer" id="circuit"></div>
<div class="glow" aria-hidden="true"></div>
<main>
<p class="eyebrow">Seeded SVG generator · occupancy-grid engine</p>
<h1>Live circuitry</h1>
<p class="lede">
Procedural generation of Nitrogen's trace pattern: collision-free routed traces,
45° chamfers, vias, and glowing signals that hop across the generated topology.
</p>
<p class="hint">Move the pointer to attract signals · click anywhere for a burst</p>
</main>
</section>
<form class="controls" id="controls">
<label>
Seed
<input id="seed" type="text" value="nitrogen" autocomplete="off" spellcheck="false" />
</label>
<div class="presets-field">
<span class="presets-label">Animation</span>
<div class="presets" id="presets"></div>
</div>
<button class="primary" type="submit">Generate</button>
<button id="download" type="button">Download SVG</button>
<button id="downloadTile" type="button">Download Tile</button>
<details class="tuning">
<summary>Generation tuning</summary>
<div class="knobs">
<label class="knob">
<span>Density <output>1.05</output></span>
<input type="range" data-key="density" min="0.35" max="1.45" step="0.05" value="1.05" />
</label>
<label class="knob">
<span>Formations <output>96</output></span>
<input type="range" data-key="formations" min="24" max="96" step="4" value="96" />
</label>
<label class="knob">
<span>Bus width <output>1</output></span>
<input type="range" data-key="busWidth" min="0.5" max="2" step="0.1" value="1" />
</label>
<label class="knob">
<span>Turn penalty <output>0.55</output></span>
<input type="range" data-key="turnPenalty" min="0.1" max="1.2" step="0.05" value="0.55" />
</label>
<label class="knob">
<span>Weave spacing <output>0.22</output></span>
<input type="range" data-key="weave" min="0" max="0.45" step="0.01" value="0.22" />
</label>
<label class="knob">
<span>Cramp aversion <output>0.4</output></span>
<input type="range" data-key="cramp" min="0" max="0.8" step="0.05" value="0.4" />
</label>
<label class="knob">
<span>Horizontal bias <output>0.6</output></span>
<input type="range" data-key="horizontalBias" min="0.35" max="0.9" step="0.05" value="0.6" />
</label>
<label class="knob">
<span>Net reach <output>42</output></span>
<input type="range" data-key="netReach" min="16" max="60" step="2" value="42" />
</label>
<label class="knob">
<span>Jog bias <output>5</output></span>
<input type="range" data-key="jogBias" min="0" max="10" step="1" value="5" />
</label>
<label class="knob">
<span>Stub chance <output>0</output></span>
<input type="range" data-key="stubChance" min="0" max="1" step="0.05" value="0" />
</label>
<label class="knob">
<span>Inline vias <output>0.03</output></span>
<input type="range" data-key="inlineViaChance" min="0" max="0.12" step="0.005" value="0.03" />
</label>
<label class="knob">
<span>Tap combs <output>8</output></span>
<input type="range" data-key="combs" min="0" max="16" step="1" value="8" />
</label>
</div>
<div class="reset-row">
<button class="reset-tuning" type="button">Reset generation</button>
</div>
</details>
<details class="tuning" data-panel="animation">
<summary>Animation tuning</summary>
<div class="knobs">
<label class="knob">
<span>Signal speed <output>420</output></span>
<input type="range" data-key="signalSpeed" min="100" max="1200" step="20" value="420" />
</label>
<label class="knob">
<span>Spawn rate (ms) <output>240</output></span>
<input type="range" data-key="signalInterval" min="80" max="900" step="20" value="240" />
</label>
<label class="knob">
<span>Cursor range <output>110</output></span>
<input type="range" data-key="spawnRadius" min="25" max="200" step="5" value="110" />
</label>
<label class="knob">
<span>Max signals <output>12</output></span>
<input type="range" data-key="maxSignals" min="2" max="20" step="1" value="12" />
</label>
<label class="knob">
<span>Passive survival <output>0.4</output></span>
<input type="range" data-key="hopSurvival" min="0" max="0.95" step="0.05" value="0.4" />
</label>
<label class="knob">
<span>Click survival <output>0.72</output></span>
<input type="range" data-key="burstSurvival" min="0" max="0.95" step="0.05" value="0.72" />
</label>
<label class="knob">
<span>Click boost <output>7</output></span>
<input type="range" data-key="burstBoost" min="0" max="11" step="1" value="7" />
</label>
</div>
<div class="reset-row">
<button class="reset-tuning" type="button">Reset animation</button>
</div>
</details>
</form>
<script type="module">
import { createCircuitBackground, ANIMATION_PRESETS } from "./circuit-background.js";
const mount = document.querySelector("#circuit");
const controls = document.querySelector("#controls");
const seed = document.querySelector("#seed");
const download = document.querySelector("#download");
const downloadTile = document.querySelector("#downloadTile");
const presetBar = document.querySelector("#presets");
// Animation on/off now lives entirely in the presets ("None" = off);
// the page opens on None, so the board starts static.
let signalsOn = false;
const circuit = createCircuitBackground(mount, { seed: seed.value, signals: signalsOn });
// Every slider (density included) is a data-key knob inside a tuning
// panel, so `tuning()` collects them all straight into the config.
const knobs = [...document.querySelectorAll(".tuning input[type=range]")];
const knobDefaults = new Map(knobs.map((knob) => [knob, knob.value]));
const knobByKey = new Map(knobs.map((knob) => [knob.dataset.key, knob]));
const tuning = () =>
Object.fromEntries(knobs.map((knob) => [knob.dataset.key, Number(knob.value)]));
const settings = () => ({
seed: seed.value.trim() || "nitrogen",
signals: signalsOn,
...tuning(),
});
// --- animation presets --------------------------------------------------
const presetButtons = new Map();
const clearActivePreset = () => {
for (const button of presetButtons.values()) button.classList.remove("active");
};
const syncKnob = (knob) => {
knob.parentElement.querySelector("output").value = knob.value;
};
const applyPreset = (name) => {
const preset = ANIMATION_PRESETS[name];
for (const [key, value] of Object.entries(preset)) {
if (key === "signals") { signalsOn = value; continue; }
const knob = knobByKey.get(key);
if (knob) { knob.value = value; syncKnob(knob); }
}
clearActivePreset();
presetButtons.get(name).classList.add("active");
circuit.regenerate(settings());
};
for (const name of Object.keys(ANIMATION_PRESETS)) {
const button = document.createElement("button");
button.type = "button";
button.textContent = name;
button.addEventListener("click", () => applyPreset(name));
presetBar.appendChild(button);
presetButtons.set(name, button);
}
// Reflect the initial "None" state in the preset bar.
presetButtons.get("None").classList.add("active");
// Only animation knobs invalidate the highlighted preset — retuning
// generation (density, formations, ...) leaves the animation untouched.
const isAnimationPanel = (node) => node.closest("details")?.dataset.panel === "animation";
for (const knob of knobs) {
const out = knob.parentElement.querySelector("output");
const show = () => { out.value = knob.value; };
show();
knob.addEventListener("input", show);
knob.addEventListener("change", () => {
if (isAnimationPanel(knob)) clearActivePreset();
circuit.regenerate(settings());
});
}
// Each panel's reset button restores only that panel's own knobs.
for (const resetButton of document.querySelectorAll(".reset-tuning")) {
resetButton.addEventListener("click", () => {
const panelKnobs = resetButton.closest("details").querySelectorAll("input[type=range]");
for (const knob of panelKnobs) {
knob.value = knobDefaults.get(knob);
knob.parentElement.querySelector("output").value = knob.value;
}
if (isAnimationPanel(resetButton)) clearActivePreset();
circuit.regenerate(settings());
});
}
controls.addEventListener("submit", (event) => {
event.preventDefault();
circuit.regenerate(settings());
});
function saveBlob(text, name) {
const blob = new Blob([text], { type: "image/svg+xml" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = name;
link.click();
URL.revokeObjectURL(url);
}
const slug = () => (seed.value || "seed").replace(/[^a-z0-9_-]+/gi, "-");
download.addEventListener("click", () => {
saveBlob(circuit.exportSvg(), `circuit-${slug()}.svg`);
});
downloadTile.addEventListener("click", () => {
saveBlob(circuit.exportTileSvg(), `circuit-tile-${slug()}.svg`);
});
</script>
</body>
</html>