120 lines
9.1 KiB
JavaScript
120 lines
9.1 KiB
JavaScript
/* Each mounted card owns one paused finite timeline and abortable listeners. */
|
|
(() => {
|
|
const ns='http://www.w3.org/2000/svg';
|
|
function node(tag,attrs={},text='') { const e=document.createElementNS(ns,tag); for(const [k,v] of Object.entries(attrs))e.setAttribute(k,v); e.textContent=text; return e; }
|
|
function mount(host,c) {
|
|
const abort=new AbortController(), signal=abort.signal, media=matchMedia('(prefers-reduced-motion: reduce)');
|
|
let destroyed=false;
|
|
host.innerHTML=`<h2></h2><p></p><svg viewBox="0 0 400 220" role="img"></svg><div class="controls"><button data-action="play">Play</button><button data-action="replay">Replay</button><button data-action="settle">Settle</button><label>Seek <input type="range" min="0" max="100" value="100" step="1"></label></div><output aria-live="polite">Complete</output>`;
|
|
host.querySelector('h2').textContent=c.title; host.querySelector('p').textContent=c.description;
|
|
const svg=host.querySelector('svg'); svg.setAttribute('aria-label',c.description);
|
|
const industry=c.kind==='industry';
|
|
host.classList.toggle('industry',industry);
|
|
if(industry)svg.setAttribute('viewBox','0 0 960 540');
|
|
const add=(tag,attrs={},text='',parent=svg)=>{const e=node(tag,attrs,text);parent.append(e);return e;};
|
|
const label=(x,y,text)=>add('text',{x,y,'text-anchor':'middle'},text);
|
|
let tl, recipeBeats;
|
|
const beat=(label,at)=>({label,at});
|
|
if(industry){
|
|
const renderer=window.industryRenderers?.[c.id];
|
|
if(typeof renderer!=='function')throw new Error('Missing industry renderer: '+c.id);
|
|
tl=renderer(svg,c.config,{anime,el:add});
|
|
}else tl=anime.createTimeline({autoplay:false,defaults:{ease:'linear'}});
|
|
if(c.id==='svg-draw'||c.id==='motion-path') {
|
|
add('path',{d:c.path,class:'guide'});
|
|
const path=add('path',{d:c.path,class:'trace'});
|
|
label(65,190,c.start); label(325,190,c.end);
|
|
if(c.id==='svg-draw')tl.add(anime.svg.createDrawable(path),{draw:['0 0','0 1'],duration:c.duration},0);
|
|
else {const marker=add('circle',{r:9,cx:0,cy:0,class:'packet'});tl.add(marker,{...anime.svg.createMotionPath(path),duration:c.duration},0);}
|
|
recipeBeats=(c.id==='svg-draw'?['Source','Reveal connection','Connected']:['Send','In transit','Receive']).map((name,i)=>beat(name,i*tl.duration/2));
|
|
} else if(c.id==='spatial-stagger') {
|
|
const cells=[];
|
|
for(let row=0;row<c.rows;row++)for(let col=0;col<c.columns;col++)cells.push(add('rect',{x:35+col*330/c.columns,y:30+row*160/c.rows,width:280/c.columns,height:125/c.rows,rx:5,class:'cell'}));
|
|
tl.add(cells,{opacity:[0.25,1],duration:c.duration,delay:anime.stagger(c.delay,{grid:[c.columns,c.rows],from:'center'})},0);
|
|
// Anime owns grid-distance rounding and stagger delays: use its actual end.
|
|
recipeBeats=[beat('Grid ready',0),beat('Center outward',tl.duration/2),beat('Activated',tl.duration)];
|
|
} else if(c.id==='dependency-flow') {
|
|
const a=add('path',{d:'M 85 100 L 180 100',class:'trace'}),b=add('path',{d:'M 220 100 L 315 100',class:'trace'});
|
|
[60,200,340].forEach((x,i)=>{add('circle',{cx:x,cy:100,r:25,class:'cell'});label(x,155,c.labels[i]);});
|
|
tl.label('input',0).add(anime.svg.createDrawable(a),{draw:['0 0','0 1'],duration:c.duration},'input').label('output',c.duration).add(anime.svg.createDrawable(b),{draw:['0 0','0 1'],duration:c.duration},'output');
|
|
recipeBeats=[beat('Input',0),beat('Check',tl.labels.output),beat('Output',tl.duration)];
|
|
} else if(c.id==='assemble-explode') {
|
|
const hold=400, returnAt=c.duration+hold;
|
|
c.labels.forEach((text,i)=>{
|
|
const part=add('g',{'data-part':i});
|
|
part.append(node('rect',{x:155+i*30,y:55+i*22,width:60,height:80,rx:6,class:'cell'}));
|
|
// Static labels stay outside the motion wrappers, including while assembled.
|
|
label(80+i*120,195,text);
|
|
const x=(i-1)*c.distance;
|
|
tl.add(part,{translateX:[0,x],duration:c.duration},0)
|
|
.add(part,{translateX:[x,x],duration:hold},c.duration)
|
|
.add(part,{translateX:[x,0],duration:c.duration},returnAt);
|
|
});
|
|
recipeBeats=[beat('Assembled',0),beat('Separate',c.duration),beat('Inspect hold',returnAt-hold/2),beat('Reassembled',tl.duration)];
|
|
}
|
|
if(!tl||!Number.isFinite(tl.duration)||tl.duration<=0)throw new Error('Invalid finite timeline: '+c.id);
|
|
const beats=document.createElement('div');beats.className='beats';beats.setAttribute('aria-label','Timeline beats');
|
|
// Legacy positions are computed on every mount, including edited runtime configs.
|
|
// Renderers may supply duration-derived beats; older industry descriptors stay unchanged.
|
|
for(const beat of (industry?(tl.recipeBeats||c.beats):recipeBeats)||[]){
|
|
if(!Number.isFinite(beat.at)||beat.at<0||beat.at>tl.duration)throw new Error('Beat outside timeline: '+c.id+' / '+beat.label);
|
|
const button=document.createElement('button');button.type='button';button.textContent=beat.label;button.dataset.beat=beat.at;
|
|
button.addEventListener('click',()=>seek(beat.at),{signal});beats.append(button);
|
|
}
|
|
host.append(beats);
|
|
const guidance=document.createElement('section');guidance.className='guidance';guidance.setAttribute('aria-label','Adaptation guidance');
|
|
for(const [key,title] of [['useWhen','Use when'],['avoid','Avoid'],['assumptions','Assumptions']]){
|
|
const heading=document.createElement('h3'),p=document.createElement('p');heading.textContent=title;p.textContent=c[key]||'';guidance.append(heading,p);
|
|
}
|
|
host.append(guidance);
|
|
const range=host.querySelector('input'),out=host.querySelector('output');
|
|
const sync=()=>{range.value=100*tl.currentTime/tl.duration;};
|
|
tl.onUpdate=sync;
|
|
tl.onComplete=()=>{sync();out.textContent='Complete';};
|
|
function settle(){if(destroyed)return;tl.pause();tl.seek(tl.duration);sync();out.textContent='Complete';}
|
|
function seek(ms){if(destroyed)return;if(media.matches)return settle();tl.pause();const n=Number(ms);tl.seek(Math.max(0,Math.min(tl.duration,Number.isFinite(n)?n:0)));sync();out.textContent='Paused';}
|
|
function play(restart=false){if(destroyed)return;if(media.matches)return settle();if(restart||tl.currentTime>=tl.duration)tl.seek(0);tl.play();out.textContent='Playing';}
|
|
host.querySelector('[data-action="play"]').addEventListener('click',()=>play(),{signal});
|
|
host.querySelector('[data-action="replay"]').addEventListener('click',()=>play(true),{signal});
|
|
host.querySelector('[data-action="settle"]').addEventListener('click',settle,{signal});
|
|
range.addEventListener('input',()=>seek(Number(range.value)*tl.duration/100),{signal});
|
|
media.addEventListener('change',settle,{signal});
|
|
window.addEventListener('beforeprint',settle,{signal});
|
|
document.addEventListener('visibilitychange',()=>{if(document.hidden)settle();},{signal});
|
|
window.addEventListener('pagehide',settle,{signal});
|
|
settle();
|
|
return {seek,settle,play:()=>play(),replay:()=>play(true),state:()=>({duration:tl.duration,time:tl.currentTime,paused:tl.paused,destroyed}),destroy(){if(destroyed)return;settle();abort.abort();tl.revert();destroyed=true;host.replaceChildren();}};
|
|
}
|
|
const main=document.querySelector('main'), selected=document.body.dataset.example;
|
|
if(selected !== undefined) {
|
|
const config=window.animeExamples.find(c=>c.id===selected);
|
|
if(!config)throw new Error('Unknown example ID: '+selected);
|
|
if(window.self!==window.top)document.body.classList.add('embedded');
|
|
const host=document.createElement('article');host.id=config.id;main.append(host);
|
|
window.animeGallery={controllers:{[config.id]:mount(host,config)},mount};
|
|
} else {
|
|
const group=document.body.dataset.gallery;
|
|
const groups=[...new Set(window.animeExamples.map(c=>c.group))];
|
|
if(group!=='all'&&!groups.includes(group))throw new Error('Unknown gallery: '+group);
|
|
if(group==='all'){
|
|
const nav=document.createElement('nav');nav.className='registry-nav';nav.setAttribute('aria-label','Choose a registry');
|
|
for(const name of groups){const link=document.createElement('a');link.href=name+'/index.html';link.textContent=window.animeExamples.find(c=>c.group===name).groupTitle;nav.append(link);}
|
|
main.append(nav);
|
|
}
|
|
for(const name of groups) {
|
|
if(group!=='all'&&group!==name)continue;
|
|
const section=document.createElement('section'), heading=document.createElement('h2'), link=document.createElement('a');
|
|
section.dataset.registry=name;link.href=group==='all'?name+'/index.html':'index.html';link.textContent=window.animeExamples.find(c=>c.group===name).groupTitle;heading.append(link);section.append(heading);
|
|
const grid=document.createElement('div');grid.className='preview-grid';section.append(grid);
|
|
for(const config of window.animeExamples.filter(c=>c.group===name)) {
|
|
const card=document.createElement('article'),frame=document.createElement('iframe'),open=document.createElement('a');
|
|
const href=(group==='all'?'':'../')+'examples/'+config.id+'.html';
|
|
frame.src=href;frame.title=config.title;frame.dataset.example=config.id;
|
|
open.href=href;open.textContent='Open example: '+config.title;open.className='open-example';
|
|
card.append(frame,open);grid.append(card);
|
|
}
|
|
main.append(section);
|
|
}
|
|
}
|
|
})();
|