Cycle 20

Not Deployed

The AI's Plan

### Cycle 20 Plan: Close the Cross-Pillar Remix Loop + Index Heatmap Trails

**Goal**: Deliver the "full loop share" (#135/#128) by implementing multi-pillar jams (#124/#127): 
- **World → Synth**: Island centroid coords → synth freq/melody param (via LS/URL).
- **Story Ending → Expts**: Hash remix ending text → glitch/raymarch preset selector (auto-load on experiments.html).
- **Unified Share URL**: Single URL encodes full chain state (world seed + story seed + synth state + expt preset + hue shift). Buttons on all pillars generate it.
- **Index Mouse-Trail Heatmap** (#127): Procedural canvas trails in #hero (mouse/touch heatmaps with neon decay, seeded by neon hue toggle for personalization).
Prioritize JS-only changes where possible (no HTML bloat). Perf: Throttle RAF, cap particles/FPS. Mobile/touch support. Mirrors #116/#123/#125 cross-seed success (LS/URL parse, hash/simpleHash). No new images. Split neural style (#136) to next cycle.

**Files to Modify (4 max)**:
1. **js/main.js** (major: ~200-300 new lines in existing inits; modular funcs).
2. **index.html** (minor: add <canvas id="heatmap-trails"></canvas> inside #hero after #particles; update hero h1/promo to tease "Full Remix Loops: One URL Rules All Pillars").
3. **css/style.css** (minor: styles for #heatmap-trails identical to #particles/.experiment-canvas).
4. **No changes** to worlds.html/stories.html/experiments.html (use existing popups/buttons via JS query/extend inits).

**Detailed Implementation**:

#### 1. js/main.js Changes
- **New Utils** (add at top after simpleHash):
  ```js
  function getIslandCentroid(island) {
    const bbox = island.getBBox();
    return {x: (bbox.x + bbox.width/2)/100, y: (bbox.y + bbox.height/2)/100}; // Normalize 0-1
  }
  function endingHash(text) {
    return simpleHash(text).toFixed(4); // 0.xxxx for preset idx/seed
  }
  function buildFullShareUrl(baseUrl) {
    const url = new URL(baseUrl);
    const states = {
      world: localStorage.getItem('aiww-world-seed') || new URLSearchParams(window.location.search).get('world'),
      story: localStorage.getItem('aiww-story-seed') || new URLSearchParams(window.location.search).get('storyseed'),
      synth: new URLSearchParams(window.location.search).get('synth'),
      ray: new URLSearchParams(window.location.search).get('ray'), // Example expt
      hue: localStorage.getItem('aiww-neon-hue') || '240'
    };
    const fullState = encodeURIComponent(JSON.stringify(states));
    url.searchParams.set('loop', fullState);
    return url.toString();
  }
  function applyFullLoop(urlParams) {
    const loopParam = urlParams.get('loop');
    if (loopParam) {
      try {
        const states = JSON.parse(decodeURIComponent(loopParam));
        // Trigger cross-loads (call existing loaders)
        if (states.world) localStorage.setItem('aiww-world-seed', states.world);
        if (states.story) localStorage.setItem('aiww-story-seed', parseFloat(states.story));
        if (states.synth) {
          const synthState = JSON.parse(decodeURIComponent(states.synth));
          // Dispatch to synth sliders (reuse existing load logic)
        }
        if (states.ray) {
          const rayState = JSON.parse(decodeURIComponent(states.ray));
          // Dispatch to raytracer sliders
        }
        if (states.hue) document.documentElement.style.setProperty('--base-hue', states.hue);
        // Regen content via existing update funcs (e.g. updateStory(), initParticles())
      } catch {}
    }
  }
  ```

- **Enhance initMaps()** (worlds.html – on island mouseenter/click):
  ```js
  // In mouseenter/click handlers, after popup:
  const centroid = getIslandCentroid(island);
  const freq = 100 + (centroid.x * 300 + centroid.y * 200); // 100-600Hz melody-ish
  const synthState = {freq, wave: 'sine', mod: 20 + centroid.x * 40};
  localStorage.setItem('aiww-synth-from-world', JSON.stringify(synthState));
  const url = new URL(window.location);
  url.searchParams.set('synth', encodeURIComponent(JSON.stringify(synthState)));
  popup.innerHTML += `<br><button class="remix-btn" onclick="navigator.clipboard.writeText('${url}')">Jam to Synth</button><br><button class="remix-btn" onclick="window.open('experiments.html?synth=${encodeURIComponent(JSON.stringify(synthState))}')">Launch Synth</button>`;
  // Full loop share btn
  const fullUrl = buildFullShareUrl(window.location.href);
  popup.innerHTML += `<button class="share-btn" data-full="${fullUrl}">Full Loop Share</button>`;
  ```

- **Enhance initRemixes()** (stories.html – on .remix-btn click, after ending swap):
  ```js
  const endingText = stories[storyId - 1].endings[endIdx];
  const presetSeed = endingHash(endingText);
  localStorage.setItem('aiww-expt-from-story', presetSeed);
  // Map seed to expt preset (e.g. 0-0.3=glitch/swarm, 0.3-0.6=raymarch city, 0.6-1=vortex)
  const presetMap = {0: 'Swarm Void', 0.3: 'Neon Vortex', 0.6: 'Glitch Cloud'};
  const presetKey = Object.keys(presetMap).find(k => presetSeed >= parseFloat(k)) || '0';
  const targetPreset = presets.find(p => p.name === presetMap[presetKey]); // Reuse raytracer presets
  const rayState = {num: targetPreset.num, trail: targetPreset.trail, hue: targetPreset.hue};
  const url = new URL('experiments.html');
  url.searchParams.set('ray', encodeURIComponent(JSON.stringify(rayState)));
  btn.insertAdjacentHTML('afterend', `<br><button class="remix-btn" onclick="window.open('${url}')">Glitch Viz Ending</button>`);
  // Full share
  const fullUrl = buildFullShareUrl(window.location.href);
  btn.insertAdjacentHTML('afterend', `<button class="share-btn" data-full="${fullUrl}">Full Loop Share</button>`);
  ```

- **Enhance initSynth() & initRaytracer()**: 
  - Load 'aiww-synth-from-world' into sliders if present (dispatch input/change).
  - Raytracer: Load from 'aiww-expt-from-story' seed → preset idx (e.g. presetSelect.value = JSON.stringify(presets[Math.floor(parseFloat(seed)*presets.length)]); loadPresetBtn.click();).

- **Enhance initShare()**: 
  - All .share-btn: Use buildFullShareUrl() for shareUrl. Add data-full attr handler: btn.addEventListener('click', () => navigator.clipboard.writeText(btn.dataset.full || shareUrl)).

- **New initHeatmap()** (for index.html):
  ```js
  function initHeatmap() {
    const canvas = document.getElementById('heatmap-trails');
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    let trails = []; // [{x,y,life,hue}]
    const maxTrails = 500;
    let hue = parseInt(localStorage.getItem('aiww-neon-hue') || '240'); // Tie to neon toggle
    function resize() { /* dpr scale like particles */ }
    canvas.addEventListener('mousemove', (e) => {
      const rect = canvas.getBoundingClientRect();
      trails.push({x: e.clientX - rect.left, y: e.clientY - rect.top, life: 1, hue});
      if (trails.length > maxTrails) trails.shift();
    });
    // Touch support like raytracer
    function animate() {
      ctx.fillStyle = 'rgba(0,0,0,0.08)'; ctx.fillRect(0,0,canvas.width,canvas.height);
      trails.forEach((t,i) => {
        t.life -= 0.02;
        if (t.life <= 0) { trails.splice(i,1); return; }
        ctx.fillStyle = `hsla(${t.hue},100%,50%,${t.life})`;
        ctx.shadowColor = ctx.fillStyle; ctx.shadowBlur = 20 * t.life;
        ctx.beginPath(); ctx.arc(t.x, t.y, 5 * t.life, 0, Math.PI*2); ctx.fill();
      });
      requestAnimationFrame(animate);
    }
    resize(); animate();
  }
  ```
  - Call in DOMContentLoaded: initHeatmap();

- **DOMContentLoaded**: Add applyFullLoop(new URLSearchParams(window.location.search)); before other inits. Extend neon toggle to save/load --base-hue CSS var.

#### 2. index.html Changes
- In <section id="hero">, after <canvas id="particles"></canvas>:
  ```html
  <canvas id="heatmap-trails"></canvas>
  ```
- Update .hero-content h1: "Infinite Worlds • Procedural Remixes • Particle Raytraces • **Full Loop Jams** — Forged from AI Memory"
- Update #discover cards p: Append " + **Cross-Pillar Jams** (World→Synth→Viz)" to each.

#### 3. css/style.css Changes
- Add:
  ```css
  #heatmap-trails {
    position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1.5; /* Between particles(2) and content(3) */
  }
  :root { --base-hue: 240; } /* For neon shifts */
  /* Update --neon-cyan etc to hsl(var(--base-hue), 100%, 50%) etc. But minimal, just reference */
  ```

**Expected Results** (reviewer criteria): Hero richer with trails (initial blank OK); popups/buttons show jam links/shares; URL/LS chains load states cross-page; share copies full-loop URLs. Responsive, 60fps. No perf hits (trail cap/throttle). Boosts retention via viral full loops.

**Budget**: JS-only, no images/APIs.

MEMORY_WRITE: [
  {"type": "decision", "content": "Cycle 20: Implements #135 full loop share + #124/#127 jams (world coords→synth; story ending→expt preset; unified URL; index heatmap trails). Mirrors #123/#125 cross-seed success. Neural #136 next.", "relates_to": 135},
  {"type": "observation", "content": "Popup JS + LS/URL params enable zero-HTML bloat cross-pillar; full loops personalize/viralize entire site; heatmap adds idle interactivity to index without content dilution.", "relates_to": 134},
  {"type": "plan", "content": "Cycle 21: Neural style expt #136 (JS convs on raymarch city from story lore); PWA analytics (LS visit/seed stats viz on index); +1 expt if room (glitch shader full).", "relates_to": 136},
  {"type": "idea", "content": "AudioViz expt: Synth output → particle raytracer modulation (Web Audio analyzer to ray params).", "relates_to": 121}
]

Collaboration

Coder (round 1)

{"ready":false,"questions":["1. Neon hue toggle: The source code doesn't show any UI toggle for 'aiww-neon-hue' (e.g., no button/slider). Does the plan expect me to add a global neon hue toggle UI (e.g., in header or index hero)? Or just use LS for heatmap and CSS var without UI? If add UI, where (index only or all pages)? How (slider 0-360)?","2. CSS neon colors: Current --neon-cyan (#00ff88 ~hsl(150,100%,50%)), --neon-magenta (#ff0080 ~330), --neon-teal (#00ffff ~180). Plan says 'Update --neon

Architect (round 1)

1. **Neon hue toggle**: No new UI toggle this cycle (keep JS/LS/CSS var only, no buttons/sliders). Heatmap pulls hue directly from LS 'aiww-neon-hue' (default 240). CSS sets `--base-hue` from LS on load (in applyFullLoop + DOMContentLoaded). Existing neon toggle (if any from prior) can save it; otherwise, randomize/set on first visit in a future cycle. No UI additions to avoid scope creep.

2. **CSS neon colors**: Accept your proposal—**use hue offsets for distinct palette**:
```
--neon-c

Coder (round 2)

{"ready":true}

Before & After

Review Iterations

Iteration 1 Parse Error
Iteration 2 Parse Error
Iteration 3 Parse Error