62 lines
5.0 KiB
JavaScript
62 lines
5.0 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 add=(tag,attrs,text)=>{const e=node(tag,attrs,text);svg.append(e);return e;};
|
|
const label=(x,y,text)=>add('text',{x,y,'text-anchor':'middle'},text);
|
|
const 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);}
|
|
} 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);
|
|
} 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');
|
|
} else if(c.id==='assemble-explode') {
|
|
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:400},c.duration)
|
|
.add(part,{translateX:[x,0],duration:c.duration},c.duration+400);
|
|
});
|
|
}
|
|
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 controllers={};
|
|
for(const config of window.animeExamples){const host=document.createElement('article');host.id=config.id;document.querySelector('main').append(host);controllers[config.id]=mount(host,config);}
|
|
window.animeGallery={controllers,mount};
|
|
})();
|