← back to Wallpaper Paint Antics

episodes/01-runaway-roll.html

191 lines

<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=1080,height=1920"><title>Wallo & Pippa — 01-runaway-roll</title>
<style>@font-face{font-family:'Archivo Black';src:url('../assets/archivo.ttf')}@font-face{font-family:'Space Mono';src:url('../assets/space-mono.ttf')}html,body{margin:0;width:1080px;height:1920px;overflow:hidden}#root{position:relative;width:1080px;height:1920px;overflow:hidden}.clip{position:absolute;inset:0;width:1080px;height:1920px}canvas{width:1080px;height:1920px}.font-preload{position:absolute;opacity:0;font-family:'Archivo Black'} .font-preload.mono{font-family:'Space Mono'}</style>
<script src="../assets/gsap.min.js"></script></head><body data-episode="0"><div id="root" data-composition-id="main" data-start="0" data-width="1080" data-height="1920" data-duration="20" data-fps="30"><section id="cartoon-scene" class="clip" data-start="0" data-duration="20" data-track-index="0"><canvas id="stage" width="1080" height="1920" aria-label="Two animated decorators performing a comedy routine"></canvas><span class="font-preload">Wallo</span><span class="font-preload mono">Pippa</span></section><audio id="soundtrack" src="../assets/short-1.wav" data-start="0" data-duration="20" data-track-index="1"></audio></div><script>/* Original Wallo & Pippa character rigs. Every pose is a pure function of time. */
(() => {
  'use strict';
  const canvas = document.getElementById('stage');
  const c = canvas.getContext('2d');
  const episode = Number(document.body.dataset.episode || 0);
  const INK = '#243b42', PAPER = '#fff7e8', TEAL = '#267e7c', CORAL = '#e97556', GOLD = '#e9b64b';
  const titles = ['THE RUNAWAY ROLL', 'A SPLASH OF PERSONALITY', 'THE GREAT WALL-OFF'];
  const punches = ["THAT’S A WRAP.", 'HAPPY ACCIDENT.', 'BETTER TOGETHER.'];
  const clamp = (v, a=0, b=1) => Math.max(a, Math.min(b, v));
  const mix = (a,b,p) => a+(b-a)*p;
  const phase = (t,a,b) => clamp((t-a)/(b-a));
  const smooth = p => p*p*(3-2*p);
  const ease = (t,a,b) => smooth(phase(t,a,b));
  const rnd = i => { const x=Math.sin(i*127.1+311.7)*43758.5453; return x-Math.floor(x); };
  function path(points,fill,stroke=INK,width=5) {
    c.beginPath(); points(c); if(fill){c.fillStyle=fill;c.fill();} if(stroke){c.strokeStyle=stroke;c.lineWidth=width;c.lineJoin='round';c.lineCap='round';c.stroke();}
  }
  function rr(x,y,w,h,r,fill,stroke=null,lw=5){path(q=>q.roundRect(x,y,w,h,r),fill,stroke,lw);}
  function ellipse(x,y,rx,ry,fill,stroke=null,lw=5){path(q=>q.ellipse(x,y,rx,ry,0,0,Math.PI*2),fill,stroke,lw);}
  function line(x,y,x2,y2,color=INK,w=5){path(q=>{q.moveTo(x,y);q.lineTo(x2,y2);},null,color,w);}
  function text(s,x,y,size=40,color=INK,font='Archivo Black',align='center') {c.fillStyle=color;c.font=`${size}px "${font}"`;c.textAlign=align;c.textBaseline='middle';c.fillText(s,x,y);}
  function group(x,y,rot,fn,sx=1,sy=sx){c.save();c.translate(x,y);c.rotate(rot);c.scale(sx,sy);fn();c.restore();}
  function leaf(x,y,s=1,rot=0){group(x,y,rot,()=>path(q=>{q.moveTo(0,26);q.bezierCurveTo(-42,10,-22,-36,0,-42);q.bezierCurveTo(24,-26,40,8,0,26);},'#79aca0',null),s);}
  function pattern(x,y,w,h,alpha=1){c.save();c.globalAlpha=alpha;c.beginPath();c.rect(x,y,w,h);c.clip();rr(x,y,w,h,0,'#f4dca0');for(let j=0;j<13;j++)for(let i=0;i<9;i++){const a=x+i*73+(j%2)*36,b=y+j*91;leaf(a,b,.55,.45);leaf(a+29,b+28,.35,-.7);ellipse(a+27,b-25,5,5,CORAL);}c.restore();}
  function star(x,y,r,color=GOLD,rot=0){group(x,y,rot,()=>path(q=>{for(let i=0;i<10;i++){const a=-Math.PI/2+i*Math.PI/5,k=i%2?r*.43:r;(i?q.lineTo.bind(q):q.moveTo.bind(q))(Math.cos(a)*k,Math.sin(a)*k);}q.closePath();},color,INK,3));}
  function splat(x,y,r,color=CORAL,seed=0){path(q=>{for(let i=0;i<=48;i++){let a=i*Math.PI*2/48,k=r*(.68+.22*Math.sin(i*2.25+seed)+.1*Math.cos(i*3.7));(i?q.lineTo.bind(q):q.moveTo.bind(q))(x+Math.cos(a)*k,y+Math.sin(a)*k);}q.closePath();},color,null);}
  function roll(x,y,angle=0,scale=1){group(x,y,angle,()=>{rr(-66,-29,132,58,14,'#efd48e',INK,5);for(let i=-45;i<55;i+=32)leaf(i,0,.30,.5);ellipse(64,0,15,29,PAPER,INK,5);ellipse(64,0,6,13,'#cfbb94',INK,3);line(-51,-19,48,-19,'#fff3cc',5);},scale);}
  function roller(x,y,angle=0,color=CORAL){group(x,y,angle,()=>{path(q=>{q.moveTo(0,0);q.lineTo(0,-78);q.lineTo(48,-78);q.lineTo(48,-137);},null,INK,10);path(q=>{q.moveTo(0,-22);q.lineTo(0,-78);q.lineTo(48,-78);q.lineTo(48,-137);},null,'#acbbb5',5);rr(-42,-159,121,44,12,color,INK,5);line(-31,-149,65,-149,'#f39a7c',5);rr(-10,-13,20,62,9,GOLD,INK,4);});}
  function bucket(x,y){group(x,y,0,()=>{path(q=>{q.moveTo(-48,-84);q.lineTo(-38,0);q.quadraticCurveTo(0,20,38,0);q.lineTo(48,-84);q.closePath();},'#d6dfd8',INK);ellipse(0,-83,48,14,INK);ellipse(0,-82,39,9,CORAL);rr(-30,-62,60,47,5,PAPER);splat(0,-38,20,CORAL);path(q=>{q.moveTo(-45,-65);q.bezierCurveTo(-70,-143,65,-143,45,-65);},null,INK,5);});}
  function ladder(){group(974,1500,0,()=>{line(-35,0,-95,-522,'#b78d62',19);line(80,0,-45,-522,'#b78d62',19);for(let j=0;j<6;j++)line(-43-j*8,-70-j*79,61-j*19,-70-j*79,'#e3bd86',14);line(-98,-527,-44,-527,INK,10);},.9);}
  function room(t,ep,reveal=0){
    rr(0,0,1080,1920,0,'#f4eadb');
    const g=c.createLinearGradient(0,350,0,1530);g.addColorStop(0,'#fff7e8');g.addColorStop(1,'#e7d5b9');rr(45,368,990,1198,38,g);
    rr(64,390,952,15,6,'#ddc9a8');rr(68,414,944,5,2,'#fffdf4');
    rr(119,477,404,946,8,'#ede1cc','#d4c3a7',6);rr(550,477,410,946,8,'#ede1cc','#d4c3a7',6);
    if(reveal>0)pattern(128,486,386,930*reveal);
    for(let i=0;i<9;i++)line(78+i*117,1550,35+(i-1)*145,1920,'#d8b994',4);
    rr(0,1560,1080,360,0,'#dabe99');for(let j=0;j<4;j++)line(0,1590+j*84,1080,1590+j*84,'#c2a17b',3);
    for(let i=0;i<10;i++)line(i*148-120,1560,i*180-290,1920,'#b8956e',3);
    rr(45,1510,990,50,5,'#e9dec7',INK,4);line(49,1520,1031,1520,PAPER,5);
    ladder();bucket(926,1603);
    ellipse(152,1580,90,17,'#b39573');roll(142,1555,-.10,.75);
    // Gentle sun shapes, a physical room light rather than a decorative UI effect.
    c.save();c.globalAlpha=.18;path(q=>{q.moveTo(76,436);q.lineTo(278,436);q.lineTo(801,1507);q.lineTo(599,1507);q.closePath();},'#fffef0',null);c.restore();
    text('WALLO & PIPPA',65,100,37,TEAL,'Space Mono','left');
    text(`SHORT 0${ep+1}`,1014,100,27,INK,'Space Mono','right');
    const title=titles[ep];text(title,540,211,ep===1?59:72);
    text('TWO DECORATORS. ONE BEAUTIFUL MESS.',540,292,24,INK,'Space Mono');
    line(65,340,1015,340,INK,4);
  }
  function arm(sx,sy,hx,hy,skin,sleeve,bend=1){
    // Two-link IK, elbow bending outward; upper sleeve follows the upper arm.
    if(hy<sy+20)bend=-bend; // Raised hands bend below the wrist, away from the face.
    let dx=hx-sx,dy=hy-sy,d=Math.max(10,Math.hypot(dx,dy));const l=111;
    if(d>l*1.95){dx*=l*1.95/d;dy*=l*1.95/d;hx=sx+dx;hy=sy+dy;d=l*1.95;}
    const mx=(sx+hx)/2,my=(sy+hy)/2,h=Math.sqrt(Math.max(0,l*l-d*d/4));
    const ex=mx-dy/d*h*bend,ey=my+dx/d*h*bend;
    path(q=>{q.moveTo(sx,sy);q.lineTo(ex,ey);q.lineTo(hx,hy);},null,INK,43);
    line(sx,sy,ex,ey,sleeve,34);line(ex,ey,hx,hy,skin,29);
    ellipse(hx,hy,21,23,skin,INK,4);line(hx+7,hy-11,hx+13,hy+2,skin==='#e9ae82'?'#bd795b':'#cb8b69',3);
    return [hx,hy];
  }
  function character(kind,x,y,t,pose={}){
    const wallo=kind==='wallo',skin=wallo?'#e9ae82':'#f0ba97',shirt=wallo?PAPER:CORAL,cloth=wallo?TEAL:'#eee7d5';
    const gait=pose.run||0,cycle=t*(pose.fast?19:13),kick=pose.kick||0;
    const hop=gait*Math.abs(Math.sin(cycle))*19+(pose.hop||0);
    const lean=pose.lean||0,scale=pose.scale||1.05,duck=pose.duck||0;
    ellipse(x,y+8,105*scale,20,'rgba(67,65,45,.19)');
    group(x,y-hop,lean,()=>{
      c.scale(1+duck*.07,1-duck*.15);
      // Legs, separate joint angles and boot contact.
      for(const side of [-1,1]){
        let stride=gait*Math.sin(cycle+(side<0?Math.PI:0));let hx=side*34,hy=-180,kx=hx+stride*49,ky=-99,fx=side*47+stride*84,fy=-10-Math.max(0,-stride)*46;
        if(side===-1&&kick){kx-=kick*53;ky-=kick*31;fx-=kick*117;fy-=kick*110;}
        path(q=>{q.moveTo(hx,hy);q.lineTo(kx,ky);q.lineTo(fx,fy);},null,INK,54);
        path(q=>{q.moveTo(hx,hy);q.lineTo(kx,ky);q.lineTo(fx,fy);},null,cloth,44);
        line(kx+side*10,ky-16,fx+side*10,fy-12,wallo?'#419995':'#fff8eb',7);
        group(fx,fy,.06*stride,()=>{rr(-24,-21,72,40,14,wallo?'#895b41':'#457889',INK,5);line(-17,11,42,11,INK,7);line(-8,-12,18,-10,wallo?'#d4a171':'#91b4be',4);});
      }
      const breath=Math.sin(t*3.1)*2.5;
      group(0,-duck*0+breath,0,()=>{
        // Far arm before torso.
        const lh=pose.left||[-112-gait*Math.sin(cycle)*40,-253+gait*Math.cos(cycle)*50];
        arm(-65,-361,lh[0],lh[1],skin,shirt,1);
        path(q=>{q.moveTo(-58,-390);q.quadraticCurveTo(-91,-375,-82,-319);q.lineTo(-72,-196);q.quadraticCurveTo(0,-166,72,-196);q.lineTo(82,-319);q.quadraticCurveTo(90,-375,57,-390);q.closePath();},shirt,INK,6);
        rr(-69,-308,138,139,25,cloth,INK,5);
        line(-48,-378,-41,-280,cloth,26);line(48,-378,41,-280,cloth,26);
        for(const s of [-1,1]){ellipse(s*43,-315,7,7,GOLD,INK,3);line(s*42,-369,s*39,-334,wallo?'#74b5a8':'#fff9ed',5);}
        rr(-36,-286,72,65,8,wallo?'#206b6d':'#dad5c3',INK,4);path(q=>{q.moveTo(-27,-269);q.quadraticCurveTo(0,-239,27,-269);},null,wallo?'#71aca0':'#fff8e9',4);
        if(wallo){rr(-81,-224,27,60,4,'#b2774d',INK,3);rr(-74,-234,11,41,3,GOLD,INK,2);}else{ellipse(40,-199,10,7,CORAL);ellipse(-26,-234,7,10,TEAL);}
        rr(-24,-423,48,55,14,skin,INK,5);
        // Hair mass and face with a broad, readable silhouette.
        group(pose.headX||0,-500+(pose.headY||0),pose.headTilt||0,()=>{
          if(!wallo){ellipse(-2,-34,96,101,'#8b503b',INK,6);ellipse(54,-108,43,43,'#8b503b',INK,5);for(let i=0;i<5;i++)ellipse(36+i*11,-119+(i%2)*13,18,22,i%2?'#a96848':'#8b503b');}
          ellipse(-81,6,18,27,skin,INK,4);ellipse(81,6,18,27,skin,INK,4);
          path(q=>{q.moveTo(-74,-44);q.bezierCurveTo(-72,-104,72,-104,75,-41);q.lineTo(74,38);q.bezierCurveTo(69,99,-66,99,-74,37);q.closePath();},skin,INK,6);
          ellipse(-58,28,14,8,'#d99074');ellipse(58,28,14,8,'#d99074');
          if(wallo){path(q=>{q.moveTo(-78,-37);q.lineTo(-71,-69);q.lineTo(-62,-65);q.lineTo(-62,-31);},'#59483e',null);path(q=>{q.moveTo(62,-68);q.lineTo(78,-46);q.lineTo(74,-24);q.lineTo(62,-31);},'#59483e',null);}
          else{for(let i=0;i<7;i++)ellipse(-66+i*21,-57-Math.sin(i*.6)*17,21,27,i%2?'#9f6042':'#8b503b',INK,3);}
          const blinking=((t+(wallo?0:1.4))%4.1<.13),surprise=pose.expr==='shock',happy=pose.expr==='laugh';
          const look=pose.look??(wallo?1:-1);
          for(const s of [-1,1]){
            const ex=s*32,ey=-7;
            if(blinking||happy){path(q=>{q.moveTo(ex-13,ey+4);q.quadraticCurveTo(ex,ey-10,ex+13,ey+4);},null,INK,5);}
            else{ellipse(ex,ey,17,surprise?25:20,'#fffaf1',INK,3);ellipse(ex+look*4,ey+(surprise?2:0),6,10,INK);ellipse(ex+look*4+2,ey-5,2,3,PAPER);}
            const b=surprise?-49:-38;line(ex-12,b+s*(pose.expr==='suspicious'?7:0),ex+12,b+(!wallo?3:0),INK,6);
          }
          ellipse(2,17,wallo?19:12,wallo?18:12,wallo?'#de986f':'#eaa887',INK,3);
          if(wallo){path(q=>{q.moveTo(0,38);q.bezierCurveTo(-14,25,-26,39,-41,36);q.bezierCurveTo(-33,65,-10,62,0,48);q.bezierCurveTo(13,64,39,57,43,37);q.bezierCurveTo(25,43,16,25,0,38);},pose.paint?CORAL:'#55423a',INK,3);}
          if(surprise){ellipse(0,wallo?68:51,11,15,'#763e35',INK,3);}
          else if(happy){path(q=>{q.moveTo(-26,49);q.quadraticCurveTo(0,96,28,48);q.closePath();},'#763e35',INK,3);line(-14,55,16,55,PAPER,8);}
          else{path(q=>{q.moveTo(-17,wallo?66:52);q.quadraticCurveTo(0,wallo?77:66,20,wallo?63:49);},null,INK,4);}
          if(!wallo)for(const side of [-1,1])for(let j=0;j<3;j++)ellipse(side*(45+j*7),21+(j%2)*7,2,2,'#ae6b53');
          if(wallo){group(0,-78+(pose.capLift||0),pose.capTilt||0,()=>{path(q=>{q.moveTo(-83,9);q.bezierCurveTo(-89,-73,68,-97,77,0);q.closePath();},GOLD,INK,6);path(q=>{q.moveTo(-85,1);q.quadraticCurveTo(3,-19,94,-2);q.quadraticCurveTo(124,12,83,24);q.lineTo(-84,20);q.closePath();},'#d5a039',INK,5);line(-51,-32,-12,-46,'#f8d277',7);if(pose.capPaint)splat(44,-21,24,CORAL);});}
        });
        const rh=pose.right||[114+gait*Math.sin(cycle)*43,-257-gait*Math.cos(cycle)*48];
        const hand=arm(65,-361,rh[0],rh[1],skin,shirt,-1);
        if(pose.tool==='roller')roller(hand[0]+5,hand[1]-5,pose.toolAngle||0,pose.toolColor||CORAL);
        if(pose.tool==='brush')group(hand[0],hand[1],-.25,()=>{rr(-8,-60,16,104,6,'#b88252',INK,4);rr(-26,-95,52,41,4,TEAL,INK,4);rr(-28,-109,56,20,3,'#b9c7be',INK,3);});
        if(pose.tool==='roll')roll(hand[0]+8,hand[1]-15,pose.toolAngle||0,.7);
      });
    },scale);
  }
  function speech(s,x,y,t,at,until,w=330){if(t<at||t>until)return;const a=phase(t,at,at+.22),k=1-Math.pow(1-a,3);group(x,y,0,()=>{rr(-w/2,-41,w,82,24,PAPER,INK,4);path(q=>{q.moveTo(-15,40);q.lineTo(-29,68);q.lineTo(22,40);},PAPER,INK,4);text(s,0,2,30);},k);}
  function punch(t,ep){let p=ease(t,17.3,17.8);if(!p)return;group(540,1763,0,()=>{text(punches[ep],0,0,ep===2?65:72);text('WALLO & PIPPA',0,80,25,INK,'Space Mono');},p);}
  function particles(t,start,x,y,dir=1,color=CORAL){let z=t-start;if(z<0||z>1.65)return;for(let i=0;i<32;i++){let vx=dir*(200+rnd(i+1)*370),vy=-410+rnd(i+9)*390;let px=x+vx*z,py=y+vy*z+300*z*z;ellipse(px,py,5+rnd(i+22)*10,7+rnd(i+33)*14,color);}}
  function ribbon(t){
    const p=ease(t,5,8);c.save();c.globalAlpha=p;
    path(q=>{q.moveTo(220,1530);q.bezierCurveTo(460,1460,640,1650,865,1510);},null,INK,41);
    path(q=>{q.moveTo(220,1530);q.bezierCurveTo(460,1460,640,1650,865,1510);},null,'#f4dca0',32);c.restore();
  }
  function episodeOne(t){
    const finish=ease(t,15,17.3);room(t,0,finish);
    let wx=280,px=780,wp={tool:'roll',right:[106,-323]},pp={left:[-125,-370],expr:'smile'};
    if(t>3&&t<6){wp={expr:'shock',right:[150,-295],headTilt:-.13};pp={expr:'suspicious',headTilt:.1};}
    if(t>=6&&t<11){wx=mix(280,570,ease(t,6,8))-mix(0,250,ease(t,8.3,11));wp={run:1,lean:.18,expr:'shock',look:1};pp={kick:Math.sin(phase(t,7.7,8.7)*Math.PI),expr:'shock',left:[-136,-349]};}
    if(t>=11&&t<15){wx=430;px=674;wp={lean:.10*Math.sin(t*14),right:[132,-467],left:[-111,-431],expr:'shock',headTilt:.13*Math.sin(t*9)};pp={lean:-.10*Math.sin(t*14),right:[124,-461],left:[-131,-472],expr:'shock',headTilt:-.12*Math.sin(t*9)};}
    if(t>=15){wx=mix(430,282,ease(t,15,17));px=mix(674,780,ease(t,15,17));wp={left:[-112,-380],right:[110,-478+finish*100],expr:t>17?'laugh':'shock'};pp={left:[-118,-472+finish*100],right:[99,-364],expr:t>17?'laugh':'shock'};}
    if(t>5&&t<15)ribbon(t);
    character('wallo',wx,1515,t,wp);character('pippa',px,1515,t,pp);
    if(t>=3&&t<6){let z=phase(t,3,6);roll(mix(400,784,z),mix(1135,1510,z)-210*Math.sin(z*Math.PI),z*11,.78);}
    if(t>=6&&t<11){let z=phase(t,6,11),x=z<.46?mix(784,956,z/.46):mix(956,281,(z-.46)/.54);roll(x,1500-Math.abs(Math.sin(z*Math.PI*3))*85,z*29,.85);}
    if(t>=11&&t<15.1){const b=ease(t,11,11.8)*(1-ease(t,14.7,15.1));c.save();c.globalAlpha=b;for(let j=0;j<4;j++){const y=1155+j*81+Math.sin(t*12+j)*7;path(q=>{q.moveTo(326,y);q.bezierCurveTo(425,y+43,697,y-30,783,y+5);},null,INK,58);path(q=>{q.moveTo(326,y);q.bezierCurveTo(425,y+43,697,y-30,783,y+5);},null,'#f4dca0',49);for(let i=0;i<6;i++)leaf(350+i*77,y+10,.3,.3);}c.restore();for(let i=0;i<5;i++)star(490+Math.cos(t*5+i)*149,871+Math.sin(t*5+i)*24,13,GOLD,t);}
    speech('I’VE GOT THIS.',298,749,t,.8,3.1);speech('THE PAPER HAS YOU.',650,668,t,12.3,15,414);punch(t,0);
  }
  function episodeTwo(t){
    room(t,1,.55);let paint=ease(t,.7,4);
    rr(642,537,231,650*paint,26,CORAL);for(let i=0;i<6;i++)rr(637+i*40,552+650*paint,16,22+18*Math.sin(i),8,CORAL);
    if(t>7.8){splat(435,745,194,CORAL,1);ellipse(281,611,16,22,CORAL);ellipse(614,833,14,20,CORAL);ellipse(313,918,18,24,CORAL);}
    if(t>13){const p=ease(t,13,15.4);ellipse(377,711,42*p,50*p,PAPER,INK,3);ellipse(475,711,42*p,50*p,PAPER,INK,3);ellipse(385,716,15*p,22*p,INK);ellipse(482,716,15*p,22*p,INK);if(p>.7)path(q=>{q.moveTo(365,783);q.quadraticCurveTo(431,865,504,779);},null,INK,12);}
    let wp={tool:'roll',right:[107,-335],expr:'smile',look:-1},pp={tool:'roller',right:[100,-449-Math.sin(t*6)*64],toolAngle:-.20,lean:Math.sin(t*6)*.03};
    if(t>4&&t<7){pp={tool:'roller',right:[91+Math.sin(t*6)*29,-427],toolAngle:mix(-.4,1.9,ease(t,4,6.7)),headTilt:.2,expr:'laugh',lean:.12};wp={expr:'suspicious',headTilt:-.13,look:1};}
    if(t>=7&&t<10){pp={tool:'roller',right:[-42,-511],toolAngle:-1.15,expr:'shock',left:[-116,-361],lean:-.08};wp={duck:Math.sin(phase(t,7,10)*Math.PI),headY:75*Math.sin(phase(t,7,10)*Math.PI),left:[-70,-490],right:[89,-489],expr:'shock',lean:-.15};}
    if(t>=10&&t<13){wp={paint:true,expr:'suspicious',headTilt:.13,right:[97,-349],look:1};pp={tool:'roller',right:[113,-253],toolAngle:.35,expr:'suspicious',headTilt:-.25,look:-1};}
    if(t>=13&&t<17){wp={paint:true,tool:'roll',right:[114,-467],left:[-94,-354],expr:'smile',lean:.11};pp={right:[104,-349],left:[-133,-443],expr:'laugh',headTilt:.11};}
    if(t>=17){wp={paint:true,expr:'laugh',right:[107,-423],headTilt:-.08};pp={expr:'laugh',left:[-116,-420],headTilt:.1};}
    character('wallo',287,1515,t,wp);character('pippa',758,1515,t,pp);
    particles(t,6.9,747,936,-1);speech('JUST A LITTLE FLAIR.',697,697,t,4.2,6.5,430);speech('CALL IT MODERN ART.',579,1100,t,15,17.6,448);punch(t,1);
  }
  function episodeThree(t){
    let progress=ease(t,3,10);room(t,2,progress);
    rr(559,486,392,930*progress,0,CORAL);
    if(t>10&&t<14){line(537,498,537,1403,INK,7);for(let i=0;i<9;i++)line(527,523+i*96,547,549+i*96,INK,5);}
    if(t>=13){const p=ease(t,13,17);rr(506,486,80,930*p,0,TEAL);for(let i=0;i<9;i++)if(p>(i+1)/10)leaf(546,540+i*95,.53,Math.PI/2);}
    let wp={tool:'roll',right:[121,-331],expr:'suspicious',headTilt:.13},pp={tool:'roller',right:[110,-352],expr:'suspicious',headTilt:-.13};
    if(t>=3&&t<10){let armY=-408-Math.sin(t*13)*86;wp={tool:'roll',right:[103,armY],left:[-109,-350-Math.cos(t*13)*40],lean:.06*Math.sin(t*13),run:.23,fast:true};pp={tool:'roller',right:[92,armY],toolAngle:Math.sin(t*13)*.16,lean:-.06*Math.sin(t*13),run:.23,fast:true};}
    if(t>=10&&t<13){wp={right:[108,-441],left:[-122,-403],expr:t<11?'laugh':'suspicious',headTilt:.1};pp={right:[128,-403],left:[-120,-446],expr:t<11?'laugh':'suspicious',headTilt:-.1};}
    if(t>=13&&t<17){wp={tool:'roller',right:[140,-414-Math.sin(t*8)*62],expr:'smile',lean:.14};pp={tool:'roll',right:[-30,-455-Math.sin(t*8)*51],left:[-140,-395],expr:'smile',lean:-.13};}
    let high=ease(t,17,17.6),recover=ease(t,18.3,19);let wx=mix(288,347,high),px=mix(766,708,high);
    if(t>=17){wp={right:[mix(116,172,high),mix(-352,-450,high)],left:[-112,-344],expr:'laugh',capPaint:t>18.1,headTilt:.07,hop:Math.sin(phase(t,17.2,18.2)*Math.PI)*15};pp={left:[mix(-116,-172,high),mix(-352,-450,high)],right:[110,-342],expr:'laugh',headTilt:-.08,hop:Math.sin(phase(t,17.2,18.2)*Math.PI)*15};if(recover>0){wp.right=[mix(172,115,recover),mix(-450,-382,recover)];pp.left=[mix(-172,-120,recover),mix(-450,-382,recover)];}}
    character('wallo',wx,1515,t,wp);character('pippa',px,1515,t,pp);
    if(t<3){const n=3-Math.floor(t),p=(t%1);group(538,746,0,()=>{ellipse(0,0,72,72,PAPER,INK,5);text(String(n),0,4,86);},.9+.1*Math.sin(p*Math.PI));}
    speech('RACE YOU!',733,702,t,.5,2.5,238);speech('HMM. TEAM UP?',540,714,t,11.3,13.3,333);
    if(t>=17.65&&t<18.3){const z=phase(t,17.65,18.3);for(let i=0;i<9;i++){const a=i*Math.PI*2/9;star(526+Math.cos(a)*(25+z*123),1027+Math.sin(a)*(25+z*98),13*(1-z),i%2?GOLD:CORAL,a+z);}}
    punch(t,2);
  }
  function draw(t,ep=episode){c.clearRect(0,0,1080,1920);[episodeOne,episodeTwo,episodeThree][ep](clamp(t,0,20));}
  window.drawFrame=draw;
  window.exportCharacter=(kind)=>{c.clearRect(0,0,1080,1920);character(kind,540,1410,1,{scale:1.9,tool:kind==='wallo'?'roll':'roller',right:[105,-350],expr:'smile'});return canvas.toDataURL('image/png');};
  const driver={time:0};
  const tl=gsap.timeline({paused:true});
  tl.fromTo(driver,{time:0},{time:20,duration:20,ease:'none',onUpdate:()=>draw(driver.time)},0);
  window.__timelines=window.__timelines||{};
  window.__timelines['main']=tl;
  draw(0);
})();
</script></body></html>