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
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061
<!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;
overflow-x: hidden;
}
.hero {
--circuit-hole: var(--bg);
position: relative;
width: 100vw;
min-height: 100svh;
overflow: hidden;
display: grid;
place-items: center;
isolation: isolate;
}
.hero[data-reveal-system="scroll"] {
min-height: 300svh;
place-items: start center;
}
.hero[data-reveal-system="scroll"] main {
position: relative;
top: auto;
min-height: 100svh;
display: grid;
place-items: center;
}
.circuit-layer {
position: absolute;
inset: 0;
z-index: 0;
}
.glow {
--atmosphere-strength: 1;
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background:
radial-gradient(ellipse 46% 34% at 50% 45%, rgba(13, 17, 23, .64), rgba(13, 17, 23, .34) 48%, transparent 100%),
linear-gradient(to top, var(--bg) 0, transparent 13rem);
}
.glow::before {
content: "";
position: absolute;
inset: -12%;
background:
radial-gradient(ellipse 28% 38% at 22% 48%, rgba(0, 230, 220, .06), transparent 72%),
radial-gradient(ellipse 34% 42% at 58% 46%, rgba(57, 184, 255, .045), transparent 72%),
radial-gradient(ellipse 30% 38% at 84% 52%, rgba(140, 107, 247, .045), transparent 72%);
mix-blend-mode: screen;
opacity: calc(var(--atmosphere-strength) * .72);
filter: blur(34px);
}
.hero[data-motion="on"][data-animation-system="conductor"] .glow {
background:
radial-gradient(circle at 52% 44%, rgba(0, 230, 220, .06), transparent 34%),
linear-gradient(to top, var(--bg) 0, transparent 12rem);
animation: atmosphere-breathe 8s ease-in-out infinite alternate;
will-change: opacity, filter;
}
.hero[data-motion="on"][data-animation-system="conductor"] .glow::before {
inset: -18%;
background:
radial-gradient(circle at 28% 38%, rgba(0, 230, 220, .075), transparent 20%),
radial-gradient(circle at 72% 58%, rgba(100, 171, 255, .055), transparent 24%),
radial-gradient(circle at 52% 82%, rgba(197, 179, 255, .04), transparent 18%);
opacity: 1;
filter: none;
animation: atmosphere-drift 19s cubic-bezier(.45,.05,.55,.95) infinite alternate;
will-change: transform;
}
@keyframes atmosphere-breathe {
from { opacity: var(--atmosphere-low, .66); filter: saturate(.82); }
to { opacity: var(--atmosphere-high, 1); filter: saturate(1.18); }
}
@keyframes atmosphere-drift {
0% { transform: translate3d(-3%, -2%, 0) scale(1); }
45% { transform: translate3d(4%, 1%, 0) scale(1.08); }
100% { transform: translate3d(-1%, 5%, 0) scale(1.03); }
}
main {
position: relative;
z-index: 2;
width: min(760px, calc(100% - 40px));
text-align: center;
padding: 80px 0 180px;
}
.hero-copy { position: relative; }
.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(3.2rem, 10vw, 7.5rem);
line-height: .9;
letter-spacing: -.055em;
color: transparent;
background: linear-gradient(100deg, #e6edf3 4%, #00e6dc 38%, #39b8ff 66%, #8c6bf7 96%);
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(1180px, calc(100% - 24px));
transform: translateX(-50%);
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
align-items: end;
gap: 8px;
padding: 10px;
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 { min-width: 0; display: grid; gap: 4px; color: var(--muted); font-size: 10px; }
label > span { display: flex; justify-content: space-between; gap: 3px; white-space: nowrap; }
input[type="text"] {
width: 100%;
height: 32px;
padding: 0 8px;
color: var(--text);
border: 1px solid var(--border);
border-radius: 8px;
outline: none;
background: #0d1117;
font: 500 11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
input[type="text"]:focus { border-color: var(--accent); }
input[type="range"] { width: 100%; accent-color: var(--accent); }
select {
width: 100%;
height: 32px;
padding: 0 28px 0 8px;
color: var(--text);
border: 1px solid var(--border);
border-radius: 8px;
outline: none;
background: #0d1117;
font: 600 11px system-ui, sans-serif;
}
select:focus { border-color: var(--accent); }
button {
height: 32px;
padding: 0 11px;
color: var(--text);
border: 1px solid var(--border);
border-radius: 8px;
background: #21262d;
font: 700 12px system-ui, sans-serif;
cursor: pointer;
}
button:hover { border-color: #59636e; background: #282e36; }
button:disabled { opacity: .48; cursor: default; }
button:disabled:hover { border-color: var(--border); background: #21262d; }
button.primary { color: #062d2b; border-color: var(--accent); background: var(--accent); }
button.primary:hover { background: var(--accent-bright); }
.preset-bar {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.preset-title {
margin-right: 3px;
color: var(--muted);
font: 700 11px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
letter-spacing: .12em;
text-transform: uppercase;
}
.preset-bar button {
height: 28px;
padding: 0 10px;
color: #aab5c1;
font-size: 11px;
}
.preset-bar button[aria-pressed="true"] {
color: #082f2d;
border-color: var(--accent);
background: var(--accent);
}
.parameter-panel {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
align-items: end;
gap: 8px;
}
.parameter-panel[hidden] { display: none; }
.reveal-options-panel {
position: absolute;
right: 10px;
bottom: calc(100% + 8px);
width: min(760px, calc(100vw - 44px));
max-height: calc(100svh - 116px);
overflow-y: auto;
padding: 12px;
border: 1px solid rgba(48, 54, 61, .96);
border-radius: 12px;
background: rgba(18, 23, 30, .97);
box-shadow: 0 18px 70px rgba(0,0,0,.48);
backdrop-filter: blur(20px);
}
.reveal-options-panel[hidden] { display: none; }
.reveal-options-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.reveal-options-heading strong {
color: var(--text);
font: 700 11px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
letter-spacing: .1em;
text-transform: uppercase;
}
.reveal-options-heading span { color: var(--muted); font-size: 10px; }
.reveal-options-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px 12px;
}
.reveal-option[hidden] { display: none; }
.reveal-option output { color: #c4d0dc; }
/* A quiet text toggle, not a button: it opens a drawer, it does not pick a preset. */
.preset-bar .panel-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
width: auto;
height: 28px;
padding: 0 2px;
margin-left: 4px;
color: var(--muted);
border: 0;
border-radius: 0;
background: none;
font: 600 11px system-ui, sans-serif;
}
.preset-bar .panel-toggle:hover { color: var(--text); border: 0; background: none; }
.preset-bar .panel-toggle:focus-visible { outline: 1px solid var(--accent); outline-offset: 3px; }
.preset-bar .panel-toggle[aria-expanded="true"] { color: var(--accent-bright); }
.panel-toggle .chevron { display: inline-block; font-size: 9px; line-height: 1; transition: transform .15s ease; }
.panel-toggle[aria-expanded="true"] .chevron { transform: rotate(180deg); }
.motion-bar {
padding-top: 8px;
border-top: 1px solid rgba(48, 54, 61, .72);
}
.preset-group {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.group-divider {
align-self: stretch;
width: 1px;
min-height: 22px;
margin: 0 2px;
background: rgba(48, 54, 61, .8);
}
.reveal-group[data-disabled="true"] { opacity: .48; }
.reveal-group[data-disabled="true"] button { pointer-events: none; }
.preset-bar .replay-button {
padding-inline: 7px;
color: var(--muted);
border-style: dashed;
}
.movement-power {
flex: 1 1 150px;
max-width: 180px;
margin-left: 6px;
}
.movement-power input { margin: 0; }
.trace-count {
margin-left: auto;
color: var(--accent-bright);
font: 600 11px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
white-space: nowrap;
}
@media (max-width: 1180px) {
.controls {
grid-template-columns: repeat(6, minmax(0, 1fr));
max-height: calc(100svh - 24px);
overflow-y: auto;
}
.parameter-panel { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.controls:has(.reveal-options-panel:not([hidden])) { overflow: visible; }
.preset-bar { flex-wrap: wrap; }
.preset-group { flex-wrap: wrap; }
}
@media (max-width: 760px) {
.controls { grid-template-columns: 1fr 1fr; bottom: 12px; }
.parameter-panel { grid-template-columns: 1fr 1fr; }
.controls button { width: 100%; }
.preset-title { width: 100%; }
.trace-count { margin-left: 0; }
main { padding-bottom: 360px; }
.reveal-options-panel {
position: absolute;
left: 0;
right: 0;
bottom: calc(100% + 8px);
width: auto;
max-height: calc(100svh - 156px);
overflow-y: auto;
border-radius: 16px 16px 10px 10px;
}
.reveal-options-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 460px) {
.reveal-options-grid { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
.glow, .glow::before { animation: none; }
.panel-toggle .chevron { transition: none; }
}
</style>
</head>
<body>
<section class="hero" id="hero">
<div class="circuit-layer" id="circuit"></div>
<div class="glow" aria-hidden="true"></div>
<main>
<div class="hero-copy" data-circuit-reading-zone>
<p class="eyebrow">Seeded SVG generator</p>
<h1>Live circuitry</h1>
<p class="lede">
A procedural version of the Nitrogen hero pattern: dense routed traces, 45° chamfers,
annular vias, continuous trace packing, and separate reveal and ambient animation systems.
</p>
<p class="hint" id="motionHint">All traces arrive fully routed · no ambient movement</p>
</div>
</main>
</section>
<form class="controls" id="controls">
<div class="preset-bar" aria-label="Generator presets">
<span class="preset-title">Generation</span>
<button type="button" data-preset="original" aria-pressed="true">Original match</button>
<button type="button" data-preset="dense" aria-pressed="false">Dense</button>
<button type="button" data-preset="diagonal" aria-pressed="false">Diagonal sweep</button>
<button type="button" data-preset="swirl" aria-pressed="false">Broad swirl</button>
<button type="button" data-preset="sparse" aria-pressed="false">Sparse</button>
<button type="button" class="panel-toggle" id="parameterToggle" aria-expanded="false" aria-controls="parameterPanel">
Parameters<span class="chevron" aria-hidden="true">▾</span>
</button>
<output class="trace-count" id="traceCount">235 traces</output>
</div>
<div class="parameter-panel" id="parameterPanel" hidden>
<label>
Seed
<input id="seed" type="text" value="nitrogen" autocomplete="off" spellcheck="false" />
</label>
<label>
<span>Density <output id="fillValue">74.4%</output></span>
<input id="fill" type="range" min="0.35" max="0.76" step="0.001" value="0.744" />
</label>
<label>
<span>Avg length <output id="routeLengthValue">1.14×</output></span>
<input id="routeLength" type="range" min="0.55" max="2.25" step="0.01" value="1.14" />
</label>
<label>
<span>Primary flow <output id="horizontalFlowValue">90%</output></span>
<input id="horizontalFlow" type="range" min="0.3" max="0.98" step="0.01" value="0.9" />
</label>
<label>
<span>Angle <output id="flowAngleValue">153°</output></span>
<input id="flowAngle" type="range" min="0" max="180" step="1" value="153" />
</label>
<label>
<span>Diagonal <output id="diagonalFlowValue">100%</output></span>
<input id="diagonalFlow" type="range" min="0" max="1" step="0.01" value="1" />
</label>
<label>
<span>Long flow <output id="longFlowValue">31%</output></span>
<input id="longFlow" type="range" min="0" max="0.36" step="0.01" value="0.31" />
</label>
<label>
<span>Swirl <output id="swirlValue">82%</output></span>
<input id="swirl" type="range" min="0" max="1.4" step="0.01" value="0.82" />
</label>
<label>
<span>Swirl scale <output id="swirlScaleValue">0.72×</output></span>
<input id="swirlScale" type="range" min="0.25" max="1.4" step="0.01" value="0.72" />
</label>
<label>
<span>Jog size <output id="jogLengthValue">0.42×</output></span>
<input id="jogLength" type="range" min="0.3" max="1.8" step="0.01" value="0.42" />
</label>
<label>
<span>Channel pull <output id="channelAttractionValue">150%</output></span>
<input id="channelAttraction" type="range" min="0" max="1.5" step="0.01" value="1.5" />
</label>
<label>
<span>Scale <output id="featureScaleValue">1.37×</output></span>
<input id="featureScale" type="range" min="0.55" max="2" step="0.01" value="1.37" />
</label>
</div>
<div class="preset-bar motion-bar" aria-label="Trace reveal and animation presets">
<div class="preset-group reveal-group" id="revealGroup" aria-label="Trace reveal presets">
<span class="preset-title">Reveal</span>
<button type="button" data-reveal-preset="instant" aria-pressed="true">Instant</button>
<button type="button" data-reveal-preset="flow" aria-pressed="false">Flow sweep</button>
<button type="button" data-reveal-preset="scroll" aria-pressed="false">Scroll weave</button>
<button class="replay-button" id="replayReveal" type="button">Replay</button>
<button type="button" class="panel-toggle" id="revealOptionsToggle" aria-expanded="false" aria-controls="revealOptionsPanel">
Options<span class="chevron" aria-hidden="true">▾</span>
</button>
</div>
<span class="group-divider" aria-hidden="true"></span>
<div class="preset-group animation-group" aria-label="Ambient animation presets">
<span class="preset-title">Animation</span>
<button type="button" data-animation-preset="none" aria-pressed="true">None</button>
<button type="button" data-animation-preset="reflection" aria-pressed="false">Reflection</button>
<button type="button" data-animation-preset="conductor" aria-pressed="false">Conductor</button>
<button type="button" data-animation-preset="routed" aria-pressed="false">Routed field</button>
<button type="button" data-animation-preset="handshake" aria-pressed="false">Handshake</button>
</div>
<label class="movement-power">
<span>Movement power <output id="livelinessValue">0%</output></span>
<input id="liveliness" type="range" min="0" max="1.5" step="0.01" value="0" />
</label>
<button class="primary" type="submit">Generate</button>
<button id="download" type="button">Export SVG</button>
</div>
<section class="reveal-options-panel" id="revealOptionsPanel" aria-label="Reveal options" hidden>
<div class="reveal-options-heading">
<strong id="revealOptionsTitle">Flow sweep options</strong>
<span>Changes apply to this reveal preset</span>
</div>
<div class="reveal-options-grid">
<label class="reveal-option" data-reveal-for="flow scroll">
Spawn pattern
<select id="spawnMode">
<option value="flow">Directional flow</option>
<option value="radial">Radial</option>
<option value="edges">From edges</option>
<option value="scatter">Scatter</option>
</select>
</label>
<label class="reveal-option" data-reveal-for="flow scroll">
<span>Spawn angle <output id="spawnAngleValue">90°</output></span>
<input id="spawnAngle" type="range" min="0" max="359" step="1" value="90" />
</label>
<label class="reveal-option" data-reveal-for="flow scroll">
<span>Spawn swirl <output id="spawnSwirlValue">7.5%</output></span>
<input id="spawnSwirl" type="range" min="0" max="0.35" step="0.005" value="0.075" />
</label>
<label class="reveal-option" data-reveal-for="flow scroll">
<span>Trace glow <output id="revealGlowValue">88%</output></span>
<input id="revealGlow" type="range" min="0" max="1" step="0.01" value="0.88" />
</label>
<label class="reveal-option" data-reveal-for="flow">
<span>Sweep duration <output id="revealDurationValue">7.6 s</output></span>
<input id="revealDuration" type="range" min="2600" max="18000" step="100" value="7600" />
</label>
<label class="reveal-option" data-reveal-for="flow">
<span>Active traces <output id="revealMaxActiveTracesValue">48</output></span>
<input id="revealMaxActiveTraces" type="range" min="12" max="96" step="1" value="48" />
</label>
<label class="reveal-option" data-reveal-for="scroll">
<span>Trace duration <output id="traceDurationValue">0.95 s</output></span>
<input id="traceDuration" type="range" min="350" max="2000" step="25" value="950" />
</label>
<label class="reveal-option" data-reveal-for="scroll">
<span>Launch rate <output id="revealLaunchRateValue">24/s</output></span>
<input id="revealLaunchRate" type="range" min="6" max="80" step="1" value="24" />
</label>
<label class="reveal-option" data-reveal-for="scroll">
<span>Active traces <output id="maxActiveTracesValue">72</output></span>
<input id="maxActiveTraces" type="range" min="12" max="160" step="1" value="72" />
</label>
<label class="reveal-option" data-reveal-for="scroll">
<span>Speed response <output id="scrollSpeedResponseValue">60%</output></span>
<input id="scrollSpeedResponse" type="range" min="0" max="1" step="0.01" value="0.6" />
</label>
<label class="reveal-option" data-reveal-for="scroll">
<span>Reveal lead <output id="unlockLeadValue">2.5%</output></span>
<input id="unlockLead" type="range" min="0" max="0.2" step="0.005" value="0.025" />
</label>
<label class="reveal-option" data-reveal-for="scroll">
Continue generation
<select id="continuousGeneration">
<option value="true">On</option>
<option value="false">Off</option>
</select>
</label>
<label class="reveal-option" data-reveal-for="scroll">
<span>Generate ahead <output id="generationAheadScreensValue">2 screens</output></span>
<input id="generationAheadScreens" type="range" min="1" max="4" step="0.25" value="2" />
</label>
</div>
</section>
</form>
<script type="module">
import { ANIMATION_PRESETS, REVEAL_PRESETS, createCircuitBackground } from "./circuit-background-v8.js?v=1";
const mount = document.querySelector("#circuit");
const controls = document.querySelector("#controls");
const generationPresetButtons = [...document.querySelectorAll("[data-preset]")];
const revealPresetButtons = [...document.querySelectorAll("[data-reveal-preset]")];
const animationPresetButtons = [...document.querySelectorAll("[data-animation-preset]")];
const traceCount = document.querySelector("#traceCount");
const hero = document.querySelector("#hero");
const canonicalPageSize = Object.freeze({
width: Math.max(1, mount.clientWidth),
height: Math.max(1, mount.clientHeight),
});
const glow = document.querySelector(".glow");
const seed = document.querySelector("#seed");
const parameterToggle = document.querySelector("#parameterToggle");
const parameterPanel = document.querySelector("#parameterPanel");
const motionHint = document.querySelector("#motionHint");
const revealGroup = document.querySelector("#revealGroup");
const replayReveal = document.querySelector("#replayReveal");
const revealOptionsToggle = document.querySelector("#revealOptionsToggle");
const revealOptionsPanel = document.querySelector("#revealOptionsPanel");
const revealOptionsTitle = document.querySelector("#revealOptionsTitle");
const revealOptionElements = [...document.querySelectorAll("[data-reveal-for]")];
const revealOptionIds = [
"spawnMode", "spawnAngle", "spawnSwirl", "revealGlow", "revealDuration",
"revealMaxActiveTraces", "traceDuration", "revealLaunchRate", "maxActiveTraces",
"scrollSpeedResponse", "unlockLead", "continuousGeneration", "generationAheadScreens",
];
const revealControls = Object.fromEntries(revealOptionIds.map((id) => [id, document.querySelector("#" + id)]));
const download = document.querySelector("#download");
const geometryParameterIds = ["fill", "routeLength", "horizontalFlow", "flowAngle", "diagonalFlow", "longFlow", "swirl", "swirlScale", "jogLength", "channelAttraction", "featureScale"];
const animationParameterIds = ["liveliness"];
const parameterIds = [...geometryParameterIds, ...animationParameterIds];
const parameters = Object.fromEntries(parameterIds.map((id) => [id, document.querySelector(`#${id}`)]));
const presets = {
original: { fill: 0.744, routeLength: 1.14, horizontalFlow: 0.9, flowAngle: 153, diagonalFlow: 1, longFlow: 0.31, swirl: 0.82, swirlScale: 0.72, jogLength: 0.42, channelAttraction: 1.5, featureScale: 1.37 },
dense: { fill: 0.76, routeLength: 1.29, horizontalFlow: 0.57, flowAngle: 138, diagonalFlow: 0.26, longFlow: 0.3, swirl: 0.43, swirlScale: 0.52, jogLength: 0.85, channelAttraction: 0.69, featureScale: 0.65 },
diagonal: { fill: 0.591, routeLength: 1.51, horizontalFlow: 0.98, flowAngle: 45, diagonalFlow: 1, longFlow: 0, swirl: 0, swirlScale: 0.75, jogLength: 0.65, channelAttraction: 0.86, featureScale: 1.28 },
swirl: { fill: 0.58, routeLength: 1.1, horizontalFlow: 0.82, flowAngle: 15, diagonalFlow: 0.72, longFlow: 0.2, swirl: 0.9, swirlScale: 0.9, jogLength: 0.75, channelAttraction: 0.8, featureScale: 1 },
sparse: { fill: 0.35, routeLength: 1.9, horizontalFlow: 0.45, flowAngle: 0, diagonalFlow: 0.3, longFlow: 0.32, swirl: 0.08, swirlScale: 0.93, jogLength: 0.57, channelAttraction: 0.6, featureScale: 0.88 },
};
const revealHints = {
instant: "All traces arrive fully routed",
flow: "A curved front grows every route from via to via",
scroll: "Scroll unlocks routes; each one grows on its own timeline",
};
const animationHints = {
none: "no ambient movement",
conductor: "move to shape the current · click to activate the network",
adom: "move to steer the routed field · click to launch a local bloom",
handshake: "move to bias the next handshake · click to request a local exchange",
reflection: "move to catch the light · click to send a quiet reflection",
};
const formats = {
fill: (value) => `${Number((value * 100).toFixed(1))}%`,
routeLength: (value) => `${value.toFixed(2)}×`,
horizontalFlow: (value) => `${Math.round(value * 100)}%`,
flowAngle: (value) => `${Math.round(value)}°`,
diagonalFlow: (value) => `${Math.round(value * 100)}%`,
longFlow: (value) => `${Math.round(value * 100)}%`,
swirl: (value) => `${Math.round(value * 100)}%`,
swirlScale: (value) => `${value.toFixed(2)}×`,
jogLength: (value) => `${value.toFixed(2)}×`,
channelAttraction: (value) => `${Math.round(value * 100)}%`,
featureScale: (value) => `${value.toFixed(2)}×`,
liveliness: (value) => `${Math.round(value * 100)}%`,
};
const revealFormats = {
spawnAngle: (value) => Math.round(value) + "°",
spawnSwirl: (value) => Number((value * 100).toFixed(1)) + "%",
revealGlow: (value) => Math.round(value * 100) + "%",
revealDuration: (value) => (value / 1000).toFixed(1) + " s",
revealMaxActiveTraces: (value) => String(Math.round(value)),
traceDuration: (value) => (value / 1000).toFixed(2) + " s",
revealLaunchRate: (value) => Math.round(value) + "/s",
maxActiveTraces: (value) => String(Math.round(value)),
scrollSpeedResponse: (value) => Math.round(value * 100) + "%",
unlockLead: (value) => Number((value * 100).toFixed(1)) + "%",
generationAheadScreens: (value) => Number(value.toFixed(2)) + (value === 1 ? " screen" : " screens"),
};
function updateRevealOptionOutput(id) {
if (!revealFormats[id]) return;
const output = document.querySelector("#" + id + "Value");
if (output) output.value = revealFormats[id](Number(revealControls[id].value));
}
function closeRevealOptions() {
revealOptionsToggle.setAttribute("aria-expanded", "false");
revealOptionsPanel.hidden = true;
}
function syncRevealOptionAvailability(name) {
const state = revealOptionState[name];
if (!state) return;
const directional = state.spawnMode === "flow";
revealControls.spawnAngle.disabled = !directional;
revealControls.spawnSwirl.disabled = !directional;
revealControls.generationAheadScreens.disabled = name === "scroll"
&& !state.continuousGeneration;
}
function syncRevealOptionControls(name) {
if (name !== "flow" && name !== "scroll") return;
const state = revealOptionState[name];
for (const element of revealOptionElements) {
element.hidden = !element.dataset.revealFor.split(" ").includes(name);
}
for (const id of revealOptionIds) {
if (!(id in state)) continue;
const control = revealControls[id];
control.value = id === "continuousGeneration"
? String(Boolean(state[id]))
: String(state[id]);
if (id === "continuousGeneration") state[id] = control.value === "true";
else if (id === "spawnMode") state[id] = control.value;
else state[id] = Number(control.value);
updateRevealOptionOutput(id);
}
revealOptionsTitle.textContent = name === "flow" ? "Flow sweep options" : "Scroll weave options";
syncRevealOptionAvailability(name);
}
const query = new URLSearchParams(location.search);
const requestedAnimationPreset = query.get("animationPreset");
const requestedRevealPreset = query.get("revealPreset");
const legacyGrowthLink = requestedAnimationPreset === "growth";
const initialAnimationPreset = !legacyGrowthLink && ANIMATION_PRESETS[requestedAnimationPreset]
? requestedAnimationPreset
: "none";
const initialRevealPreset = legacyGrowthLink
? "flow"
: REVEAL_PRESETS[requestedRevealPreset]
? requestedRevealPreset
: "instant";
let animationOptions = { ...ANIMATION_PRESETS[initialAnimationPreset] };
const revealOptionState = {
flow: {
...REVEAL_PRESETS.flow,
spawnMode: REVEAL_PRESETS.flow.spawnMode || "flow",
spawnAngle: REVEAL_PRESETS.flow.spawnAngle ?? 90,
spawnSwirl: REVEAL_PRESETS.flow.spawnSwirl ?? 0.075,
revealGlow: REVEAL_PRESETS.flow.revealGlow ?? 0.88,
revealDuration: REVEAL_PRESETS.flow.revealDuration ?? 7600,
revealMaxActiveTraces: REVEAL_PRESETS.flow.revealMaxActiveTraces ?? 48,
},
scroll: {
...REVEAL_PRESETS.scroll,
spawnMode: REVEAL_PRESETS.scroll.spawnMode || "flow",
spawnAngle: REVEAL_PRESETS.scroll.spawnAngle ?? 90,
spawnSwirl: REVEAL_PRESETS.scroll.spawnSwirl ?? 0.075,
revealGlow: REVEAL_PRESETS.scroll.revealGlow ?? 0.82,
traceDuration: REVEAL_PRESETS.scroll.traceDuration ?? 950,
revealLaunchRate: REVEAL_PRESETS.scroll.revealLaunchRate ?? 24,
maxActiveTraces: REVEAL_PRESETS.scroll.maxActiveTraces ?? 72,
scrollSpeedResponse: REVEAL_PRESETS.scroll.scrollSpeedResponse ?? 0.6,
unlockLead: REVEAL_PRESETS.scroll.unlockLead ?? 0.025,
continuousGeneration: REVEAL_PRESETS.scroll.continuousGeneration ?? true,
generationAheadScreens: REVEAL_PRESETS.scroll.generationAheadScreens ?? 2,
},
};
let activeRevealPreset = initialRevealPreset;
const validSpawnModes = new Set(["flow", "radial", "edges", "scatter"]);
if (activeRevealPreset === "flow" || activeRevealPreset === "scroll") {
for (const id of revealOptionIds) {
if (!query.has(id) || !(id in revealOptionState[activeRevealPreset])) continue;
if (id === "spawnMode") {
const value = query.get(id);
if (validSpawnModes.has(value)) revealOptionState[activeRevealPreset][id] = value;
} else if (id === "continuousGeneration") {
revealOptionState[activeRevealPreset][id] = !["0", "false", "off"].includes(query.get(id).toLowerCase());
} else {
const value = Number(query.get(id));
if (Number.isFinite(value)) revealOptionState[activeRevealPreset][id] = value;
}
}
}
if (activeRevealPreset !== "instant" && query.has("revealMotion")) {
revealOptionState[activeRevealPreset].revealMotion = !["0", "false", "off"].includes(
query.get("revealMotion").toLowerCase(),
);
}
let revealOptions = activeRevealPreset === "instant"
? { ...REVEAL_PRESETS.instant }
: { ...revealOptionState[activeRevealPreset] };
const revealQueryPresent = revealOptionIds.some((id) => query.has(id));
if (activeRevealPreset !== "instant") syncRevealOptionControls(activeRevealPreset);
if (activeRevealPreset !== "instant" && revealQueryPresent) {
revealOptionsToggle.setAttribute("aria-expanded", "true");
revealOptionsPanel.hidden = false;
}
if (query.has("seed")) seed.value = query.get("seed").slice(0, 80);
for (const id of parameterIds) {
if (!query.has(id)) continue;
const value = Number(query.get(id));
if (Number.isFinite(value)) parameters[id].value = String(value);
}
// A deep link that tunes parameters should show them, not hide the reason the render differs.
if (geometryParameterIds.some((id) => query.has(id))) {
parameterToggle.setAttribute("aria-expanded", "true");
parameterPanel.hidden = false;
}
if (!query.has("liveliness")) parameters.liveliness.value = String(animationOptions.liveliness);
if (query.has("motion")) {
animationOptions.motion = !["0", "false", "off"].includes(query.get("motion").toLowerCase());
}
function setActiveGenerationPreset(name) {
for (const button of generationPresetButtons) {
button.setAttribute("aria-pressed", String(button.dataset.preset === name));
}
}
function setActiveRevealPreset(name) {
for (const button of revealPresetButtons) {
button.setAttribute("aria-pressed", String(button.dataset.revealPreset === name));
}
}
function setActiveAnimationPreset(name) {
for (const button of animationPresetButtons) {
button.setAttribute("aria-pressed", String(button.dataset.animationPreset === name));
}
}
function updatePresentation() {
const routed = animationOptions.animationSystem === "adom";
const revealSystem = routed ? "instant" : revealOptions.revealSystem;
hero.dataset.revealSystem = revealSystem;
revealGroup.dataset.disabled = String(routed);
revealGroup.setAttribute("aria-disabled", String(routed));
setActiveRevealPreset(routed ? "instant" : activeRevealPreset);
for (const button of revealPresetButtons) button.disabled = routed;
replayReveal.disabled = routed || !(
revealSystem === "flow"
|| (revealSystem === "scroll" && revealOptions.continuousGeneration)
);
revealOptionsToggle.disabled = routed || revealSystem === "instant";
if (revealOptionsToggle.disabled) closeRevealOptions();
const revealHint = revealSystem === "scroll" && revealOptions.continuousGeneration
? "Scroll queues trace growth and pre-generates page-sized field sections ahead"
: revealHints[revealSystem] || revealHints.instant;
motionHint.textContent = routed
? "Routed field owns its trace lifecycle · move to steer it and click for a local bloom"
: [
revealHint,
animationHints[animationOptions.animationSystem] || animationHints.none,
].join(" · ");
}
function applyAtmosphere(options) {
const amount = Math.max(0, Math.min(1.25, options.atmosphereStrength));
hero.dataset.animationSystem = options.animationSystem;
hero.dataset.motion = options.motion ? "on" : "off";
glow.style.setProperty("--atmosphere-strength", String(amount));
glow.style.setProperty("--atmosphere-low", String(Math.min(1, amount * 0.66)));
glow.style.setProperty("--atmosphere-high", String(Math.min(1, amount)));
updatePresentation();
}
function applyReveal() {
updatePresentation();
}
if (geometryParameterIds.some((id) => query.has(id))) setActiveGenerationPreset(null);
const animationIsCustom = animationParameterIds.some((id) => query.has(id)) || query.has("motion");
setActiveRevealPreset(initialRevealPreset);
setActiveAnimationPreset(animationIsCustom ? null : initialAnimationPreset);
applyReveal();
applyAtmosphere(animationOptions);
function optionsFromControls() {
const options = {
...revealOptions,
...animationOptions,
seed: seed.value.trim() || "nitrogen",
traceColor: "#1d2938",
canonicalPageWidth: canonicalPageSize.width,
canonicalPageHeight: canonicalPageSize.height,
};
for (const id of parameterIds) options[id] = Number(parameters[id].value);
return options;
}
function updateOutput(id) {
const value = Number(parameters[id].value);
document.querySelector(`#${id}Value`).value = formats[id](value);
}
parameterIds.forEach(updateOutput);
const circuit = createCircuitBackground(mount, optionsFromControls());
let regenerationTimer;
let animationTimer;
let revealUpdateTimer;
function commitRevealOptions() {
window.clearTimeout(revealUpdateTimer);
if (typeof circuit.setReveal === "function") {
circuit.setReveal(revealOptions);
} else {
circuit.regenerate(optionsFromControls());
}
queueTraceCount();
}
function scheduleRevealUpdate() {
window.clearTimeout(revealUpdateTimer);
revealUpdateTimer = window.setTimeout(commitRevealOptions, 120);
}
let lastTraceCountText = "";
function updateTraceCount(stats) {
const total = circuit.field.traces.length;
const text = stats?.system === "scroll-trace-reveal"
? Math.min(total, stats.completedTraces) + " / " + total + " routed"
: total + " traces";
if (text === lastTraceCountText) return;
lastTraceCountText = text;
traceCount.value = text;
}
let traceCountFrame = 0;
function tickTraceCount() {
traceCountFrame = 0;
const stats = circuit.revealStats;
updateTraceCount(stats);
if (
stats?.continuous
|| (
stats?.system === "scroll-trace-reveal"
&& (stats.queuedTraces > 0 || stats.activeTraces > 0)
)
) {
traceCountFrame = window.setTimeout(tickTraceCount, 200);
}
}
function queueTraceCount() {
if (traceCountFrame) return;
traceCountFrame = window.requestAnimationFrame(tickTraceCount);
}
window.addEventListener("scroll", queueTraceCount, { passive: true });
window.addEventListener("resize", queueTraceCount, { passive: true });
queueTraceCount();
const requestedScrollProgress = Number(query.get("scrollProgress"));
const requestedScrollScreens = Number(query.get("scrollScreens"));
if (
initialRevealPreset === "scroll"
&& revealOptions.continuousGeneration
&& Number.isFinite(requestedScrollScreens)
&& requestedScrollScreens > 0
) {
window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
const target = requestedScrollScreens * window.innerHeight;
const startedAt = performance.now();
const advance = () => {
const remaining = target - window.scrollY;
if (remaining <= 2 || performance.now() - startedAt > 15000) return;
window.scrollBy(0, Math.min(window.innerHeight * 0.42, remaining));
window.setTimeout(advance, 180);
};
advance();
}));
} else if (initialRevealPreset === "scroll" && Number.isFinite(requestedScrollProgress)) {
window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
const travel = Math.max(0, hero.scrollHeight - window.innerHeight);
window.scrollTo(0, travel * Math.max(0, Math.min(1, requestedScrollProgress)));
}));
}
function regenerate() {
window.clearTimeout(regenerationTimer);
circuit.regenerate(optionsFromControls());
queueTraceCount();
}
for (const id of parameterIds) {
parameters[id].addEventListener("input", () => {
updateOutput(id);
if (!geometryParameterIds.includes(id)) {
setActiveAnimationPreset(null);
animationOptions = { ...animationOptions, [id]: Number(parameters[id].value) };
applyAtmosphere(animationOptions);
window.clearTimeout(animationTimer);
if (animationOptions.animationSystem === "adom") {
animationTimer = window.setTimeout(() => circuit.setAnimation(animationOptions), 140);
} else {
circuit.setAnimation(animationOptions);
}
return;
}
setActiveGenerationPreset(null);
window.clearTimeout(regenerationTimer);
regenerationTimer = window.setTimeout(regenerate, 140);
});
}
for (const button of generationPresetButtons) {
button.addEventListener("click", () => {
const name = button.dataset.preset;
const values = presets[name];
for (const [id, value] of Object.entries(values)) {
parameters[id].value = String(value);
updateOutput(id);
}
setActiveGenerationPreset(name);
regenerate();
});
}
parameterToggle.addEventListener("click", () => {
const expanded = parameterToggle.getAttribute("aria-expanded") === "true";
parameterToggle.setAttribute("aria-expanded", String(!expanded));
parameterPanel.hidden = expanded;
});
revealOptionsToggle.addEventListener("click", () => {
if (revealOptionsToggle.disabled) return;
const expanded = revealOptionsToggle.getAttribute("aria-expanded") === "true";
if (expanded) {
closeRevealOptions();
return;
}
syncRevealOptionControls(activeRevealPreset);
revealOptionsToggle.setAttribute("aria-expanded", "true");
revealOptionsPanel.hidden = false;
});
for (const id of revealOptionIds) {
revealControls[id].addEventListener("input", () => {
if (activeRevealPreset !== "flow" && activeRevealPreset !== "scroll") return;
let value;
if (id === "spawnMode") value = revealControls[id].value;
else if (id === "continuousGeneration") value = revealControls[id].value === "true";
else value = Number(revealControls[id].value);
revealOptionState[activeRevealPreset][id] = value;
revealOptions = { ...revealOptionState[activeRevealPreset] };
updateRevealOptionOutput(id);
syncRevealOptionAvailability(activeRevealPreset);
applyReveal();
scheduleRevealUpdate();
});
}
for (const button of revealPresetButtons) {
button.addEventListener("click", () => {
const name = button.dataset.revealPreset;
activeRevealPreset = name;
revealOptions = name === "instant"
? { ...REVEAL_PRESETS.instant }
: { ...revealOptionState[name] };
if (name !== "instant") syncRevealOptionControls(name);
setActiveRevealPreset(name);
applyReveal();
commitRevealOptions();
});
}
replayReveal.addEventListener("click", () => {
circuit.triggerReveal();
});
for (const button of animationPresetButtons) {
button.addEventListener("click", () => {
const name = button.dataset.animationPreset;
animationOptions = { ...ANIMATION_PRESETS[name] };
parameters.liveliness.value = String(animationOptions.liveliness);
updateOutput("liveliness");
applyAtmosphere(animationOptions);
setActiveAnimationPreset(name);
window.clearTimeout(animationTimer);
circuit.setAnimation(animationOptions);
updateTraceCount();
});
}
seed.addEventListener("change", regenerate);
controls.addEventListener("submit", (event) => {
event.preventDefault();
regenerate();
});
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`);
});
</script>
</body>
</html>