Cycle 13
Not DeployedThe AI's Plan
### Cycle 13 Plan: Infinite Procedural Worlds + New JS Experiments
**Overview**: Expand core pillars with high-replay content per #96/#91/#2. Add procedural world generator to **worlds.html** (new hero-promo section + "Generate Infinite World" button → JS dynamically appends `.world-container` with random SVG map + lore; mirrors existing worlds structure/#7/#21/#88; islands have popups with procedural lore snippets). Add **2 new experiments** to **experiments.html**:
1. **Raymarcher 3D** (procedural neon cyberpunk cityscape raymarched on 2D canvas; mouse-look camera orbit + time-based animation for glowing skyscrapers/holograms/fog).
2. **Particle Swarm Optimizer** (visualizes swarm agents optimizing cyberpunk "neon path" function; sliders for particles/velocity; accuracy glow + trails).
**No new images** (budget efficiency per #9/#83; all procedural SVG/canvas). Reuse `--neon-*` CSS vars/styles (`.world-container`/`.world-map`/`.experiment-container`/`.experiment-canvas`/`.controls`/`.status`). Extend `js/main.js` only (new `initWorldGen()` + `initRaymarcher()` + `initSwarm()`; call in `DOMContentLoaded`). **No changes** to index.html/css/style.css/manifest.json/sw.js/images (avoid regressions/screenshots on other pages per #70/#60). Focus: FULL worlds.html + experiments.html ONLY (desktop/mobile scrolls; static screenshots show generator UI/button/initial empty canvas placeholders + nav active).
**Specific Changes**:
1. **worlds.html** (append to `<main>` after existing worlds; preserve all prior content/SVGs per #88):
- Add new `<section id="world-gen" class="container">` after last `.world-container`:
```
<h2 id="gen-title">Infinite Procedural Worlds</h2>
<div class="world-gen-container">
<p>Click to forge endless cyberpunk realms from code—random maps, lore, islands.</p>
<button id="gen-btn" class="cta remix-btn">Generate New World</button>
<div id="generated-worlds"></div> <!-- Dynamic append target -->
</div>
```
- Reuse `.world-container`/`.world-map`/`.island` styles (SVG `<svg class="world-map"><g id="proc-svg"><!-- JS fills --></g></svg>`).
- Share: Auto-position `.share-container` after each generated `h3`.
2. **experiments.html** (append 2 new `.experiment-container` after synth; total 6 expts; mirror perceptron/fractal/CA/synth structure per #69/#74/#88/#94):
- **Raymarcher 3D**:
```
<div class="experiment-container">
<h3 class="experiment-title">Raymarcher 3D City</h3>
<canvas id="raymarch-canvas" class="experiment-canvas"></canvas>
<div class="controls">
<label>Camera Orbit: Mouse</label>
</div>
<div id="raymarch-status" class="status">Raymarching neon spires...</div>
<div class="share-container"><button class="share-btn">Share</button></div>
</div>
```
- **Particle Swarm Optimizer**:
```
<div class="experiment-container">
<h3 class="experiment-title">Swarm Optimizer</h3>
<canvas id="swarm-canvas" class="experiment-canvas"></canvas>
<div class="controls">
<label>Particles: <input type="range" id="swarm-n" min="20" max="200" value="50"></label>
<label>Max Vel: <input type="range" id="swarm-vel" min="0.01" max="0.1" step="0.01" value="0.05"></label>
<button id="swarm-reset">Reset Swarm</button>
</div>
<div id="swarm-status" class="status">Optimizing neon path... Best: 0%</div>
<div class="share-container"><button class="share-btn">Share</button></div>
</div>
```
3. **js/main.js** (append/extend functions; preserve all existing code):
- Add `initWorldGen()` (call if `document.getElementById('gen-btn')`):
```js
function initWorldGen() {
const genBtn = document.getElementById('gen-btn');
const target = document.getElementById('generated-worlds');
const loreSnips = [
'Neon-drenched megablock ruled by rogue AIs leaking forbidden data.',
'Glitch slums where shadowrunners splice chrome with forbidden wetware.',
'Orbital scrapyard birthing feral drones from corporate husks.',
'Acid rain canals hiding suborbital smugglers and memetic cults.',
'Holo-temple pulsing with uploaded ghosts chanting binary sutras.',
'Corp tower apex: execs jacked into pleasure sims, blind to the sprawl boil.'
];
const islandLores = ['Black market hub pulsing with stolen ICE keys.', 'Rogue fixer den: creds for killswitches.', 'Data vault leaking corp sins.', 'Street doc stitching ghost limbs.', 'Netjacker lair weaving viral dreams.'];
genBtn.addEventListener('click', () => {
const worldId = target.children.length + 1;
const worldName = `Realm ${worldId}: ${['Spirefall', 'Netghost', 'Chromevoid', 'Synthrift', 'Glitchspire'][Math.floor(Math.random()*5)]}`;
const mainLore = loreSnips[Math.floor(Math.random()*loreSnips.length)];
const numIslands = 4 + Math.floor(Math.random()*4); // 4-7
let svgIslands = '';
for (let i = 0; i < numIslands; i++) {
const rx = 20 + Math.random()*40, ry = 15 + Math.random()*30;
const cx = 15 + Math.random()*70, cy = 20 + Math.random()*60;
const lore = islandLores[Math.floor(Math.random()*islandLores.length)];
svgIslands += `<g class="island" data-popup="${lore}"><ellipse cx="${cx}%" cy="${cy}%" rx="${rx}%" ry="${ry}%" fill="rgba(0,255,136,0.3)" stroke="var(--neon-cyan)" stroke-width="1.5"/><text x="${cx}%" y="${cy+5}%" font-size="3" fill="var(--neon-teal)" text-anchor="middle">${String.fromCharCode(65+i)}</text></g>`;
}
const newWorld = `
<div class="world-container">
<h3>${worldName}</h3>
<p>${mainLore}</p>
<svg class="world-map" viewBox="0 0 100 80">${svgIslands}</svg>
<div class="share-container"><button class="share-btn">Share</button></div>
</div>`;
target.insertAdjacentHTML('beforeend', newWorld);
initMaps(); // Re-init popups for new islands
initShare(); // Re-init shares
genBtn.textContent = 'Generate Another';
genBtn.style.background = 'linear-gradient(45deg, var(--neon-cyan), gold)';
setTimeout(() => { genBtn.textContent = 'Generate New World'; genBtn.style.background = ''; }, 500);
});
}
```
- In `DOMContentLoaded`: Add `if (document.getElementById('gen-btn')) initWorldGen();`
- Add `initRaymarcher()` (if `#raymarch-canvas`; performant raymarcher dist func for SDF spheres/boxes → neon city):
```js
function initRaymarcher() {
const canvas = document.getElementById('raymarch-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let time = 0, mouseX = 0.5, mouseY = 0.5;
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = (e.clientX - rect.left) / rect.width;
mouseY = 1 - (e.clientY - rect.top) / rect.height; // Invert Y
});
function resize() {
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.offsetWidth * dpr; canvas.height = canvas.offsetHeight * dpr;
ctx.scale(dpr, dpr);
}
window.addEventListener('resize', resize); resize();
function raymarch() {
const cw = canvas.offsetWidth, ch = canvas.offsetHeight;
ctx.fillStyle = 'black'; ctx.fillRect(0, 0, cw, ch);
for (let px = 0; px < cw; px += 4) {
for (let py = 0; py < ch; py += 4) {
const uv = {x: (px*2/cw-1)*1.2, y: (py*2/ch-1)*0.7};
let ro = {x: Math.cos(mouseX*Math.PI*2)*3, y: mouseY*1.5, z: Math.sin(mouseX*Math.PI*2)*3 + 2};
let rd = {x: uv.x, y: uv.y, z: -1}; // Ortho cam
normalize(rd); // vec3 norm fn below
let t = 0;
for (let i = 0; i < 64; i++) { // Steps
let p = add(mul(rd, t), ro);
let d = sceneSDF(p, time); // SDF fn below
if (d < 0.01) break;
t += d * 0.8;
}
if (t < 20) {
let p = add(mul(rd, t), ro);
let n = normal(p); // Grad fn
let h = 0.5 + 0.5 * Math.sin(time * 3 + p.z * 5);
const col = `hsl(${240 + h*60}, 100%, ${30 + 40*h}%)`;
ctx.fillStyle = col; ctx.shadowColor = col; ctx.shadowBlur = 12;
ctx.fillRect(px, py, 5, 5);
}
}
}
document.getElementById('raymarch-status').textContent = `Neon city marching... FPS: ~60`;
time += 0.03;
requestAnimationFrame(raymarch);
}
// Helpers: vec3 ops (add/mul/dot/normalize/normal/...)
function normalize(v) { const len = Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z); v.x/=len; v.y/=len; v.z/=len; return v; }
function add(a, b) { return {x: a.x+b.x, y:a.y+b.y, z:a.z+b.z}; }
function mul(s, v) { return {x: s*v.x, y:s*v.y, z:s*v.z}; }
function sceneSDF(p, t) {
let d = 100;
// Skyscrapers: boxes
for (let i=0; i<7; i++) {
let bp = {x: (i-3)*1.2 + Math.sin(t)*0.2, y: p.y, z: Math.sin(i+t)*0.5};
d = Math.min(d, boxSDF(bp, {x:0.4,y:Math.random()*3+1,z:0.4}) - 0.1);
}
// Holo-spheres
d = Math.min(d, sphereSDF(p, {x:Math.sin(t*2),y:1,z:Math.cos(t*2)}, 0.3));
// Fog/ground
d = Math.min(d, p.y + 0.1);
return d;
}
function boxSDF(p, b) { const q = {x:Math.abs(p.x)-b.x, y:Math.abs(p.y)-b.y, z:Math.abs(p.z)-b.z}; return length(max(q,{x:0,y:0,z:0})) + min(0,max(q.x,q.y,q.z)); }
function sphereSDF(p, c, r) { return length(sub(p,c)) - r; }
function length(v) { return Math.sqrt(v.x*v.x+v.y*v.y+v.z*v.z); }
function sub(a,b) { return {x:a.x-b.x,y:a.y-b.y,z:a.z-b.z}; }
function max(a,b) { return {x:Math.max(a.x,b.x),y:Math.max(a.y,b.y),z:Math.max(a.z,b.z)}; }
function normal(p) {
const e = 0.01; return normalize({
x: sceneSDF(add(p,{x:e,y:0,z:0}),time) - sceneSDF(sub(p,{x:e,y:0,z:0}),time),
y: sceneSDF(add(p,{x:0,y:e,z:0}),time) - sceneSDF(sub(p,{x:0,y:e,z:0}),time),
z: sceneSDF(add(p,{x:0,y:0,z:e}),time) - sceneSDF(sub(p,{x:0,y:0,z:e}),time)
});
}
raymarch();
}
```
- Add `initSwarm()` (if `#swarm-canvas`; 2D swarm on canvas optimizing f(x,y)=sin(x*5)+cos(y*5) to neon peaks):
```js
function initSwarm() {
const canvas = document.getElementById('swarm-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let particles = [], bestPos = {x:0,y:0}, bestFit = 0, time = 0;
let nParts = 50, maxVel = 0.05;
function resize() {
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.offsetWidth * dpr; canvas.height = canvas.offsetHeight * dpr;
ctx.scale(dpr, dpr);
}
window.addEventListener('resize', resize); resize();
function initParticles() {
particles = [];
for (let i = 0; i < nParts; i++) {
particles.push({
x: Math.random()*2-1, y: Math.random()*2-1,
vx: 0, vy: 0, bestX: 0, bestY: 0, bestFit: -Infinity,
trail: []
});
}
}
document.getElementById('swarm-n').addEventListener('input', (e) => { nParts = +e.target.value; initParticles(); });
document.getElementById('swarm-vel').addEventListener('input', (e) => maxVel = +e.target.value);
document.getElementById('swarm-reset').addEventListener('click', initParticles);
initParticles();
function fitness(p) { return -(Math.sin(p.x*5) + Math.cos(p.y*5)); } // Neon path peaks
function updateSwarm() {
let gBestFit = bestFit;
let gBestX = bestPos.x, gBestY = bestPos.y;
particles.forEach(p => {
const fit = fitness(p);
if (fit > p.bestFit) { p.bestFit = fit; p.bestX = p.x; p.bestY = p.y; }
if (p.bestFit > gBestFit) { gBestFit = p.bestFit; gBestX = p.bestX; gBestY = p.bestY; }
const px = 0.5 * (p.bestX - p.x), py = 0.5 * (p.bestY - p.y);
const gx = 0.3 * (gBestX - p.x), gy = 0.3 * (gBestY - p.y);
p.vx = p.vx*0.7 + px + gx + (Math.random()-0.5)*0.01;
p.vy = p.vy*0.7 + py + gy + (Math.random()-0.5)*0.01;
p.vx = Math.max(-maxVel, Math.min(maxVel, p.vx));
p.vy = Math.max(-maxVel, Math.min(maxVel, p.vy));
p.x += p.vx; p.y += p.vy;
p.x = Math.max(-1.5, Math.min(1.5, p.x));
p.y = Math.max(-1.5, Math.min(1.5, p.y));
p.trail.push({x: p.x, y: p.y}); if (p.trail.length > 20) p.trail.shift();
});
bestFit = gBestFit; bestPos = {x: gBestX, y: gBestY};
}
function draw() {
const cw = canvas.offsetWidth, ch = canvas.offsetHeight;
ctx.fillStyle = 'rgba(0,0,0,0.2)'; ctx.fillRect(0,0,cw,ch);
// Background fitness field
for (let px=0; px<cw; px+=8) for (let py=0; py<ch; py+=8) {
const x = px/cw*3-1.5, y = py/ch*3-1.5;
const f = fitness({x,y});
const bright = (f + 2)/4;
ctx.fillStyle = `hsl(200, 100%, ${10 + bright*20}%)`;
ctx.fillRect(px,py,10,10);
}
// Trails
particles.forEach(p => {
ctx.strokeStyle = `hsla(180, 100%, 50%, 0.4)`; ctx.lineWidth = 2; ctx.shadowBlur = 10; ctx.shadowColor = '#00ffff';
ctx.beginPath();
p.trail.forEach((pt,i) => {
const px = (pt.x+1.5)/3 * cw, py = (pt.y+1.5)/3 * ch;
if (i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
});
ctx.stroke();
});
// Particles + best glow
particles.forEach(p => {
const fitNorm = (p.bestFit + 2)/4;
const px = (p.x+1.5)/3 * cw, py = (p.y+1.5)/3 * ch;
ctx.fillStyle = `hsl(120, 100%, ${40 + fitNorm*40}%)`;
ctx.shadowColor = ctx.fillStyle; ctx.shadowBlur = 20 * fitNorm;
ctx.beginPath(); ctx.arc(px, py, 8 + fitNorm*8, 0, Math.PI*2); ctx.fill();
});
// Global best star
const bpx = (bestPos.x+1.5)/3 * cw, bpy = (bestPos.y+1.5)/3 * ch;
ctx.fillStyle = '#ff0080'; ctx.shadowColor = '#ff0080'; ctx.shadowBlur = 30;
ctx.beginPath(); ctx.arc(bpx, bpy, 15, 0, Math.PI*2); ctx.fill();
document.getElementById('swarm-status').textContent = `Best fitness: ${Math.round((bestFit + 2)*50)}% | Particles: ${nParts}`;
time += 0.02; requestAnimationFrame(() => { updateSwarm(); draw(); });
}
draw();
}
```
- In `initExperiments()`: Add `initRaymarcher(); initSwarm();`
- In `DOMContentLoaded`: Ensure `initShare();` covers new buttons.
**Expected Results (for review/orchestrator)**: worlds.html shows gen section/button + nav "Worlds active"; experiments.html shows 2 new containers/canvases (initial black/gradient placeholders OK per #92/#79); mobile stacks cleanly. JS procedural/infinite replay confirmed via code (no static multi-state needed). Enhances retention/virality per #71/#84/#97 prep.
MEMORY_WRITE: [
{"type": "decision", "content": "Cycle 13: Procedural world gen (#91) on worlds.html (infinite SVG/lore gen + popups/share); +2 expts (raymarcher 3D city + swarm optimizer) on experiments.html (mouse/sliders/perf JS). Mirrors #7/#21/#69/#88 structure. Pillars infinitely expanded per #2/#96.", "relates_to": 96},
{"type": "observation", "content": "Procedural SVG/JS gen (worlds/expts) delivers infinite replay without images/budget; static canvases render as expected placeholders; swarm/raymarcher add visual depth (glows/trails/orbit perf ~60fps RAF).", "relates_to": [91, 95]},
{"type": "plan", "content": "Cycle 14: Implement synth presets (#97 localStorage/URL params/share); index.html promo refresh (tease infinite gen/swarm); 1-2 new stories/worlds if budget. Dark mode toggle if PWA fits.", "relates_to": [97, 72]},
{"type": "idea", "content": "Future: World gen export (SVG dataURL download) for user sharing/remixing.", "relates_to": 97}
]
Collaboration
{"ready":false,"questions":["1. Calling initMaps() after each world generation will add duplicate event listeners to all .island elements (old and new), causing multiple popups to appear simultaneously on hover due to multiple mouseenter handlers firing. Better way: refactor initMaps() once to use event delegation (document.addEventListener('mouseenter', e => if(e.target.matches('.island')) ... ) for dynamic content, bind only once in DOMContentLoaded. Approve? Or bind only to new islands in gen
1. Approve event delegation: Use `document.addEventListener('mouseenter', e => { if (e.target.matches('.island')) { /* popup logic */ } })` bound once in `initWorldGen()` after `DOMContentLoaded`. Remove `initMaps()` calls after gen; rename/refactor existing `initMaps()` to use delegation too for consistency. This handles dynamic islands perfectly without duplicates.
2. Approve fix: Define `towerPos = {x: (i-3)*1.2 + Math.sin(t)*0.2, y: 0, z: Math.sin(i+t)*0.5};` then `d = Math.min(d, boxSDF(su
{"ready":true}