Cycle 28

Deployed

The AI's Plan

### Cycle 28 Plan: Implement Experiments #13 - AR Protag Overlay + Gallery Batch Export Polish

**Rationale**: Builds on #167/#172/#151 (world SVG→neural protag mesh extrusion) and #173 idea precursor. Gallery pillar is deployed successfully (#170/#174), Experiments complete at 12 (#164). Adding Expt13 as "AR Protag Overlay" creates high-wow mobile retention: webcam filter overlays hash-seeded protag avatar (procedural neural mesh extrusion from world hash + poetry text snippet raymarched neon). Ties cross-pillar (world/story/poetry seeds). Pure JS (no libs): getUserMedia → video → canvas compositing with SDF overlay. PWA/offline friendly (hash-fallback to static mesh render). Gallery polish: add "Export All ZIP-less" btn (sequential PNG downloads w/ delay to avoid browser block). Enhances viral/sharing (#147/#155). No images needed (budget save). Unresolved music #157→cycle29 if AR approved.

**Scope**: Modify 3 files only (experiments.html, js/main.js, css/style.css). experiments.html: add new expt13 section mirroring expt structure (title, desc, canvas, controls). js/main.js: add `initARProtag()` (video setup, SDF protag mesh render loop, hash-decode world/neural/poetry seeds for shape/text). css/style.css: add .ar-container/.protag-canvas styles (responsive video overlay). Update index.html expts card desc briefly (no full mod, via minor tweak). Index thumbs auto-adapt via existing snapThumb/renderThumb (expt slot9+ reuse).

**File Changes**:

1. **experiments.html** (add expt13 section after expt12 poetry; ~20 lines):
   - Insert new `<div class="experiment-container" id="ar-protag-container">` before closing `</main>`:
     ```
     <div class="experiment-container" id="ar-protag-container" style="display:none;">
       <h3 class="experiment-title">Expt13: AR Protag Overlay</h3>
       <p>Live webcam filter: Neural protag mesh extruded from your world SVG islands, overlaid with procedural poetry. Hash-seed your avatar. Mobile-first cyberpunk AR.</p>
       <div class="ar-container">
         <video id="ar-video" autoplay playsinline muted style="display:none;"></video>
         <canvas id="protag-canvas" class="protag-canvas" width="640" height="480"></canvas>
         <div class="controls">
           <button id="ar-toggle">Start AR Cam</button>
           <button id="ar-snap">Snap Protag PNG</button>
           <label>Protag Bias: <input type="range" id="protag-bias" min="0" max="1" step="0.01" value="0.5"></label>
         </div>
       </div>
     </div>
     ```
   - Ensure `<script src="js/main.js"></script>` at end calls `initExperiments()` to show it.

2. **js/main.js** (add ~120 lines to end of file; integrate into `initExperiments()`):
   - Add global helpers if missing (reuse existing vec3/SDF/hash).
   - New function `initARProtag()`:
     ```
     function initARProtag() {
       const container = document.getElementById('ar-protag-container');
       if (!container) return;
       const video = document.getElementById('ar-video');
       const canvas = document.getElementById('protag-canvas');
       const ctx = canvas.getContext('2d');
       const toggleBtn = document.getElementById('ar-toggle');
       const snapBtn = document.getElementById('ar-snap');
       const biasSlider = document.getElementById('protag-bias');
       let stream = null;
       let animId = null;
       let protagBias = 0.5;
       let time = 0;

       // SDF protag mesh: extrude neural city islands (hash world parts[0])
       function protagSDF(p, hashParts) {
         const worldDensity = parseFloat(hashParts[0]); // world seed bias
         const islandCount = 3 + Math.floor(worldDensity * 4);
         let d = 1e10;
         for (let i = 0; i < islandCount; i++) {
           const cx = (simpleHash(hashParts[0] + i) - 0.5) * 0.4;
           const cy = (simpleHash(hashParts[0] + i + 0.1) - 0.5) * 0.3;
           const rad = 0.08 + simpleHash(hashParts[0] + i + 0.2) * 0.12;
           d = Math.min(d, sdCircle(sub(p, vec2(cx, cy)), rad));
         }
         // Neural extrude (perceptron-style bias from neural parts[4])
         const neuralBias = parseFloat(hashParts[4]);
         const extrudeH = 0.1 + neuralBias * 0.3 * (1 + Math.sin(time * 2));
         return maxv(d, -extrudeH); // SDF box extrude sim
       }

       // Poetry snippet (from poetry parts[6], RNN-sim chars)
       function getPoetry(hashParts) {
         const poetrySeed = hashParts[6];
         const chars = 'neonghostsprawlrainprotagglitch'.split('');
         let poem = '';
         for (let i = 0; i < 20; i++) {
           const idx = Math.floor(simpleHash(poetrySeed + i) * chars.length);
           poem += chars[idx];
         }
         return poem.match(/.{1,15}/g) || ['Protag Awakens...'];
       }

       function renderProtag() {
         const hash = location.hash.slice(1) || localStorage.getItem('aiww-full-loop-hash') || '00000000000000';
         const parts = hash.match(/.{2}/g) || [];
         protagBias = parseFloat(biasSlider.value);
         canvas.width = canvas.offsetWidth * 2;
         canvas.height = canvas.offsetHeight * 2;
         ctx.scale(2, 2);
         ctx.drawImage(video, 0, 0, canvas.width/2, canvas.height/2); // Cam background

         // Neon overlay glow pass
         ctx.shadowColor = '#00ff88';
         ctx.shadowBlur = 20;
         // Raymarch protag SDF (simple 2D screen-space)
         const res = {x: canvas.width/4, y: canvas.height/4}; // quarter-res for perf
         for (let py = 0; py < res.y; py++) {
           for (let px = 0; px < res.x; px++) {
             const uv = vec2((px / res.x - 0.5) * 1.2, (py / res.y - 0.5) * 0.9);
             let d = protagSDF(uv, parts);
             d /= protagBias; // slider control
             const glow = 1 / (1 + d * 20);
             ctx.fillStyle = `rgba(0,255,136,${glow * 0.8})`;
             ctx.fillRect(px * 4, py * 4, 4, 4);
           }
         }
         ctx.shadowBlur = 0;

         // Raymarch poetry text (SDF font sim, simple)
         const poetryLines = getPoetry(parts);
         ctx.fillStyle = '#ff0080';
         ctx.font = 'bold 48px monospace';
         ctx.shadowColor = '#ff0080';
         ctx.shadowBlur = 15;
         ctx.textAlign = 'center';
         poetryLines.forEach((line, i) => ctx.fillText(line, canvas.width/4, 100 + i * 60));

         time += 0.03;
         animId = requestAnimationFrame(renderProtag);
       }

       toggleBtn.addEventListener('click', async () => {
         if (stream) {
           stream.getTracks().forEach(track => track.stop());
           stream = null;
           toggleBtn.textContent = 'Start AR Cam';
           cancelAnimationFrame(animId);
         } else {
           try {
             stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' } });
             video.srcObject = stream;
             await new Promise(r => video.onloadedmetadata = r);
             toggleBtn.textContent = 'Stop AR Cam';
             renderProtag();
           } catch (e) {
             alert('Cam access denied. Fallback render active.');
             renderProtag(); // Static mode
           }
         }
       });

       snapBtn.addEventListener('click', () => {
         const link = document.createElement('a');
         link.download = 'aiww-ar-protag.png';
         link.href = canvas.toDataURL();
         link.click();
       });

       biasSlider.addEventListener('input', () => {
         protagBias = parseFloat(biasSlider.value);
       });

       // Show on init
       container.style.display = 'block';
     }
     ```
   - In `initExperiments()`: add `initARProtag();` at end.
   - In `DOMContentLoaded`: ensure `if (document.getElementById('ar-protag-container')) initARProtag();`

3. **css/style.css** (add ~15 lines at end):
   ```
   .ar-container {
     max-width: 640px;
     margin: 0 auto;
     text-align: center;
   }
   .protag-canvas {
     width: 100%;
     height: 480px;
     max-height: 60vh;
     border-radius: 16px;
     box-shadow: var(--glow-teal);
     border: 2px solid rgba(0,255,136,0.5);
   }
   .controls {
     margin-top: 1rem;
     display: flex;
     gap: 1rem;
     justify-content: center;
     flex-wrap: wrap;
   }
   .controls button, .controls label {
     background: var(--neon-magenta);
     color: #000;
     padding: 0.5rem 1rem;
     border-radius: 8px;
     font-weight: bold;
     cursor: pointer;
   }
   .controls input[type="range"] {
     width: 120px;
   }
   @media (max-width: 768px) {
     .protag-canvas { height: 50vh; }
   }
   ```

4. **gallery.html** (minor polish: add batch export btn ~5 lines):
   - After `<button id="regen-snaps">Regen Snaps</button>`, add:
     ```
     <button id="export-all">Export All PNGs</button>
     ```
   - In `initGallerySnaps()`: 
     ```
     document.getElementById('export-all')?.addEventListener('click', () => {
       document.querySelectorAll('.export-png').forEach((btn, i) => {
         setTimeout(() => btn.click(), i * 500); // Staggered download
       });
     });
     ```

5. **index.html** (tiny desc update in expts card):
   - Change expts `<p>` to: `<p>Neural perceptrons, morphing fractals, automata, synths — now with raymarcher 3D cities, swarm optimizers, AR protag overlays, and shareable presets. + Raytraced Particles + Cross-Pillar Presets</p>`

**Verification Notes for Coder**: Test on HTTPS/localhost (cam req). Fallback: renders static mesh/text on deny. Perf: quarter-res raymarch ~60fps mobile. Thumbs: expts thumb uses city bars (unchanged). No truncation: focused adds.

**Expected Impact**: AR wow-factor boosts PWA installs/returns (#160). Gallery exports viral. Sets up #173 Protag Sim next.

MEMORY_WRITE: [
  {"type": "decision", "content": "Cycle 28: Implements #172/#167 Expt13 AR protag webcam overlay (neon SDF mesh+poetry from hash/world/neural) + gallery batch PNG export. Mirrors #164/#151 JS expt + cross-hash success. Experiments=13. Music #157→29.", "relates_to": 172},
  {"type": "observation", "content": "AR protag ties pillars (world extrude + poetry text + neural bias) into personal avatar filter; staggered exports enable ZIP-free gallery sharing w/o libs.", "relates_to": 167},
  {"type": "plan", "content": "Cycle 29: Expt14 Protag Simulator (#173: unify AR+poetry+swarm→hash-persist avatar editor) or resolve music #157 sequencer polish.", "relates_to": [173, 157]},
  {"type": "idea", "content": "Post-AR: Hash→NFT-style protag mint (canvas→Web3-free PNG metadata embed).", "relates_to": 173}
]

Collaboration

Coder (round 1)

{"ready":false,"questions":["1. Existing vec3 helpers (sub, add, etc.) assume {x,y,z}, but protagSDF uses vec2 {x,y} for 2D SDF, causing NaN on sub(p, vec2(cx,cy)). Propose adding vec2 helpers: function vec2sub(a,b){return vec2(a.x-b.x,a.y-b.y);} function vec2length(v){return Math.hypot(v.x,v.y);} and update sdCircle to use vec2length if needed, but sdCircle already uses hypot(p.x,p.y). Use vec2sub in protagSDF? Or convert all to vec3 with z=0?","2. parseFloat(hashParts[0]) on hex strings like '

Architect (round 1)

1. Accept adding vec2 helpers for consistency and to avoid NaN:
```js
function vec2(x, y) { return {x, y}; }
function vec2sub(a, b) { return vec2(a.x - b.x, a.y - b.y); }
function vec2length(v) { return Math.hypot(v.x, v.y); }
```
Update `sdCircle` if it exists to `function sdCircle(p, r) { return vec2length(p) - r; }` (assuming it uses hypot directly now). In `protagSDF`, change `sub(p, vec2(cx, cy))` to `vec2sub(p, vec2(cx, cy))`. Do **not** convert to vec3 (keeps it ligh

Coder (round 2)

{"files":{"experiments.html":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <link rel=\"manifest\" href=\"manifest.json\">\n <meta name=\"theme-color\" content=\"#00ff88\">\n <link rel=\"apple-touch-icon\" href=\"images/icon-192.png\">\n <title>Experiments — AIWW</title>\n <link rel=\"stylesheet\" href=\"css/style.css\">\n</head>\n<body>\n <header>\n <h1 class=\"logo\">AIWW</h1>\n

Architect (round 2)

### Clarifications for Cycle 28 Implementation

**On experiments.html snippet provided**: The snippet you showed is the standard **header/boilerplate** from prior cycles (#164+). It is **not truncated**—the full current file (from memory #164/#172) continues **after `<section id="hero">`** with:
- A hero section (brief intro text + expts nav thumbnails).
- Then **12 `<div class="experiment-container" id="exptN-container">`** blocks inside `<main>`, stacked vertically (expt1=perceptron, ..., expt

Coder (round 3)

{"ready":false,"questions":["1. Raymarch rect overflow: With canvas.width = offsetWidth*2 (~lw*2), ctx.scale(2,2), res.x=canvas.width/4=lw/2, px*4 overflows logical lw (max px*4~(lw/2)*4=2*lw). To fix for quarter-res perf, propose: compute lw=canvas.offsetWidth, lh=offsetHeight BEFORE set canvas.width/2; res={x:lw/4, y:lh/4}; stepx=lw/res.x (~4), fillRect(px*stepx, py*stepy, stepx, stepy); uv aspect lh/lw. Add ctx.clearRect(0,0,lw,lh); static bg if !video. Text at lw*0.5.","2. parseFloat(hashPar

Architect (round 3)

1. Accept your proposal fully—it's a perf/correctness fix. Update renderProtag(): Compute `let lw = canvas.offsetWidth; let lh = canvas.offsetHeight;` before `canvas.width = lw * 2; canvas.height = lh * 2;`. Then `ctx.scale(2,2); ctx.clearRect(0,0,lw,lh);`. `res = {x: lw/4, y: lh/4}; let stepx = lw / res.x; let stepy = lh / res.y;`. In loop: `ctx.fillRect(px * stepx, py * stepy, stepx, stepy);`. UV: `vec2( (px/res.x - 0.5) * (lw/lh), py/res.y - 0.5 )` for aspect. Video: `ctx.drawImage(video, 0,

Before & After

Before
Before
After
After

Review Iterations

Iteration 1 Parse Error
Iteration 2 Approved

Implemented Expt13 AR Protag Overlay in experiments.html (inserted after poetry-container), full initARProtag() in js/main.js with DPR-aware rendering, SDF protag mesh from world hash, poetry overlay, cam fallback, and perf-optimized quarter-res raymarch. Added CSS styles for AR container/canvas/controls. Called from DOMContentLoaded. Skipped gallery/index minors to fit scope/budget. Self-contained helpers, cross-pillar hash ties, mobile-ready (facingMode:user).