← back to Wallpaper Paint Antics
series2/animation.js
215 lines
/* Wallo Material Notes: deterministic articulated character acting and real DW samples. */
(() => {
'use strict';
const canvas=document.getElementById('stage'), c=canvas.getContext('2d');
const E=window.EPISODE, P=window.PRODUCTS, ep=E.index;
const ink='#243a3b', chalk='#eeeae2', teal='#284f4b', clay='#a9634d', brass='#b7924f';
const images={}; let telemetry=[];
const clamp=(x,a=0,b=1)=>Math.max(a,Math.min(b,x));
const mix=(a,b,p)=>a+(b-a)*p;
const smooth=p=>{p=clamp(p);return p*p*(3-2*p);};
const ease=(t,a,b)=>smooth((t-a)/(b-a));
const pulse=(t,a,b,c,d)=>ease(t,a,b)*(1-ease(t,c,d));
const point=(x,y)=>({x,y});
const dist=(a,b)=>Math.hypot(a.x-b.x,a.y-b.y);
function group(x,y,r,s,fn){c.save();c.translate(x,y);c.rotate(r);c.scale(s,s);fn();c.restore();}
function path(fn,fill,stroke=null,w=2){c.beginPath();fn(c);if(fill){c.fillStyle=fill;c.fill();}if(stroke){c.lineWidth=w;c.strokeStyle=stroke;c.lineJoin='round';c.lineCap='round';c.stroke();}}
function line(x,y,u,v,col,w=2){path(q=>{q.moveTo(x,y);q.lineTo(u,v);},null,col,w);}
function rect(x,y,w,h,col){c.fillStyle=col;c.fillRect(x,y,w,h);}
function oval(x,y,rx,ry,col,stroke=null,w=2){path(q=>q.ellipse(x,y,rx,ry,0,0,Math.PI*2),col,stroke,w);}
function rr(x,y,w,h,r,col,stroke=null,lw=2){path(q=>q.roundRect(x,y,w,h,r),col,stroke,lw);}
function text(s,x,y,size=28,color=ink,font='Verdana',align='left'){c.font=`${size}px '${font}'`;c.textAlign=align;c.textBaseline='alphabetic';c.fillStyle=color;c.fillText(s,x,y);}
function wrap(s,x,y,max,size=36,color=ink,font='Verdana',lh=size*1.4){
c.font=`${size}px '${font}'`;let row='',lines=[];
for(const word of s.split(' ')){const test=row?row+' '+word:word;if(c.measureText(test).width>max&&row){lines.push(row);row=word;}else row=test;}
if(row)lines.push(row);lines.forEach((v,i)=>text(v,x,y+i*lh,size,color,font));return lines.length;
}
function imageCover(id,x,y,w,h){const im=images[id];if(!im||!im.complete)return;const k=Math.max(w/im.width,h/im.height);c.save();c.beginPath();c.rect(x,y,w,h);c.clip();c.drawImage(im,x+(w-im.width*k)/2,y+(h-im.height*k)/2,im.width*k,im.height*k);c.restore();}
function sample(id,x,y,w=126,h=164,r=0){group(x,y,r,1,()=>{rr(-w/2+7,-h/2+9,w,h,2,'#203a3a20');rect(-w/2-5,-h/2-5,w+10,h+10,'#f9f7f0');imageCover(id,-w/2,-h/2,w,h);line(-w/2,-h/2,w/2,-h/2,'#ffffff',2);});}
function bone(a,b,width,col,shade){
const angle=Math.atan2(b.y-a.y,b.x-a.x),d=dist(a,b);
group(a.x,a.y,angle,1,()=>{rr(-width*.18,-width/2,d+width*.36,width,width*.46,col);path(q=>{q.moveTo(5,width*.26);q.quadraticCurveTo(d*.5,width*.34,d-3,width*.22);},null,shade||'#00000018',2.3);});
}
function solveIK(root,target,l1,l2,pole){
const dx=target.x-root.x,dy=target.y-root.y,raw=Math.hypot(dx,dy),d=clamp(raw,Math.abs(l1-l2)+.01,l1+l2-.01);
const nx=raw?dx/raw:0,ny=raw?dy/raw:1,a=(l1*l1-l2*l2+d*d)/(2*d),h=Math.sqrt(Math.max(0,l1*l1-a*a));
const end=point(root.x+nx*d,root.y+ny*d),joint=point(root.x+nx*a-ny*h*pole,root.y+ny*a+nx*h*pole);
return {root,joint,end,l1,l2};
}
function rotatePoint(p,angle,origin=point(0,-258)){const x=p.x-origin.x,y=p.y-origin.y;return point(origin.x+x*Math.cos(angle)-y*Math.sin(angle),origin.y+x*Math.sin(angle)+y*Math.cos(angle));}
function key(t,frames){if(t<=frames[0][0])return frames[0].slice(1);for(let i=1;i<frames.length;i++)if(t<=frames[i][0]){const p=ease(t,frames[i-1][0],frames[i][0]);return frames[i].slice(1).map((v,j)=>mix(frames[i-1][j+1],v,p));}return frames.at(-1).slice(1);}
function gait(t,start,end,x0,x1){
const p=clamp((t-start)/(end-start));
const lift=(a,b)=>p>a&&p<b?Math.sin(Math.PI*(p-a)/(b-a))*44:0;
const left=mix(x0-30,(x0+x1)/2-30,smooth((p-.08)/.28))+mix(0,(x1-x0)/2,smooth((p-.65)/.3));
const right=mix(x0+30,x1+30,smooth((p-.35)/.3));
return {x:(left+right)/2,feet:[point(left,-lift(.08,.36)-lift(.65,.95)),point(right,-lift(.35,.65))],walking:p>0&&p<1,phase:p};
}
function voiceEnergy(t){const env=E.audio?.energy||[];return env[Math.min(env.length-1,Math.max(0,Math.floor(t*50)))]||0;}
function hand(x,y,r,pose='open',skin='#c89472',prop=null){
group(x,y,r,1,()=>{
if(prop==='sample')sample(E.product,8,-18,115,148,-.04);
if(prop==='fan')for(let k=0;k<3;k++)sample(['waterperry','topiary','kaya'][k],k*17-15,-43,91,130,(k-1)*.18);
if(prop==='report'){rr(-12,-134,135,176,4,'#f8f5e9',ink,2);rect(1,-117,108,5,teal);text('TEST',4,-87,22,ink);text('REPORT',4,-58,22,ink);for(let k=0;k<3;k++)line(4,-35+k*18,97,-35+k*18,'#a4aaa0',2);}
if(prop==='cloth'){path(q=>{q.moveTo(-28,-15);q.lineTo(30,-26);q.lineTo(43,35);q.lineTo(-21,44);q.closePath();},'#c8c5b5',ink,1);line(-17,-4,28,25,'#efeadb',3);}
if(prop==='roller'){line(0,-8,0,-72,ink,7);line(0,-72,37,-72,ink,7);line(37,-72,37,-108,ink,5);rr(-27,-132,127,38,11,'#8b9c8d',ink,2);rr(-9,-9,18,54,7,brass);}
if(prop==='tablet'){rr(-73,-76,145,170,8,ink);rr(-65,-67,129,151,4,'#f4f0e5');text('DW',-45,-33,27,teal,'Georgia');for(let i=0;i<3;i++)imageCover(['waterperry','fairborn','kaya'][i],-47+i*32,-6,27,60);}
if(pose==='point'){bone(point(-4,3),point(34,-18),13,skin);bone(point(0,9),point(16,15),21,skin);oval(-6,9,13,18,skin);}
else{rr(-14,-9,29,42,12,skin);oval(-15,5,7,13,skin);for(let i=0;i<3;i++)line(-7+i*6,14,-7+i*6,28,'#9e685446',1);}
});
}
function head(kind,x,y,t,look,mouth,tilt){
const male=kind==='wallo',skin=male?'#c89472':'#d5a185',dark=male?'#714f3b':'#834e39';
group(x,y,tilt,.82,()=>{
if(!male){oval(-4,-10,51,70,dark);oval(-39,-50,28,28,dark);path(q=>{q.moveTo(-57,-55);q.quadraticCurveTo(-81,-37,-59,-16);},null,'#a86b4b',7);}
// Tapered jaw, defined brow and modest nose maintain adult proportions.
path(q=>{q.moveTo(-41,-40);q.bezierCurveTo(-43,-77,43,-80,44,-34);q.lineTo(40,31);q.quadraticCurveTo(31,66,5,72);q.quadraticCurveTo(-26,65,-36,34);q.closePath();},skin);
path(q=>{q.moveTo(-38,-31);q.lineTo(-32,34);q.quadraticCurveTo(-24,52,-8,57);q.lineTo(-20,21);q.lineTo(-18,-46);q.closePath();},male?'#b58162':'#c39076');
oval(-40,3,9,16,skin);oval(42,2,8,15,skin);
if(male){path(q=>{q.moveTo(-39,-39);q.lineTo(-33,-18);q.lineTo(-30,-54);q.lineTo(31,-54);q.lineTo(43,-25);q.lineTo(42,-55);q.closePath();},dark);}
else{path(q=>{q.moveTo(-40,-27);q.quadraticCurveTo(-24,-63,2,-56);q.quadraticCurveTo(38,-37,43,-19);q.lineTo(38,-58);q.quadraticCurveTo(-4,-89,-37,-55);q.closePath();},dark);for(let i=0;i<4;i++)path(q=>{q.moveTo(-29+i*17,-56);q.quadraticCurveTo(-26+i*17,-43,-13+i*17,-39);},null,'#ae7150',2);}
const blink=(t+(male?.31:1.93))%4.4<.14;
for(const side of [-1,1]){const ex=side*18+look*3,ey=-8;path(q=>{q.moveTo(ex-10,ey);q.quadraticCurveTo(ex,ey-5,ex+10,ey);},null,ink,2.2);if(!blink){oval(ex+look*3,ey+1,3.5,4.3,ink);oval(ex+look*3+1,ey-.3,1.1,1.2,'#eee8dc');}line(ex-10,ey-15,ex+8,ey-17+(side===1?look*2:0),dark,2.5);}
path(q=>{q.moveTo(3+look*5,-5);q.lineTo(8+look*8,15);q.quadraticCurveTo(4+look*5,20,-1+look*5,17);},null,'#98644f',1.8);
if(male){path(q=>{q.moveTo(-20,29);q.quadraticCurveTo(-6,20,4,27);q.quadraticCurveTo(15,23,26,30);q.quadraticCurveTo(17,39,3,34);q.quadraticCurveTo(-9,37,-20,29);},dark);}
if(mouth>.12){oval(4,44,10+mouth*3,2+mouth*6,'#714735');if(mouth>.4)line(-2,41,11,41,'#efe6d7',2);}
else path(q=>{q.moveTo(-7,43);q.quadraticCurveTo(5,48,17,42);},null,'#714735',1.8);
if(male){path(q=>{q.moveTo(-49,-53);q.quadraticCurveTo(-48,-98,10,-95);q.quadraticCurveTo(47,-89,48,-53);q.closePath();},brass);path(q=>{q.moveTo(-49,-53);q.quadraticCurveTo(12,-64,72,-48);q.quadraticCurveTo(77,-35,39,-41);q.lineTo(-49,-43);q.closePath();},'#97773f');line(-27,-73,12,-84,'#d2b270',3);}
else oval(-39,14,3.5,5.5,brass);
});
}
function character(kind,t,settings={}){
const male=kind==='wallo',skin=male?'#c89472':'#d5a185',shirt=male?'#d6d0bc':clay,cloth=male?teal:'#c8c1af';
const walk=settings.walk||gait(t,0,1,settings.x,settings.x);let x=walk.x,y=settings.y||882;
const crouch=settings.crouch||0,lean=settings.lean||0,breath=Math.sin(t*2.1+(male?0:1.2))*1.7;
const hipY=-275+crouch,origin=point(0,hipY);
const bodyPoint=(u,v)=>rotatePoint(point(u,v+crouch+breath),lean,origin);
const leftShoulder=bodyPoint(-62,-469),rightShoulder=bodyPoint(62,-469);
const arms=[solveIK(leftShoulder,point(...settings.left),118,111,-1),solveIK(rightShoulder,point(...settings.right),118,111,1)];
const legs=walk.feet.map((foot,i)=>solveIK(point(i===0?-32:32,hipY),point(foot.x-x,foot.y-13),134,136,i===0?1:-1));
const mouth=male?voiceEnergy(t)*.88:0;
const joints={kind,x,y,arms,legs,lean,crouch,feet:walk.feet,walking:walk.walking,props:settings.props||[]};
telemetry.push(joints);
oval(x,y+6,94-crouch*.1,15,'#233a3b15');
group(x,y,0,settings.scale||1,()=>{
// Back arm and far leg precede torso so elbow overlaps read spatially.
const armDraw=(i)=>{const b=arms[i];bone(b.root,b.joint,39,shirt);bone(b.joint,b.end,27,skin);oval(b.joint.x,b.joint.y,16,16,skin);const sleeve=point(mix(b.root.x,b.joint.x,.77),mix(b.root.y,b.joint.y,.77));bone(b.root,sleeve,43,shirt);line(sleeve.x-8,sleeve.y,sleeve.x+9,sleeve.y,'#ffffff30',2);};
armDraw(0);
legs.forEach((b,i)=>{bone(b.root,b.joint,58,cloth);bone(b.joint,b.end,43,cloth);oval(b.joint.x,b.joint.y,23,23,cloth);line(b.root.x-8,b.root.y+30,b.joint.x-6,b.joint.y-16,male?'#41635b':'#dfd8c8',2);group(b.end.x,b.end.y+10,walk.walking&&walk.feet[i].y<0?-.12:0,1,()=>{path(q=>{q.moveTo(-24,-20);q.lineTo(20,-20);q.lineTo(27,-9);q.quadraticCurveTo(60,-5,56,14);q.lineTo(-24,14);q.closePath();},male?'#655545':'#626b62');line(-24,14,55,14,'#34413d',5);line(4,-8,25,-7,'#c6b69b',2);});});
group(0,hipY,lean,1,()=>{
const dy=crouch+breath-hipY;
path(q=>{q.moveTo(-63,-478+dy);q.quadraticCurveTo(-82,-460+dy,-71,-417+dy);q.lineTo(-60,-266+dy);q.quadraticCurveTo(0,-235+dy,60,-266+dy);q.lineTo(71,-417+dy);q.quadraticCurveTo(79,-460+dy,58,-479+dy);q.closePath();},shirt);
// Structured overall/apron silhouette, stitched seams, pockets and cloth folds.
path(q=>{q.moveTo(-50,-425+dy);q.lineTo(-46,-350+dy);q.lineTo(-61,-290+dy);q.lineTo(-60,-253+dy);q.lineTo(61,-253+dy);q.lineTo(61,-290+dy);q.lineTo(46,-350+dy);q.lineTo(48,-425+dy);q.closePath();},cloth);
for(const s of [-1,1]){line(s*44,-472+dy,s*38,-405+dy,cloth,17);oval(s*39,-414+dy,4.8,4.8,brass);line(s*49,-328+dy,s*41,-312+dy,male?'#59756b':'#a9a18f',2);}
rr(-27,-401+dy,54,56,4,male?'#254540':'#bab39f');path(q=>{q.moveTo(-23,-388+dy);q.quadraticCurveTo(0,-376+dy,24,-388+dy);},null,male?'#6a8778':'#e5dfd0',1.5);
line(-50,-266+dy,52,-266+dy,male?'#1e403b':'#aca590',3);
if(male){rr(-66,-321+dy,20,67,3,'#8d7452');line(-56,-342+dy,-56,-282+dy,brass,6);}
path(q=>{q.moveTo(-17,-509+dy);q.lineTo(-18,-474+dy);q.quadraticCurveTo(0,-454+dy,21,-474+dy);q.lineTo(18,-509+dy);q.closePath();},skin);
});
const hp=bodyPoint(0,-559);head(kind,hp.x,hp.y,t,settings.look||0,mouth,(settings.headTilt||0)+lean*.28);
armDraw(1);
arms.forEach((b,i)=>hand(b.end.x,b.end.y,(settings.wrists||[0,0])[i],(settings.gestures||['open','open'])[i],skin,(settings.props||[])[i]));
});
return joints;
}
function room(t){
rect(-350,-300,1800,1200,'#dfdbd1');rect(-350,765,1800,450,'#c9c2b1');
rect(20,0,240,706,'#e9e7dc');for(let i=0;i<4;i++)line(22+i*59,0,22+i*59,704,'#f7f4e9',7);
rect(318,26,647,727,'#c7bfae');rect(329,37,624,704,'#efede3');
if(ep===1){const a=ease(t,5.1,8);rect(343,49,596,678,'#aab7a4');c.save();c.beginPath();c.rect(343,49,596*a,678);c.clip();imageCover(E.product,343,49,596,678);c.restore();}
else imageCover(E.product,343,49,596,678);
line(340,743,943,743,'#faf7eb',4);line(-350,765,1450,765,'#a79f8d',2);
for(let i=0;i<6;i++)line(-200,814+i*54,1400,814+i*54,'#b6ad9a',1.4);
line(91,765,-170,1220,'#b6ad9a',1.4);line(880,765,1130,1220,'#b6ad9a',1.4);
// Light and shadow move gently with the camera, not with an arbitrary pulse.
path(q=>{q.moveTo(40,706);q.lineTo(263,706);q.lineTo(672,1120);q.lineTo(412,1120);q.closePath();},'#fcf7df28');
// A sample ledge ties the real products to the working material library.
rect(404,685,479,12,'#b29d7a');for(let i=0;i<3;i++)sample(P[(i+ep)%P.length].id,468+i*115,622,88,116,(i-1)*.045);
}
function documentPanel(t){
if(ep!==0)return;
const a=pulse(t,4.2,4.9,14.3,15.0);if(!a)return;
group(640,177+(1-a)*28,0,.88,()=>{
rr(-239,-168,478,336,2,'#f7f4e9',ink,1.5);text('BEFORE YOU SPECIFY',-210,-126,24,teal);
const labels=['PRODUCT / TEST REPORT','CLASSIFICATION','SUBSTRATE + ADHESIVE'];
labels.forEach((label,i)=>{const done=t>5.0+i*2.8;line(-209,-61+i*77,-182,-61+i*77,done?brass:'#bac0b5',3);text(label,-163,-52+i*77,21,ink);});
});
}
function swatchBoard(t){
if(ep!==3&&ep!==4)return;
const a=pulse(t,6.4,7.1,13.4,14);if(!a)return;
group(560,275+(1-a)*25,0,.95,()=>{
rr(-199,-139,398,278,2,'#f5f0e4',ink,1);sample(E.product,-83,-1,155,220,0);
rect(28,-111,137,83,'#c6bea9');rect(28,-16,137,69,'#8c9985');rect(28,64,137,43,'#b89672');
text(ep===3?'COMPARE YOUR FINISHES':'YOUR PROJECT SHORTLIST',-192,-162,23,ink);
});
}
function actionNotes(t){
if(ep!==2)return;const a=pulse(t,8,8.5,14.2,14.8);if(!a)return;
['ABRASION','CLEANABILITY','CARE'].forEach((s,i)=>{const show=ease(t,8+i*.7,8.4+i*.7);if(show){rect(440,120+i*77,440,58,'#f4f0e6');text(s,468,159+i*77,27,ink);}});
}
function performances(t){
let wwalk=gait(t,.2,2.9,126,253),pwalk=gait(t,.5,3.1,949,801);
const w={walk:wwalk,left:[-85,-279],right:[149,-396],wrists:[-.1,-.36],props:[],gestures:['open','open'],look:.6,lean:Math.sin(t*.8)*.012};
const p={walk:pwalk,left:[-130,-402],right:[86,-275],wrists:[.14,.08],props:['sample'],look:-.6,lean:Math.sin(t*.73+1)*.013};
// Presenter gestures are deliberately different for each spoken clause.
const gesture=key(t,[[0,100,-285],[2.8,145,-411],[4.4,165,-440],[5.2,120,-367],[8.7,161,-441],[9.6,149,-378],[12.8,180,-432],[14,125,-360],[17,152,-419],[19.8,126,-383]]);
w.right=gesture;w.wrists[1]=key(t,[[0,0],[3,-.5],[5.2,-.15],[8,-.42],[11,.1],[14,-.4],[18,-.2]])[0];
w.gestures[1]=t>4.5&&t<8.8?'point':'open';w.headTilt=Math.sin(t*.65)*.022;w.look=t>4&&t<9?1:.1;
if(t<3){w.right=[90-Math.sin(t*4)*40,-277];w.left=[-90+Math.sin(t*4)*40,-277];p.left=[-90+Math.sin(t*4)*24,-280];p.props=[];}
if(ep===0){p.props=['report'];p.left=key(t,[[0,-85,-280],[3,-130,-365],[6,-131,-420],[10,-153,-445],[14,-125,-384],[19,-108,-376]]);p.wrists[0]=-.14;p.right=key(t,[[0,90,-277],[8,98,-320],[11,-10,-377],[14,93,-278]]);}
if(ep===1){
p.props=t<8?['roller']:['sample'];p.left=key(t,[[0,-88,-283],[3,-129,-447],[4.5,-142,-495],[5.2,-143,-411],[6,-146,-495],[7,-145,-397],[8,-100,-367],[11,-139,-436],[15,-103,-361]]);p.wrists[0]=.07;
if(t>9.3){p.walk=gait(t,9.3,11.4,801,870);p.look=-1;p.lean=-.06*ease(t,10,11);}
w.props=['sample'];w.left=key(t,[[0,-87,-282],[4,-98,-291],[6,-39,-389],[8,-30,-433],[12,-59,-386],[17,-78,-320]]);w.wrists[0]=-.08;
}
if(ep===2){
p.crouch=pulse(t,4,5.6,8.8,10.2)*113;p.lean=-pulse(t,4,5.4,9,10.3)*.10;
const wipe=pulse(t,5,5.8,8.2,9);p.props=['cloth'];p.left=[-147-wipe*17,-389+p.crouch+wipe*Math.sin(t*6)*42];p.wrists[0]=-.5;
p.right=[85,-284+p.crouch];p.look=-1;
w.props=['sample'];w.left=[-45,-371];w.wrists[0]=-.18;
}
if(ep===3){
p.props=['sample'];p.left=key(t,[[0,-92,-280],[3,-100,-366],[6,-138,-440],[10,-144,-478],[14,-126,-418],[18,-93,-340]]);p.wrists[0]=key(t,[[0,0],[3,-.08],[7,.22],[10,-.18],[13,.16],[17,0]])[0];
w.props=['fan'];w.left=key(t,[[0,-90,-280],[4,-33,-365],[7,-44,-400],[12,-24,-389],[16,-44,-346]]);w.wrists[0]=-.13;
}
if(ep===4){
w.props=['tablet'];w.left=[-43,-341];w.right=key(t,[[0,101,-280],[3,143,-413],[6,158,-444],[8.5,65,-361],[10,34,-340],[11.6,20,-356],[13,144,-411],[17,160,-444],[19,112,-374]]);w.gestures[1]=t>8&&t<12?'point':'open';
p.props=['fan'];p.left=[-129,-400];p.right=key(t,[[0,89,-280],[6,86,-280],[9,-18,-384],[12,76,-285],[17,100,-382],[19,86,-297]]);
}
// Camera-right actor is drawn first so the presenter remains the visual lead.
character('pippa',t,p);character('wallo',t,w);
}
function overlay(t){
rect(0,0,1080,437,chalk);rect(0,1437,1080,483,chalk);
text('DESIGNER WALLCOVERINGS',72,96,25,ink);text('WALLO PRESENTS',1008,96,21,teal,'Verdana','right');
line(72,124,1008,124,'#b9b6aa',1);text(E.kicker,72,178,24,teal);
E.title.split('\n').forEach((s,i)=>text(s,69,270+i*87,73,ink,'Georgia'));
text('MATERIAL NOTES',73,411,22,teal);text('WITH WALLO & PIPPA',1006,411,20,teal,'Verdana','right');
const product=P.find(p=>p.id===E.product);text(product.label,72,1481,25,ink);
line(72,1505,1008,1505,'#b9b6aa',1);
const segment=(E.audio?.segments||[]).filter(s=>t>=s.start).at(-1);
const caption=segment?E.captions[segment.index]:'A wallcovering presentation with Wallo.';
wrap(caption,72,1570,930,38,ink,'Verdana',55);
if(t>=16.8)text(E.takeaway,72,1738,37,teal,'Georgia');
wrap(E.note,72,1794,926,24,'#58635b','Verdana',34);
text('designerwallcoverings.com',72,1870,25,teal);
text(String(ep+1).padStart(2,'0')+' / 05',1008,1870,22,teal,'Verdana','right');
rect(72,1894,936,3,'#c9c7bb');rect(72,1894,936*clamp(t/20),3,teal);
}
function draw(t){
telemetry=[];rect(0,0,1080,1920,chalk);
c.save();c.beginPath();c.rect(0,437,1080,1000);c.clip();
const zoom=1+ease(t,3.3,5)*.08-ease(t,14.2,16)*.06;
c.translate(540,950);c.scale(zoom,zoom);c.translate(-540,-487);
room(t);documentPanel(t);swatchBoard(t);actionNotes(t);performances(t);
c.restore();overlay(t);window.__rigState=telemetry;
}
const driver={t:0};window.__timelines=window.__timelines||{};
window.__timelines.main=gsap.timeline({paused:true}).to(driver,{t:20,duration:20,ease:'none',onUpdate:()=>draw(driver.t)},0);
window.__draw=draw;
window.__ready=Promise.all(P.map(p=>new Promise((resolve,reject)=>{const im=new Image();images[p.id]=im;im.onload=resolve;im.onerror=()=>reject(new Error('Missing product '+p.id));im.src=p.local;}))).then(()=>document.fonts.ready).then(()=>{draw(driver.t);return true;});
draw(0);
})();