master
AdityaAngajala Publish 0.3.0 6d39e2e 2mo ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138
<!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); }
      label:has(input:disabled), label:has(select:disabled) { opacity: .42; }

      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" data-preset="adom" aria-pressed="false">ADOM embed</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>
      <label>
        Embedded mark
        <select id="obstaclePreset">
          <option value="none" selected>None</option>
          <option value="adom">ADOM wordmark</option>
        </select>
      </label>
      <label data-obstacle-control>
        Routing mode
        <select id="obstacleMode">
          <option value="avoid" selected>Avoid</option>
          <option value="outline">Outline</option>
          <option value="outline-wrap">Outline + wrap</option>
        </select>
      </label>
      <label data-obstacle-control>
        <span>Mark size <output id="obstacleScaleValue">68%</output></span>
        <input id="obstacleScale" type="range" min="0.34" max="0.86" step="0.01" value="0.68" />
      </label>
      <label data-obstacle-control>
        <span>Horizontal <output id="obstacleXValue">50%</output></span>
        <input id="obstacleX" type="range" min="0.18" max="0.82" step="0.01" value="0.5" />
      </label>
      <label data-obstacle-control>
        <span>Vertical <output id="obstacleYValue">66%</output></span>
        <input id="obstacleY" type="range" min="0.2" max="0.82" step="0.01" value="0.66" />
      </label>
      <label data-obstacle-control>
        <span>Clearance <output id="obstacleClearanceValue">1.1 lanes</output></span>
        <input id="obstacleClearance" type="range" min="0" max="3" step="0.1" value="1.1" />
      </label>
      <label data-obstacle-control>
        <span>Edge pull <output id="obstaclePullValue">82%</output></span>
        <input id="obstaclePull" type="range" min="0" max="1.5" step="0.01" value="0.82" />
      </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-v9.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 numericGeometryParameterIds = [
        "fill", "routeLength", "horizontalFlow", "flowAngle", "diagonalFlow", "longFlow",
        "swirl", "swirlScale", "jogLength", "channelAttraction", "featureScale",
        "obstacleScale", "obstacleX", "obstacleY", "obstacleClearance", "obstaclePull",
      ];
      const enumGeometryParameterIds = ["obstaclePreset", "obstacleMode"];
      const geometryParameterIds = [...numericGeometryParameterIds, ...enumGeometryParameterIds];
      const animationParameterIds = ["liveliness"];
      const numericParameterIds = [...numericGeometryParameterIds, ...animationParameterIds];
      const parameterIds = [...geometryParameterIds, ...animationParameterIds];
      const parameters = Object.fromEntries(parameterIds.map((id) => [id, document.querySelector(`#${id}`)]));
      const obstacleOff = {
        obstaclePreset: "none", obstacleMode: "avoid", obstacleScale: 0.68,
        obstacleX: 0.5, obstacleY: 0.66, obstacleClearance: 1.1, obstaclePull: 0.82,
      };
      const presets = {
        adom: { 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, obstaclePreset: "adom", obstacleMode: "outline-wrap", obstacleScale: 0.68, obstacleX: 0.5, obstacleY: 0.66, obstacleClearance: 1.1, obstaclePull: 0.82 },
        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, ...obstacleOff },
        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, ...obstacleOff },
        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, ...obstacleOff },
        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, ...obstacleOff },
        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, ...obstacleOff },
      };
      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)}×`,
        obstacleScale: (value) => `${Math.round(value * 100)}%`,
        obstacleX: (value) => `${Math.round(value * 100)}%`,
        obstacleY: (value) => `${Math.round(value * 100)}%`,
        obstacleClearance: (value) => `${value.toFixed(1)} lanes`,
        obstaclePull: (value) => `${Math.round(value * 100)}%`,
        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 syncObstacleControls() {
        const enabled = parameters.obstaclePreset.value !== "none";
        for (const label of document.querySelectorAll("[data-obstacle-control]")) {
          for (const control of label.querySelectorAll("input, select")) control.disabled = !enabled;
        }
        if (enabled) parameters.obstaclePull.disabled = parameters.obstacleMode.value !== "outline-wrap";
      }

      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);
      const obstaclePresetValues = new Set(["none", "adom"]);
      const obstacleModeValues = new Set(["avoid", "outline", "outline-wrap"]);
      for (const id of parameterIds) {
        if (!query.has(id)) continue;
        if (id === "obstaclePreset") {
          if (obstaclePresetValues.has(query.get(id))) parameters[id].value = query.get(id);
          continue;
        }
        if (id === "obstacleMode") {
          if (obstacleModeValues.has(query.get(id))) parameters[id].value = query.get(id);
          continue;
        }
        const value = Number(query.get(id));
        if (Number.isFinite(value)) parameters[id].value = String(value);
      }
      syncObstacleControls();
      // 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 numericParameterIds) options[id] = Number(parameters[id].value);
        for (const id of enumGeometryParameterIds) options[id] = parameters[id].value;
        return options;
      }

      function updateOutput(id) {
        if (!formats[id]) return;
        const value = Number(parameters[id].value);
        document.querySelector(`#${id}Value`).value = formats[id](value);
      }
      numericParameterIds.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 (id === "obstaclePreset" || id === "obstacleMode") syncObstacleControls();
          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);
          }
          syncObstacleControls();
          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>