Spaces:
Running
Running
File size: 21,935 Bytes
ad91e86 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 | /* Illustrative fixtures only: this is not a live model or benchmark replay. */
(() => {
'use strict';
const root = document.querySelector('#world-explorer');
if (!root) return;
const $ = id => root.querySelector(`#ex-${id}`);
const scenes = {
"living": {
"name": "Living & kitchen",
"sceneId": "104348511_171513654",
"image": "assets/explorer/hssd_104348511_topdown.png",
"imageSize": [
1300,
1500
],
"crop": [
120,
100,
1080,
1080
],
"object": "mug",
"id": "023",
"appearance": "Illustrative ceramic mug",
"locations": [
"Dining table",
"Coffee table",
"Kitchen island",
"Unknown"
],
"prior": [
0.15,
0.65,
0.1,
0.1
],
"relation": "Supported by dining table",
"pattern": "In this illustrative routine, a used mug is likely to move toward the living area. The coffee table is the first inspection hypothesis.",
"activity": "Daily use may move the target between surfaces.",
"recovered": "Kitchen island",
"shape": "mug",
"anchors": [
[
1030,
870
],
[
686,
408
],
[
487,
985
]
],
"start": [
361,
661
],
"routes": [
[
[
361,
661
],
[
468,
457
]
],
[
[
468,
457
],
[
528.5,
695.3
],
[
719.1,
870.6
],
[
788,
1007
]
]
]
},
"dining": {
"name": "Dining & kitchen",
"sceneId": "102344280",
"image": "assets/explorer/hssd_102344280_topdown.png",
"imageSize": [
1400,
900
],
"crop": [
240,
170,
1150,
585
],
"object": "bowl",
"id": "041",
"appearance": "Illustrative ceramic bowl",
"locations": [
"Center table",
"Dining table",
"Kitchen sink",
"Unknown"
],
"prior": [
0.15,
0.65,
0.1,
0.1
],
"relation": "Supported by center table",
"pattern": "In this illustrative routine, a bowl is likely to move to the dining table. A fresh inspection tests that hypothesis.",
"activity": "Daily use may move the target between surfaces.",
"recovered": "Kitchen sink",
"shape": "bowl",
"anchors": [
[
1040,
470
],
[
493,
423
],
[
945.4,
644.1
]
],
"start": [
720,
310
],
"routes": [
[
[
720,
310
],
[
650,
429
]
],
[
[
650,
429
],
[
926.9,
558.3
]
]
]
}
};
let shown = [0.15,0.65,0.10,0.10], beliefFrame = 0, story = false, storyRun = 0;
const initial = [.96,.02,.01,.01];
let key = 'living', hours = 48, stage = 'predict', busy = false, generation = 0;
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
const delay = ms => new Promise(resolve => setTimeout(resolve, reduced.matches ? 0 : ms));
function beliefs() {
const s = scenes[key], t = hours / 48;
let p = initial.map((v,i) => v * (1-t) + s.prior[i] * t);
if (stage === 'updated' || stage === 'found') {
// P(not detected | candidate): 95% visibility at the inspected location.
p = p.map((v,i) => v * (i === 1 ? .05 : 1));
const z = p.reduce((a,b) => a+b,0); p = p.map(v => v/z);
}
if (stage === 'found') {
// Positive detection: high likelihood at the recovered location, small false-positive likelihood elsewhere.
p = p.map((v,i) => v * (i === 2 ? .98 : .002));
const z = p.reduce((a,b) => a+b,0); p = p.map(v => v/z);
}
return p;
}
function percentages(p) {
const values=p.map(v=>Math.floor(v*100));
const order=p.map((v,i)=>({i,r:v*100-values[i]})).sort((a,b)=>b.r-a.r);
const remaining=100-values.reduce((a,b)=>a+b,0);
for(let i=0;i<remaining;i++) values[order[i].i]++;
return values;
}
function mapLayout() {
const [x,y,w,h]=scenes[key].crop, scale=Math.min(660/w,500/h);
return {x,y,w,h,scale,left:(720-w*scale)/2,top:(600-h*scale)/2};
}
function mapPoint([x,y]) {
const m=mapLayout();return [m.left+(x-m.x)*m.scale,m.top+(y-m.y)*m.scale];
}
// A small articulated 3D model projected into the same coordinates as the room.
// Diagonal leg pairs share a trot phase; heading follows the path tangent.
let robotHeading = -Math.PI / 2;
function quadruped(heading, phase=0, walking=false) {
const c=Math.cos(heading),s=Math.sin(heading);
const bob=walking?Math.cos(phase*2)*.65:0;
const point=([x,y,z])=>[(x*c-y*s-(x*s+y*c))*.82,(x*c-y*s+x*s+y*c)*.45-z];
const coord=p=>point(p).map(n=>n.toFixed(2)).join(',');
const poly=(v,fill,stroke='#8d8d8d')=>`<polygon points="${v.map(coord).join(' ')}" fill="${fill}" stroke="${stroke}" stroke-width=".65" stroke-linejoin="round"/>`;
function chassis(x,y,z,w,d,h,top,side) {
const faces=[
{v:[[x,y,z],[x+w,y,z],[x+w,y,z+h],[x,y,z+h]],fill:side},
{v:[[x+w,y,z],[x+w,y+d,z],[x+w,y+d,z+h],[x+w,y,z+h]],fill:side},
{v:[[x+w,y+d,z],[x,y+d,z],[x,y+d,z+h],[x+w,y+d,z+h]],fill:side},
{v:[[x,y+d,z],[x,y,z],[x,y,z+h],[x,y+d,z+h]],fill:side}
].sort((a,b)=>a.v.reduce((n,p)=>n+point([p[0],p[1],0])[1],0)-b.v.reduce((n,p)=>n+point([p[0],p[1],0])[1],0));
return faces.map(f=>poly(f.v,f.fill)).join('')+poly([[x,y,z+h],[x+w,y,z+h],[x+w,y+d,z+h],[x,y+d,z+h]],top,'#d4d4d4');
}
const legs=[[-18,-12,0],[18,-12,Math.PI],[-18,12,Math.PI],[18,12,0]].map(([x,y,offset])=>{
const cycle=phase+offset,stride=walking?Math.sin(cycle)*6:0,lift=walking?Math.max(0,Math.cos(cycle))*5:0;
const hip=[x,y,24+bob],knee=[x-5+stride*.4,y*1.1,13+lift*.4],foot=[x+stride+2,y*1.2,2+lift];
const h=point(hip),k=point(knee),f=point(foot),near=point([x,y,0])[1];
return {near,html:`<g class="ex-robot-leg"><path d="M${coord(hip)}L${coord(knee)}L${coord(foot)}" fill="none" stroke="#1c1c1c" stroke-width="5.5" stroke-linejoin="round" stroke-linecap="round"/><path d="M${coord(hip)}L${coord(knee)}" stroke="#c5c5c5" stroke-width="3.4"/><path d="M${coord(knee)}L${coord(foot)}" stroke="#909090" stroke-width="2.5"/><circle cx="${h[0]}" cy="${h[1]}" r="3.5" fill="#4d4d4d" stroke="#dedede" stroke-width=".8"/><circle cx="${k[0]}" cy="${k[1]}" r="2.4" fill="#555" stroke="#bdbdbd" stroke-width=".7"/><ellipse cx="${f[0]}" cy="${f[1]}" rx="3.6" ry="1.9" fill="#171717" stroke="#8b8b8b" stroke-width=".5"/></g>`};
}).sort((a,b)=>a.near-b.near);
let body=chassis(-25,-11,22+bob,50,22,11,'url(#ex-robot-metal)','#707070');
body+=chassis(-14,-7,33+bob,26,14,2,'#3c3c3c','#555');
body+=chassis(24,-8,24+bob,6,16,7,'#8c8c8c','#292929');
// Forward stereo cameras, top lidar, side panel and restrained status light.
for(const y of [-4,4]) {const p=point([30,y,28+bob]);body+=`<circle cx="${p[0]}" cy="${p[1]}" r="1.8" fill="#0c0c0c" stroke="#dfdfdf" stroke-width=".8"/>`;}
const lidar=point([9,0,38+bob]);body+=`<ellipse cx="${lidar[0]}" cy="${lidar[1]}" rx="5" ry="3.5" fill="#2a2a2a" stroke="#d3d3d3" stroke-width="1"/>`;
for(let i=0;i<4;i++)body+=`<path d="M${coord([-10+i*4,-4,35+bob])}L${coord([-10+i*4,4,35+bob])}" stroke="#858585" stroke-width=".8"/>`;
return `<ellipse cy="2" rx="32" ry="12" fill="#090909" opacity=".35"/>${legs.slice(0,2).map(l=>l.html).join('')}${body}${legs.slice(2).map(l=>l.html).join('')}`;
}
function poseRobot(phase=0,walking=false) {$('agent').innerHTML=quadruped(robotHeading,phase,walking);}
function robotState(text,mode='idle') {
$('robot-status').textContent=text;root.dataset.robotState=mode;
}
function buildScene() {
const s=scenes[key],m=mapLayout();
$('task').textContent=`Find the ${s.object}`;
const overlays=s.anchors.map((point,i)=>{
const [a,b]=mapPoint(point),labelX=Math.min(648,Math.max(72,a)),labelY=b-44;
return `<g><ellipse id="ex-halo-${i}" class="ex-halo" cx="${a}" cy="${b}" rx="55" ry="36" fill="url(#ex-glow)"/><circle cx="${a}" cy="${b}" r="17" fill="none" stroke="#fff" stroke-opacity=".65" stroke-dasharray="2 4"/><circle cx="${a}" cy="${b}" r="3" fill="#fff"/><path d="M${a} ${b-18}L${labelX} ${labelY+13}" stroke="#ddd" stroke-width="1"/><rect class="ex-location-chip" x="${labelX-66}" y="${labelY-24}" width="132" height="43" rx="6"/><text class="ex-location-label" x="${labelX}" y="${labelY-7}">${s.locations[i]}</text><text class="ex-probability" id="ex-prob-${i}" x="${labelX}" y="${labelY+10}"></text></g>`;
}).join('');
$('map').setAttribute('viewBox','0 0 720 600');
$('map').innerHTML=`<title id="ex-map-title">HSSD ${s.sceneId}: ${s.name}</title><desc id="ex-map-desc"></desc><defs><linearGradient id="ex-robot-metal" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#f5f5f5"/><stop offset=".45" stop-color="#bcbcbc"/><stop offset="1" stop-color="#ececec"/></linearGradient><radialGradient id="ex-glow"><stop stop-color="#fff" stop-opacity=".65"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient></defs><g class="ex-room-enter"><rect x="${m.left-1}" y="${m.top-1}" width="${m.w*m.scale+2}" height="${m.h*m.scale+2}" rx="2" fill="#181818" stroke="#666"/><svg class="ex-captured-scene" style="width:${m.w*m.scale}px;height:${m.h*m.scale}px;overflow:hidden;aspect-ratio:auto" x="${m.left}" y="${m.top}" width="${m.w*m.scale}" height="${m.h*m.scale}" viewBox="${s.crop.join(' ')}"><image href="${s.image}" width="${s.imageSize[0]}" height="${s.imageSize[1]}"/></svg><path id="ex-route-shadow" fill="none" stroke="#171717" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" opacity="0"/><path id="ex-route" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" opacity="0"/>${overlays}<g id="ex-agent" aria-label="Illustrative quadruped inspection robot"></g><g id="ex-scan"><ellipse class="ex-scan" rx="45" ry="30"/></g><g id="ex-object"><circle r="12" fill="#eee" stroke="#292929" stroke-width="1.5"/><path d="M-4-5H3V4H-4Z M3-3H6V1H3" fill="none" stroke="#525252" stroke-width="1.5"/></g></g>`;
$('distribution').innerHTML=s.locations.map((name,i)=>`<div class="ex-bar" id="ex-bar-${i}"><div class="ex-bar-label"><span>${name}</span><strong id="ex-value-${i}"></strong></div><div class="ex-track"><i id="ex-fill-${i}"></i></div></div>`).join('');
if(s.shape==='bowl')$('object').innerHTML='<circle r="12" fill="#eee" stroke="#292929" stroke-width="1.5"/><path d="M-7-2Q-6 7 0 7Q6 7 7-2Z" fill="#999" stroke="#333" stroke-width="1.2"/>';
$('robot-preview').innerHTML=quadruped(-.35);$('scene-source').textContent=`HSSD ${s.sceneId}`;$('scene-source').href=s.image;
shown=beliefs();
root.querySelectorAll('[data-scene]').forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.scene===key)));
}
function render() {
if(!busy)robotState(stage==='found'?'Target confirmed':stage==='updated'?'Ready to replan':'Standing by',stage==='found'?'success':'idle');
const s=scenes[key], p=beliefs(), pc=percentages(p), leader=p.indexOf(Math.max(...p));
animateBelief(p);
$('map-clock').textContent=`DAY ${String(1+Math.floor((9+hours)/24)).padStart(2,'0')} / ${String((9+hours)%24).padStart(2,'0')}:00`;
$('map-desc').textContent=`HSSD ${s.sceneId}; captured scene with illustrative beliefs and robot. ${s.locations.map((n,i)=>`${n}: ${pc[i]}%`).join(', ')}. ${stage==='found'?'Object recovered.':hours===0?'Object directly observed.':'Object location unobserved.'}`;
$('time').value=hours; $('time').setAttribute('aria-valuetext',`${hours} hours since observation`); $('elapsed').textContent=hours===0?'Just observed':`${hours} hours`;
root.querySelectorAll('[data-time]').forEach(b=>b.setAttribute('aria-pressed',String(Number(b.dataset.time)===hours)));
$('phase').textContent=busy?'Inspecting':stage==='found'?'Target recovered':stage==='updated'?'Belief updated':hours===0?'Direct observation':hours<48?'Time elapsing':'Current query';
$('evidence-age').textContent=stage==='found'?'Just observed':`${hours}h since observation`;
$('evidence-source').textContent=stage==='found'?'Positive detection':stage==='updated'?'Negative inspection':'Historical observation';
$('evidence-location').textContent=stage==='found'?s.recovered:stage==='updated'?s.locations[1]:s.locations[0];
$('context').textContent=stage==='found'?`New observation · ${s.recovered} · Day 3, 09:15`: `Last seen: ${s.locations[0]} · Day 1, 09:00 · ${hours}h ago`;
$('reason-title').textContent=stage==='updated'?'VISIBILITY-AWARE UPDATE':stage==='found'?'NEW EVIDENCE, NEW MEMORY':'WHY THIS PREDICTION?';
$('explanation').textContent=stage==='updated'?`No ${s.object} detected at the ${s.locations[1].toLowerCase()}. With 95% inspection visibility, its probability falls to ${pc[1]}%. Other locations remain plausible.`:stage==='found'?`A positive observation at the ${s.recovered.toLowerCase()} raises its belief to ${pc[2]}%. The observation is recorded as a new memory version.`:hours===0?`The ${s.object} is directly observed on the ${s.locations[0].toLowerCase()}. A fresh observation anchors the belief at 96%.`:hours<48?`${s.activity} No new observation is available; uncertainty grows as the original memory ages.`:s.pattern;
$('action').disabled=busy;
$('action').innerHTML=busy?'Navigating & inspecting… <span>···</span>':stage==='found'?'Replay this scenario <span>↺</span>':stage==='updated'?`Inspect ${s.recovered.toLowerCase()} <span>→</span>`:hours<48?'Advance to current query <span>→</span>':`Inspect ${s.locations[1].toLowerCase()} <span>→</span>`;
$('action-hint').textContent=stage==='updated'?'Inspect another plausible location to resolve uncertainty.':stage==='found'?'Evidence closes the loop. Memory stays current.':'Follow the belief. Gather new evidence.';
$('object').setAttribute('transform',`translate(${(mapPoint(s.anchors[stage==='found'?2:0])).join(' ')})`);
$('object').style.opacity=hours===0||stage==='found'?'1':'0';
const active=busy?1:stage==='found'?3:stage==='updated'?2:0;
root.querySelectorAll('[data-step]').forEach(el=>{el.dataset.active=String(Number(el.dataset.step)<=active);if(Number(el.dataset.step)===active)el.setAttribute('aria-current','step');else el.removeAttribute('aria-current');});
$('status').textContent=busy?`Moving to the ${stage==='updated'?s.recovered.toLowerCase():s.locations[1].toLowerCase()} to gather evidence…`:stage==='found'?`Target found at the ${s.recovered.toLowerCase()}. The agent’s memory now contains a fresh observation.`:stage==='updated'?`${s.locations[1]} checked · target not detected · posterior renormalized across all candidates.`:hours===0?`Day 1 · ${s.object} observed on the ${s.locations[0].toLowerCase()}.`:`${hours} hours without direct observation. ${hours===48?'Inspect the leading hypothesis to see how evidence changes the belief.':'Move through time to see the belief evolve.'}`;
$('memory-name').textContent=`${s.object} · object_${s.id}`;
$('memory-content').innerHTML=`<div><h4>IDENTITY</h4><p>object_${s.id}<br>${s.appearance}<br>First observed: Day 1, 09:00</p></div><div><h4>OBSERVATION HISTORY</h4><p>Day 1 · ${s.locations[0]} · confidence 0.96<br>${hours>0?`Following ${hours}h · no direct observation`:'Fresh observation'}${stage==='updated'||stage==='found'?`<br>Day 3 · ${s.locations[1]} · negative evidence`:''}${stage==='found'?`<br>Day 3, 09:15 · ${s.recovered} · detected`:''}</p></div><div><h4>RELATIONS & PROVENANCE</h4><p>${stage==='found'?`Located at ${s.recovered}`:s.relation}<br>Scene: HSSD ${s.sceneId}<br>Beliefs and target: illustrative scenario<br>${stage==='updated'?'P(s | no detection) ∝ P(no detection | s) P(s)':'Persistent identity · versioned observations'}</p></div>`;
}
function paintBelief(values) {
shown=values;const rounded=percentages(values),leader=values.indexOf(Math.max(...values));
for(let i=0;i<4;i++) {
$(`value-${i}`).textContent=`${rounded[i]}%`;$(`fill-${i}`).style.width=`${values[i]*100}%`;$(`bar-${i}`).dataset.leader=String(i===leader);
if(i<3){$(`prob-${i}`).textContent=`${rounded[i]}%`;$(`halo-${i}`).style.opacity=.12+values[i]*.88;$(`halo-${i}`).setAttribute('rx',40+values[i]*65);$(`halo-${i}`).setAttribute('ry',22+values[i]*34);}
}
}
function animateBelief(target) {
cancelAnimationFrame(beliefFrame);
if(reduced.matches){paintBelief(target);return;}
const from=[...shown],start=performance.now();
const tick=now=>{const t=Math.min(1,(now-start)/700),ease=1-Math.pow(1-t,4);paintBelief(from.map((v,i)=>v+(target[i]-v)*ease));if(t<1)beliefFrame=requestAnimationFrame(tick);};
beliefFrame=requestAnimationFrame(tick);
}
function stopStory(){storyRun++;story=false;$('play').textContent='▶ Start demonstration';$('play').setAttribute('aria-pressed','false');}
function reset(time=48, keepStory=false) {
if(!keepStory)stopStory();
generation++;busy=false;stage='predict';hours=time;
robotHeading=-Math.PI/2;poseRobot();
$('route').style.opacity=0;$('route-shadow').style.opacity=0;$('agent').setAttribute('transform',`translate(${mapPoint(scenes[key].start).join(' ')})`);
$('scan').querySelector('ellipse').classList.remove('scanning');render();
}
function travel(path,token) {
return new Promise(resolve=>{
const route=$('route');route.setAttribute('d',path);route.style.opacity=1;
const shadow=$('route-shadow');shadow.setAttribute('d',path);shadow.style.opacity='.75';
const length=route.getTotalLength();shadow.style.strokeDasharray=length;shadow.style.strokeDashoffset=length;route.style.strokeDasharray=length;route.style.strokeDashoffset=length;
const start=performance.now(),duration=reduced.matches?0:2600;
robotState('Walking · route tracking','walking');
let last= start;
function frame(now){
if(token!==generation){resolve(false);return;}
const t=duration?Math.min(1,(now-start)/duration):1;
const ease=t<.5?2*t*t:1-Math.pow(-2*t+2,2)/2;
const point=route.getPointAtLength(length*ease);
const before=route.getPointAtLength(Math.max(0,length*ease-.6));
const after=route.getPointAtLength(Math.min(length,length*ease+.6));
const dx=after.x-before.x,dy=after.y-before.y;
const heading=Math.atan2((dy/.45-dx/.82)/2,(dy/.45+dx/.82)/2);
const delta=Math.atan2(Math.sin(heading-robotHeading),Math.cos(heading-robotHeading));
robotHeading+=delta*(reduced.matches?1:1-Math.exp(-Math.min(60,now-last)/85));last=now;
poseRobot(length*ease*.12,t>0&&t<1&&!reduced.matches);
$('agent').setAttribute('transform',`translate(${point.x} ${point.y})`);route.style.strokeDashoffset=length*(1-ease);shadow.style.strokeDashoffset=length*(1-ease);
if(t<1)requestAnimationFrame(frame);else resolve(true);
}
requestAnimationFrame(frame);
});
}
async function inspect() {
if(busy)return;
if(stage==='found'){reset();return;}
if(hours<48){reset();return;}
const token=++generation,recovering=stage==='updated';busy=true;render();
// Keep the measured NavMesh waypoints; rounded line joins don't cut corners.
const path=scenes[key].routes[recovering?1:0].map((v,i)=>`${i?'L':'M'}${mapPoint(v).join(' ')}`).join(' ');
if(!await travel(path,token))return;
robotState('Stationary · visual scan','scanning');
$('phase').textContent='Scanning';$('action').innerHTML='Acquiring visual evidence… <span>◌</span>';
$('status').textContent=`Inspecting the ${recovering?scenes[key].recovered.toLowerCase():scenes[key].locations[1].toLowerCase()} · checking target visibility.`;
$('scan').setAttribute('transform',`translate(${mapPoint(scenes[key].anchors[recovering?2:1]).join(' ')})`);
$('scan').querySelector('ellipse').classList.add('scanning');
await delay(1100);if(token!==generation)return;
$('scan').querySelector('ellipse').classList.remove('scanning');stage=recovering?'found':'updated';busy=false;render();
}
root.querySelectorAll('[data-scene]').forEach(b=>b.addEventListener('click',()=>{key=b.dataset.scene;stage='predict';hours=48;buildScene();reset();}));
root.querySelectorAll('[data-time]').forEach(b=>b.addEventListener('click',()=>reset(Number(b.dataset.time))));
$('time').addEventListener('input',e=>reset(Number(e.target.value)));
$('reset').addEventListener('click',()=>reset());
$('inspect-memory').addEventListener('click',()=>{$('memory').open=true;$('memory').querySelector('summary').focus();});
$('action').addEventListener('click',()=>{stopStory();inspect();});
$('play').addEventListener('click',async()=>{
if(story){reset(hours);return;}
reset(0);const run=++storyRun;story=true;$('play').textContent='Ⅱ Stop demonstration';$('play').setAttribute('aria-pressed','true');
let token=generation;await delay(1000);if(!story||run!==storyRun||token!==generation)return;
// Advance historical time at a steady cadence; manual controls always cancel the story.
for(let h=1;h<=48;h++){if(!story||run!==storyRun||token!==generation)return;hours=h;render();await delay(45);}
await delay(600);if(!story||run!==storyRun||token!==generation)return;
await inspect();if(!story||run!==storyRun)return;token=generation;
await delay(1500);if(!story||run!==storyRun||token!==generation)return;
await inspect();if(story&&run===storyRun)stopStory();
});
document.addEventListener('visibilitychange',()=>{if(document.hidden&&(story||busy))reset(hours);});
buildScene();reset();root.hidden=false;
})();
|