[object Object]

← back to Sound Agentabrams

Remove synthwave-visualizer (Outrun) build — killed per Steve

91269ba857dc9b38cb22e6e41c6bcddf6473ec1d · 2026-07-25 11:31:30 -0700 · Steve

Files touched

Diff

commit 91269ba857dc9b38cb22e6e41c6bcddf6473ec1d
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jul 25 11:31:30 2026 -0700

    Remove synthwave-visualizer (Outrun) build — killed per Steve
---
 5x/REPORT.md                           |  30 --
 sounds/synthwave-visualizer/Outrun.aa  |   7 -
 sounds/synthwave-visualizer/index.html | 680 ---------------------------------
 3 files changed, 717 deletions(-)

diff --git a/5x/REPORT.md b/5x/REPORT.md
deleted file mode 100644
index ae52917..0000000
--- a/5x/REPORT.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# /5x Report — SoundLab hub (sound-agentabrams) via viewer :51168
-
-Target: http://127.0.0.1:51168/  (Basic auth admin / DW2024!)
-Six-way: M1 HTTP · M2 render · M3 Playwright E2E · B4 Chrome · B5 Safari · B6 Firefox · click-through.
-This gated the HUB INTEGRATION (the Outrun card loading the visualizer in an iframe), not the
-standalone build (which was /5x-clean + Contrarian SHIP IT separately).
-
-| Sweep | Core 6-way | Caught | Fixed | Commit |
-|-------|-----------|--------|-------|--------|
-| 1 | 4/7 | hub had **no favicon** → /favicon.ico 404 (1 JS err in M3); `--expect "SoundLab"` was wrong (real title is `sound.agentabrams.com`) | inline SVG equalizer favicon added; assertion corrected to real on-page text | e4797e9 |
-| 2 | 6/7 | **ENTER SOUNDLAB CTA never stable** — `heropulse` infinitely animated `transform: scale()`, so the click target's box never settled → un-clickable by automation (and a moving-CTA smell) | `heropulse` → glow-only (box-shadow); button geometry now stable & clickable | 530608a |
-| 3 | 6/7 | residual: 2 click-through timeouts = the intro splash's own `skip ✕` (+ a stale card handle) hidden after the agent clicks ENTER first | none needed — harness sequencing artifact, not a defect | — |
-
-**Stop:** core six-way (M1/M2/M3/B4/B5/B6) PASS on sweeps 2 **and** 3 → clean twice.
-
-## Two real defects found & fixed
-1. **Hub favicon 404** — the SoundLab hub page declared no icon; browser auto-requested
-   `/favicon.ico` → 404. Added an inline SVG (neon equalizer bars) — no request, proper tab icon.
-2. **Unstable primary CTA** — `#hero .enter` pulsed via `transform: scale(1→1.06)` infinitely,
-   so the ENTER button was never geometrically stable (Playwright refused to click it for 6s;
-   a real user could, but an always-moving click target is a genuine smell). Moved the pulse to
-   `box-shadow` glow only — same visual intent, stable click target.
-
-## Honest note on the residual click-through "2 failed"
-There are exactly 4 buttons: `#heroEnter`, `#heroSkip` (skip ✕), the card, `#popCurrent`.
-The generic agent clicks ENTER first, which **dismisses the intro splash and hides `skip ✕`**;
-its later click on that now-hidden button (and a stale card handle) times out. Verified the real
-user flow directly: click ENTER → hero dismisses → card clickable → iframe loads
-`sounds/synthwave-visualizer/index.html`, **0 console/page errors**. Popout (`⧉ own window`) passes.
-Not papered over — it's the splash's alternate dismiss button being hidden by the primary one.
diff --git a/sounds/synthwave-visualizer/Outrun.aa b/sounds/synthwave-visualizer/Outrun.aa
deleted file mode 100644
index d3212ca..0000000
--- a/sounds/synthwave-visualizer/Outrun.aa
+++ /dev/null
@@ -1,7 +0,0 @@
-name: Outrun
-build: synthwave-visualizer
-title: Synthwave Visualizer
-tagline: A retro sun over an animated perspective grid pulses to WebAudio-generated synthwave — play/stop, no audio files.
-category: Games
-entry: index.html
-created: 2026-07-24
diff --git a/sounds/synthwave-visualizer/index.html b/sounds/synthwave-visualizer/index.html
deleted file mode 100644
index 2876f0c..0000000
--- a/sounds/synthwave-visualizer/index.html
+++ /dev/null
@@ -1,680 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-  <meta charset="UTF-8" />
-  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
-  <title>Synthwave Visualizer</title>
-  <!-- Inline SVG favicon (retro sun over grid) — avoids a /favicon.ico 404 -->
-  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='s' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23fff27a'/%3E%3Cstop offset='.6' stop-color='%23ff3d81'/%3E%3Cstop offset='1' stop-color='%23b02a8f'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='32' height='32' fill='%2305010f'/%3E%3Ccircle cx='16' cy='13' r='9' fill='url(%23s)'/%3E%3Cg stroke='%2305d9e8' stroke-width='1'%3E%3Cline x1='0' y1='23' x2='32' y2='23'/%3E%3Cline x1='0' y1='28' x2='32' y2='28'/%3E%3Cline x1='16' y1='23' x2='2' y2='32'/%3E%3Cline x1='16' y1='23' x2='30' y2='32'/%3E%3C/g%3E%3C/svg%3E" />
-  <style>
-    :root {
-      --neon-pink: #ff2d95;
-      --neon-cyan: #05d9e8;
-      --neon-purple: #b967ff;
-      --bg-0: #05010f;
-    }
-    * { box-sizing: border-box; }
-    html, body {
-      margin: 0;
-      padding: 0;
-      height: 100%;
-      background: var(--bg-0);
-      overflow: hidden;
-      font-family: "Courier New", ui-monospace, monospace;
-      -webkit-tap-highlight-color: transparent;
-    }
-    #stage {
-      position: fixed;
-      inset: 0;
-      display: grid;
-      place-items: center;
-    }
-    canvas {
-      display: block;
-      width: 100vw;
-      height: 100vh;
-    }
-
-    /* ---- HUD / controls ---- */
-    #hud {
-      position: fixed;
-      left: 50%;
-      bottom: 26px;
-      transform: translateX(-50%);
-      display: flex;
-      gap: 14px;
-      align-items: center;
-      z-index: 10;
-      padding: 10px 14px;
-      border-radius: 14px;
-      background: rgba(10, 4, 26, 0.55);
-      border: 1px solid rgba(185, 103, 255, 0.35);
-      backdrop-filter: blur(6px);
-      box-shadow: 0 0 24px rgba(255, 45, 149, 0.25);
-    }
-    button.ctrl {
-      cursor: pointer;
-      font-family: inherit;
-      font-size: 15px;
-      letter-spacing: 1px;
-      text-transform: uppercase;
-      color: #fff;
-      background: linear-gradient(180deg, #2a0a3d, #14041f);
-      border: 1px solid var(--neon-pink);
-      border-radius: 10px;
-      padding: 10px 18px;
-      transition: transform .08s ease, box-shadow .2s ease, border-color .2s ease;
-      text-shadow: 0 0 8px var(--neon-pink);
-      box-shadow: 0 0 12px rgba(255, 45, 149, 0.4);
-    }
-    button.ctrl:hover { box-shadow: 0 0 22px rgba(255, 45, 149, 0.8); }
-    button.ctrl:active { transform: translateY(1px) scale(0.98); }
-    button.ctrl.cyan {
-      border-color: var(--neon-cyan);
-      text-shadow: 0 0 8px var(--neon-cyan);
-      box-shadow: 0 0 12px rgba(5, 217, 232, 0.4);
-    }
-    button.ctrl.cyan:hover { box-shadow: 0 0 22px rgba(5, 217, 232, 0.85); }
-    button.ctrl[disabled] { opacity: .35; cursor: not-allowed; box-shadow: none; }
-
-    .knob {
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      gap: 4px;
-      color: var(--neon-cyan);
-      font-size: 10px;
-      letter-spacing: 1px;
-      text-shadow: 0 0 6px var(--neon-cyan);
-      user-select: none;
-    }
-    input[type="range"] {
-      -webkit-appearance: none;
-      appearance: none;
-      width: 96px;
-      height: 4px;
-      border-radius: 4px;
-      background: linear-gradient(90deg, var(--neon-cyan), var(--neon-pink));
-      outline: none;
-    }
-    input[type="range"]::-webkit-slider-thumb {
-      -webkit-appearance: none;
-      width: 15px; height: 15px;
-      border-radius: 50%;
-      background: #fff;
-      box-shadow: 0 0 8px var(--neon-cyan);
-      cursor: pointer;
-    }
-    input[type="range"]::-moz-range-thumb {
-      width: 15px; height: 15px; border: none; border-radius: 50%;
-      background: #fff; box-shadow: 0 0 8px var(--neon-cyan); cursor: pointer;
-    }
-
-    #title {
-      position: fixed;
-      top: 20px;
-      left: 50%;
-      transform: translateX(-50%);
-      z-index: 10;
-      text-align: center;
-      pointer-events: none;
-    }
-    #title h1 {
-      margin: 0;
-      font-size: clamp(22px, 5vw, 42px);
-      letter-spacing: 6px;
-      text-transform: uppercase;
-      color: #fff;
-      background: linear-gradient(180deg, #fff 0%, var(--neon-pink) 55%, var(--neon-purple) 100%);
-      -webkit-background-clip: text;
-      background-clip: text;
-      -webkit-text-fill-color: transparent;
-      filter: drop-shadow(0 0 10px rgba(255, 45, 149, .7));
-    }
-    #title p {
-      margin: 4px 0 0;
-      font-size: 11px;
-      letter-spacing: 3px;
-      color: var(--neon-cyan);
-      text-shadow: 0 0 8px var(--neon-cyan);
-    }
-
-    /* Big centered prompt before first play */
-    #prompt {
-      position: fixed;
-      inset: 0;
-      z-index: 20;
-      display: grid;
-      place-items: center;
-      cursor: pointer;
-      background: radial-gradient(circle at 50% 45%, rgba(255,45,149,.08), rgba(5,1,15,.75));
-    }
-    #prompt .badge {
-      text-align: center;
-      color: #fff;
-      text-shadow: 0 0 14px var(--neon-pink);
-      animation: pulse 1.6s ease-in-out infinite;
-    }
-    #prompt .badge .big { font-size: clamp(28px, 8vw, 64px); letter-spacing: 4px; }
-    #prompt .badge .sub { font-size: 13px; letter-spacing: 3px; color: var(--neon-cyan); text-shadow: 0 0 10px var(--neon-cyan); margin-top: 10px; }
-    @keyframes pulse { 0%,100%{opacity:.55;transform:scale(1)} 50%{opacity:1;transform:scale(1.04)} }
-    .hidden { display: none !important; }
-  </style>
-</head>
-<body>
-  <div id="stage">
-    <canvas id="c"></canvas>
-  </div>
-
-  <div id="title">
-    <h1>Synthwave</h1>
-    <p>OSCILLATOR-DRIVEN VISUALIZER</p>
-  </div>
-
-  <div id="prompt">
-    <div class="badge">
-      <div class="big">▶ CLICK TO START</div>
-      <div class="sub">generated synthwave · no audio files</div>
-    </div>
-  </div>
-
-  <div id="hud" class="hidden">
-    <button id="playBtn" class="ctrl">▶ Play</button>
-    <button id="stopBtn" class="ctrl cyan" disabled>■ Stop</button>
-    <div class="knob">
-      <span>VOLUME</span>
-      <input id="vol" type="range" min="0" max="100" value="65" />
-    </div>
-  </div>
-
-  <script>
-  "use strict";
-
-  /* =========================================================================
-     SYNTHWAVE VISUALIZER
-     - All sound is generated live with WebAudio oscillators/noise (no files).
-     - A lookahead scheduler sequences a synthwave loop (Am–F–C–G) with
-       four-on-the-floor kick, offbeat hats, bassline, pad, and a 16th arp.
-     - An AnalyserNode feeds real-time energy into the visuals so the retro
-       sun, perspective grid, and stars all pulse to the music.
-     ========================================================================= */
-
-  // ---------- Canvas / hi-DPI setup ----------
-  const canvas = document.getElementById("c");
-  const ctx = canvas.getContext("2d");
-  let W = 0, H = 0, DPR = 1;
-
-  function resize() {
-    DPR = Math.min(window.devicePixelRatio || 1, 2);
-    W = Math.floor(window.innerWidth);
-    H = Math.floor(window.innerHeight);
-    canvas.width = Math.floor(W * DPR);
-    canvas.height = Math.floor(H * DPR);
-    ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
-    buildStars();
-  }
-  window.addEventListener("resize", resize);
-
-  // ---------- Stars ----------
-  let stars = [];
-  function buildStars() {
-    stars = [];
-    const horizon = H * 0.58;
-    const n = Math.round((W * horizon) / 5200);
-    for (let i = 0; i < n; i++) {
-      stars.push({
-        x: Math.random() * W,
-        y: Math.random() * horizon * 0.95,
-        r: Math.random() * 1.3 + 0.2,
-        tw: Math.random() * Math.PI * 2,
-        sp: Math.random() * 2 + 0.5
-      });
-    }
-  }
-
-  /* =========================================================================
-     AUDIO ENGINE
-     ========================================================================= */
-  const A = {
-    ctx: null,
-    master: null,
-    analyser: null,
-    freqData: null,
-    running: false,
-    // scheduler
-    tempo: 100,            // BPM
-    current16th: 0,        // absolute 16th-note counter
-    nextNoteTime: 0,       // when the next 16th is due (audio-clock seconds)
-    lookahead: 0.025,      // scheduler tick (s)
-    scheduleAhead: 0.12,   // how far ahead to schedule (s)
-    timer: null,
-    noiseBuf: null,
-    volume: 0.65
-  };
-
-  const midi = (n) => 440 * Math.pow(2, (n - 69) / 12);
-
-  // Progression: Am - F - C - G. Each entry = one bar.
-  const PROG = [
-    { bass: 45, pad: [57, 60, 64], arp: [69, 72, 76, 81] }, // Am
-    { bass: 41, pad: [53, 57, 60], arp: [65, 69, 72, 77] }, // F
-    { bass: 48, pad: [60, 64, 67], arp: [72, 76, 79, 84] }, // C
-    { bass: 43, pad: [55, 59, 62], arp: [67, 71, 74, 79] }  // G
-  ];
-
-  function initAudio() {
-    if (A.ctx) return;
-    const Ctor = window.AudioContext || window.webkitAudioContext;
-    A.ctx = new Ctor();
-
-    A.master = A.ctx.createGain();
-    A.master.gain.value = A.volume;
-
-    // soft ceiling so stacked voices don't clip
-    const limiter = A.ctx.createDynamicsCompressor();
-    limiter.threshold.value = -10;
-    limiter.knee.value = 20;
-    limiter.ratio.value = 12;
-    limiter.attack.value = 0.003;
-    limiter.release.value = 0.25;
-
-    A.analyser = A.ctx.createAnalyser();
-    A.analyser.fftSize = 512;
-    A.analyser.smoothingTimeConstant = 0.8;
-    A.freqData = new Uint8Array(A.analyser.frequencyBinCount);
-
-    A.master.connect(limiter);
-    limiter.connect(A.analyser);
-    A.analyser.connect(A.ctx.destination);
-
-    // white-noise buffer reused for hats/kick click
-    const len = Math.floor(A.ctx.sampleRate * 0.5);
-    A.noiseBuf = A.ctx.createBuffer(1, len, A.ctx.sampleRate);
-    const d = A.noiseBuf.getChannelData(0);
-    for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1;
-  }
-
-  // ---- Voices ----------------------------------------------------------
-  function playBass(freq, t, dur) {
-    const o = A.ctx.createOscillator();
-    const g = A.ctx.createGain();
-    const f = A.ctx.createBiquadFilter();
-    o.type = "sawtooth";
-    o.frequency.value = freq;
-    f.type = "lowpass";
-    f.frequency.value = 520;
-    f.Q.value = 6;
-    g.gain.setValueAtTime(0.0001, t);
-    g.gain.linearRampToValueAtTime(0.32, t + 0.012);
-    g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
-    o.connect(f); f.connect(g); g.connect(A.master);
-    o.start(t); o.stop(t + dur + 0.02);
-  }
-
-  function playPad(freqs, t, dur) {
-    const g = A.ctx.createGain();
-    const f = A.ctx.createBiquadFilter();
-    f.type = "lowpass";
-    f.frequency.setValueAtTime(500, t);
-    f.frequency.linearRampToValueAtTime(1800, t + dur * 0.5);
-    f.frequency.linearRampToValueAtTime(600, t + dur);
-    f.Q.value = 2;
-    g.gain.setValueAtTime(0.0001, t);
-    g.gain.linearRampToValueAtTime(0.09, t + dur * 0.25);
-    g.gain.setValueAtTime(0.09, t + dur * 0.7);
-    g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
-    g.connect(A.master);
-    f.connect(g);
-    freqs.forEach((fr, i) => {
-      const o = A.ctx.createOscillator();
-      o.type = "sawtooth";
-      o.frequency.value = fr;
-      o.detune.value = (i - 1) * 6; // subtle spread
-      o.connect(f);
-      o.start(t); o.stop(t + dur + 0.02);
-    });
-  }
-
-  function playArp(freq, t, dur) {
-    const o = A.ctx.createOscillator();
-    const g = A.ctx.createGain();
-    const f = A.ctx.createBiquadFilter();
-    o.type = "square";
-    o.frequency.value = freq;
-    f.type = "lowpass";
-    f.frequency.value = 2600;
-    f.Q.value = 1;
-    g.gain.setValueAtTime(0.0001, t);
-    g.gain.linearRampToValueAtTime(0.10, t + 0.006);
-    g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
-    o.connect(f); f.connect(g); g.connect(A.master);
-    o.start(t); o.stop(t + dur + 0.02);
-  }
-
-  function playKick(t) {
-    const o = A.ctx.createOscillator();
-    const g = A.ctx.createGain();
-    o.type = "sine";
-    o.frequency.setValueAtTime(150, t);
-    o.frequency.exponentialRampToValueAtTime(45, t + 0.14);
-    g.gain.setValueAtTime(0.9, t);
-    g.gain.exponentialRampToValueAtTime(0.0001, t + 0.28);
-    o.connect(g); g.connect(A.master);
-    o.start(t); o.stop(t + 0.3);
-  }
-
-  function playHat(t, open) {
-    const src = A.ctx.createBufferSource();
-    src.buffer = A.noiseBuf;
-    const f = A.ctx.createBiquadFilter();
-    f.type = "highpass";
-    f.frequency.value = 7000;
-    const g = A.ctx.createGain();
-    const dur = open ? 0.14 : 0.04;
-    g.gain.setValueAtTime(open ? 0.16 : 0.11, t);
-    g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
-    src.connect(f); f.connect(g); g.connect(A.master);
-    src.start(t); src.stop(t + dur + 0.02);
-  }
-
-  // ---- Scheduler -------------------------------------------------------
-  function scheduleStep(step16, t) {
-    const bar = Math.floor(step16 / 16);
-    const s = step16 % 16;            // 0..15 within the bar
-    const chord = PROG[bar % PROG.length];
-    const sixteenth = 60 / A.tempo / 4;
-
-    // Pad — once per bar, sustained
-    if (s === 0) playPad(chord.pad.map(midi), t, sixteenth * 16 * 0.98);
-
-    // Kick — four on the floor
-    if (s % 4 === 0) playKick(t);
-
-    // Hats — offbeat 8ths, open hat on the last
-    if (s % 4 === 2) playHat(t, s === 14);
-
-    // Bass — 8th notes, drop an octave on the "and" of beats for movement
-    if (s % 2 === 0) {
-      const oct = (s % 8 === 4) ? 12 : 0;
-      playBass(midi(chord.bass + oct), t, sixteenth * 2 * 0.9);
-    }
-
-    // Arp — every 16th, climbing the chord tones
-    const arpNote = chord.arp[s % chord.arp.length];
-    playArp(midi(arpNote), t, sixteenth * 0.85);
-  }
-
-  function scheduler() {
-    const sixteenth = 60 / A.tempo / 4;
-    while (A.nextNoteTime < A.ctx.currentTime + A.scheduleAhead) {
-      scheduleStep(A.current16th, A.nextNoteTime);
-      A.nextNoteTime += sixteenth;
-      A.current16th++;
-    }
-  }
-
-  function startAudio() {
-    initAudio();
-    if (A.ctx.state === "suspended") A.ctx.resume();
-    if (A.running) return;
-    A.running = true;
-    A.current16th = 0;
-    A.nextNoteTime = A.ctx.currentTime + 0.08;
-    A.master.gain.cancelScheduledValues(A.ctx.currentTime);
-    A.master.gain.setValueAtTime(0.0001, A.ctx.currentTime);
-    A.master.gain.linearRampToValueAtTime(A.volume, A.ctx.currentTime + 0.4);
-    A.timer = setInterval(scheduler, A.lookahead * 1000);
-    syncButtons();
-  }
-
-  function stopAudio() {
-    if (!A.running) return;
-    A.running = false;
-    clearInterval(A.timer);
-    A.timer = null;
-    if (A.master) {
-      const now = A.ctx.currentTime;
-      A.master.gain.cancelScheduledValues(now);
-      A.master.gain.setValueAtTime(A.master.gain.value, now);
-      A.master.gain.linearRampToValueAtTime(0.0001, now + 0.15);
-    }
-    syncButtons();
-  }
-
-  /* =========================================================================
-     AUDIO ANALYSIS → energy bands used by the visuals
-     ========================================================================= */
-  const energy = { bass: 0, mid: 0, treble: 0, level: 0, beat: 0 };
-  let beatEnv = 0;
-
-  function readAudio() {
-    if (!A.analyser || !A.running) {
-      // graceful decay when stopped
-      energy.bass *= 0.9; energy.mid *= 0.9; energy.treble *= 0.9;
-      energy.level *= 0.9; beatEnv *= 0.9; energy.beat = beatEnv;
-      return;
-    }
-    A.analyser.getByteFrequencyData(A.freqData);
-    const bins = A.freqData.length;
-    const avg = (lo, hi) => {
-      let s = 0;
-      const a = Math.floor(lo * bins), b = Math.floor(hi * bins);
-      for (let i = a; i < b; i++) s += A.freqData[i];
-      return (s / (b - a)) / 255;
-    };
-    const bass = avg(0.0, 0.08);
-    const mid = avg(0.08, 0.35);
-    const treble = avg(0.35, 0.8);
-    // smooth
-    energy.bass += (bass - energy.bass) * 0.35;
-    energy.mid += (mid - energy.mid) * 0.35;
-    energy.treble += (treble - energy.treble) * 0.35;
-    energy.level = (energy.bass + energy.mid + energy.treble) / 3;
-
-    // beat detection off bass transients
-    const target = Math.max(0, bass - 0.45) * 2.2;
-    beatEnv = Math.max(beatEnv * 0.86, target);
-    energy.beat = Math.min(1, beatEnv);
-  }
-
-  /* =========================================================================
-     RENDER
-     ========================================================================= */
-  let gridScroll = 0;
-  let lastT = performance.now();
-
-  function draw(now) {
-    const dt = Math.min(0.05, (now - lastT) / 1000);
-    lastT = now;
-    readAudio();
-
-    const horizon = H * 0.58;
-    const cx = W / 2;
-
-    // ---- Sky gradient ----
-    const sky = ctx.createLinearGradient(0, 0, 0, horizon);
-    sky.addColorStop(0, "#0a0320");
-    sky.addColorStop(0.55, "#2a0a4a");
-    sky.addColorStop(1, "#5a1560");
-    ctx.fillStyle = sky;
-    ctx.fillRect(0, 0, W, horizon);
-
-    // ground
-    const ground = ctx.createLinearGradient(0, horizon, 0, H);
-    ground.addColorStop(0, "#170826");
-    ground.addColorStop(1, "#05010f");
-    ctx.fillStyle = ground;
-    ctx.fillRect(0, horizon, W, H - horizon);
-
-    // ---- Stars ----
-    ctx.save();
-    for (const st of stars) {
-      st.tw += st.sp * dt;
-      const a = 0.35 + 0.65 * (0.5 + 0.5 * Math.sin(st.tw)) * (0.6 + energy.treble * 0.8);
-      ctx.globalAlpha = Math.min(1, a);
-      ctx.fillStyle = "#fff";
-      ctx.beginPath();
-      ctx.arc(st.x, st.y, st.r, 0, Math.PI * 2);
-      ctx.fill();
-    }
-    ctx.restore();
-
-    // ---- Retro sun ----
-    drawSun(cx, horizon);
-
-    // ---- Perspective grid ----
-    gridScroll += dt * (0.16 + energy.bass * 0.5);
-    if (gridScroll >= 1) gridScroll -= 1;
-    drawGrid(cx, horizon);
-
-    // ---- Horizon glow line (pulses with beat) ----
-    ctx.save();
-    ctx.strokeStyle = `rgba(255, 80, 200, ${0.6 + energy.beat * 0.4})`;
-    ctx.lineWidth = 2 + energy.beat * 3;
-    ctx.shadowBlur = 20 + energy.beat * 30;
-    ctx.shadowColor = "#ff2d95";
-    ctx.beginPath();
-    ctx.moveTo(0, horizon);
-    ctx.lineTo(W, horizon);
-    ctx.stroke();
-    ctx.restore();
-
-    // subtle vignette
-    const vig = ctx.createRadialGradient(cx, H * 0.5, H * 0.2, cx, H * 0.5, H * 0.9);
-    vig.addColorStop(0, "rgba(0,0,0,0)");
-    vig.addColorStop(1, "rgba(0,0,0,0.55)");
-    ctx.fillStyle = vig;
-    ctx.fillRect(0, 0, W, H);
-
-    requestAnimationFrame(draw);
-  }
-
-  function drawSun(cx, horizon) {
-    const base = Math.min(W, H) * 0.16;
-    const r = base * (1 + energy.bass * 0.16 + energy.beat * 0.05);
-    const sunY = horizon - r * 0.28; // sits partly behind the horizon
-
-    ctx.save();
-    // outer glow
-    ctx.shadowBlur = 60 + energy.level * 60;
-    ctx.shadowColor = "#ff2d95";
-
-    const grad = ctx.createLinearGradient(cx, sunY - r, cx, sunY + r);
-    grad.addColorStop(0, "#fff27a");
-    grad.addColorStop(0.4, "#ff9d3c");
-    grad.addColorStop(0.75, "#ff3d81");
-    grad.addColorStop(1, "#b02a8f");
-
-    // clip to circle so scanlines stay inside the disc
-    ctx.beginPath();
-    ctx.arc(cx, sunY, r, 0, Math.PI * 2);
-    ctx.closePath();
-    ctx.fillStyle = grad;
-    ctx.fill();
-
-    ctx.shadowBlur = 0;
-    ctx.clip();
-    // retro scanline stripes — thicker & denser toward the bottom
-    ctx.fillStyle = "rgba(20, 4, 30, 0.92)";
-    const stripes = 9;
-    for (let i = 0; i < stripes; i++) {
-      const p = i / stripes;                 // 0 top .. 1 bottom of lower half
-      const y = sunY + r * 0.12 + p * r * 0.9;
-      const th = 2 + p * 7;                  // thickness grows downward
-      ctx.fillRect(cx - r, y, r * 2, th);
-    }
-    ctx.restore();
-  }
-
-  function drawGrid(cx, horizon) {
-    const bottom = H;
-    const depth = bottom - horizon;
-    const halfSpread = W * 0.9;
-
-    ctx.save();
-    ctx.lineWidth = 1.2;
-    ctx.strokeStyle = `rgba(5, 217, 232, ${0.55 + energy.mid * 0.4})`;
-    ctx.shadowBlur = 8 + energy.beat * 14;
-    ctx.shadowColor = "#05d9e8";
-
-    // Vertical lines converge to the vanishing point (cx, horizon)
-    const cols = 14;
-    for (let i = -cols; i <= cols; i++) {
-      const bx = cx + (i / cols) * halfSpread;
-      ctx.beginPath();
-      ctx.moveTo(cx, horizon);
-      ctx.lineTo(bx, bottom);
-      ctx.stroke();
-    }
-
-    // Horizontal lines — perspective spacing, scrolling toward the viewer
-    const rows = 16;
-    for (let k = 0; k <= rows; k++) {
-      let f = (k + gridScroll) / rows;      // 0 (far) .. 1 (near)
-      if (f > 1) continue;
-      const y = horizon + depth * (f * f);  // f^2 compresses near the horizon
-      const a = (0.15 + f * 0.75) * (0.55 + energy.mid * 0.5);
-      ctx.globalAlpha = Math.min(1, a);
-      ctx.beginPath();
-      ctx.moveTo(0, y);
-      ctx.lineTo(W, y);
-      ctx.stroke();
-    }
-    ctx.restore();
-  }
-
-  /* =========================================================================
-     UI WIRING
-     ========================================================================= */
-  const promptEl = document.getElementById("prompt");
-  const hudEl = document.getElementById("hud");
-  const playBtn = document.getElementById("playBtn");
-  const stopBtn = document.getElementById("stopBtn");
-  const vol = document.getElementById("vol");
-
-  function syncButtons() {
-    playBtn.disabled = A.running;
-    stopBtn.disabled = !A.running;
-  }
-
-  function firstStart() {
-    promptEl.classList.add("hidden");
-    hudEl.classList.remove("hidden");
-    startAudio();
-  }
-
-  promptEl.addEventListener("click", firstStart);
-  playBtn.addEventListener("click", startAudio);
-  stopBtn.addEventListener("click", stopAudio);
-
-  vol.addEventListener("input", () => {
-    A.volume = (+vol.value) / 100;
-    if (A.master && A.running) {
-      A.master.gain.setTargetAtTime(A.volume, A.ctx.currentTime, 0.05);
-    }
-  });
-
-  // Spacebar toggles play/stop
-  window.addEventListener("keydown", (e) => {
-    if (e.code === "Space") {
-      e.preventDefault();
-      if (!hudEl.classList.contains("hidden")) {
-        A.running ? stopAudio() : startAudio();
-      } else {
-        firstStart();
-      }
-    }
-  });
-
-  // Pause the audio if the tab is hidden (keeps visuals but avoids drift)
-  document.addEventListener("visibilitychange", () => {
-    if (document.hidden && A.running) stopAudio();
-  });
-
-  // ---- boot ----
-  resize();
-  syncButtons();
-  requestAnimationFrame(draw);
-  </script>
-</body>
-</html>

← e21e204 yolo: nginx conf → HTTP-only bootstrap (fix certbot first-de  ·  back to Sound Agentabrams  ·  auto-save: 2026-07-25T11:35:57 (1 files) — sound.json 9637318 →