master
AdityaAngajala Publish 0.3.0 6d39e2e 2mo ago
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Procedural Circuit Background — reveal</title>
    <style>
      :root {
        color-scheme: dark;
        --bg: #0d1117;
        --surface: #161b22;
        --border: #30363d;
        --text: #e6edf3;
        --muted: #8b949e;
        --accent: #00b8b0;
        --accent-bright: #00e6dc;
      }

      * { box-sizing: border-box; }
      html, body { min-height: 100%; margin: 0; }
      body {
        background: var(--bg);
        color: var(--text);
        font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      }

      .hero {
        --circuit-hole: var(--bg);
        position: relative;
        min-height: 100svh;
        overflow: hidden;
        display: grid;
        place-items: center;
        isolation: isolate;
      }

      .circuit-layer {
        position: absolute;
        inset: 0;
        z-index: -2;
      }

      .glow {
        position: absolute;
        inset: 0;
        z-index: -1;
        pointer-events: none;
        background:
          radial-gradient(circle at 52% 44%, rgba(0, 230, 220, .06), transparent 34%),
          linear-gradient(to top, var(--bg) 0, transparent 12rem);
      }

      main {
        width: min(760px, calc(100% - 40px));
        text-align: center;
        padding: 80px 0 180px;
      }

      .eyebrow {
        margin: 0 0 20px;
        color: var(--accent-bright);
        font: 600 12px/1.3 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
        letter-spacing: .18em;
        text-transform: uppercase;
      }

      h1 {
        margin: 0;
        font-size: clamp(2.6rem, 8vw, 5.6rem);
        line-height: .9;
        letter-spacing: -.055em;
        color: transparent;
        background: linear-gradient(110deg, #e9ffff, #7bc9c9 62%, var(--accent-bright));
        background-clip: text;
        filter: drop-shadow(0 0 .4em rgba(255,255,255,.18));
      }

      .lede {
        max-width: 570px;
        margin: 28px auto 0;
        color: #bac6d3;
        font-size: clamp(1rem, 2vw, 1.25rem);
        line-height: 1.6;
      }

      .hint {
        margin-top: 24px;
        color: var(--muted);
        font: 500 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
      }

      .controls {
        position: fixed;
        left: 50%;
        bottom: 12px;
        z-index: 5;
        width: min(940px, calc(100% - 24px));
        transform: translateX(-50%);
        display: grid;
        grid-template-columns: minmax(104px, .6fr) minmax(0, 3fr) auto auto auto;
        align-items: end;
        gap: 8px;
        padding: 10px 12px;
        border: 1px solid rgba(48, 54, 61, .9);
        border-radius: 12px;
        background: rgba(22, 27, 34, .88);
        box-shadow: 0 18px 70px rgba(0,0,0,.38);
        backdrop-filter: blur(18px);
      }

      label { display: grid; gap: 5px; color: var(--muted); font-size: 12px; }
      input[type="text"] {
        width: 100%;
        height: 32px;
        padding: 0 10px;
        color: var(--text);
        border: 1px solid var(--border);
        border-radius: 7px;
        outline: none;
        background: #0d1117;
        font: 500 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
      }
      input[type="text"]:focus { border-color: var(--accent); }
      input[type="range"] { width: 100%; accent-color: var(--accent); }

      button {
        height: 32px;
        padding: 0 12px;
        color: var(--text);
        border: 1px solid var(--border);
        border-radius: 7px;
        background: #21262d;
        font: 700 13px system-ui, sans-serif;
        cursor: pointer;
      }
      button:hover { border-color: #59636e; background: #282e36; }
      button.primary { color: #062d2b; border-color: var(--accent); background: var(--accent); }
      button.primary:hover { background: var(--accent-bright); }

      /* Sits in the top row where Density + Live signals used to be, and is
         labelled like the other fields ("Seed" above its input). */
      .presets-field { display: grid; gap: 5px; min-width: 0; }
      .presets-label { color: var(--muted); font-size: 12px; }
      .presets {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 5px;
      }
      .presets button {
        height: 32px;
        padding: 0 9px;
        font: 600 12px system-ui, sans-serif;
      }
      .presets button.active {
        color: #062d2b;
        border-color: var(--accent);
        background: var(--accent);
      }

      /* The reveal row spans the whole control bar, under the top row. */
      .reveal-field { grid-column: 1 / -1; }
      .reveal-field .presets { align-items: center; }
      .presets .replay { margin-left: 6px; }

      .tuning {
        grid-column: 1 / -1;
        border-top: 1px solid rgba(48, 54, 61, .7);
        padding-top: 8px;
      }
      .tuning summary {
        cursor: pointer;
        color: var(--muted);
        font: 600 12px system-ui, sans-serif;
        user-select: none;
      }
      .tuning summary:hover { color: var(--text); }
      .tuning .knobs {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
        gap: 7px 14px;
        padding-top: 9px;
      }
      .knob span {
        display: flex;
        justify-content: space-between;
        gap: 8px;
      }
      .knob output { color: var(--accent-bright); font-variant-numeric: tabular-nums; }
      .tuning .reset-row { margin-top: 9px; display: flex; justify-content: flex-end; }
      .tuning button { height: 27px; font-size: 12px; }

      @media (max-width: 760px) {
        .controls { grid-template-columns: 1fr 1fr; max-height: 60svh; overflow-y: auto; }
        /* Direct children only — preset chips and panel resets keep their size. */
        .controls > button { width: 100%; }
        .presets-field { grid-column: 1 / -1; }
        main { padding-bottom: 270px; }
      }
    </style>
  </head>
  <body>
    <section class="hero" id="hero">
      <div class="circuit-layer" id="circuit"></div>
      <div class="glow" aria-hidden="true"></div>
      <main>
        <p class="eyebrow">Seeded SVG generator · occupancy-grid engine · reveal build</p>
        <h1>Live circuitry</h1>
        <p class="lede">
          Procedural generation of Nitrogen's trace pattern — routed live: every trace grows
          out of a via with a spark on its tip, and the pads light up as it reaches them.
        </p>
        <p class="hint">Move the pointer to attract signals · click anywhere for a burst</p>
      </main>
    </section>

    <form class="controls" id="controls" autocomplete="off">
      <label>
        Seed
        <input id="seed" type="text" value="nitrogen" autocomplete="off" spellcheck="false" />
      </label>
      <div class="presets-field">
        <span class="presets-label">Animation</span>
        <div class="presets" id="presets"></div>
      </div>
      <button class="primary" type="submit">Generate</button>
      <button id="download" type="button">Download SVG</button>
      <button id="downloadTile" type="button">Download Tile</button>

      <div class="presets-field reveal-field">
        <span class="presets-label">Reveal — how the board grows in</span>
        <div class="presets" id="revealPresets">
          <button class="replay" id="replay" type="button">Replay ⟳</button>
        </div>
      </div>

      <details class="tuning">
        <summary>Generation tuning</summary>
        <div class="knobs">
          <label class="knob">
            <span>Density <output>1.05</output></span>
            <input type="range" data-key="density" min="0.35" max="1.45" step="0.05" value="1.05" />
          </label>
          <label class="knob">
            <span>Formations <output>96</output></span>
            <input type="range" data-key="formations" min="24" max="96" step="4" value="96" />
          </label>
          <label class="knob">
            <span>Bus width <output>1</output></span>
            <input type="range" data-key="busWidth" min="0.5" max="2" step="0.1" value="1" />
          </label>
          <label class="knob">
            <span>Turn penalty <output>0.55</output></span>
            <input type="range" data-key="turnPenalty" min="0.1" max="1.2" step="0.05" value="0.55" />
          </label>
          <label class="knob">
            <span>Weave spacing <output>0.22</output></span>
            <input type="range" data-key="weave" min="0" max="0.45" step="0.01" value="0.22" />
          </label>
          <label class="knob">
            <span>Cramp aversion <output>0.4</output></span>
            <input type="range" data-key="cramp" min="0" max="0.8" step="0.05" value="0.4" />
          </label>
          <label class="knob">
            <span>Horizontal bias <output>0.6</output></span>
            <input type="range" data-key="horizontalBias" min="0.35" max="0.9" step="0.05" value="0.6" />
          </label>
          <label class="knob">
            <span>Net reach <output>42</output></span>
            <input type="range" data-key="netReach" min="16" max="60" step="2" value="42" />
          </label>
          <label class="knob">
            <span>Jog bias <output>5</output></span>
            <input type="range" data-key="jogBias" min="0" max="10" step="1" value="5" />
          </label>
          <label class="knob">
            <span>Stub chance <output>0</output></span>
            <input type="range" data-key="stubChance" min="0" max="1" step="0.05" value="0" />
          </label>
          <label class="knob">
            <span>Inline vias <output>0.03</output></span>
            <input type="range" data-key="inlineViaChance" min="0" max="0.12" step="0.005" value="0.03" />
          </label>
          <label class="knob">
            <span>Tap combs <output>8</output></span>
            <input type="range" data-key="combs" min="0" max="16" step="1" value="8" />
          </label>
        </div>
        <div class="reset-row">
          <button class="reset-tuning" type="button">Reset generation</button>
        </div>
      </details>

      <details class="tuning" data-panel="animation">
        <summary>Animation tuning</summary>
        <div class="knobs">
          <label class="knob">
            <span>Signal speed <output>420</output></span>
            <input type="range" data-key="signalSpeed" min="100" max="1200" step="20" value="420" />
          </label>
          <label class="knob">
            <span>Spawn rate (ms) <output>240</output></span>
            <input type="range" data-key="signalInterval" min="80" max="900" step="20" value="240" />
          </label>
          <label class="knob">
            <span>Cursor range <output>110</output></span>
            <input type="range" data-key="spawnRadius" min="25" max="200" step="5" value="110" />
          </label>
          <label class="knob">
            <span>Max signals <output>12</output></span>
            <input type="range" data-key="maxSignals" min="2" max="20" step="1" value="12" />
          </label>
          <label class="knob">
            <span>Passive survival <output>0.4</output></span>
            <input type="range" data-key="hopSurvival" min="0" max="0.95" step="0.05" value="0.4" />
          </label>
          <label class="knob">
            <span>Click survival <output>0.72</output></span>
            <input type="range" data-key="burstSurvival" min="0" max="0.95" step="0.01" value="0.72" />
          </label>
          <label class="knob">
            <span>Click boost <output>7</output></span>
            <input type="range" data-key="burstBoost" min="0" max="11" step="1" value="7" />
          </label>
        </div>
        <div class="reset-row">
          <button class="reset-tuning" type="button">Reset animation</button>
        </div>
      </details>

      <details class="tuning" data-panel="reveal">
        <summary>Reveal tuning</summary>
        <div class="knobs">
          <label class="knob">
            <span>Total reveal (ms) <output>3400</output></span>
            <input type="range" data-key="revealDuration" min="600" max="9000" step="100" value="3400" />
          </label>
          <label class="knob">
            <span>Tip speed <output>420</output></span>
            <input type="range" data-key="revealSpeed" min="120" max="1200" step="20" value="420" />
          </label>
          <label class="knob">
            <span>Head length <output>46</output></span>
            <input type="range" data-key="revealHead" min="12" max="160" step="2" value="46" />
          </label>
          <label class="knob">
            <span>Tip bloom <output>0.18</output></span>
            <input type="range" data-key="revealBloom" min="0" max="0.5" step="0.02" value="0.18" />
          </label>
          <label class="knob">
            <span>Hot swell <output>1.35</output></span>
            <input type="range" data-key="revealSwell" min="1" max="3" step="0.05" value="1.35" />
          </label>
          <label class="knob">
            <span>Heat fade (ms) <output>25</output></span>
            <input type="range" data-key="revealCool" min="25" max="3000" step="25" value="25" />
          </label>
          <label class="knob">
            <span>Settle tint <output>1</output></span>
            <input type="range" data-key="revealSettle" min="0" max="1" step="0.02" value="1" />
          </label>
          <label class="knob">
            <span>Colour zones (px) <output>380</output></span>
            <input type="range" data-key="revealZone" min="120" max="900" step="20" value="380" />
          </label>
        </div>
        <div class="reset-row">
          <button class="reset-tuning" type="button">Reset reveal</button>
        </div>
      </details>
    </form>

    <script type="module">
      import {
        createCircuitBackground,
        ANIMATION_PRESETS,
        REVEAL_PRESETS,
      } from "./circuit-background-reveal.js";

      const mount = document.querySelector("#circuit");
      const controls = document.querySelector("#controls");
      const seed = document.querySelector("#seed");
      const download = document.querySelector("#download");
      const downloadTile = document.querySelector("#downloadTile");
      const presetBar = document.querySelector("#presets");
      const revealBar = document.querySelector("#revealPresets");
      const replay = document.querySelector("#replay");

      // Animation on/off now lives entirely in the presets ("None" = off);
      // the page opens on None, so the board starts static.
      let signalsOn = false;
      // Which reveal style the board grows in with — the page opens on Etch,
      // so the first thing a visitor sees is the board routing itself.
      let revealStyle = "flood";

      const circuit = createCircuitBackground(mount, {
        seed: seed.value,
        signals: signalsOn,
        reveal: revealStyle,
      });

      // Every slider (density included) is a data-key knob inside a tuning
      // panel, so `tuning()` collects them all straight into the config.
      const knobs = [...document.querySelectorAll(".tuning input[type=range]")];
      const knobDefaults = new Map(knobs.map((knob) => [knob, knob.value]));
      const knobByKey = new Map(knobs.map((knob) => [knob.dataset.key, knob]));
      const tuning = () =>
        Object.fromEntries(knobs.map((knob) => [knob.dataset.key, Number(knob.value)]));
      const settings = () => ({
        seed: seed.value.trim() || "nitrogen",
        signals: signalsOn,
        reveal: revealStyle,
        ...tuning(),
      });

      // --- animation presets --------------------------------------------------
      const presetButtons = new Map();
      const clearActivePreset = () => {
        for (const button of presetButtons.values()) button.classList.remove("active");
      };
      const syncKnob = (knob) => {
        knob.parentElement.querySelector("output").value = knob.value;
      };
      const applyPreset = (name) => {
        const preset = ANIMATION_PRESETS[name];
        for (const [key, value] of Object.entries(preset)) {
          if (key === "signals") { signalsOn = value; continue; }
          const knob = knobByKey.get(key);
          if (knob) { knob.value = value; syncKnob(knob); }
        }
        clearActivePreset();
        presetButtons.get(name).classList.add("active");
        circuit.regenerate(settings());
      };
      for (const name of Object.keys(ANIMATION_PRESETS)) {
        const button = document.createElement("button");
        button.type = "button";
        button.textContent = name;
        button.addEventListener("click", () => applyPreset(name));
        presetBar.appendChild(button);
        presetButtons.set(name, button);
      }
      // Reflect the initial "None" state in the preset bar.
      presetButtons.get("None").classList.add("active");

      // --- reveal presets -----------------------------------------------------
      // Same shape as the animation presets, but these only touch how the
      // board arrives: pick a style and it regrows immediately.
      const revealButtons = new Map();
      const syncRevealBar = () => {
        for (const [name, button] of revealButtons) {
          button.classList.toggle("active", REVEAL_PRESETS[name].reveal === revealStyle);
        }
      };
      const applyRevealPreset = (name) => {
        for (const [key, value] of Object.entries(REVEAL_PRESETS[name])) {
          if (key === "reveal") { revealStyle = value; continue; }
          const knob = knobByKey.get(key);
          if (knob) { knob.value = value; syncKnob(knob); }
        }
        syncRevealBar();
        // Regrow the board that is already up — no need to re-route it.
        circuit.replayReveal(revealStyle);
      };
      for (const name of Object.keys(REVEAL_PRESETS)) {
        const button = document.createElement("button");
        button.type = "button";
        button.textContent = name;
        button.addEventListener("click", () => applyRevealPreset(name));
        revealBar.insertBefore(button, replay);
        revealButtons.set(name, button);
      }
      syncRevealBar();
      replay.addEventListener("click", () => circuit.replayReveal(revealStyle));

      // Only animation knobs invalidate the highlighted preset — retuning
      // generation (density, formations, ...) leaves the animation untouched.
      const isAnimationPanel = (node) => node.closest("details")?.dataset.panel === "animation";

      for (const knob of knobs) {
        const out = knob.parentElement.querySelector("output");
        const show = () => { out.value = knob.value; };
        show();
        knob.addEventListener("input", show);
        knob.addEventListener("change", () => {
          if (isAnimationPanel(knob)) clearActivePreset();
          circuit.regenerate(settings());
        });
      }

      // Each panel's reset button restores only that panel's own knobs.
      for (const resetButton of document.querySelectorAll(".reset-tuning")) {
        resetButton.addEventListener("click", () => {
          const panelKnobs = resetButton.closest("details").querySelectorAll("input[type=range]");
          for (const knob of panelKnobs) {
            knob.value = knobDefaults.get(knob);
            knob.parentElement.querySelector("output").value = knob.value;
          }
          if (isAnimationPanel(resetButton)) clearActivePreset();
          circuit.regenerate(settings());
        });
      }

      controls.addEventListener("submit", (event) => {
        event.preventDefault();
        circuit.regenerate(settings());
      });

      function saveBlob(text, name) {
        const blob = new Blob([text], { type: "image/svg+xml" });
        const url = URL.createObjectURL(blob);
        const link = document.createElement("a");
        link.href = url;
        link.download = name;
        link.click();
        URL.revokeObjectURL(url);
      }

      const slug = () => (seed.value || "seed").replace(/[^a-z0-9_-]+/gi, "-");

      download.addEventListener("click", () => {
        saveBlob(circuit.exportSvg(), `circuit-${slug()}.svg`);
      });

      downloadTile.addEventListener("click", () => {
        saveBlob(circuit.exportTileSvg(), `circuit-tile-${slug()}.svg`);
      });
    </script>
  </body>
</html>