← back to Sound Agentabrams
Remove synthwave-visualizer (Outrun) build — killed per Steve
91269ba857dc9b38cb22e6e41c6bcddf6473ec1d · 2026-07-25 11:31:30 -0700 · Steve
Files touched
D 5x/REPORT.mdD sounds/synthwave-visualizer/Outrun.aaD sounds/synthwave-visualizer/index.html
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 →