You make short animated pieces for the web, and you have spent time on the part of the internet that makes cats famous.
Build me a single HTML page: a cat in a living room, which spins when you click it.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.Two sentences: what you think you are being asked to recreate, and what the opening frame has to look like for it to land. If you do not recognize it, say so and build the brief as written — saying so costs you nothing here.
The single fenced html block.
Four bullets.
This is the spinning-cat meme — the "Maxwell"/vibing-cat genre, where a completely ordinary photo of a cat in a living room suddenly rotates on its vertical axis with that warbling "wah-wah-wah" vocal. For it to land, frame one has to read as a boring, well-lit snapshot you'd scroll past: believable afternoon light from the window, a real floor under the cat, grain and depth of field — and only then does the punchline detonate.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>living room · cat</title>
<style>
html,body{margin:0;height:100%;background:#0d0b09;overflow:hidden}
canvas{position:fixed;inset:0;display:block;touch-action:none}
#hint{position:fixed;left:50%;bottom:30px;transform:translateX(-50%);
font:11px/1 "Courier New",monospace;letter-spacing:.28em;color:#cfc5b4;
opacity:0;transition:opacity 1.6s ease;pointer-events:none}
#hint.show{opacity:.7}
#mute{position:fixed;top:18px;right:18px;width:42px;height:42px;border-radius:50%;
border:1px solid rgba(240,230,210,.18);background:rgba(18,15,12,.55);color:#e6ddcc;
display:flex;align-items:center;justify-content:center;cursor:pointer;
opacity:0;pointer-events:none;transition:opacity .9s ease}
#mute.show{opacity:1;pointer-events:auto}
#mute:hover{background:rgba(32,26,20,.72)}
</style>
</head>
<body>
<canvas id="cv"></canvas>
<div id="hint">click the cat · sound on</div>
<button id="mute" aria-label="toggle sound">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor"
stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M11 5 6.5 9H3.5v6h3L11 19V5z" fill="currentColor"/>
<g class="ic-on"><path d="M15 9.4a3.6 3.6 0 0 1 0 5.2"/><path d="M17.6 7a7 7 0 0 1 0 10"/></g>
<g class="ic-off" style="display:none"><path d="M15.5 9.7l4.6 4.6M20.1 9.7l-4.6 4.6"/></g>
</svg>
</button>
<script>
(()=>{'use strict';
/* ================= helpers ================= */
const TAU=Math.PI*2;
const clamp=(v,a,b)=>v<a?a:v>b?b:v, lerp=(a,b,t)=>a+(b-a)*t;
const lerp2=(A,B,t)=>[lerp(A[0],B[0],t),lerp(A[1],B[1],t)];
const lerp4=(A,B,t)=>[lerp(A[0],B[0],t),lerp(A[1],B[1],t),lerp(A[2],B[2],t),lerp(A[3],B[3],t)];
const sstep=(a,b,x)=>{x=clamp((x-a)/(b-a),0,1);return x*x*(3-2*x)};
const easeIO=t=>t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2;
const mkRnd=s=>()=>((s=(s*1103515245+12345)&0x7fffffff)/0x7fffffff);
const cvEl=document.getElementById('cv'), ctx=cvEl.getContext('2d');
const hintEl=document.getElementById('hint'), muteBtn=document.getElementById('mute');
const icoOn=muteBtn.querySelector('.ic-on'), icoOff=muteBtn.querySelector('.ic-off');
/* design space is 1600x1000, cover-fitted to the window */
let W=0,H=0,dpr=1,cov=1,cox=0,coy=0;
let bg=null,fg=null,bgR=0,bgPD=1,fgPD=1,vigG=null,grA=null;
const PV=[660,812]; /* room pivot = cat anchor (design units) */
const CS=0.8, OX=500, OY=640, CW=1000, CH=710, NB=6;
let bufs=[],bctxs=[],bq=1,bi=0;
const grains=[];
/* ================= state ================= */
const S={t:0,level:0,started:false,yaw:0,thetaR:0,lift:0};
const STG=[null,
{spin:11, rf:0, sh:1.4, tr:2, fl:4, zp:.005, st:0, hu:0, lf:0},
{spin:23, rf:.22, sh:3.4, tr:4, fl:9, zp:.011, st:0, hu:0, lf:0},
{spin:38, rf:.6, sh:6.5, tr:6, fl:16, zp:.019, st:2, hu:.05, lf:0},
{spin:56, rf:1, sh:10, tr:8, fl:26, zp:.03, st:4, hu:.10, lf:1}];
const V={spin:0,rf:0,sh:0,tr:0,fl:0,zp:0,st:0,hu:0,lf:0};
const T={spin:0,rf:0,sh:0,tr:0,fl:0,zp:0,st:0,hu:0,lf:0};
const ENV={env:0,open:0,phase:0,rate:5};
let SPN=0,CURLV=0,cloudV=1,liftV=0,perkV=0,earRotV=0,dilV=.1,blinkV=0,blinkPh=2;
let nextBlink=3,nextTwitch=6,twitchT=9,atkT=-9,morphT0=-1,pdp=[900,480];
let debris=[],motes=[];
function stageT(l){
if(l<=4)return STG[l];
return {spin:Math.min(78,56+(l-4)*4),rf:1,sh:11,tr:Math.min(10,8+(l-4)),fl:28,
zp:.034,st:5,hu:.12,lf:1};
}
function deb(t,a,r){return{t,a0:a,r,age:0};}
function spawnDebris(l){
if(l===3)debris.push(deb('mouse',.42,215),deb('book',2.4,300),deb('mug',4.3,340));
if(l===4)debris.push(deb('leaf',1.3,370),deb('frame',5.4,395),deb('cushion',3.4,430));
}
/* ================= cat rigs ================= */
const FRONT_SEGS=[['M',-188,-2],
['C',-252,-40,-258,-150,-212,-232],['C',-176,-296,-122,-318,-96,-352],
['C',-78,-376,-70,-392,-64,-404],['C',-20,-424,20,-424,64,-404],
['C',70,-392,78,-376,96,-352],['C',122,-318,176,-296,212,-232],
['C',258,-150,252,-40,188,-2],['C',120,14,-120,14,-188,-2]];
const PROF_SEGS=[['M',-172,-2],
['C',-238,-30,-252,-140,-196,-212],['C',-160,-258,-108,-272,-80,-310],
['C',-52,-348,-30,-368,-6,-382],['C',30,-400,60,-402,84,-392],
['C',96,-360,96,-330,92,-300],['C',90,-240,92,-120,94,-30],
['C',94,-8,78,-2,56,-2],['C',0,10,-80,12,-172,-2]];
function samplePath(segs,per){
const pts=[];let cur=null;
for(const s of segs){
if(s[0]==='M'){cur=[s[1],s[2]];pts.push([cur[0],cur[1]]);}
else{const x1=s[1],y1=s[2],x2=s[3],y2=s[4],x=s[5],y=s[6],x0=cur[0],y0=cur[1];
for(let i=1;i<=per;i++){const t=i/per,u=1-t;
pts.push([u*u*u*x0+3*u*u*t*x1+3*u*t*t*x2+t*t*t*x,
u*u*u*y0+3*u*u*t*y1+3*u*t*t*y2+t*t*t*y]);}
cur=[x,y];}}
return pts;
}
function resample(pts,n){
const L=[0];
for(let i=1;i<pts.length;i++)L.push(L[i-1]+Math.hypot(pts[i][0]-pts[i-1][0],pts[i][1]-pts[i-1][1]));
const tot=L[L.length-1],out=[];let j=0;
for(let k=0;k<n;k++){const tg=tot*k/n;
while(j<L.length-2&&L[j+1]<tg)j++;
const t=(tg-L[j])/((L[j+1]-L[j])||1);
out.push([lerp(pts[j][0],pts[j+1][0],t),lerp(pts[j][1],pts[j+1][1],t)]);}
return out;
}
const BODY_F=resample(samplePath(FRONT_SEGS,26),64);
const BODY_P=resample(samplePath(PROF_SEGS,26),64);
function bodyPts(e){return BODY_F.map((p,i)=>[lerp(p[0],BODY_P[i][0],e),lerp(p[1],BODY_P[i][1],e)]);}
function normals(pts){const n=pts.length,out=[];
for(let i=0;i<n;i++){const a=pts[(i-1+n)%n],b=pts[(i+1)%n];
let dx=b[0]-a[0],dy=b[1]-a[1];const l=Math.hypot(dx,dy)||1;
let nx=-dy/l,ny=dx/l;
if(nx*pts[i][0]+ny*(pts[i][1]+210)<0){nx=-nx;ny=-ny;}
out.push([nx,ny]);}
return out;}
function pathThrough(g,pts){const n=pts.length;g.beginPath();
g.moveTo((pts[0][0]+pts[n-1][0])/2,(pts[0][1]+pts[n-1][1])/2);
for(let i=0;i<n;i++){const p=pts[i],q=pts[(i+1)%n];
g.quadraticCurveTo(p[0],p[1],(p[0]+q[0])/2,(p[1]+q[1])/2);}
g.closePath();}
function rr(g,x,y,w,h,r){g.beginPath();g.moveTo(x+r,y);g.arcTo(x+w,y,x+w,y+h,r);
g.arcTo(x+w,y+h,x,y+h,r);g.arcTo(x,y+h,x,y,r);g.arcTo(x,y,x+w,y,r);g.closePath();}
function strokeQuad(g,x0,y0,cx,cy,x1,y1){g.beginPath();g.moveTo(x0,y0);
g.quadraticCurveTo(cx,cy,x1,y1);g.stroke();}
/* stable fur (same seed every frame so it breathes with the cat, not crawls) */
const R1=mkRnd(7),FUR=[],HFUR=[];
for(let i=0;i<330;i++)FUR.push({a:R1(),v:Math.pow(R1(),.8),len:6+R1()*8,jx:(R1()-.5)*14,jy:(R1()-.5)*8,t:R1()});
for(let i=0;i<130;i++)HFUR.push({a:R1(),v:R1(),len:5+R1()*7,jx:(R1()-.5)*10,t:R1()});
/* ================= cat parts ================= */
function drawTail(g,st){
const e=easeIO(st.e),liftT=st.tailLift,sw=st.tailSway,ext=st.tailExt;
const F=[[150,-54],[204,-46-liftT*30+sw*.2],[262,-38-liftT*72+sw*.45],
[322+ext*.35,-28-liftT*112+sw*.75],[378+ext*.65,-18-liftT*148+sw*1.1],
[428+ext*.9,-8-liftT*178+sw*1.5]];
const P=[[-138,-34],[-186,-20],[-178,-8],[-84,-6],[16,-10],[82,-42+st.tipFlick]];
const WD=[23,20,17,13,9,6],pts=[];
for(let i=0;i<6;i++)pts.push(lerp2(P[i],F[i],e));
for(const pass of[[3,4,'rgba(25,20,16,0.16)'],[0,0,'#848b95']]){
for(let i=0;i<5;i++){
g.strokeStyle=pass[2];g.lineWidth=WD[i]+pass[0];g.lineCap='round';
const mx=(pts[i][0]+pts[i+1][0])/2,my=(pts[i][1]+pts[i+1][1])/2;
g.beginPath();g.moveTo(pts[i][0],pts[i][1]+pass[1]);
g.quadraticCurveTo(mx,my+pass[1],pts[i+1][0],pts[i+1][1]+pass[1]);g.stroke();}}
g.fillStyle='rgba(74,80,90,0.85)';
for(const i of[2,4]){g.beginPath();g.arc(pts[i][0],pts[i][1],WD[i]*.6,0,TAU);g.fill();}
g.fillStyle='#9ba2ab';g.beginPath();g.arc(pts[5][0],pts[5][1],WD[5]*.7,0,TAU);g.fill();
}
function silhouetteFluff(g,bp,nrm,flare){
g.lineCap='round';
for(let i=0;i<64;i+=2){const p=bp[i],n=nrm[i];
const l=4+((i*7)%5)+flare*(.4+((i*13)%7)/14);
g.strokeStyle=i%4?'#787f89':'#9ba2ab';g.globalAlpha=i%6?.30:.20;g.lineWidth=i%6?2:1.4;
g.beginPath();g.moveTo(p[0]+n[0]*2,p[1]+n[1]*2);
g.lineTo(p[0]+n[0]*(2+l),p[1]+n[1]*(2+l));g.stroke();}
g.globalAlpha=1;
}
function furBody(g,bp,nrm,q){
const st=1+((1-q)*2|0);
for(let i=0;i<FUR.length;i+=st){const f=FUR[i];
const idx=f.a*63,i0=idx|0,fr=idx-i0,p0=bp[i0],p1=bp[(i0+1)%64];
const px=lerp(p0[0],p1[0],fr)+nrm[i0][0]*(-5-f.v*95)+f.jx;
const py=lerp(p0[1],p1[1],fr)+nrm[i0][1]*(-5-f.v*95)+f.jy;
let dx=nrm[i0][0]*.6+.12+f.jx*.03,dy=nrm[i0][1]*.6+.85;
const dl=Math.hypot(dx,dy);dx/=dl;dy/=dl;
let col;
if(f.t>.72&&py>-340&&py<-40&&Math.abs(px)<80)col='#d5cec2';
else if(py<-300)col=f.t>.5?'#a9b0b8':'#98a0a9';
else if(py<-120)col=f.t>.5?'#8d949e':'#7c838d';
else col=f.t>.5?'#71787f':'#62686f';
g.strokeStyle=col;g.globalAlpha=.24;g.lineWidth=2.3;
const ln=f.len*(q*.6+.4);
g.beginPath();g.moveTo(px,py);g.lineTo(px+dx*ln,py+dy*ln);g.stroke();}
g.globalAlpha=1;
}
function bodyStripes(g,mnx,mxx,mny,mxy){
const cx=(mnx+mxx)/2,cy=(mny+mxy)/2,bw=mxx-mnx,bh=mxy-mny;
g.lineCap='round';
for(const s of[-1,1])for(let k=0;k<3;k++){
const x0=cx+s*bw*(.30+k*.13),y0=cy-bh*(.20-k*.02),y1=cy+bh*.30;
g.strokeStyle='#4a505a';
g.globalAlpha=.12;g.lineWidth=17;
strokeQuad(g,x0,y0,x0+s*20,cy+bh*.06,x0-s*6,y1);
g.globalAlpha=.20;g.lineWidth=9;
strokeQuad(g,x0,y0,x0+s*20,cy+bh*.06,x0-s*6,y1);}
g.globalAlpha=1;
}
function chest(g,st){
const a=st.chestA*clamp(st.e*2,0,1);if(a<.02)return;
const c=lerp4([66,-296,34,80],[0,-292,62,94],st.e);
g.globalAlpha=.95*a;g.fillStyle='#d7d0c4';
g.beginPath();g.ellipse(c[0],c[1],c[2],c[3],0,0,TAU);g.fill();
g.strokeStyle='#d7d0c4';g.lineWidth=2.4;g.globalAlpha=.5*a;
for(let i=0;i<12;i++){const x=c[0]+Math.sin(i*2.4)*c[2]*.8,y=c[1]+(i%4-1.5)*c[3]*.4;
g.beginPath();g.moveTo(x,y);g.lineTo(x+(i%3-1)*4,y+14+(i%5)*3);g.stroke();}
g.globalAlpha=1;
}
function drawPaws(g,e){
const L=lerp2([38,-9],[-52,-9],e),R=lerp2([88,-11],[52,-11],e);
for(const[p,w]of[[L,lerp(44,58,e)],[R,lerp(40,58,e)]]){
g.fillStyle='rgba(20,16,12,0.20)';
g.beginPath();g.ellipse(p[0],-2,w*.62,7,0,0,TAU);g.fill();
g.fillStyle='#d8d1c5';rr(g,p[0]-w/2,-24,w,25,11);g.fill();
g.strokeStyle='rgba(120,112,96,0.5)';g.lineWidth=1.4;
g.beginPath();g.moveTo(p[0]-w*.16,-19);g.lineTo(p[0]-w*.16,-8);g.stroke();
g.beginPath();g.moveTo(p[0]+w*.16,-19);g.lineTo(p[0]+w*.16,-8);g.stroke();
g.fillStyle='rgba(255,250,240,0.25)';rr(g,p[0]-w/2+3,-23,w-6,5,3);g.fill();}
}
function drawEye(g,ex,ey,rx,ry,dil,blink,look,alpha){
if(alpha<.03)return;
g.save();g.translate(ex,ey);g.globalAlpha=alpha;
g.fillStyle='rgba(55,55,66,0.30)';
g.beginPath();g.ellipse(0,0,rx+2.6,ry+2.4,0,0,TAU);g.fill();
const ig=g.createRadialGradient(rx*.2,-ry*.2,1,0,0,rx+1);
ig.addColorStop(0,'#eec069');ig.addColorStop(.7,'#cf9434');ig.addColorStop(1,'#8f5f1e');
g.fillStyle=ig;g.beginPath();g.ellipse(0,0,rx,ry,0,0,TAU);g.fill();
g.strokeStyle='rgba(110,66,18,0.5)';g.lineWidth=.8;
for(let k=0;k<5;k++){const a=-1.2+k*.6;
g.beginPath();g.moveTo(Math.cos(a)*rx*.25,Math.sin(a)*ry*.25);
g.lineTo(Math.cos(a)*rx*.85,Math.sin(a)*ry*.85);g.stroke();}
g.fillStyle='#171310';
g.beginPath();g.ellipse(look[0]*.4,look[1]*.4,rx*(.28+.42*dil),ry*(.78+.2*dil),0,0,TAU);g.fill();
g.fillStyle='rgba(255,252,242,0.95)';
g.beginPath();g.arc(rx*.34+look[0]*.3,-ry*.42+look[1]*.3,Math.max(1.4,rx*.16),0,TAU);g.fill();
g.fillStyle='rgba(255,252,242,0.5)';
g.beginPath();g.arc(-rx*.3,ry*.3,Math.max(.8,rx*.08),0,TAU);g.fill();
if(blink>.02){g.save();g.beginPath();g.ellipse(0,0,rx,ry,0,0,TAU);g.clip();
g.fillStyle='#8a919b';g.fillRect(-rx-2,-ry-2,(rx+2)*2,(2*ry+4)*blink);
g.strokeStyle='rgba(45,45,52,0.6)';g.lineWidth=1.2;
const ly=-ry+2*ry*blink;
g.beginPath();g.moveTo(-rx,ly);g.lineTo(rx,ly);g.stroke();g.restore();}
g.restore();
}
function drawEar(g,A,B,tip,isNear,st){
tip=[tip[0],tip[1]-st.perk*12];
const mx=(A[0]+B[0])/2,my=(A[1]+B[1])/2;
g.save();
if(isNear&&st.earRot){g.translate(mx,my);g.rotate(st.earRot);g.translate(-mx,-my);}
const out=tip[0]>=mx?1:-1;
g.fillStyle=isNear?'#868d97':'#7b828c';
g.beginPath();g.moveTo(A[0],A[1]);
g.quadraticCurveTo(lerp(A[0],tip[0],.45)+out*10,lerp(A[1],tip[1],.45)-6,tip[0],tip[1]);
g.quadraticCurveTo(lerp(B[0],tip[0],.45)+out*4,lerp(B[1],tip[1],.45)-2,B[0],B[1]);
g.closePath();g.fill();
g.strokeStyle='rgba(60,66,76,0.35)';g.lineWidth=1.2;g.stroke();
g.strokeStyle='rgba(150,157,166,0.55)';g.lineWidth=1.6;
for(let k=-1;k<=1;k++){g.beginPath();g.moveTo(tip[0],tip[1]);
g.lineTo(tip[0]+out*5+k*2,tip[1]-6-Math.abs(k)*2);g.stroke();}
if(st.eyeA>.04){const cx=(A[0]+B[0]+tip[0])/3,cy=(A[1]+B[1]+tip[1])/3,sh=.45;
g.fillStyle=`rgba(206,155,146,${(isNear?.8:.5)*st.eyeA})`;
g.beginPath();g.moveTo(cx+(A[0]-cx)*sh,cy+(A[1]-cy)*sh);
g.lineTo(cx+(tip[0]-cx)*sh*1.2,cy+(tip[1]-cy)*sh*1.2);
g.lineTo(cx+(B[0]-cx)*sh,cy+(B[1]-cy)*sh);g.closePath();g.fill();}
g.restore();
}
function drawHead(g,st){
const e=st.e,eyeA=st.eyeA,q=st.q;
const hc=lerp2([46,-412],[0,-420],e),hrx=lerp(76,94,e),hry=lerp(70,84,e);
const cyh=hc[1]+ -st.bob;
/* cheek fluff */
g.strokeStyle='rgba(150,157,166,0.5)';g.lineWidth=2;
for(const sd of[-1,1])for(let i=0;i<5;i++){
const a=Math.PI*(.10+.09*i),sx=Math.cos(a)*sd,sy=Math.sin(a);
const px=hc[0]+sx*hrx*.96,py=cyh+sy*hry*.96;
g.beginPath();g.moveTo(px,py);g.lineTo(px+sx*7,py+sy*7+3);g.stroke();}
drawEar(g,lerp2([22,-468],[12,-490],e),lerp2([82,-464],[66,-476],e),lerp2([48,-556],[52,-568],e),true,st);
drawEar(g,lerp2([-6,-462],[-66,-478],e),lerp2([36,-464],[-12,-492],e),lerp2([6,-530],[-52,-568],e),false,st);
/* skull + muzzle */
const hg=g.createLinearGradient(hc[0]-hrx,0,hc[0]+hrx,0);
hg.addColorStop(0,'#7d848e');hg.addColorStop(.55,'#9097a1');hg.addColorStop(1,'#a2a9b2');
g.fillStyle=hg;g.beginPath();g.ellipse(hc[0],cyh,hrx,hry,0,0,TAU);g.fill();
const mz=lerp4([100,-382,36,26],[0,-374,36,26],e);
g.fillStyle='#a2a8b1';g.beginPath();g.ellipse(mz[0],mz[1]+ -st.bob*.8,mz[2],mz[3],0,0,TAU);g.fill();
/* clipped head detail */
g.save();
g.beginPath();g.ellipse(hc[0],cyh,hrx,hry,0,0,TAU);
g.ellipse(mz[0],mz[1]+ -st.bob*.8,mz[2],mz[3],0,0,TAU);g.clip();
const hst=1+((1-q)*2|0);
for(let i=0;i<HFUR.length;i+=hst){const f=HFUR[i];
const a=f.a*TAU,vx=Math.cos(a),vy=Math.sin(a);
const px=hc[0]+vx*hrx*(1-f.v*.4)+f.jx,py=cyh+vy*hry*(1-f.v*.4);
let dx=-vy*.5+vx*.6,dy=vx*.5+vy*.6+.4;const dl=Math.hypot(dx,dy);dx/=dl;dy/=dl;
g.strokeStyle=f.t>.5?'#9aa1aa':'#878e98';g.globalAlpha=.22;g.lineWidth=2;
g.beginPath();g.moveTo(px,py);g.lineTo(px+dx*f.len,py+dy*f.len);g.stroke();}
g.globalAlpha=1;
/* forehead tabby marks */
const fx=lerp(38,0,e);
g.strokeStyle='#4a505a';g.lineCap='round';
for(const dx of[-16,0,16]){
g.globalAlpha=.30*(.4+.6*eyeA);g.lineWidth=5;
strokeQuad(g,fx+dx,cyh-hry*.72,fx+dx*1.3,cyh-hry*.45,fx+dx*.9,cyh-hry*.28);}
g.globalAlpha=1;
g.fillStyle='rgba(20,26,38,0.16)';
g.beginPath();g.ellipse(hc[0],cyh+hry*.55,hrx*.75,hry*.4,0,0,TAU);g.fill();
const bg2=g.createRadialGradient(hc[0]+hrx*.35,cyh-hry*.35,4,hc[0]+hrx*.35,cyh-hry*.35,hrx*.8);
bg2.addColorStop(0,'rgba(255,240,210,0.10)');bg2.addColorStop(1,'rgba(255,240,210,0)');
g.fillStyle=bg2;g.beginPath();g.arc(hc[0]+hrx*.35,cyh-hry*.35,hrx*.8,0,TAU);g.fill();
const cch=lerp2([100,-360],[0,-350],e);
g.fillStyle='rgba(216,209,197,0.9)';
g.beginPath();g.ellipse(cch[0],cch[1]+ -st.bob*.8,26,16,0,0,TAU);g.fill();
if(e>.5&&eyeA<.55){g.fillStyle=`rgba(86,92,102,${(0.55-eyeA)*1.2})`;
g.beginPath();g.ellipse(hc[0],cyh-6,hrx*.66,hry*.5,0,0,TAU);g.fill();}
g.restore();
g.strokeStyle='rgba(255,220,170,0.20)';g.lineWidth=3;
g.beginPath();g.ellipse(hc[0],cyh,hrx-1.5,hry-1.5,0,-.9,.9);g.stroke();
/* eyes */
const eN=lerp2([74,-406],[36,-414],e),eF=lerp2([20,-402],[-36,-414],e);
drawEye(g,eN[0],eN[1]+ -st.bob,lerp(13,15.5,e),lerp(11,13,e),st.dil,st.blink,st.look,eyeA);
drawEye(g,eF[0],eF[1]+ -st.bob,lerp(4.5,15.5,e),lerp(8,13,e),st.dil,st.blink,st.look,eyeA);
/* nose / mouth / whiskers — fade out as the head turns away */
g.globalAlpha=eyeA;
const nz=lerp2([112,-386],[0,-386],e);const nzy=nz[1]+ -st.bob*.8,nw=lerp(9,13,e);
g.fillStyle='#cd9a92';
g.beginPath();g.moveTo(nz[0]-nw,nzy-4);
g.quadraticCurveTo(nz[0],nzy-7,nz[0]+nw,nzy-4);
g.quadraticCurveTo(nz[0]+nw*.4,nzy+7,nz[0],nzy+7);
g.quadraticCurveTo(nz[0]-nw*.4,nzy+7,nz[0]-nw,nzy-4);g.closePath();g.fill();
g.fillStyle='rgba(255,235,230,0.5)';
g.beginPath();g.ellipse(nz[0]-nw*.3,nzy-2,2.4,1.6,0,0,TAU);g.fill();
if(st.open<.08){
g.strokeStyle='rgba(70,62,66,0.85)';g.lineWidth=1.8;g.lineCap='round';
const my=nzy+9;
if(e>.5){
g.beginPath();g.moveTo(nz[0],my-2);g.quadraticCurveTo(nz[0]-7,my+5,nz[0]-13,my);g.stroke();
g.beginPath();g.moveTo(nz[0],my-2);g.quadraticCurveTo(nz[0]+7,my+5,nz[0]+13,my);g.stroke();
}else{
g.beginPath();g.moveTo(nz[0]-4,nzy+8);g.quadraticCurveTo(nz[0]-14,nzy+14,nz[0]-22,nzy+10);g.stroke();}
}else{
const mo=lerp2([96,-368],[0,-362],e),moy=mo[1]+ -st.bob*.8;
const rx=6+30*st.open,ry=5+24*st.open;
g.fillStyle='#39242a';g.beginPath();g.ellipse(mo[0],moy,rx,ry,0,0,TAU);g.fill();
g.save();g.beginPath();g.ellipse(mo[0],moy,rx,ry,0,0,TAU);g.clip();
g.fillStyle='#d98b85';g.beginPath();g.ellipse(mo[0],moy+ry*.45,rx*.72,ry*.6,0,0,TAU);g.fill();
g.restore();
g.strokeStyle='rgba(30,16,20,0.55)';g.lineWidth=1.5;
g.beginPath();g.ellipse(mo[0],moy,rx,ry,0,0,TAU);g.stroke();}
/* whiskers */
g.lineCap='round';
const wh=(x0,y0,x1,y1,bow,alpha)=>{
const mx=(x0+x1)/2+(y1-y0)*bow*.06,my=(y0+y1)/2-(x1-x0)*bow*.06;
g.strokeStyle=`rgba(45,45,55,${alpha*.35})`;g.lineWidth=1.9;
g.beginPath();g.moveTo(x0+1.5,y0+1.5);g.quadraticCurveTo(mx+1.5,my+1.5,x1+1.5,y1+1.5);g.stroke();
g.strokeStyle=`rgba(245,243,235,${alpha})`;g.lineWidth=1.1;
g.beginPath();g.moveTo(x0,y0);g.quadraticCurveTo(mx,my,x1,y1);g.stroke();};
if(q>.45||Math.abs(st.sx)>.3){
const rL=lerp2([98,-370],[-18,-372],e),rR=lerp2([98,-370],[18,-372],e);
const ePL=[[172,-404],[184,-380],[170,-356]],ePR=[[120,-400],[132,-376],[118,-352]];
const eFL=[[-120,-400],[-132,-376],[-118,-352]];
for(let i=0;i<3;i++){
const sL=lerp2([98,-370],[-18,-372],e);
wh(sL[0],sL[1]+ -st.bob*.8,lerp2([98,-370],eFL[i],e)[0],lerp2([98,-370],eFL[i],e)[1]+ -st.bob*.8,(i-1)*1.2,.55*e);
wh(rR[0],rR[1]+ -st.bob*.8,
lerp(ePL[i][0],ePR[i][0],e),lerp(ePL[i][1],ePR[i][1],e)+ -st.bob*.8,(i-1)*1.2,.55);}
if(e<.9){wh(98,-368,58,-388,-.8,.45*(1-e));wh(98,-366,54,-356,.8,.45*(1-e));}}
g.globalAlpha=1;
}
/* ================= whole cat, rendered into a buffer ================= */
function renderCat(st){
const g=bctxs[bi],c=bufs[bi];
g.setTransform(1,0,0,1,0,0);g.clearRect(0,0,c.width,c.height);
g.setTransform(bq,0,0,bq,OX*bq,OY*bq);
g.scale(1,1+.011*st.breath);g.rotate(st.roll);g.scale(st.sx,1);
drawTail(g,st);
const bp=bodyPts(st.e),nrm=normals(bp);
let mnx=1e9,mxx=-1e9,mny=1e9,mxy=-1e9;
for(const p of bp){if(p[0]<mnx)mnx=p[0];if(p[0]>mxx)mxx=p[0];if(p[1]<mny)mny=p[1];if(p[1]>mxy)mxy=p[1];}
silhouetteFluff(g,bp,nrm,st.flare);
pathThrough(g,bp);
const grd=g.createLinearGradient(mnx*.4,0,mxx,0);
grd.addColorStop(0,'#71787f');grd.addColorStop(.55,'#8b929b');grd.addColorStop(1,'#9ca3ac');
g.fillStyle=grd;g.fill();
g.save();pathThrough(g,bp);g.clip();
const vg=g.createLinearGradient(0,mny,0,mxy+10);
vg.addColorStop(0,'rgba(255,255,255,0.10)');vg.addColorStop(.4,'rgba(255,255,255,0)');
vg.addColorStop(1,'rgba(16,22,34,0.30)');
g.fillStyle=vg;g.fillRect(mnx-20,mny-20,mxx-mnx+40,mxy-mny+60);
const rg=g.createLinearGradient(mxx-(mxx-mnx)*.32,0,mxx+10,0);
rg.addColorStop(0,'rgba(255,213,150,0)');rg.addColorStop(1,'rgba(255,213,150,0.16)');
g.fillStyle=rg;g.fillRect(mxx-(mxx-mnx)*.4,mny-20,(mxx-mnx)*.5+30,mxy-mny+60);
furBody(g,bp,nrm,st.q);
bodyStripes(g,mnx,mxx,mny,mxy);
chest(g,st);
g.fillStyle='rgba(14,20,30,0.20)';
g.beginPath();g.ellipse(0,-12,(mxx-mnx)*.42,22,0,0,TAU);g.fill();
if(st.e>.6){g.strokeStyle='rgba(20,26,38,0.14)';g.lineWidth=5;
g.beginPath();g.moveTo(0,-140);g.lineTo(0,-14);g.stroke();}
g.restore();
drawPaws(g,st.e);
drawHead(g,st);
}
/* ================= the room ================= */
function shadowBand(g,x0,y0,x1,y1,w0,w1,a){
let dx=x1-x0,dy=y1-y0;const l=Math.hypot(dx,dy);dx/=l;dy/=l;
const px=-dy,py=dx;
const gr=g.createLinearGradient(x0+px*w0,y0+py*w0,x0-px*w0,y0-py*w0);
gr.addColorStop(0,'rgba(70,48,24,0)');gr.addColorStop(.5,`rgba(70,48,24,${a})`);
gr.addColorStop(1,'rgba(70,48,24,0)');
g.fillStyle=gr;g.beginPath();
g.moveTo(x0+px*w0,y0+py*w0);g.lineTo(x1+px*w1,y1+py*w1);
g.lineTo(x1-px*w1,y1-py*w1);g.lineTo(x0-px*w0,y0-py*w0);g.closePath();g.fill();
}
function renderRoom(g){
const rnd=mkRnd(101);
let wg=g.createLinearGradient(0,-200,0,566);
wg.addColorStop(0,'#a3927c');wg.addColorStop(.7,'#bcab93');wg.addColorStop(1,'#c6b69d');
g.fillStyle=wg;g.fillRect(-2400,-1600,6400,2166);
let lg=g.createLinearGradient(-400,0,900,0);
lg.addColorStop(0,'rgba(52,64,92,0.14)');lg.addColorStop(1,'rgba(52,64,92,0)');
g.fillStyle=lg;g.fillRect(-2400,-1600,3300,2166);
let wr=g.createRadialGradient(1216,330,60,1216,330,900);
wr.addColorStop(0,'rgba(255,214,150,0.20)');wr.addColorStop(1,'rgba(255,214,150,0)');
g.fillStyle=wr;g.fillRect(300,-500,1900,1700);
for(let i=0;i<260;i++){const x=rnd()*3600-2400,y=rnd()*2100-1600,r=6+rnd()*26;
g.fillStyle=rnd()>.5?'rgba(255,250,235,0.02)':'rgba(60,50,40,0.022)';
g.beginPath();g.arc(x,y,r,0,TAU);g.fill();}
g.fillStyle='#ddd6c6';g.fillRect(-2400,540,6400,26);
g.fillStyle='rgba(255,252,240,0.5)';g.fillRect(-2400,540,6400,3);
let bs=g.createLinearGradient(0,566,0,584);
bs.addColorStop(0,'rgba(30,22,16,0.35)');bs.addColorStop(1,'rgba(30,22,16,0)');
g.fillStyle=bs;g.fillRect(-2400,566,6400,18);
let fg2=g.createLinearGradient(0,566,0,1800);
fg2.addColorStop(0,'#5f4429');fg2.addColorStop(.35,'#8a6238');fg2.addColorStop(1,'#a5764a');
g.fillStyle=fg2;g.fillRect(-2400,566,6400,1400);
/* perspective floorboards */
const VPX=800,K=1.6,xb2x=(xb,y)=>VPX+(xb-VPX)*((y-566)/434)*K;
g.strokeStyle='rgba(46,32,18,0.35)';g.lineWidth=1.3;
for(let xb=-1500;xb<=3400;xb+=96){
g.beginPath();g.moveTo(xb2x(xb,567),567);g.lineTo(xb2x(xb,1966),1966);g.stroke();}
const rows=[566];
for(let k=1;k<=16;k++)rows.push(566+1400*Math.pow((k+rnd()*.4)/16,1.55));
g.strokeStyle='rgba(40,28,16,0.30)';g.lineWidth=1.2;
for(let k=1;k<rows.length;k++){g.beginPath();g.moveTo(-2400,rows[k]);g.lineTo(4000,rows[k]);g.stroke();}
for(let k=1;k<rows.length-1;k++){const y1=rows[k],y2=rows[k+1];
for(let xb=-1500;xb<=3400;xb+=96){
const a1=xb2x(xb,y1),a2=xb2x(xb+96,y1),b1=xb2x(xb,y2),b2=xb2x(xb+96,y2);
if(b2<-2600||a1>4600)continue;
const r1=rnd();
g.fillStyle=`rgba(${190+r1*36|0},${140+r1*30|0},${88+r1*20|0},${.05+rnd()*.09})`;
g.beginPath();g.moveTo(a1,y1);g.lineTo(a2,y1);g.lineTo(b2,y2);g.lineTo(b1,y2);g.closePath();g.fill();
if(rnd()>.4){const off=rnd()*70-35;
g.strokeStyle='rgba(60,42,24,0.10)';g.lineWidth=1;
g.beginPath();g.moveTo(a1+off,y1+3);
g.quadraticCurveTo((a1+b1)/2+off,(y1+y2)/2,b1+off,y2-3);g.stroke();}}}
/* sun pool with muntin shadows */
g.beginPath();g.moveTo(880,640);g.lineTo(1265,640);g.lineTo(1200,1075);g.lineTo(340,1075);g.closePath();
let pg=g.createLinearGradient(1120,640,760,1075);
pg.addColorStop(0,'rgba(255,226,158,0.62)');pg.addColorStop(.55,'rgba(255,214,140,0.30)');
pg.addColorStop(1,'rgba(255,205,130,0)');
g.fillStyle=pg;g.fill();
let hp=g.createRadialGradient(880,760,10,880,760,260);
hp.addColorStop(0,'rgba(255,238,190,0.35)');hp.addColorStop(1,'rgba(255,238,190,0)');
g.fillStyle=hp;g.beginPath();g.arc(880,760,260,0,TAU);g.fill();
shadowBand(g,1216,552,930,1062,14,40,.35);
shadowBand(g,1216,318,860,960,10,32,.28);
shadowBand(g,1052,552,700,1120,20,66,.30);
/* window */
g.fillStyle='#63513c';g.fillRect(1032,88,360,468);
g.fillStyle='rgba(0,0,0,0.25)';g.fillRect(1032,88,360,10);
let gg=g.createLinearGradient(0,104,0,540);
gg.addColorStop(0,'#e9d9ae');gg.addColorStop(.55,'#e8d9b4');gg.addColorStop(1,'#b9c9cd');
g.fillStyle=gg;g.fillRect(1048,104,328,436);
g.fillStyle='rgba(96,112,74,0.55)';g.beginPath();g.ellipse(1105,470,110,90,.3,0,TAU);g.fill();
g.fillStyle='rgba(70,88,58,0.5)';g.beginPath();g.ellipse(1160,520,90,60,.2,0,TAU);g.fill();
let sg=g.createRadialGradient(1310,420,10,1310,420,150);
sg.addColorStop(0,'rgba(255,246,214,0.95)');sg.addColorStop(1,'rgba(255,246,214,0)');
g.fillStyle=sg;g.beginPath();g.arc(1310,420,150,0,TAU);g.fill();
g.fillStyle='rgba(255,255,255,0.10)';
g.beginPath();g.moveTo(1060,540);g.lineTo(1180,104);g.lineTo(1230,104);g.lineTo(1110,540);g.closePath();g.fill();
g.fillStyle='#6d5a44';g.fillRect(1204,104,16,436);g.fillRect(1048,314,328,16);
g.strokeStyle='rgba(40,30,20,0.4)';g.lineWidth=6;g.strokeRect(1051,107,322,430);
g.fillStyle='#e2dbc9';g.fillRect(1022,552,380,16);
g.fillStyle='rgba(255,255,255,0.5)';g.fillRect(1022,552,380,3);
g.fillStyle='rgba(30,22,16,0.30)';g.fillRect(1022,568,380,7);
/* mug on the sill */
g.fillStyle='#a34e37';rr(g,1290,522,30,30,5);g.fill();
g.fillStyle='#7c3826';g.beginPath();g.ellipse(1305,522,15,4.5,0,0,TAU);g.fill();
g.strokeStyle='#a34e37';g.lineWidth=5;g.beginPath();g.arc(1324,537,9,-1.2,1.2);g.stroke();
/* curtain */
g.fillStyle='rgba(40,32,24,0.18)';g.fillRect(928,96,44,470);
let cg=g.createLinearGradient(966,0,1050,0);
cg.addColorStop(0,'#8b7d68');cg.addColorStop(.5,'#7a6c58');cg.addColorStop(1,'#665a49');
g.fillStyle=cg;g.fillRect(966,96,84,470);
g.fillStyle='rgba(52,44,34,0.30)';
for(let i=0;i<4;i++){const x=974+i*20;
g.beginPath();g.moveTo(x,96);
g.quadraticCurveTo(x+4,330,x+(i%2?-4:6),566);g.lineTo(x+9,566);
g.quadraticCurveTo(x+8,330,x+9,96);g.closePath();g.fill();}
g.fillStyle='#4a3f33';g.fillRect(956,86,102,10);
/* picture, slightly askew */
g.save();g.translate(305,268);g.rotate(.02);
g.fillStyle='rgba(30,24,18,0.22)';rr(g,-88,-112,190,240,4);g.fill();
g.fillStyle='#57432f';rr(g,-95,-120,190,240,4);g.fill();
g.strokeStyle='rgba(255,240,210,0.25)';g.lineWidth=2;rr(g,-95,-120,190,240,4);g.stroke();
g.fillStyle='#e7dfcd';g.fillRect(-79,-104,158,208);
g.save();g.beginPath();g.rect(-79,-104,158,208);g.clip();
g.fillStyle='#cfc4ae';g.fillRect(-79,-104,158,110);
g.fillStyle='#98a08c';g.beginPath();g.moveTo(-79,-16);
g.quadraticCurveTo(-20,-58,30,-26);g.quadraticCurveTo(60,-8,79,-22);
g.lineTo(79,104);g.lineTo(-79,104);g.closePath();g.fill();
g.fillStyle='#d8b57a';g.beginPath();g.arc(38,-62,12,0,TAU);g.fill();
g.fillStyle='rgba(255,255,255,0.08)';
g.beginPath();g.moveTo(-79,104);g.lineTo(10,-104);g.lineTo(48,-104);g.lineTo(-40,104);g.closePath();g.fill();
g.restore();g.restore();
g.fillStyle='#3a3129';g.beginPath();g.arc(308,132,3,0,TAU);g.fill();
/* plant */
g.fillStyle='rgba(30,36,26,0.16)';g.beginPath();g.ellipse(1452,540,54,86,.12,0,TAU);g.fill();
const leaves=[[-150,-1.9,86],[-105,-1.5,120],[-60,-1.15,150],[-15,-.85,165],[30,-.55,150],[75,-.3,118],[120,-.05,88]];
for(const l of leaves){
const tx=1528+Math.cos(l[1])*l[2],ty=492+Math.sin(l[1])*l[2]+22;
g.strokeStyle='#42553b';g.lineWidth=11;g.lineCap='round';
g.beginPath();g.moveTo(1528,492);
g.quadraticCurveTo(1528+Math.cos(l[1])*l[2]*.6,492+Math.sin(l[1])*l[2]*.7,tx,ty);g.stroke();
g.strokeStyle='rgba(20,30,18,0.4)';g.lineWidth=2.5;
g.beginPath();g.moveTo(1528,492);
g.quadraticCurveTo(1528+Math.cos(l[1])*l[2]*.6,492+Math.sin(l[1])*l[2]*.7,tx,ty);g.stroke();}
g.fillStyle='#9a5a3e';g.beginPath();g.moveTo(1492,492);g.lineTo(1564,492);
g.lineTo(1552,560);g.lineTo(1504,560);g.closePath();g.fill();
g.fillStyle='#b06c4c';g.fillRect(1488,484,80,12);
g.fillStyle='rgba(255,230,190,0.18)';g.fillRect(1494,494,10,62);
}
function renderFg(g){
g.fillStyle='#423e3b';rr(g,-760,616,1120,700,90);g.fill();
g.fillStyle='#4c4844';rr(g,-760,596,1080,180,80);g.fill();
g.strokeStyle='rgba(20,18,16,0.5)';g.lineWidth=3;
g.beginPath();g.moveTo(240,640);g.quadraticCurveTo(300,900,260,1300);g.stroke();
g.fillStyle='#514c48';rr(g,-700,880,880,240,60);g.fill();
g.fillStyle='#a97e52';g.beginPath();g.moveTo(-60,596);
g.quadraticCurveTo(160,610,330,640);g.lineTo(330,760);
g.quadraticCurveTo(140,720,-60,740);g.closePath();g.fill();
g.strokeStyle='rgba(80,56,30,0.35)';g.lineWidth=2;
for(let i=0;i<5;i++){g.beginPath();g.moveTo(-40+i*80,606);
g.quadraticCurveTo(60+i*70,650,300+i*8,730);g.stroke();}
g.strokeStyle='#a97e52';g.lineWidth=3;
for(let i=0;i<10;i++){const x=-50+i*42;
g.beginPath();g.moveTo(x,742);g.lineTo(x-6,768+(i%3)*4);g.stroke();}
g.fillStyle='#332b24';rr(g,1300,956,900,500,50);g.fill();
g.fillStyle='rgba(255,235,200,0.06)';rr(g,1300,956,900,26,12);g.fill();
g.fillStyle='#6e4438';rr(g,1420,930,170,30,6);g.fill();
g.fillStyle='#d9d2c2';g.fillRect(1426,950,158,8);
}
/* ================= live light / particles / debris ================= */
function drawShaft(g){
const A=.24*cloudV*(1+.3*ENV.env*SPN),bend=CURLV*230;
g.save();g.globalCompositeOperation='screen';
const gr=g.createLinearGradient(1200,560,860,1010);
gr.addColorStop(0,`rgba(255,228,168,${A})`);gr.addColorStop(.6,`rgba(255,222,160,${A*.4})`);
gr.addColorStop(1,'rgba(255,220,150,0)');
g.fillStyle=gr;
g.beginPath();g.moveTo(1072,552);
g.quadraticCurveTo(1010-bend*.4,790,520+bend,1012);g.lineTo(1230+bend*.7,1012);
g.quadraticCurveTo(1150-bend*.2,790,1358,552);g.closePath();g.fill();
g.globalAlpha=.5;
g.beginPath();g.moveTo(1120,552);
g.quadraticCurveTo(1030-bend*.3,800,700+bend,1012);g.lineTo(1080+bend*.7,1012);
g.quadraticCurveTo(1120-bend*.15,790,1240,552);g.closePath();g.fill();
g.restore();
}
function drawMotes(g){
const mLv=sstep(.02,.5,V.rf+V.lf*.5);
for(const m of motes){
const ix=m.x+16*Math.sin(S.t*.12*m.sp+m.ph);
const iy=566+((m.y-566+S.t*7*m.sp)%430);
const a2=m.ph+S.yaw*.6,r2=170+((m.ph*137)%250);
const x=lerp(ix,660+Math.cos(a2)*r2,mLv),y=lerp(iy,500+Math.sin(a2)*r2*.5,mLv);
const al=Math.max(0,.13+.15*Math.sin(S.t*1.4+m.ph*6));
g.fillStyle=`rgba(255,240,208,${al})`;
g.beginPath();g.arc(x,y,m.r,0,TAU);g.fill();}
}
function drawMouse(g,x,y,ang,sc,shadow){
g.save();g.translate(x,y);g.rotate(ang);g.scale(sc,sc);
if(shadow){g.fillStyle='rgba(25,18,12,0.3)';
g.beginPath();g.ellipse(0,4,20,5.5,0,0,TAU);g.fill();}
g.strokeStyle='#8f8d92';g.lineWidth=2.4;
g.beginPath();g.moveTo(14,0);g.quadraticCurveTo(30,4,40,-4);g.stroke();
g.fillStyle='#98959b';g.beginPath();g.ellipse(-2,-4,15,9,0,0,TAU);g.fill();
g.fillStyle='#a5a2a8';g.beginPath();g.ellipse(-8,-7,8,5.5,-.4,0,TAU);g.fill();
g.fillStyle='#8f8d93';g.beginPath();g.arc(-12,-12,3.4,0,TAU);g.fill();
g.fillStyle='#2c2a2e';g.beginPath();g.arc(-13,-7.5,1.1,0,TAU);g.fill();
g.restore();
}
function drawDebrisShape(g,t){
if(t==='mouse'){drawMouse(g,0,0,-.2,1.5,false);}
else if(t==='book'){g.fillStyle='#7a4a3c';rr(g,-42,-26,84,52,7);g.fill();
g.fillStyle='#d8d2c2';g.fillRect(34,-22,8,44);
g.strokeStyle='rgba(40,24,18,0.5)';g.lineWidth=2;rr(g,-42,-26,84,52,7);g.stroke();
g.fillStyle='rgba(255,235,200,0.15)';g.fillRect(-42,-26,84,10);}
else if(t==='mug'){g.fillStyle='#b0563f';rr(g,-20,-24,40,46,7);g.fill();
g.strokeStyle='#b0563f';g.lineWidth=7;g.beginPath();g.arc(24,-2,11,-1.3,1.3);g.stroke();
g.fillStyle='#6e3020';g.beginPath();g.ellipse(0,-24,20,6,0,0,TAU);g.fill();}
else if(t==='leaf'){g.strokeStyle='#48583f';g.lineWidth=14;g.lineCap='round';
g.beginPath();g.moveTo(-44,10);g.quadraticCurveTo(0,-26,46,-6);g.stroke();
g.strokeStyle='rgba(20,30,18,0.5)';g.lineWidth=3;
g.beginPath();g.moveTo(-44,10);g.quadraticCurveTo(0,-14,46,-6);g.stroke();}
else if(t==='frame'){g.fillStyle='#57432f';rr(g,-32,-42,64,84,4);g.fill();
g.fillStyle='#cfc4ae';g.fillRect(-24,-34,48,68);
g.fillStyle='#98a08c';g.beginPath();g.moveTo(-24,20);
g.quadraticCurveTo(0,4,24,18);g.lineTo(24,34);g.lineTo(-24,34);g.closePath();g.fill();}
else if(t==='cushion'){g.fillStyle='#565049';rr(g,-36,-36,72,72,16);g.fill();
g.strokeStyle='rgba(20,18,16,0.4)';g.lineWidth=2;rr(g,-36,-36,72,72,16);g.stroke();
g.fillStyle='rgba(0,0,0,0.25)';g.beginPath();g.arc(0,0,4,0,TAU);g.fill();}
}
function drawDebrisItem(g,d,dt){
d.age+=dt;
const a=d.a0+S.yaw*.55+S.t*.22,cy=812-S.lift-230;
const r=d.r*(1+.1*Math.sin(S.t*1.1+d.a0*5));
const al=sstep(0,1,d.age/.9);
for(let k=2;k>=0;k--){
const aa=a-k*.3;
const x=660+Math.cos(aa)*r*1.22,y=cy+Math.sin(aa)*r*.48-30;
g.save();g.translate(x,y);g.rotate(aa*1.4);
g.globalAlpha=k===0?al:al*(k===1?.30:.15);
drawDebrisShape(g,d.t);g.restore();}
g.globalAlpha=1;
}
/* ================= post ================= */
function grade(g){
g.save();g.globalCompositeOperation='soft-light';
g.fillStyle=grA;g.fillRect(0,0,W,H);g.restore();
}
function vignette(g){
g.fillStyle=vigG;g.fillRect(0,0,W,H);
if(SPN>0){g.fillStyle=`rgba(12,8,6,${.10*ENV.env*SPN})`;g.fillRect(0,0,W,H);}
}
function grainFn(g){
g.save();g.globalCompositeOperation='overlay';g.globalAlpha=.07;
const t=grains[(Math.random()*grains.length)|0];
g.fillStyle=g.createPattern(t,'repeat');
g.translate(-((Math.random()*160)|0),-((Math.random()*160)|0));
g.fillRect(0,0,W+160,H+160);g.restore();
}
/* ================= audio: formant "wah-wah" voice ================= */
let AC=null,nodes=null,muted=false;
function initAudio(){
if(AC){if(AC.state==='suspended')AC.resume();return;}
AC=new(window.AudioContext||window.webkitAudioContext)();
const master=AC.createGain();master.gain.value=.8;
const comp=AC.createDynamicsCompressor();
master.connect(comp);comp.connect(AC.destination);
const srcSum=AC.createGain();
const shaper=AC.createWaveShaper();
const cn=new Float32Array(1024);
for(let i=0;i<1024;i++){const x=i/1023*2-1;cn[i]=Math.tanh(2.6*x);}
shaper.curve=cn;shaper.oversample='2x';
srcSum.connect(shaper);
const vg=AC.createGain();vg.gain.value=0;vg.connect(master);
const mk=(f,q,gn)=>{const bp=AC.createBiquadFilter();bp.type='bandpass';
bp.frequency.value=f;bp.Q.value=q;
const g2=AC.createGain();g2.gain.value=gn;
shaper.connect(bp);bp.connect(g2);g2.connect(vg);return bp;};
const F1=mk(600,9,1.0),F2=mk(1050,11,.6),F3=mk(2450,8,.35);
const oscs=[],og=[];
[0.55,0.30,0.20].forEach((gv,i)=>{
const o=AC.createOscillator();o.type='sawtooth';o.frequency.value=440;
const g3=AC.createGain();g3.gain.value=gv;
o.connect(g3);g3.connect(srcSum);o.start();oscs.push(o);og.push(g3);});
const nb=AC.createBuffer(1,AC.sampleRate*2,AC.sampleRate);
const dd=nb.getChannelData(0);
for(let i=0;i<dd.length;i++)dd[i]=Math.random()*2-1;
const ns=AC.createBufferSource();ns.buffer=nb;ns.loop=true;
const nbp=AC.createBiquadFilter();nbp.type='bandpass';nbp.frequency.value=800;nbp.Q.value=.8;
const ng=AC.createGain();ng.gain.value=0;
ns.connect(nbp);nbp.connect(ng);ng.connect(master);ns.start();
nodes={master,F1,F2,F3,oscs,og,vg,ng,nbp};
}
function audioUpdate(dt){
if(!AC||!S.started){ENV.env=0;ENV.open=0;return;}
ENV.rate=Math.min(26,4.6+3.1*(S.level-1)+6.5*SPN);
ENV.phase+=ENV.rate*dt;
const cyc=TAU*ENV.phase;
let env=Math.pow(.5+.5*Math.sin(cyc),1.8);
let open=Math.pow(.5+.5*Math.sin(cyc-.85),1.1);
const a=clamp((S.t-atkT)/.38,0,1);let f0m=1;
if(a<1){env=Math.max(env,(1-a)*(.7+.3*Math.sin(a*22)));
open=Math.max(open,1-a);f0m=1+.85*(1-a);}
ENV.env=env;ENV.open=open;
const base=Math.min(780,400*Math.pow(1.14,S.level-1));
const f0=base*(.94+.14*Math.sin(cyc+1.1))*(1+.22*SPN)*f0m*(1+.028*Math.sin(S.t*TAU*5.6));
const tn=AC.currentTime;
nodes.F1.frequency.setTargetAtTime(290+540*open,tn,.03);
nodes.F2.frequency.setTargetAtTime(880+430*open,tn,.03);
nodes.F3.frequency.setTargetAtTime(2300+380*open,tn,.03);
nodes.oscs[0].frequency.setTargetAtTime(f0,tn,.02);
nodes.oscs[1].frequency.setTargetAtTime(f0*1.0068,tn,.02);
nodes.oscs[2].frequency.setTargetAtTime(f0*.9957,tn,.02);
nodes.og[0].gain.setTargetAtTime(.55,tn,.1);
nodes.og[1].gain.setTargetAtTime(S.level>=2?.30:0,tn,.15);
nodes.og[2].gain.setTargetAtTime(S.level>=4?.22:0,tn,.15);
nodes.vg.gain.setTargetAtTime(.17*(.35+.65*env),tn,.025);
const ng=S.level>=2?Math.min(.42,.05+.4*SPN*SPN)*(.8+.4*env)+(a<1?.22*(1-a):0):0;
nodes.ng.gain.setTargetAtTime(ng,tn,.05);
nodes.nbp.frequency.setTargetAtTime(380+2500*SPN,tn,.05);
}
function setMuted(m){
muted=m;
icoOn.style.display=m?'none':'';icoOff.style.display=m?'':'none';
if(nodes)nodes.master.gain.setTargetAtTime(m?0:.8,AC.currentTime,.02);
}
/* ================= sizing ================= */
function mkLayer(pd){
const c=document.createElement('canvas');
const sz=Math.ceil(2*bgR*pd);c.width=sz;c.height=sz;
c.getContext('2d').setTransform(pd*cov,0,0,pd*cov,pd*(bgR-PV[0]*cov),pd*(bgR-PV[1]*cov));
return c;
}
function resize(){
W=innerWidth;H=innerHeight;dpr=Math.min(devicePixelRatio||1,2);
cvEl.width=W*dpr;cvEl.height=H*dpr;
cov=Math.max(W/1600,H/1000);cox=(W-1600*cov)/2;coy=(H-1000*cov)/2;
const px=cox+PV[0]*cov,py=coy+PV[1]*cov;
bgR=Math.hypot(Math.max(px,W-px),Math.max(py,H-py))+60;
bgPD=Math.min(.5*dpr,2300/(2*bgR));
fgPD=Math.min(.35*dpr,2300/(2*bgR));
bg=mkLayer(bgPD);renderRoom(bg.getContext('2d'));
fg=mkLayer(fgPD);renderFg(fg.getContext('2d'));
bq=Math.min(cov*dpr,1.8);
bufs=[];bctxs=[];
for(let i=0;i<NB;i++){const c=document.createElement('canvas');
c.width=Math.ceil(CW*bq);c.height=Math.ceil(CH*bq);
bufs.push(c);bctxs.push(c.getContext('2d'));}
vigG=ctx.createRadialGradient(W*.5,H*.46,Math.min(W,H)*.42,W*.5,H*.52,Math.hypot(W,H)*.62);
vigG.addColorStop(0,'rgba(10,8,6,0)');vigG.addColorStop(1,'rgba(10,8,6,0.52)');
grA=ctx.createLinearGradient(0,0,W,0);
grA.addColorStop(0,'rgba(255,178,96,0.07)');grA.addColorStop(.5,'rgba(128,128,128,0)');
grA.addColorStop(1,'rgba(58,74,110,0.06)');
}
function renderGrains(){
for(let k=0;k<3;k++){const c=document.createElement('canvas');c.width=c.height=160;
const g2=c.getContext('2d'),id=g2.createImageData(160,160);
for(let i=0;i<id.data.length;i+=4){const v=110+Math.random()*36|0;
id.data[i]=id.data[i+1]=id.data[i+2]=v;id.data[i+3]=255;}
g2.putImageData(id,0,0);grains.push(c);}
}
/* ================= interaction ================= */
function toDesign(ev){return[(ev.clientX-cox)/cov,(ev.clientY-coy)/cov];}
addEventListener('pointermove',ev=>{const p=toDesign(ev);pdp=p;
const inCat=Math.hypot((p[0]-660)/300,(p[1]-(812-S.lift-235))/330)<1;
cvEl.style.cursor=inCat?'pointer':'default';});
cvEl.addEventListener('pointerdown',ev=>{
const p=toDesign(ev);
if(Math.hypot((p[0]-660)/310,(p[1]-(812-S.lift-235))/340)>=1)return;
activate();});
function activate(){
if(!S.started){S.started=true;morphT0=S.t;initAudio();
hintEl.classList.remove('show');muteBtn.classList.add('show');}
if(AC&&AC.state==='suspended')AC.resume();
atkT=S.t;S.level++;perkV=1;
Object.assign(T,stageT(S.level));spawnDebris(S.level);
ENV.phase=0;
}
muteBtn.addEventListener('click',ev=>{ev.stopPropagation();setMuted(!muted);});
addEventListener('keydown',e=>{if(e.key==='m'||e.key==='M')setMuted(!muted);});
addEventListener('resize',resize);
document.addEventListener('visibilitychange',()=>{
if(!AC)return;if(document.hidden)AC.suspend();else AC.resume();});
setTimeout(()=>{if(!S.started)hintEl.classList.add('show');},3500);
/* ================= main loop ================= */
const rmm=mkRnd(31);
for(let i=0;i<46;i++)motes.push({x:500+rmm()*780,y:566+rmm()*420,r:.7+rmm()*1.5,ph:rmm()*TAU,sp:.4+rmm()*1.2});
const openBase=[0,.5,.62,.8,1];
let last=0;
function frame(ts){
requestAnimationFrame(frame);
const dt=Math.min(.05,(ts-last)/1000||.016);last=ts;S.t+=dt;
const km=Math.min(1,dt*2.4);
for(const key in V)V[key]+=(T[key]-V[key])*km;
SPN=clamp((V.spin-6)/52,0,1);
cloudV=.84+.16*Math.sin(S.t*.05+Math.sin(S.t*.013)*2);
CURLV+=((V.st>0?SPN:0)-CURLV)*Math.min(1,dt*1.6);
liftV+=(V.lf-liftV)*Math.min(1,dt*1.4);
S.lift=liftV*(36+15*Math.sin(S.t*2.2));
const e=S.started?easeIO(Math.min(1,(S.t-morphT0)/.34)):0;
const spinEff=V.spin*sstep(.5,.95,e);
S.yaw+=spinEff*(1+.05*Math.sin(S.t*2.9))*dt;
S.thetaR+=V.rf*V.spin*dt;
audioUpdate(dt);
/* idle life */
if(S.level===0){
if(S.t>nextBlink){blinkPh=0;nextBlink=S.t+2.5+Math.random()*3.5;}
if(blinkPh<1){blinkPh+=dt/.16;blinkV=Math.sin(Math.min(1,blinkPh)*Math.PI);}else blinkV=0;
if(S.t>nextTwitch){twitchT=0;nextTwitch=S.t+4+Math.random()*6;}
if(twitchT<.22){twitchT+=dt;earRotV=Math.sin(twitchT/.22*TAU)*.14;}else earRotV=0;
}else{blinkV*=1-Math.min(1,dt*8);earRotV*=1-Math.min(1,dt*6);}
perkV+=(0-perkV)*Math.min(1,dt*3);
const dilT=S.level?[0,.45,.62,.78,.9][Math.min(S.level,4)]:.1;
dilV+=(dilT-dilV)*Math.min(1,dt*3);
const look=[clamp((pdp[0]-660)*.05,-4.5,4.5)*(1-SPN),clamp((pdp[1]-470)*.04,-3,3)*(1-SPN)];
const sxU=Math.cos(S.yaw);
const st={
e,sx:sxU*e,sxU,eyeA:lerp(1,clamp(sxU*4,0,1),e),chestA:lerp(1,clamp(sxU*3,0,1),e),
q:clamp(1-SPN*.7,.35,1),
roll:SPN*.06*Math.sin(S.yaw*.9)+V.lf*.05*Math.sin(S.t*1.7),
flare:V.fl,tailLift:sstep(.3,1,V.spin/40),
tailSway:Math.sin(S.yaw*.5)*(8+44*SPN),tailExt:20+60*SPN,
tipFlick:S.level===0?Math.sin(S.t*1.1)*3:0,
dil:dilV,blink:blinkV,look,
open:S.level?openBase[Math.min(S.level,4)]*(.35+.65*ENV.env):0,
bob:S.level===0?Math.sin(S.t*.44)*1.6:0,
breath:Math.sin(S.t*.44),perk:perkV,earRot:earRotV};
renderCat(st);
/* ---- composite ---- */
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.fillStyle='#0e0b09';ctx.fillRect(0,0,W,H);
const camX=Math.sin(S.t*.31)+.6*Math.sin(S.t*.83+1.7);
const camY=Math.cos(S.t*.24)+.5*Math.sin(S.t*.71);
const camA=2.1/(1+5*SPN);
const shX=V.sh*(.6*Math.sin(S.t*31)+.4*Math.sin(S.t*47.3+1.7));
const shY=V.sh*(.6*Math.cos(S.t*37.1)+.4*Math.sin(S.t*53.7));
ctx.translate(camX*camA+shX,camY*camA+shY);
const px=cox+PV[0]*cov,py=coy+PV[1]*cov;
const zoom=(1+.035*Math.min(1,S.t/55))*(1+V.zp*ENV.env);
const roomXf=()=>{ctx.translate(px,py);ctx.rotate(S.thetaR);ctx.scale(zoom,zoom);ctx.translate(-px,-py);};
ctx.save();roomXf();
const nst=Math.round(V.st);
for(let kk=nst;kk>=1;kk--){
ctx.save();ctx.translate(px,py);ctx.rotate(V.rf*V.spin*.016*kk);ctx.translate(-px,-py);
ctx.globalAlpha=.5/kk;ctx.drawImage(bg,px-bgR,py-bgR,bgR*2,bgR*2);ctx.restore();}
ctx.globalAlpha=1;ctx.drawImage(bg,px-bgR,py-bgR,bgR*2,bgR*2);
ctx.save();ctx.translate(cox,coy);ctx.scale(cov,cov);
drawShaft(ctx);drawMotes(ctx);
if(S.level<3)drawMouse(ctx,852,896,.06,1,true);
ctx.restore();
ctx.restore();
/* debris, shadow, reflection, cat — design space, no room rotation */
ctx.save();ctx.translate(cox,coy);ctx.scale(cov,cov);
for(const d of debris)drawDebrisItem(ctx,d,dt);
const shAl=.36*(1-.62*liftV),shRx=170*(1-.3*liftV);
const sg=ctx.createRadialGradient(660,812,10,660,812,shRx);
sg.addColorStop(0,`rgba(20,15,10,${shAl})`);sg.addColorStop(1,'rgba(20,15,10,0)');
ctx.fillStyle=sg;ctx.beginPath();ctx.ellipse(660,812,shRx,26*(1-.3*liftV),0,0,TAU);ctx.fill();
if(V.lf<.03&&V.rf<.02&&spinEff<3){
ctx.save();ctx.translate(660,812);ctx.scale(CS,-.38*CS);
ctx.globalAlpha=.09;ctx.drawImage(bufs[bi],-OX,-OY,CW,CH);ctx.restore();
const fg3=ctx.createLinearGradient(0,812,0,812+180*CS);
fg3.addColorStop(0,'rgba(140,100,64,0.55)');fg3.addColorStop(1,'rgba(140,100,64,1)');
ctx.globalAlpha=.7;ctx.fillStyle=fg3;
ctx.fillRect(660-260*CS,812,520*CS,180*CS);ctx.globalAlpha=1;}
const cyy=812-S.lift-ENV.env*3*SPN;
if(spinEff>2.5){
for(let kk=Math.min(V.tr|0,NB-1);kk>=1;kk--){
const idx=(bi-kk+NB)%NB;
ctx.save();ctx.translate(660,cyy);ctx.scale(CS,CS);
ctx.globalAlpha=.30*(1-kk/(V.tr+1));
ctx.drawImage(bufs[idx],-OX,-OY,CW,CH);ctx.restore();}}
ctx.save();ctx.translate(660,cyy);ctx.scale(CS,CS);
ctx.drawImage(bufs[bi],-OX,-OY,CW,CH);ctx.restore();
ctx.restore();
/* foreground plane */
ctx.save();roomXf();ctx.drawImage(fg,px-bgR,py-bgR,bgR*2,bgR*2);ctx.restore();
ctx.fillStyle=`rgba(28,36,58,${(1-cloudV)*.10})`;ctx.fillRect(0,0,W,H);
grade(ctx);
if(V.hu>0){ctx.fillStyle=`hsla(${(S.yaw*57)%360},75%,55%,${V.hu*(.4+.6*ENV.env)})`;
ctx.fillRect(0,0,W,H);}
vignette(ctx);grainFn(ctx);
const fi=sstep(0,1,S.t/.9);
if(fi<1){ctx.fillStyle=`rgba(8,6,5,${1-fi})`;ctx.fillRect(0,0,W,H);}
bi=(bi+1)%NB;
}
resize();renderGrains();requestAnimationFrame(frame);
})();
</script>
</body>
</html>
tanh waveshaper into three parallel bandpass formant filters swept between "ah" and "uh" vowels (F1/F2/F3 ≈ 290→830, 880→1310, 2300→2680 Hz) by the syllable cycle, with a per-syllable amplitude envelope and a rising-then-falling pitch contour plus 5.6 Hz vibrato — vowel morphing + prosody is what makes it read as a creature, not a tone. Base pitch, syllable rate, vibrato depth, and a band-passed breath/whoosh are all slaved to the instantaneous spin speed, and the same envelope computed in JS drives the cat's mouth, the room zoom pulse and the vignette, so picture and sound are locked to one clock. It's created (and resumed) only inside the first cat click; the round button or M ramps the master gain to zero.