[verified] feat: add reusable presentation-derived animation patterns
This commit is contained in:
1 parent
ecbbcf8ba0
commit
5b07a9a3e5
127 files changed
+72164
-157
No files matched your search
@@ -9,7 +9,7 @@ import re
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
source = root / 'assets/animejs'
|
||||
vendor = root / 'assets/vendor/anime.umd.min.js'
|
||||
VERSION = '0.6.0'
|
||||
VERSION = '0.7.0'
|
||||
BASELINE = {
|
||||
'oil-gas': ['separator-upset', 'pipeline-reroute'],
|
||||
'pharma': ['batch-genealogy', 'cold-chain-quarantine'],
|
||||
@@ -23,6 +23,7 @@ BASELINE = {
|
||||
'telecom': ['congestion-reroute', 'cell-handover'],
|
||||
'agriculture': ['irrigation-zones', 'harvest-grading'],
|
||||
'infrastructure': ['bridge-inspection', 'construction-resequence'],
|
||||
'presentation-patterns': ['expressive-robot', 'approval-gate', 'delivery-cycle', 'connected-memory'],
|
||||
}
|
||||
SECTORS = list(BASELINE)
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
|
||||
@@ -157,7 +157,7 @@ try{for(const variant of ['source','dist'])for(const reducedMotion of ['no-prefe
|
||||
if(variant==='dist'&&reducedMotion==='no-preference'&&(id===ids[0]||id===catalog.find(x=>x.kind==='industry')?.id))await page.waitForFunction(id=>{const s=animeGallery.controllers[id].state();return s.paused&&s.time===s.duration;},id);
|
||||
await page.locator('[data-action="settle"]').click();sameVisual(await visual(),final);
|
||||
if(variant==='dist'&&reducedMotion==='no-preference'&&(id==='assemble-explode'||id===catalog.find(x=>x.kind==='industry')?.id)){
|
||||
await page.locator('article').screenshot({path:path.join(evidence,`animejs-0.6.0-${id}-final.png`)});await call('seek',s.duration*.45);await page.locator('article').screenshot({path:path.join(evidence,`animejs-0.6.0-${id}-mid.png`)});
|
||||
await page.locator('article').screenshot({path:path.join(evidence,`${manifest.package}-${id}-final.png`)});await call('seek',s.duration*.45);await page.locator('article').screenshot({path:path.join(evidence,`${manifest.package}-${id}-mid.png`)});
|
||||
}
|
||||
await call('replay');await page.emulateMedia({reducedMotion:'reduce'});await page.waitForFunction(id=>{const s=animeGallery.controllers[id].state();return s.paused&&s.time===s.duration;},id);await page.emulateMedia({reducedMotion:'no-preference'});
|
||||
for(const event of ['beforeprint','pagehide']){await call('replay');await page.evaluate(event=>dispatchEvent(new Event(event)),event);assert.equal((await state()).paused,true);assert.equal((await state()).time,s.duration);}
|
||||
@@ -175,4 +175,4 @@ try{for(const variant of ['source','dist'])for(const reducedMotion of ['no-prefe
|
||||
assert.deepEqual(errors,[]);assert.deepEqual(network,[]);Object.assign(run,{passed:run.examples.length===ids.length,checksApplyTo:'Successful IDs in examples; failures in findings',deterministicSeek:reducedMotion==='no-preference',cleanupRemount:true,beatControls:true,bounds:true,errors,network});await ctx.close();
|
||||
}assert.equal(report.findings.length,0,'Recipe integration findings: '+report.findings.map(f=>f.variant+'/'+f.id).join(', ')+' (see evidence report)');report.status='passed';console.log(JSON.stringify({status:report.status,examples:ids.length,runs:report.runs.map(r=>({variant:r.variant,motion:r.reducedMotion,examples:r.examples.length})),completeCatalog:manifest.complete}));}
|
||||
catch(error){report.status='failed';report.failure=String(error.stack);throw error;}
|
||||
finally{fs.writeFileSync(path.join(evidence,'animejs-gallery-test-report.json'),JSON.stringify(report,null,2)+'\n');await browser.close();}
|
||||
finally{fs.writeFileSync(path.join(evidence,manifest.package+'-test-report.json'),JSON.stringify(report,null,2)+'\n');await browser.close();}
|
||||
@@ -0,0 +1,72 @@
|
||||
import {chromium} from '@playwright/test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {pathToFileURL} from 'node:url';
|
||||
const root=path.resolve(import.meta.dirname,'..');
|
||||
const records=JSON.parse(fs.readFileSync(path.join(root,'assets/animejs/catalogs/presentation-patterns.json')));
|
||||
assert.equal(records.length,4);
|
||||
const manifest=JSON.parse(fs.readFileSync(path.join(root,'dist/animejs-gallery-manifest.json')));
|
||||
const evidence=path.join(root,'evidence');fs.mkdirSync(evidence,{recursive:true});
|
||||
const report={status:'running',runs:[],screenshots:[],network:[],errors:[]};
|
||||
const browser=await chromium.launch({headless:true,...(process.env.CHROMIUM_EXECUTABLE?{executablePath:process.env.CHROMIUM_EXECUTABLE}:{})});
|
||||
try {
|
||||
for(const variant of ['source','dist']) {
|
||||
const context=await browser.newContext({offline:true,viewport:{width:1280,height:1000}}),page=await context.newPage();
|
||||
page.on('pageerror',e=>report.errors.push(String(e)));page.on('console',m=>{if(m.type()==='error')report.errors.push(m.text());});
|
||||
context.on('request',r=>{if(!/^(file|data):/.test(r.url()))report.network.push(r.url());});
|
||||
for(const record of records){
|
||||
const id=record.id;
|
||||
await page.goto(pathToFileURL(path.join(root,variant==='source'?'assets/animejs':'dist/'+manifest.package,'examples',id+'.html')).href);
|
||||
const call=(method,arg)=>page.evaluate(({id,method,arg})=>animeGallery.controllers[id][method](arg),{id,method,arg});
|
||||
const svg=()=>page.locator('svg').evaluate(e=>e.outerHTML);
|
||||
const attr=(selector,name)=>page.locator(selector).first().getAttribute(name);
|
||||
const final=await svg();
|
||||
for(const t of [0,730,1550,2200,2700,3050,3750,4500,5400,6000]){await call('seek',6000);await call('seek',t);const back=await svg();await call('seek',0);await call('seek',t);assert.equal(await svg(),back,id+' exact bidirectional seek '+t);}
|
||||
// Live test observes geometry, not just a changing timeline counter.
|
||||
async function visiblePlayback(){await call('replay');const first=await svg();await page.waitForTimeout(1700);assert.notEqual(await svg(),first,id+' visible playback frozen');}
|
||||
await visiblePlayback();
|
||||
await page.waitForFunction(id=>{const s=animeGallery.controllers[id].state();return s.paused&&s.time===s.duration;},id);
|
||||
assert.equal(await svg(),final,id+' natural speed-1 completion');
|
||||
// Negative control substitutes a finite clock with NO visible tween. Same detector must reject it.
|
||||
await page.evaluate(id=>{window.savedRenderer=industryRenderers[id];industryRenderers[id]=(svg,c,{anime,el})=>{el('rect',{x:200,y:200,width:80,height:50},'',svg);return anime.createTimeline({autoplay:false}).add({clock:0},{clock:1,duration:6000});};animeGallery.controllers[id].destroy();animeGallery.controllers[id]=animeGallery.mount(document.getElementById(id),animeExamples.find(c=>c.id===id));},id);
|
||||
await assert.rejects(visiblePlayback(),/visible playback frozen/);
|
||||
await page.evaluate(id=>{animeGallery.controllers[id].destroy();industryRenderers[id]=savedRenderer;animeGallery.controllers[id]=animeGallery.mount(document.getElementById(id),animeExamples.find(c=>c.id===id));},id);
|
||||
if(id.endsWith('expressive-robot')){
|
||||
await call('seek',740);assert.equal(Number(await attr('[data-eye]','ry')),.65,'visor closed');
|
||||
await call('seek',1000);assert.equal(Number(await attr('[data-eye]','ry')),3.6,'visor reopened');
|
||||
await call('seek',1550);assert.equal(Number(await attr('[data-hand="1"]','cy')),-17);assert.equal(Number(await attr('[data-elbow="1"]','cx')),35);
|
||||
const raised=await attr('[data-arm="1"]','d');await call('seek',1750);assert.notEqual(await attr('[data-arm="1"]','d'),raised,'articulated scratch movement');
|
||||
await call('seek',3300);assert.equal(Number(await attr('[data-hand="1"]','cy')),14,'hand returned');
|
||||
await call('seek',0);const neutral=await attr('[data-smile]','d');await call('settle');assert.notEqual(await attr('[data-smile]','d'),neutral,'smile');
|
||||
}
|
||||
if(id.endsWith('approval-gate')){
|
||||
for(let t=0;t<=6000;t+=50){await call('seek',t);const geometry=await page.evaluate(()=>{const q=s=>document.querySelector(s),x=new DOMMatrix(getComputedStyle(q('[data-work-item]')).transform).m41;return {left:330+x,right:390+x,topEnd:Number(q('[data-leaf="top"]').getAttribute('y'))+52,bottomStart:Number(q('[data-leaf="bottom"]').getAttribute('y'))};});if(geometry.right>467&&geometry.left<493)assert.ok(geometry.topEnd<=250&&geometry.bottomStart>=330,'never intersect closed barrier');if(t<3000)assert.equal(geometry.left,330,'wait for complete opening');}
|
||||
await call('seek',3990);assert.equal(Number(await attr('[data-evidence-packet]','opacity')),0);await call('settle');assert.equal(Number(await attr('[data-evidence-packet]','cx')),180);
|
||||
}
|
||||
if(id.endsWith('delivery-cycle')){
|
||||
const edges=await page.locator('[data-edge]').evaluateAll(es=>es.map(e=>e.dataset.edge));assert.deepEqual(edges,['0-1','1-2','1-3','2-4','3-4','4-5','5-6','6-7','7-0']);
|
||||
await call('seek',1650);for(const edge of ['1-2','1-3'])assert.equal(Number(await attr(`[data-edge="${edge}"]`,'stroke-dashoffset')),0);
|
||||
assert.equal(Number(await attr('[data-edge="4-5"]','stroke-dashoffset')),1,'acceptance waits for join');
|
||||
}
|
||||
if(id.endsWith('connected-memory')){
|
||||
const routes=await page.locator('[data-source-route]').evaluateAll(es=>es.map(e=>({from:e.dataset.from,to:e.dataset.to,d:e.getAttribute('d')})));
|
||||
assert.equal(routes.length,5);for(let i=0;i<5;i++){assert.equal(routes[i].from,'source-'+i);assert.equal(routes[i].to,'agent');assert.ok(routes[i].d.endsWith('L109 177'));assert.ok(routes[i].d.startsWith(`M${[110,290,470,650,830][i]} 395`));}
|
||||
assert.equal(await page.locator('[data-source]').count(),5);
|
||||
await call('settle');const ends=await page.locator('[data-source-packet]').evaluateAll(es=>es.map(e=>{const m=new DOMMatrix(getComputedStyle(e).transform);return [m.m41,m.m42];}));for(const [x,y] of ends){assert.ok(Math.abs(x-109)<.1);assert.ok(Math.abs(y-177)<.1);}
|
||||
}
|
||||
if(variant==='dist')for(const [name,time] of [['final',6000],['mid',id.endsWith('expressive-robot')?1900:id.endsWith('approval-gate')?2450:3500]]){await call('seek',time);const file=`presentation-patterns-${id.replace('presentation-patterns-','')}-${name}.png`;await page.locator('article').screenshot({path:path.join(evidence,file)});report.screenshots.push(file);}
|
||||
for(const duration of [3000,9000,18000]){
|
||||
await page.evaluate(({id,duration})=>{animeGallery.controllers[id].destroy();const c=structuredClone(animeExamples.find(c=>c.id===id));Object.assign(c.config,{duration,scale:4,smileDepth:14,travelPx:450});animeGallery.controllers[id]=animeGallery.mount(document.getElementById(id),c);},{id,duration});
|
||||
assert.equal((await call('state')).duration,duration);assert.equal(Number(await page.locator('[data-beat]').last().getAttribute('data-beat')),duration);
|
||||
for(const t of [0,duration*.25,duration*.5,duration]){await call('seek',duration);await call('seek',t);const a=await svg();await call('seek',0);await call('seek',t);assert.equal(await svg(),a);const bad=await page.locator('svg').evaluate(svg=>{const box=svg.getBoundingClientRect();return [...svg.querySelectorAll('text,rect,circle,ellipse,path')].filter(e=>{if(getComputedStyle(e).opacity==='0')return false;const r=e.getBoundingClientRect();return r.width&&r.height&&(r.left<box.left-2||r.right>box.right+2||r.top<box.top-2||r.bottom>box.bottom+2);}).map(e=>e.outerHTML);});assert.deepEqual(bad,[],id+' edited bounds');}
|
||||
}
|
||||
await call('settle');const editedFinal=await svg();await page.emulateMedia({reducedMotion:'reduce'});await call('replay');await page.waitForTimeout(100);assert.equal(await svg(),editedFinal);assert.equal((await call('state')).paused,true);await call('seek',0);assert.equal(await svg(),editedFinal);await page.emulateMedia({reducedMotion:'no-preference'});
|
||||
await call('destroy');await call('destroy');assert.equal(await page.locator('article svg').count(),0);
|
||||
report.runs.push({variant,id,livePlayback:'speed 1 to natural completion',frozenNegativeControl:'rejected',semanticChecks:true,editedDurations:[3000,9000,18000],reducedMotion:true});
|
||||
}
|
||||
await context.close();
|
||||
}
|
||||
assert.deepEqual(report.errors,[]);assert.deepEqual(report.network,[]);report.status='passed';console.log(JSON.stringify({status:report.status,runs:report.runs.length,screenshots:report.screenshots.length}));
|
||||
}catch(e){report.status='failed';report.error=String(e.stack);throw e;}
|
||||
finally{fs.writeFileSync(path.join(evidence,'presentation-patterns-report.json'),JSON.stringify(report,null,2)+'\n');await browser.close();}
|
||||
Reference in new issue
Block a user