Skip to content

Commit 787f010

Browse files
oiseeclaude
andcommitted
renderer: animation playback + budget search presets + GitHub Pages
- docs/renderer.html: animation_flat format support, frame counter, frame-pause slider, 6 new presets (Che anim + 3 budget configs) - data/che_anim_flat.json: 25-frame delta animation (16,114 seeds, 0.07% avg) - data/budget_conf_{a,b,c}.json: budget-constrained animations (10 frames) - cuda/prng_budget_search.cu: configurable budget+shrink+center search - media/prng_images/anim_canonical/: canonical animation snapshot + README - .github/workflows/pages.yml: deploy docs/ to GitHub Pages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 435a1d5 commit 787f010

87 files changed

Lines changed: 17397 additions & 6 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/pages.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Deploy to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'docs/**'
8+
- 'data/**'
9+
- '.github/workflows/pages.yml'
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
pages: write
15+
id-token: write
16+
17+
concurrency:
18+
group: pages
19+
cancel-in-progress: true
20+
21+
jobs:
22+
deploy:
23+
environment:
24+
name: github-pages
25+
url: ${{ steps.deployment.outputs.page_url }}
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@v4
29+
- uses: actions/configure-pages@v5
30+
- uses: actions/upload-pages-artifact@v3
31+
with:
32+
path: docs
33+
- id: deployment
34+
uses: actions/deploy-pages@v4

data/budget_conf_a.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

data/budget_conf_b.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

data/budget_conf_c.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

data/che_anim_flat.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

docs/anim_delta_gallery.png

505 KB
Loading

docs/budget_gallery.png

276 KB
Loading

docs/renderer.html

Lines changed: 214 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ <h3>stats</h3>
8787
<div class="stat"><div class="val" id="st-on">0</div><div class="lbl">active</div></div>
8888
<div class="stat"><div class="val" id="st-and"></div><div class="lbl">AND-N</div></div>
8989
<div class="stat"><div class="val" id="st-lbl"></div><div class="lbl">layer</div></div>
90+
<div class="stat" id="st-frame-box" style="display:none"><div class="val" id="st-frame">1/1</div><div class="lbl">frame</div></div>
9091
</div>
9192
</div>
9293

@@ -111,6 +112,28 @@ <h3>playback</h3>
111112
<input type="range" id="speed" min="1" max="300" value="30" style="flex:1"
112113
oninput="stepsPerSec=+this.value;document.getElementById('spd-lbl').textContent=this.value+'/s'">
113114
</div>
115+
<div id="anim-controls" style="display:flex;align-items:center;gap:6px;margin-top:4px;font-size:10px;color:#555">
116+
frame pause <span id="fp-lbl">300ms</span>
117+
<input type="range" id="frame-pause" min="0" max="2000" value="300" style="flex:1"
118+
oninput="framePauseMs=+this.value;document.getElementById('fp-lbl').textContent=this.value+'ms'">
119+
</div>
120+
</div>
121+
122+
<div class="panel">
123+
<h3>export GIF</h3>
124+
<div style="display:flex;flex-direction:column;gap:5px">
125+
<div style="display:flex;gap:5px;align-items:center;font-size:10px;color:#777">
126+
FPS <input type="number" id="gif-fps" value="10" min="1" max="60"
127+
style="width:38px;background:#111;border:1px solid #333;color:#ccc;padding:2px 4px;font-size:10px">
128+
step <input type="number" id="gif-step" value="5" min="1" max="100"
129+
style="width:38px;background:#111;border:1px solid #333;color:#ccc;padding:2px 4px;font-size:10px">
130+
loop <input type="number" id="gif-loop" value="0" min="0" max="99"
131+
style="width:32px;background:#111;border:1px solid #333;color:#ccc;padding:2px 4px;font-size:10px"
132+
title="0=infinite">
133+
</div>
134+
<button onclick="startGifExport()" id="gif-btn">Export GIF</button>
135+
<div id="gif-status" style="font-size:9px;color:#555;min-height:12px"></div>
136+
</div>
114137
</div>
115138

116139
<div class="panel">
@@ -152,6 +175,7 @@ <h3>seeds <span style="color:#3a3a3a;font-size:9px">click = toggle one</span></h
152175
const W=128, H=96;
153176
let seeds=[], bufs=[], active=[], pix=new Uint8Array(W*H);
154177
let cursor=0, snapshots={}, playing=false, stepsPerSec=30;
178+
let animMode=false, frameStarts=[], framePauseMs=300;
155179

156180
// ── canvas ────────────────────────────────────────────────────────────────────
157181
const cv=document.getElementById('cv'), ctx=cv.getContext('2d');
@@ -193,15 +217,31 @@ <h3>seeds <span style="color:#3a3a3a;font-size:9px">click = toggle one</span></h
193217
cursor=n; updateUI(); redraw();
194218
}
195219
function stepBy(d){jumpTo(cursor+d);}
220+
function jumpToFrame(fi){if(animMode&&fi>=0&&fi<frameStarts.length)jumpTo(frameStarts[fi]);}
196221
function togglePlay(){
197222
playing=!playing;
198223
document.getElementById('btn-play').classList.toggle('on',playing);
199224
document.getElementById('btn-play').textContent=playing?'⏸':'▶';
200225
if(playing) tick();
201226
}
227+
function currentFrame(){
228+
if(!animMode||!frameStarts.length) return -1;
229+
let f=0;
230+
for(let i=0;i<frameStarts.length;i++) if(cursor>=frameStarts[i]) f=i;
231+
return f;
232+
}
202233
function tick(){
203234
if(!playing) return;
204235
if(cursor>=seeds.length){togglePlay();return;}
236+
// In anim mode: when crossing a frame boundary, pause briefly then continue
237+
if(animMode && frameStarts.length>1 && cursor>0){
238+
const nextFrameIdx=frameStarts.findIndex(s=>s===cursor);
239+
if(nextFrameIdx>0){
240+
updateUI(); redraw();
241+
setTimeout(tick, framePauseMs); // inter-frame pause
242+
return;
243+
}
244+
}
205245
applyBuf(pix,bufs[cursor],seeds[cursor].ox,seeds[cursor].oy,seeds[cursor].blk);
206246
active[cursor]=true; cursor++;
207247
updateUI(); redraw();
@@ -210,17 +250,37 @@ <h3>seeds <span style="color:#3a3a3a;font-size:9px">click = toggle one</span></h
210250

211251
// ── load ──────────────────────────────────────────────────────────────────────
212252
function loadSeeds(data, presetId){
213-
seeds=data.seeds||data;
253+
// Handle animation_flat format: normalize short field names
254+
animMode = data.type==='animation_flat' || data.type==='animation';
255+
frameStarts = animMode ? (data.frame_starts||[]) : [];
256+
257+
let rawSeeds = [];
258+
if(animMode && data.type==='animation_flat'){
259+
// flat animation: seeds have {f,s,ox,oy,b,n,w}
260+
rawSeeds = (data.seeds||[]).map(r=>({
261+
seed:r.s, ox:r.ox, oy:r.oy, blk:r.b, and_n:r.n, warmup:r.w,
262+
frame:r.f, label:'F'+r.f+'-AND'+r.n, step:0
263+
}));
264+
} else if(animMode && data.frames){
265+
// nested animation: flatten all frames' seeds
266+
rawSeeds=[];
267+
for(const fr of data.frames) for(const s of fr.seeds) rawSeeds.push(s);
268+
} else {
269+
rawSeeds = data.seeds||data;
270+
}
271+
272+
seeds = rawSeeds;
214273
bufs=seeds.map(r=>makeBuf(r.seed,r.warmup,r.and_n));
215274
active=new Array(seeds.length).fill(false);
216275
pix=new Uint8Array(W*H); cursor=0;
217276
snapshots={0:pix.slice()};
218277
let s=pix.slice();
219278
for(let i=0;i<seeds.length;i++){
220279
applyBuf(s,bufs[i],seeds[i].ox,seeds[i].oy,seeds[i].blk);
221-
if((i+1)%50===0||i===seeds.length-1) snapshots[i+1]=s.slice();
280+
if((i+1)%100===0||i===seeds.length-1) snapshots[i+1]=s.slice();
222281
}
223282
document.getElementById('scrubber').max=seeds.length;
283+
document.getElementById('st-frame-box').style.display=animMode?'':'none';
224284
// mark active preset
225285
document.querySelectorAll('.preset-btn').forEach(b=>b.classList.remove('active'));
226286
if(presetId) document.getElementById('pb-'+presetId)?.classList.add('active');
@@ -279,6 +339,10 @@ <h3>seeds <span style="color:#3a3a3a;font-size:9px">click = toggle one</span></h
279339
document.getElementById('st-and').textContent=r?'AND-'+r.and_n:'—';
280340
document.getElementById('st-lbl').textContent=r?r.label.replace(/L3p\d+-/,''):'—';
281341
document.getElementById('scrubber').value=cursor;
342+
if(animMode && frameStarts.length){
343+
const fi=currentFrame();
344+
document.getElementById('st-frame').textContent=(fi+1)+'/'+frameStarts.length;
345+
}
282346
updateStats(); updateLevelBtns();
283347
// seed list scroll
284348
const prev=document.querySelector('.seed-row.active');
@@ -291,11 +355,21 @@ <h3>seeds <span style="color:#3a3a3a;font-size:9px">click = toggle one</span></h
291355

292356
// ── presets ───────────────────────────────────────────────────────────────────
293357
const PRESETS=[
294-
{id:'cascade', label:'Cascade AND-3→7', meta:'1171 seeds · 1.2% @1205',
358+
{id:'foveal', label:'Foveal AND-3→7 ★', meta:'939 seeds · 0.06% @1209 · CUDA 28s',
359+
paths:['../data/foveal_cascade_seeds.json','data/foveal_cascade_seeds.json']},
360+
{id:'che_anim',label:'Che animation 🎬', meta:'25 frames · 16,114 seeds · 0.07% avg · delta',
361+
paths:['../data/che_anim_flat.json','data/che_anim_flat.json']},
362+
{id:'bgt_a',label:'Budget A · KF256/DT128', meta:'10 frames · 1384 seeds · shrink=0.90 · 23%→14%',
363+
paths:['../data/budget_conf_a.json','data/budget_conf_a.json']},
364+
{id:'bgt_b',label:'Budget B · KF128/DT64', meta:'10 frames · 704 seeds · shrink=0.70 · vignette',
365+
paths:['../data/budget_conf_b.json','data/budget_conf_b.json']},
366+
{id:'bgt_c',label:'Budget C · KF512/DT256', meta:'10 frames · 1381 seeds · shrink=0.85 · 14%',
367+
paths:['../data/budget_conf_c.json','data/budget_conf_c.json']},
368+
{id:'cascade', label:'Cascade AND-3→7', meta:'1171 seeds · 1.2% @1205',
295369
paths:['../data/cascade_seeds.json','data/cascade_seeds.json']},
296-
{id:'and4', label:'AND-4 flat', meta:'~187 seeds · 25% @213',
370+
{id:'and4', label:'AND-4 flat', meta:'~187 seeds · 25% @213',
297371
paths:['../data/flat_and4_seeds.json','data/flat_and4_seeds.json']},
298-
{id:'and7', label:'AND-7 flat', meta:'1131 seeds · 4.3% @1205',
372+
{id:'and7', label:'AND-7 flat', meta:'1131 seeds · 4.3% @1205',
299373
paths:['../data/flat_and7_seeds.json','data/flat_and7_seeds.json']},
300374
];
301375

@@ -338,7 +412,141 @@ <h3>seeds <span style="color:#3a3a3a;font-size:9px">click = toggle one</span></h
338412
});
339413

340414
buildPresets();
341-
fetchPreset(PRESETS[0]); // auto-load cascade
415+
fetchPreset(PRESETS[0]); // auto-load foveal
416+
417+
// ── GIF export ────────────────────────────────────────────────────────────────
418+
// Inline LZW + GIF89a encoder — no external libraries needed.
419+
420+
function lzwEncode(data, minCodeSize) {
421+
const clearCode = 1 << minCodeSize;
422+
const eoi = clearCode + 1;
423+
let codeSize = minCodeSize + 1;
424+
let nextCode = eoi + 1;
425+
const table = new Map();
426+
const bits = []; let buf = 0, nbits = 0;
427+
428+
const initTable = () => {
429+
table.clear();
430+
for (let i = 0; i < clearCode; i++) table.set(''+i, i);
431+
codeSize = minCodeSize + 1; nextCode = eoi + 1;
432+
};
433+
const emit = (code) => {
434+
buf |= code << nbits; nbits += codeSize;
435+
while (nbits >= 8) { bits.push(buf & 0xFF); buf >>= 8; nbits -= 8; }
436+
};
437+
438+
initTable(); emit(clearCode);
439+
let prefix = '';
440+
for (let i = 0; i < data.length; i++) {
441+
const px = data[i];
442+
const key = prefix === '' ? ''+px : prefix+','+px;
443+
if (table.has(key)) { prefix = key; continue; }
444+
emit(table.get(prefix === '' ? ''+px : prefix));
445+
table.set(key, nextCode++);
446+
prefix = ''+px;
447+
if (nextCode > (1 << codeSize) && codeSize < 12) codeSize++;
448+
if (nextCode > 4094) { emit(clearCode); initTable(); }
449+
}
450+
if (prefix !== '') emit(table.get(prefix));
451+
emit(eoi);
452+
if (nbits > 0) bits.push(buf & 0xFF);
453+
return bits;
454+
}
455+
456+
function buildGIF(frames, delayCs, loopCount) {
457+
// frames: array of Uint8Array(W*H) with values 0|1
458+
// delayCs: delay in centiseconds per frame
459+
const b = [];
460+
const u16le = (v) => [v & 0xFF, v >> 8];
461+
const push = (v) => b.push(v);
462+
const seq = (arr) => arr.forEach(v => b.push(v));
463+
464+
// Header + Logical Screen Descriptor
465+
seq([0x47,0x49,0x46,0x38,0x39,0x61]); // GIF89a
466+
seq(u16le(W)); seq(u16le(H));
467+
push(0x80); push(0); push(0); // GCT flag, bg=0, aspect=0
468+
// Global Color Table: index 0 = black, index 1 = white
469+
seq([0,0,0, 255,255,255]);
470+
471+
// Netscape loop extension
472+
seq([0x21,0xFF,0x0B]);
473+
seq([78,69,84,83,67,65,80,69,50,46,48]); // NETSCAPE2.0
474+
push(3); push(1); seq(u16le(loopCount)); push(0);
475+
476+
for (const frame of frames) {
477+
// Graphic Control Extension
478+
seq([0x21,0xF9,0x04, 0x00]); seq(u16le(delayCs)); push(0); push(0);
479+
// Image Descriptor
480+
push(0x2C); seq(u16le(0)); seq(u16le(0)); seq(u16le(W)); seq(u16le(H)); push(0);
481+
// Image Data (LZW min code size = 2 for 2-color GIF)
482+
const lzw = lzwEncode(frame, 2);
483+
push(2); // minimum code size
484+
for (let i = 0; i < lzw.length; ) {
485+
const chunk = Math.min(255, lzw.length - i);
486+
push(chunk);
487+
for (let j = 0; j < chunk; j++) push(lzw[i++]);
488+
}
489+
push(0); // block terminator
490+
}
491+
push(0x3B); // GIF trailer
492+
return new Uint8Array(b);
493+
}
494+
495+
function captureFrame(inv) {
496+
const f = new Uint8Array(W * H);
497+
for (let i = 0; i < W*H; i++) f[i] = inv ? 1 - pix[i] : pix[i];
498+
return f;
499+
}
500+
501+
async function startGifExport() {
502+
if (!seeds.length) { alert('Load a preset first'); return; }
503+
const fps = Math.max(1, Math.min(60, +document.getElementById('gif-fps').value || 10));
504+
const step = Math.max(1, Math.min(100,+document.getElementById('gif-step').value || 5));
505+
const loop = Math.max(0, +document.getElementById('gif-loop').value || 0);
506+
const inv = document.getElementById('chk-inv').checked;
507+
const delayCs = Math.round(100 / fps);
508+
const status = document.getElementById('gif-status');
509+
const btn = document.getElementById('gif-btn');
510+
511+
btn.disabled = true;
512+
const frames = [];
513+
const savedCursor = cursor;
514+
515+
// Frame 0: blank canvas
516+
const blank = new Uint8Array(W * H);
517+
frames.push(inv ? blank.map(v => 1 - v) : blank.slice());
518+
519+
// Step through seeds
520+
let s = new Uint8Array(W * H);
521+
for (let i = 0; i < seeds.length; i++) {
522+
applyBuf(s, bufs[i], seeds[i].ox, seeds[i].oy, seeds[i].blk);
523+
if ((i + 1) % step === 0 || i === seeds.length - 1) {
524+
const f = new Uint8Array(W * H);
525+
for (let j = 0; j < W*H; j++) f[j] = inv ? 1 - s[j] : s[j];
526+
frames.push(f);
527+
if (frames.length % 10 === 0) {
528+
status.textContent = `encoding… ${i+1}/${seeds.length} steps, ${frames.length} frames`;
529+
await new Promise(r => setTimeout(r, 0)); // yield to UI
530+
}
531+
}
532+
}
533+
534+
// Restore display
535+
jumpTo(savedCursor);
536+
537+
status.textContent = `building GIF (${frames.length} frames)…`;
538+
await new Promise(r => setTimeout(r, 0));
539+
540+
const gif = buildGIF(frames, delayCs, loop);
541+
const blob = new Blob([gif], { type: 'image/gif' });
542+
const url = URL.createObjectURL(blob);
543+
const a = document.createElement('a');
544+
a.href = url; a.download = 'cascade_animation.gif'; a.click();
545+
URL.revokeObjectURL(url);
546+
547+
status.textContent = `done: ${frames.length} frames, ${(gif.length/1024).toFixed(0)} KB`;
548+
btn.disabled = false;
549+
}
342550
</script>
343551
</body>
344552
</html>

0 commit comments

Comments
 (0)