27:18Gemini 4 Hydrogen: Cheaper Tokens, More Expensive Answers?
Nov 13, 2026 · YouTube
18:40ERNIE 5.5 Flash Preview vs GLM 5.3 Flash: Baidu's new model is very impressive!
Nov 11, 2026 · YouTube
36:22Fable 5.2 robbing me blind, plus: Nex AGI's N2.6 vs GLM 5.3 Flash
Nov 11, 2026 · YouTube
14:03Gemini 4 Carbon: Can it handle my challenges? (First look with one-shot coding tests)
Nov 10, 2026 · YouTube
47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
31:28Is this company reselling GPT Sol 6.1 at a loss (for non-agentic/text-only)?🤔 Is this sustainable?
Oct 20, 2026 · YouTube
17:58Nemotron 3.5 Super VL vs Opus 5.5: Testing NVIDIA’s new coding juggernaut
Oct 15, 2026 · YouTube
16:04Testing a *massive* new open-weights model 🤯: Muse Spark Open (vs. Nemotron, Opus 5.5, and more)
Oct 15, 2026 · YouTube
16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
16:37Meta's new "Muse Wonder" AI is impressive... but can it compete with Anthropic's models?
Oct 12, 2026 · YouTube
11:12WOW... Haiku 5.5 is very impressive. RIP OpenAI?
Oct 7, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
22:02Is Pareto 26.10 Preview Just Routing to GPT Sol? I Tested It Against Sol 6.1 Pro
Oct 1, 2026 · YouTube
11:22I Tested Sonnet 5.5 Against Opus 5.5. Is It Actually Cheaper?
Sep 28, 2026 · YouTube
19:21I Tested the Stealth "Space Bunny Alpha" Model Before It Went Offline
Sep 23, 2026 · YouTube
10:58So... how much better is Claude Opus 5.5 vs. GPT-6 Sol, really?
Sep 23, 2026 · YouTube
9:38Claude Opus 5.5... the BEST new model for coding!! 🤩
Sep 22, 2026 · YouTube
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 that lifts off the floor and spins in the air when you click it.
Make this the Oiia Cat — the spinning cat meme, the one whose loop is written out as "oiia oiia oiiaiooooiai". Match the original's look and feel, not a generic rotation:
Build toward what that meme feels like at full speed: too fast, too loud, and funnier every time it goes further.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.The single fenced html block.
Five bullets.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>oiia</title>
<style>
html,body{margin:0;height:100%;background:#141008;overflow:hidden;user-select:none}
#stage{position:fixed;inset:0;width:100%;height:100%;display:block;cursor:pointer;touch-action:none}
#cap{position:fixed;left:50%;bottom:9%;transform:translateX(-50%);pointer-events:none;opacity:0;white-space:nowrap;
font-family:'Impact','Haettenschweiler','Franklin Gothic Bold','Arial Narrow',sans-serif;
color:#fff;letter-spacing:.02em;will-change:transform,opacity;
text-shadow:0 3px 0 #000,0 -2px 0 #000,3px 0 0 #000,-3px 0 0 #000,0 8px 22px rgba(0,0,0,.55)}
#hint{position:fixed;left:50%;bottom:4%;transform:translateX(-50%);pointer-events:none;
font-family:Georgia,'Times New Roman',serif;font-style:italic;font-size:15px;letter-spacing:.3em;
color:rgba(255,248,235,.8);text-shadow:0 1px 3px rgba(0,0,0,.6);
animation:pulse 2.6s ease-in-out infinite;transition:opacity .8s}
@keyframes pulse{0%,100%{opacity:.35}50%{opacity:.95}}
#mute{position:fixed;top:18px;right:18px;width:44px;height:44px;border-radius:50%;cursor:pointer;padding:0;
border:1px solid rgba(255,246,230,.4);background:rgba(20,14,8,.35);display:flex;align-items:center;justify-content:center;
transition:border-color .2s,background .2s}
#mute:hover{border-color:rgba(255,246,230,.9);background:rgba(20,14,8,.6)}
#mute svg{width:20px;height:20px;fill:none;stroke:rgba(255,246,230,.9);stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
#mute .off{display:none}
#mute.muted .on{display:none}
#mute.muted .off{display:block}
</style>
</head>
<body>
<canvas id="stage" aria-label="a cream cat sitting in a living room. click it."></canvas>
<div id="cap"></div>
<div id="hint">click the cat</div>
<button id="mute" aria-label="toggle sound">
<svg class="on" viewBox="0 0 24 24"><path d="M4 9v6h4l5 4V5L8 9H4z"/><path d="M16 9a4 4 0 0 1 0 6"/><path d="M18 6.5a7.5 7.5 0 0 1 0 11"/></svg>
<svg class="off" viewBox="0 0 24 24"><path d="M4 9v6h4l5 4V5L8 9H4z"/><path d="M17 9l5 6M22 9l-5 6"/></svg>
</button>
<script>
'use strict';
/* ================= basics ================= */
const DW=1600,DH=1000,TAU=Math.PI*2;
const cv=document.getElementById('stage'),ctx=cv.getContext('2d');
const scene=document.createElement('canvas'),sc=scene.getContext('2d');
const fxA=document.createElement('canvas'),fa=fxA.getContext('2d');
const fxB=document.createElement('canvas'),fb=fxB.getContext('2d');
let VW=0,VH=0,DPR=1,VS=1,VOX=0,VOY=0;
function resize(){
DPR=Math.min(2,window.devicePixelRatio||1);
VW=innerWidth;VH=innerHeight;
cv.width=VW*DPR;cv.height=VH*DPR;
scene.width=fxA.width=fxB.width=cv.width;
scene.height=fxA.height=fxB.height=cv.height;
VS=Math.max(VW/DW,VH/DH)*DPR;
VOX=(VW*DPR-DW*VS)/2;VOY=(VH*DPR-DH*VS)/2;
}
addEventListener('resize',resize);resize();
function mkcv(w,h){const c=document.createElement('canvas');c.width=w;c.height=h;return c;}
function mulberry(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
function clamp(v,a,b){return v<a?a:v>b?b:v;}
function adist(a,b){let d=(a-b)%TAU;if(d>Math.PI)d-=TAU;if(d<-Math.PI)d+=TAU;return Math.abs(d);}
function rr(c,x,y,w,h,r){c.beginPath();c.moveTo(x+r,y);c.arcTo(x+w,y,x+w,y+h,r);c.arcTo(x+w,y+h,x,y+h,r);c.arcTo(x,y+h,x,y,r);c.arcTo(x,y,x+w,y,r);c.closePath();}
const BLUR=(()=>{const c=mkcv(2,2).getContext('2d');c.filter='blur(2px)';return c.filter.indexOf('blur')===0;})();
function spr(w,h,blur,fn){const s=mkcv(w,h),c=s.getContext('2d');if(blur&&BLUR)c.filter='blur('+blur+'px)';fn(c);c.filter='none';return s;}
/* fur helpers: short strokes along an elliptical arc (silhouette fluff) and inside a shape (texture) */
function fluff(c,cx,cy,rx,ry,a0,a1,n,len,col,w,rand){
c.strokeStyle=col;c.lineCap='round';
for(let i=0;i<n;i++){
const a=a0+(a1-a0)*(i+.5)/n+(rand()-.5)*.12;
const px=cx+Math.cos(a)*rx,py=cy+Math.sin(a)*ry;
const L=len*(.6+rand()*.7),ja=(rand()-.5)*.5;
c.lineWidth=w*(.6+rand()*.8);
c.beginPath();c.moveTo(px,py);
c.quadraticCurveTo(px+Math.cos(a+ja)*L*.5,py+Math.sin(a+ja)*L*.5,px+Math.cos(a+ja)*L,py+Math.sin(a+ja)*L);
c.stroke();
}
}
function furTex(c,cx,cy,rx,ry,n,col,rand,angBase){
c.strokeStyle=col;c.lineCap='round';
for(let i=0;i<n;i++){
const a=rand()*TAU,r=Math.sqrt(rand());
const px=cx+Math.cos(a)*rx*r,py=cy+Math.sin(a)*ry*r;
const an=angBase+(rand()-.5)*1.2,L=6+rand()*10;
c.lineWidth=1+rand()*1.4;c.globalAlpha=.12+rand()*.14;
c.beginPath();c.moveTo(px,py);c.lineTo(px+Math.cos(an)*L,py+Math.sin(an)*L);c.stroke();
}
c.globalAlpha=1;
}
function leaf(c,x,y,a,L,col1,col2){
c.save();c.translate(x,y);c.rotate(a);
c.fillStyle=col1;
c.beginPath();c.moveTo(0,0);c.quadraticCurveTo(L*.45,-L*.28,L,0);c.quadraticCurveTo(L*.45,L*.28,0,0);c.fill();
c.strokeStyle=col2;c.lineWidth=1.5;c.beginPath();c.moveTo(0,0);c.lineTo(L*.9,0);c.stroke();
c.restore();
}
/* ================= the room (baked once) ================= */
const room=mkcv(DW,DH);
(function buildRoom(){
const c=room.getContext('2d');const R=mulberry(7);
// wall
let g=c.createLinearGradient(0,0,0,640);
g.addColorStop(0,'#8f8274');g.addColorStop(.55,'#a79a89');g.addColorStop(1,'#93867a');
c.fillStyle=g;c.fillRect(0,0,DW,640);
// warm glow thrown by the window
g=c.createRadialGradient(285,300,60,285,300,640);
g.addColorStop(0,'rgba(255,214,150,.34)');g.addColorStop(1,'rgba(255,214,150,0)');
c.fillStyle=g;c.fillRect(0,0,DW,640);
// left warm / right cool
g=c.createLinearGradient(0,0,DW,0);
g.addColorStop(0,'rgba(255,220,170,.10)');g.addColorStop(.35,'rgba(0,0,0,0)');g.addColorStop(1,'rgba(30,20,25,.22)');
c.fillStyle=g;c.fillRect(0,0,DW,640);
// ceiling falloff
g=c.createLinearGradient(0,0,0,140);
g.addColorStop(0,'rgba(0,0,0,.2)');g.addColorStop(1,'rgba(0,0,0,0)');
c.fillStyle=g;c.fillRect(0,0,DW,140);
// plaster texture
c.globalAlpha=.05;
for(let i=0;i<170;i++){const x=R()*DW,y=R()*640;c.fillStyle=R()<.5?'#fff':'#000';c.fillRect(x,y,1,6+R()*10);}
c.globalAlpha=1;
// light switch
c.fillStyle='#e3dac6';rr(c,498,296,16,26,3);c.fill();
c.fillStyle='rgba(0,0,0,.15)';c.fillRect(504,306,4,8);
// baseboard
c.fillStyle='#d9cfbc';c.fillRect(0,612,DW,30);
c.fillStyle='rgba(0,0,0,.18)';c.fillRect(0,612,DW,3);
c.fillStyle='rgba(0,0,0,.10)';c.fillRect(0,638,DW,2);
// floor
g=c.createLinearGradient(0,640,0,DH);
g.addColorStop(0,'#7a5c40');g.addColorStop(.4,'#8a6a4b');g.addColorStop(1,'#9c7a55');
c.fillStyle=g;c.fillRect(0,640,DW,360);
const rows=[640,676,724,788,872,1000];
for(let r=0;r<rows.length-1;r++){
const y0=rows[r],y1=rows[r+1],f0=(y0-470)/530,f1=(y1-470)/530;
for(let k=-7;k<7;k++){
const ax=800+k*300*f0,bx=800+(k+1)*300*f0,cx2=800+(k+1)*300*f1,dx=800+k*300*f1;
c.fillStyle=R()<.5?'rgba(255,230,190,'+(0.02+R()*0.06).toFixed(3)+')':'rgba(40,20,10,'+(0.02+R()*0.07).toFixed(3)+')';
c.beginPath();c.moveTo(ax,y0);c.lineTo(bx,y0);c.lineTo(cx2,y1);c.lineTo(dx,y1);c.closePath();c.fill();
}
}
c.strokeStyle='rgba(30,15,8,.35)';c.lineWidth=1.5;
for(let k=-7;k<=7;k++){c.beginPath();c.moveTo(800+k*300*0.32,640);c.lineTo(800+k*300,DH);c.stroke();}
c.strokeStyle='rgba(30,15,8,.3)';
for(const y of rows){c.beginPath();c.moveTo(0,y);c.lineTo(DW,y);c.stroke();}
// sheen
c.globalAlpha=.07;c.strokeStyle='#ffe9c0';c.lineWidth=6;c.lineCap='round';
for(let i=0;i<14;i++){const y=660+R()*320;c.beginPath();c.moveTo(R()*DW*.4,y+R()*8-4);c.lineTo(DW*.4+R()*DW*.6,y+R()*10-5);c.stroke();}
c.globalAlpha=1;
// rug
c.save();c.translate(800,880);
c.beginPath();c.moveTo(-330,120);c.lineTo(330,120);c.lineTo(215,-95);c.lineTo(-215,-95);c.closePath();
c.fillStyle='#5e7a70';c.fill();
c.strokeStyle='#e8dfc8';c.lineWidth=10;c.stroke();
c.lineWidth=2;c.strokeStyle='rgba(232,223,200,.5)';
c.beginPath();c.moveTo(-290,88);c.lineTo(290,88);c.lineTo(188,-70);c.lineTo(-188,-70);c.closePath();c.stroke();
c.fillStyle='rgba(232,223,200,.16)';
for(let ry=-60;ry<=90;ry+=34){
const t=(ry+95)/215,half=215+115*t;
for(let k=-2;k<=2;k++){
c.save();c.translate(k*half*0.32,ry);c.rotate(Math.PI/4);const s=9+4*t;c.fillRect(-s/2,-s/2,s,s);c.restore();
}
}
c.globalAlpha=.08;
for(let i=0;i<200;i++){const y=-95+R()*215,xm=215+115*((y+95)/215);c.fillStyle=R()<.5?'#000':'#fff';c.fillRect((R()*2-1)*xm,y,2,1);}
c.globalAlpha=1;c.restore();
// window light: beam + pool (over the rug too)
c.save();c.globalAlpha=.16;c.fillStyle='#ffd9a0';
c.beginPath();c.moveTo(150,640);c.lineTo(430,640);c.lineTo(1010,1000);c.lineTo(560,1000);c.closePath();c.fill();
c.globalAlpha=.12;
c.beginPath();c.moveTo(200,640);c.lineTo(380,640);c.lineTo(880,1000);c.lineTo(640,1000);c.closePath();c.fill();
c.restore();
c.save();c.translate(620,860);c.scale(1.7,.55);
g=c.createRadialGradient(0,0,30,0,0,470);
g.addColorStop(0,'rgba(255,216,150,.32)');g.addColorStop(1,'rgba(255,216,150,0)');
c.fillStyle=g;c.beginPath();c.arc(0,0,470,0,TAU);c.fill();c.restore();
// junction haze
g=c.createLinearGradient(0,560,0,660);g.addColorStop(0,'rgba(0,0,0,0)');g.addColorStop(1,'rgba(35,22,14,.12)');
c.fillStyle=g;c.fillRect(0,560,DW,100);
// ---- window ----
const wx=130,wy=110,ww=310,wh=360;
c.save();c.beginPath();c.rect(wx,wy,ww,wh);c.clip();
g=c.createLinearGradient(0,wy,0,wy+wh);
g.addColorStop(0,'#ffe9c2');g.addColorStop(.5,'#ffd394');g.addColorStop(1,'#f4b26b');
c.fillStyle=g;c.fillRect(wx,wy,ww,wh);
g=c.createRadialGradient(wx+70,wy+80,10,wx+70,wy+80,180);
g.addColorStop(0,'rgba(255,255,240,.9)');g.addColorStop(1,'rgba(255,255,240,0)');
c.fillStyle=g;c.fillRect(wx,wy,ww,wh);
c.filter=BLUR?'blur(7px)':'none';
c.fillStyle='#7d7a4a';c.beginPath();c.ellipse(wx+60,wy+wh-30,90,55,0,0,TAU);c.fill();
c.fillStyle='#5d6b3c';c.beginPath();c.ellipse(wx+230,wy+wh-10,120,70,0,0,TAU);c.fill();
c.fillStyle='#8a8f55';c.beginPath();c.ellipse(wx+150,wy+wh-60,70,40,0,0,TAU);c.fill();
c.filter='none';
c.fillStyle='rgba(60,45,30,.5)';c.fillRect(wx+250,wy+150,10,wh);
c.restore();
c.strokeStyle='#efe8d8';c.lineWidth=14;c.strokeRect(wx+7,wy+7,ww-14,wh-14);
c.lineWidth=8;c.beginPath();c.moveTo(wx+ww/2,wy);c.lineTo(wx+ww/2,wy+wh);c.moveTo(wx,wy+wh/2);c.lineTo(wx+ww,wy+wh/2);c.stroke();
c.strokeStyle='rgba(60,40,25,.35)';c.lineWidth=3;c.strokeRect(wx-2,wy-2,ww+4,wh+4);
c.strokeStyle='rgba(255,250,235,.5)';c.lineWidth=2;c.strokeRect(wx+16,wy+16,ww-32,wh-32);
c.fillStyle='#e5dcc8';c.fillRect(wx-14,wy+wh,ww+28,14);
c.fillStyle='rgba(0,0,0,.2)';c.fillRect(wx-14,wy+wh+12,ww+28,4);
c.save();c.beginPath();c.rect(wx,wy,ww,wh);c.clip();
c.globalAlpha=.10;c.fillStyle='#fff';
c.beginPath();c.moveTo(wx+40,wy+wh);c.lineTo(wx+150,wy);c.lineTo(wx+210,wy);c.lineTo(wx+100,wy+wh);c.closePath();c.fill();
c.globalAlpha=.06;
c.beginPath();c.moveTo(wx+180,wy+wh);c.lineTo(wx+280,wy);c.lineTo(wx+310,wy);c.lineTo(wx+230,wy+wh);c.closePath();c.fill();
c.restore();
// sheer curtain
c.save();c.beginPath();c.rect(wx+ww-70,wy-8,86,wh+16);c.clip();
g=c.createLinearGradient(wx+ww-70,0,wx+ww+16,0);
g.addColorStop(0,'rgba(255,250,240,.05)');g.addColorStop(.5,'rgba(255,250,240,.42)');g.addColorStop(1,'rgba(255,250,240,.6)');
c.fillStyle=g;c.fillRect(wx+ww-70,wy-8,86,wh+16);
c.globalAlpha=.25;c.strokeStyle='#fff';c.lineWidth=3;
for(let i=0;i<5;i++){const fx=wx+ww-58+i*15;c.beginPath();c.moveTo(fx,wy-8);c.quadraticCurveTo(fx+6,wy+wh/2,fx-2,wy+wh+8);c.stroke();}
c.restore();
})();
/* ================= the cat (three baked views, eyes live) ================= */
const FUR='#ece1cb',FUR_L='#f7efdd',FUR_D='#d3c3a4',PINK='#e59d8d',PINK_D='#c97e70';
function buildFront(){
const s=mkcv(1120,1280),c=s.getContext('2d');c.scale(2,2);
const R=mulberry(11);
// body
c.fillStyle=FUR;
c.beginPath();
c.moveTo(196,596);c.bezierCurveTo(150,590,118,560,112,505);c.bezierCurveTo(106,448,128,392,168,362);
c.bezierCurveTo(190,344,206,330,214,306);c.lineTo(346,306);
c.bezierCurveTo(354,330,370,344,392,362);c.bezierCurveTo(432,392,454,448,448,505);
c.bezierCurveTo(442,560,410,590,364,596);c.closePath();c.fill();
c.strokeStyle='rgba(120,100,70,.25)';c.lineWidth=3;
c.beginPath();c.ellipse(182,505,58,76,-.12,Math.PI*.6,Math.PI*1.4);c.stroke();
c.beginPath();c.ellipse(378,505,58,76,.12,Math.PI*1.6,Math.PI*2.4);c.stroke();
// front legs + paws
c.fillStyle=FUR;rr(c,244,330,38,262,16);c.fill();rr(c,318,330,38,262,16);c.fill();
c.strokeStyle='rgba(120,100,70,.28)';c.lineWidth=3;
c.beginPath();c.moveTo(300,368);c.quadraticCurveTo(298,470,300,584);c.stroke();
c.fillStyle=FUR_L;
c.beginPath();c.ellipse(264,588,36,17,0,0,TAU);c.fill();
c.beginPath();c.ellipse(336,588,36,17,0,0,TAU);c.fill();
c.strokeStyle='rgba(120,100,70,.35)';c.lineWidth=2;
for(const px of[252,266,280,324,338,352]){c.beginPath();c.moveTo(px,582);c.lineTo(px,598);c.stroke();}
// chest light + form shading (window is on the left)
let g=c.createRadialGradient(280,410,20,280,410,140);
g.addColorStop(0,'rgba(250,243,226,.9)');g.addColorStop(1,'rgba(250,243,226,0)');
c.fillStyle=g;c.beginPath();c.ellipse(280,430,98,115,0,0,TAU);c.fill();
g=c.createLinearGradient(110,0,460,0);
g.addColorStop(0,'rgba(255,240,210,.28)');g.addColorStop(.45,'rgba(255,240,210,0)');g.addColorStop(1,'rgba(70,50,35,.22)');
c.fillStyle=g;c.beginPath();c.ellipse(280,470,172,145,0,0,TAU);c.fill();
g=c.createRadialGradient(280,325,10,280,325,120);
g.addColorStop(0,'rgba(90,70,50,.25)');g.addColorStop(1,'rgba(90,70,50,0)');
c.fillStyle=g;c.beginPath();c.ellipse(280,345,92,62,0,0,TAU);c.fill();
// ears
function ear(bx,by,tx,ty,bx2,by2){
c.fillStyle=FUR;
c.beginPath();c.moveTo(bx,by);c.quadraticCurveTo(tx-14,ty-6,tx,ty);c.quadraticCurveTo(tx+14,ty+2,bx2,by2);
c.quadraticCurveTo((bx+bx2)/2,by+16,bx,by);c.closePath();c.fill();
c.fillStyle=PINK;c.globalAlpha=.85;
c.beginPath();c.moveTo(bx+10,by+3);c.quadraticCurveTo(tx-2,ty+15,tx+4,ty+11);c.quadraticCurveTo(tx+9,ty+17,bx2-11,by2+5);
c.quadraticCurveTo((bx+bx2)/2,by+24,bx+10,by+3);c.closePath();c.fill();c.globalAlpha=1;
fluff(c,(bx+bx2)/2,(by+by2)/2+10,18,12,Math.PI*.2,Math.PI*.8,5,9,'rgba(250,244,230,.7)',1.5,R);
}
ear(192,132,150,40,258,102);ear(368,132,410,40,302,102);
// head
c.fillStyle=FUR;c.beginPath();c.ellipse(280,205,110,98,0,0,TAU);c.fill();
g=c.createRadialGradient(235,175,30,235,175,150);
g.addColorStop(0,'rgba(255,244,220,.5)');g.addColorStop(1,'rgba(255,244,220,0)');
c.fillStyle=g;c.beginPath();c.ellipse(280,205,110,98,0,0,TAU);c.fill();
g=c.createLinearGradient(300,0,395,0);
g.addColorStop(0,'rgba(80,60,40,0)');g.addColorStop(1,'rgba(80,60,40,.2)');
c.fillStyle=g;c.beginPath();c.ellipse(280,205,110,98,0,0,TAU);c.fill();
c.globalAlpha=.1;c.fillStyle='#c98d4e';
c.beginPath();c.ellipse(280,132,44,24,0,0,TAU);c.fill();c.globalAlpha=1;
// muzzle
g=c.createRadialGradient(280,256,8,280,256,70);
g.addColorStop(0,'rgba(252,246,232,.95)');g.addColorStop(1,'rgba(252,246,232,0)');
c.fillStyle=g;c.beginPath();c.ellipse(280,256,66,48,0,0,TAU);c.fill();
c.fillStyle='rgba(110,90,60,.1)';
c.beginPath();c.ellipse(230,203,36,30,0,0,TAU);c.fill();
c.beginPath();c.ellipse(330,203,36,30,0,0,TAU);c.fill();
// nose + mouth
c.fillStyle=PINK;
c.beginPath();c.moveTo(268,236);c.quadraticCurveTo(280,231,292,236);c.quadraticCurveTo(289,252,280,254);c.quadraticCurveTo(271,252,268,236);c.closePath();c.fill();
c.fillStyle='rgba(255,255,255,.35)';c.beginPath();c.ellipse(275,240,4,2.5,-.4,0,TAU);c.fill();
c.strokeStyle='#7d6a50';c.lineWidth=3;c.lineCap='round';
c.beginPath();c.moveTo(280,254);c.lineTo(280,262);
c.moveTo(280,262);c.quadraticCurveTo(268,274,256,264);
c.moveTo(280,262);c.quadraticCurveTo(292,274,304,264);c.stroke();
fluff(c,280,292,26,10,Math.PI*.15,Math.PI*.85,8,8,'rgba(250,244,230,.8)',1.5,R);
c.fillStyle='rgba(110,90,65,.5)';
for(const[dx,dy]of[[-30,-6],[-34,2],[-28,10],[30,-6],[34,2],[28,10]]){c.beginPath();c.arc(280+dx,252+dy,1.6,0,TAU);c.fill();}
// fur
furTex(c,280,470,160,130,130,FUR_D,R,Math.PI/2);
furTex(c,280,470,160,130,60,FUR_L,R,Math.PI/2);
furTex(c,280,168,95,62,60,FUR_D,R,Math.PI/2);
fluff(c,280,205,110,98,-Math.PI*.83,-Math.PI*.17,26,12,FUR_L,2,R);
fluff(c,280,225,112,92,Math.PI*.62,Math.PI*.95,14,26,FUR,2.4,R);
fluff(c,280,225,112,92,Math.PI*.05,Math.PI*.38,14,26,FUR,2.4,R);
fluff(c,280,470,168,140,Math.PI*.55,Math.PI*.95,16,18,FUR_L,2.2,R);
fluff(c,280,470,168,140,Math.PI*.05,Math.PI*.45,16,18,FUR_L,2.2,R);
fluff(c,280,530,150,72,Math.PI*.15,Math.PI*.85,18,12,FUR,2,R);
fluff(c,280,430,82,92,Math.PI*.25,Math.PI*.75,14,20,'rgba(250,244,230,.8)',1.8,R);
// whiskers on top
c.strokeStyle='rgba(255,252,244,.85)';c.lineWidth=2;
c.beginPath();c.moveTo(246,244);c.quadraticCurveTo(200,236,152,224);c.stroke();
c.beginPath();c.moveTo(246,250);c.quadraticCurveTo(198,252,148,250);c.stroke();
c.beginPath();c.moveTo(246,256);c.quadraticCurveTo(200,266,154,278);c.stroke();
c.beginPath();c.moveTo(314,244);c.quadraticCurveTo(360,236,408,224);c.stroke();
c.beginPath();c.moveTo(314,250);c.quadraticCurveTo(362,252,412,250);c.stroke();
c.beginPath();c.moveTo(314,256);c.quadraticCurveTo(360,266,406,278);c.stroke();
// window rim light
c.strokeStyle='rgba(255,236,200,.5)';c.lineWidth=3;
c.beginPath();c.ellipse(280,470,168,142,0,Math.PI*.6,Math.PI*.95);c.stroke();
c.beginPath();c.ellipse(280,205,110,98,0,Math.PI*.65,Math.PI*.9);c.stroke();
return s;
}
function buildSide(){
const s=mkcv(1120,1280),c=s.getContext('2d');c.scale(2,2);
const R=mulberry(13);
// far ear
c.fillStyle=FUR_D;c.globalAlpha=.8;
c.beginPath();c.moveTo(224,152);c.quadraticCurveTo(244,92,252,72);c.quadraticCurveTo(262,108,252,156);c.closePath();c.fill();c.globalAlpha=1;
// body
c.fillStyle=FUR;
c.beginPath();
c.moveTo(238,592);c.bezierCurveTo(214,586,204,556,208,516);c.bezierCurveTo(212,468,220,428,238,396);
c.bezierCurveTo(248,376,254,352,256,322);c.lineTo(320,322);
c.bezierCurveTo(330,352,346,376,368,398);c.bezierCurveTo(404,432,420,472,418,514);
c.bezierCurveTo(416,562,392,588,348,592);c.closePath();c.fill();
c.fillStyle=FUR;c.beginPath();c.ellipse(356,504,72,82,0,0,TAU);c.fill();
c.strokeStyle='rgba(120,100,70,.25)';c.lineWidth=3;
c.beginPath();c.ellipse(356,504,72,82,0,-Math.PI*.45,Math.PI*.7);c.stroke();
c.fillStyle=FUR_D;c.globalAlpha=.55;
c.beginPath();c.moveTo(262,400);c.lineTo(286,400);c.lineTo(290,582);c.lineTo(268,582);c.closePath();c.fill();c.globalAlpha=1;
c.fillStyle=FUR;
c.beginPath();c.moveTo(224,384);c.lineTo(260,384);c.lineTo(254,588);c.lineTo(218,588);c.closePath();c.fill();
c.fillStyle=FUR_L;
c.beginPath();c.ellipse(238,588,30,15,0,0,TAU);c.fill();
c.beginPath();c.ellipse(352,590,32,15,0,0,TAU);c.fill();
// head profile
c.fillStyle=FUR;
c.beginPath();
c.moveTo(296,318);
c.bezierCurveTo(318,282,322,232,306,196);
c.bezierCurveTo(298,172,282,156,262,150);
c.bezierCurveTo(240,144,220,152,208,166);
c.bezierCurveTo(198,178,192,190,190,202);
c.bezierCurveTo(188,212,190,220,198,226);
c.bezierCurveTo(192,236,194,246,204,252);
c.bezierCurveTo(216,262,232,268,244,280);
c.bezierCurveTo(258,294,268,306,296,318);
c.closePath();c.fill();
c.fillStyle=FUR;
c.beginPath();c.moveTo(252,152);c.quadraticCurveTo(286,84,304,66);c.quadraticCurveTo(310,110,298,160);c.closePath();c.fill();
c.strokeStyle=PINK_D;c.lineWidth=3;c.globalAlpha=.6;
c.beginPath();c.moveTo(288,122);c.quadraticCurveTo(302,84,310,70);c.stroke();c.globalAlpha=1;
let g=c.createLinearGradient(160,0,330,0);
g.addColorStop(0,'rgba(255,242,214,.35)');g.addColorStop(.5,'rgba(255,242,214,0)');g.addColorStop(1,'rgba(70,50,35,.2)');
c.fillStyle=g;c.beginPath();c.ellipse(243,212,88,82,0,0,TAU);c.fill();
g=c.createLinearGradient(200,0,430,0);
g.addColorStop(0,'rgba(255,240,210,.22)');g.addColorStop(.5,'rgba(255,240,210,0)');g.addColorStop(1,'rgba(70,50,35,.2)');
c.fillStyle=g;c.beginPath();c.ellipse(310,480,120,120,0,0,TAU);c.fill();
// eye (baked; profile is only seen mid-spin)
c.fillStyle='#57721e';c.beginPath();c.ellipse(226,196,22,17,-.15,0,TAU);c.fill();
c.fillStyle='#9cb33c';c.beginPath();c.ellipse(226,196,18,14,-.15,0,TAU);c.fill();
c.fillStyle='#15100a';c.beginPath();c.ellipse(222,197,8,10,0,0,TAU);c.fill();
c.fillStyle='rgba(255,255,255,.85)';c.beginPath();c.arc(216,190,3.2,0,TAU);c.fill();
c.strokeStyle='rgba(90,70,50,.6)';c.lineWidth=2;c.beginPath();c.ellipse(226,196,22,17,-.15,0,TAU);c.stroke();
c.fillStyle=PINK;c.beginPath();c.ellipse(193,207,7,5,.3,0,TAU);c.fill();
c.strokeStyle='#7d6a50';c.lineWidth=2.5;
c.beginPath();c.moveTo(196,216);c.quadraticCurveTo(204,226,218,224);c.stroke();
g=c.createRadialGradient(205,215,4,205,215,40);
g.addColorStop(0,'rgba(252,246,232,.8)');g.addColorStop(1,'rgba(252,246,232,0)');
c.fillStyle=g;c.beginPath();c.ellipse(208,218,40,32,0,0,TAU);c.fill();
furTex(c,320,480,110,110,110,FUR_D,R,Math.PI/2);
furTex(c,243,200,70,66,40,FUR_D,R,Math.PI/2);
fluff(c,243,212,88,82,-Math.PI*.3,Math.PI*.35,18,22,FUR,2.4,R);
fluff(c,243,212,88,82,Math.PI*.55,Math.PI*.9,12,16,FUR_L,2,R);
fluff(c,268,330,54,44,Math.PI*.5,Math.PI*.95,10,18,'rgba(250,244,230,.8)',1.8,R);
fluff(c,340,500,100,110,-Math.PI*.2,Math.PI*.4,16,18,FUR_L,2.2,R);
fluff(c,320,560,110,50,Math.PI*.15,Math.PI*.7,14,12,FUR,2,R);
fluff(c,250,300,40,60,Math.PI*.4,Math.PI*.8,10,20,FUR,2.2,R);
c.strokeStyle='rgba(255,252,244,.85)';c.lineWidth=2;
c.beginPath();c.moveTo(198,210);c.quadraticCurveTo(158,206,120,198);c.stroke();
c.beginPath();c.moveTo(198,216);c.quadraticCurveTo(156,220,118,228);c.stroke();
c.strokeStyle='rgba(255,236,200,.45)';c.lineWidth=3;
c.beginPath();c.ellipse(310,480,118,118,0,Math.PI*.6,Math.PI*.95);c.stroke();
return s;
}
function buildBack(){
const s=mkcv(1120,1280),c=s.getContext('2d');c.scale(2,2);
const R=mulberry(17);
c.fillStyle=FUR;
c.beginPath();
c.moveTo(196,596);c.bezierCurveTo(150,590,118,560,112,505);c.bezierCurveTo(106,448,128,392,168,362);
c.bezierCurveTo(190,344,206,330,214,306);c.lineTo(346,306);
c.bezierCurveTo(354,330,370,344,392,362);c.bezierCurveTo(432,392,454,448,448,505);
c.bezierCurveTo(442,560,410,590,364,596);c.closePath();c.fill();
c.strokeStyle='rgba(120,100,70,.22)';c.lineWidth=3;
c.beginPath();c.ellipse(180,505,60,76,-.12,Math.PI*.6,Math.PI*1.4);c.stroke();
c.beginPath();c.ellipse(380,505,60,76,.12,Math.PI*1.6,Math.PI*2.4);c.stroke();
c.globalAlpha=.12;c.fillStyle='#c98d4e';
c.beginPath();c.ellipse(280,330,86,64,0,0,TAU);c.fill();c.globalAlpha=1;
c.fillStyle=FUR;c.beginPath();c.ellipse(280,205,108,96,0,0,TAU);c.fill();
function earB(bx,by,tx,ty,bx2,by2){
c.fillStyle=FUR;
c.beginPath();c.moveTo(bx,by);c.quadraticCurveTo(tx-14,ty-6,tx,ty);c.quadraticCurveTo(tx+14,ty+2,bx2,by2);
c.quadraticCurveTo((bx+bx2)/2,by+16,bx,by);c.closePath();c.fill();
c.strokeStyle='rgba(105,85,60,.55)';c.lineWidth=4;
c.beginPath();c.moveTo(bx+8,by+2);c.quadraticCurveTo(tx-4,ty+12,tx+2,ty+8);c.stroke();
}
earB(192,132,150,40,258,102);earB(368,132,410,40,302,102);
let g=c.createLinearGradient(150,0,420,0);
g.addColorStop(0,'rgba(255,240,210,.3)');g.addColorStop(.5,'rgba(255,240,210,0)');g.addColorStop(1,'rgba(70,50,35,.22)');
c.fillStyle=g;c.beginPath();c.ellipse(280,205,108,96,0,0,TAU);c.fill();
c.beginPath();c.ellipse(280,470,172,145,0,0,TAU);c.fill();
fluff(c,280,300,70,26,Math.PI*.1,Math.PI*.9,14,16,FUR_L,2.2,R);
furTex(c,280,470,160,130,120,FUR_D,R,Math.PI/2);
furTex(c,280,180,95,80,60,FUR_D,R,Math.PI/2);
fluff(c,280,205,108,96,-Math.PI*.85,-Math.PI*.15,26,12,FUR_L,2,R);
fluff(c,280,205,108,96,Math.PI*.05,Math.PI*.95,22,14,FUR,2.2,R);
fluff(c,280,470,168,140,Math.PI*.05,Math.PI*.95,24,16,FUR_L,2.2,R);
fluff(c,280,540,150,70,Math.PI*.15,Math.PI*.85,16,12,FUR,2,R);
c.strokeStyle='rgba(255,236,200,.5)';c.lineWidth=3;
c.beginPath();c.ellipse(280,205,108,96,0,Math.PI*.62,Math.PI*.95);c.stroke();
c.beginPath();c.ellipse(280,470,168,140,0,Math.PI*.6,Math.PI*.95);c.stroke();
return s;
}
const sprF=buildFront(),sprL=buildSide(),sprB=buildBack();
const sprR=mkcv(sprL.width,sprL.height);
{const rc=sprR.getContext('2d');rc.translate(sprL.width,0);rc.scale(-1,1);rc.drawImage(sprL,0,0);}
const CATVIEWS=[
{img:sprF,a:0,front:true},
{img:sprL,a:Math.PI/2},
{img:sprB,a:Math.PI},
{img:sprR,a:-Math.PI/2},
];
/* ================= furniture sprites (blur baked = depth of field) ================= */
const sprSofa=spr(560,400,2.2,c=>{
let g=c.createLinearGradient(0,80,0,380);
g.addColorStop(0,'#b26845');g.addColorStop(1,'#8a4630');
c.fillStyle='rgba(25,15,8,.28)';c.beginPath();c.ellipse(280,378,250,20,0,0,TAU);c.fill();
c.fillStyle=g;rr(c,24,120,512,250,36);c.fill();
c.fillStyle='rgba(60,28,16,.5)';rr(c,60,180,440,90,24);c.fill();
c.fillStyle='#b56a48';rr(c,44,14,226,160,30);c.fill();rr(c,290,14,226,160,30);c.fill();
c.fillStyle='rgba(255,220,190,.14)';rr(c,52,20,210,40,20);c.fill();rr(c,298,20,210,40,20);c.fill();
c.fillStyle='rgba(60,28,16,.18)';rr(c,52,130,210,36,18);c.fill();rr(c,298,130,210,36,18);c.fill();
c.fillStyle='#9d5236';rr(c,0,64,86,306,40);c.fill();rr(c,492,64,68,306,40);c.fill();
c.fillStyle='rgba(255,220,190,.12)';rr(c,10,80,26,270,13);c.fill();
c.fillStyle='#4a2f1c';c.fillRect(70,368,26,24);c.fillRect(470,368,26,24);
c.strokeStyle='rgba(60,28,16,.3)';c.lineWidth=3;
c.beginPath();c.moveTo(280,180);c.lineTo(280,268);c.stroke();
});
const sprPlant=spr(380,460,1.3,c=>{
const R=mulberry(21);
c.fillStyle='#a45a38';
c.beginPath();c.moveTo(112,368);c.lineTo(268,368);c.lineTo(252,446);c.lineTo(128,446);c.closePath();c.fill();
c.fillStyle='#b06a44';rr(c,104,352,172,24,10);c.fill();
c.fillStyle='rgba(60,25,12,.35)';
c.beginPath();c.moveTo(220,368);c.lineTo(268,368);c.lineTo(252,446);c.lineTo(226,446);c.closePath();c.fill();
c.fillStyle='#3a2a1c';c.beginPath();c.ellipse(190,362,74,12,0,0,TAU);c.fill();
c.strokeStyle='#55703f';c.lineWidth=5;c.lineCap='round';
const tips=[[120,120],[190,70],[262,110],[96,210],[286,200],[160,170],[230,170]];
for(const[tx,ty]of tips){c.beginPath();c.moveTo(190,360);c.quadraticCurveTo(190+(tx-190)*.3,260,tx,ty);c.stroke();}
for(const[tx,ty]of tips){
leaf(c,tx,ty,-.9+R()*1.8,52+R()*26,'#6a8a52','#4c6a3a');
leaf(c,tx,ty,-.9+R()*1.8,40+R()*20,'#5d7d47','#42603a');
}
leaf(c,150,280,-1.2,44,'#6a8a52','#4c6a3a');
leaf(c,236,270,.4,46,'#5d7d47','#42603a');
});
const sprLampP=spr(120,520,.8,c=>{
c.fillStyle='#3c2d1e';c.beginPath();c.ellipse(60,504,46,13,0,0,TAU);c.fill();
const g=c.createLinearGradient(52,0,68,0);
g.addColorStop(0,'#6a5238');g.addColorStop(.5,'#4c3a26');g.addColorStop(1,'#33261a');
c.fillStyle=g;c.fillRect(55,26,10,464);
c.fillStyle='#5a4632';c.beginPath();c.ellipse(60,30,14,6,0,0,TAU);c.fill();
});
const sprShade=spr(240,180,1,c=>{
let g=c.createRadialGradient(120,95,10,120,95,135);
g.addColorStop(0,'rgba(255,196,120,.55)');g.addColorStop(1,'rgba(255,196,120,0)');
c.fillStyle=g;c.fillRect(0,0,240,180);
g=c.createLinearGradient(0,30,0,145);
g.addColorStop(0,'#f6e6c0');g.addColorStop(1,'#d8bd8e');
c.fillStyle=g;
c.beginPath();c.moveTo(62,32);c.lineTo(178,32);c.lineTo(206,142);c.lineTo(34,142);c.closePath();c.fill();
c.fillStyle='#efe0ba';c.beginPath();c.ellipse(120,32,58,10,0,0,TAU);c.fill();
c.fillStyle='rgba(120,90,50,.4)';c.beginPath();c.ellipse(120,142,86,12,0,0,TAU);c.fill();
g=c.createRadialGradient(120,142,4,120,142,60);
g.addColorStop(0,'rgba(255,226,160,.9)');g.addColorStop(1,'rgba(255,226,160,0)');
c.fillStyle=g;c.beginPath();c.ellipse(120,140,80,26,0,0,TAU);c.fill();
});
const sprFrame=spr(210,250,1.1,c=>{
c.fillStyle='rgba(30,18,10,.3)';rr(c,18,20,170,212,6);c.fill();
c.fillStyle='#6d4c33';rr(c,10,10,170,212,6);c.fill();
c.strokeStyle='rgba(255,230,200,.35)';c.lineWidth=2;rr(c,13,13,164,206,5);c.stroke();
c.fillStyle='#e9e2d0';rr(c,26,26,138,180,2);c.fill();
c.save();c.beginPath();c.rect(34,34,122,164);c.clip();
let g=c.createLinearGradient(0,34,0,140);
g.addColorStop(0,'#c9d6d4');g.addColorStop(1,'#aec2b8');
c.fillStyle=g;c.fillRect(34,34,122,164);
c.fillStyle='#f2d9a0';c.beginPath();c.arc(120,66,13,0,TAU);c.fill();
c.fillStyle='#7a8a5d';c.beginPath();c.ellipse(70,150,80,42,0,0,TAU);c.fill();
c.fillStyle='#5d7048';c.beginPath();c.ellipse(140,168,80,40,0,0,TAU);c.fill();
c.fillStyle='#4a5c38';c.beginPath();c.arc(60,110,16,0,TAU);c.fill();
c.fillStyle='#3c2c1c';c.fillRect(58,118,4,26);
c.restore();
});
const sprShelf=spr(320,64,1,c=>{
c.fillStyle='rgba(30,18,10,.25)';c.fillRect(6,44,308,8);
c.fillStyle='#7a5638';rr(c,0,30,320,16,4);c.fill();
c.fillStyle='rgba(255,230,200,.25)';c.fillRect(2,31,316,3);
c.fillStyle='#5d3f2a';
c.beginPath();c.moveTo(40,46);c.lineTo(58,46);c.lineTo(40,62);c.closePath();c.fill();
c.beginPath();c.moveTo(262,46);c.lineTo(280,46);c.lineTo(280,62);c.closePath();c.fill();
});
const sprTable=spr(280,230,1.6,c=>{
c.fillStyle='rgba(25,15,8,.3)';c.beginPath();c.ellipse(140,206,80,12,0,0,TAU);c.fill();
c.fillStyle='#6f4c32';c.beginPath();c.ellipse(140,56,120,30,0,0,TAU);c.fill();
c.fillStyle='#8a6242';c.beginPath();c.ellipse(140,46,120,30,0,0,TAU);c.fill();
c.strokeStyle='rgba(255,230,200,.25)';c.lineWidth=2;c.beginPath();c.ellipse(140,46,112,26,0,0,TAU);c.stroke();
c.fillStyle='#5d3f2a';c.fillRect(128,66,24,120);
c.fillStyle='#4a3222';c.beginPath();c.ellipse(140,192,50,12,0,0,TAU);c.fill();
});
const sprMug=spr(60,70,.6,c=>{
c.fillStyle='rgba(25,15,8,.3)';c.beginPath();c.ellipse(30,64,20,5,0,0,TAU);c.fill();
c.fillStyle='#e8ddc6';rr(c,10,18,38,44,6);c.fill();
c.fillStyle='#d8c9ae';c.beginPath();c.ellipse(29,20,19,6,0,0,TAU);c.fill();
c.fillStyle='#4a3220';c.beginPath();c.ellipse(29,21,14,4,0,0,TAU);c.fill();
c.strokeStyle='#e8ddc6';c.lineWidth=6;c.beginPath();c.arc(52,40,10,-1.2,1.2);c.stroke();
c.fillStyle='rgba(255,255,255,.25)';c.fillRect(14,24,5,32);
});
const sprCush=spr(230,92,1.6,c=>{
c.fillStyle='rgba(25,15,8,.25)';c.beginPath();c.ellipse(115,84,100,8,0,0,TAU);c.fill();
c.fillStyle='#c07a52';rr(c,6,8,218,72,30);c.fill();
c.fillStyle='rgba(255,225,195,.18)';rr(c,16,14,198,26,14);c.fill();
c.strokeStyle='rgba(90,40,22,.35)';c.lineWidth=2.5;
c.beginPath();c.moveTo(30,52);c.quadraticCurveTo(115,64,200,52);c.stroke();
});
const sprLeaf=mkcv(76,48);{const c=sprLeaf.getContext('2d');leaf(c,6,24,0,62,'#6a8a52','#4c6a3a');}
/* ================= props / entities ================= */
const props=[],propByName={};
function addProp(o){const p=Object.assign({x:o.hx,y:o.hy,rot:0,depth:1,fl:0,jx:0,jy:0,seed:Math.random()*7},o);props.push(p);propByName[o.name]=p;}
addProp({name:'frame',spr:sprFrame,hx:600,hy:170,ax:10,ay:10,z:0,floats:true});
addProp({name:'shelf',spr:sprShelf,hx:560,hy:445,ax:0,ay:0,z:0,floats:true});
addProp({name:'lampP',spr:sprLampP,hx:1000,hy:880,ax:60,ay:517,z:0,floats:true,seed:3.5});
addProp({name:'shade',spr:sprShade,hx:1000,hy:335,ax:120,ay:88,z:0,floats:true,seed:3.5,shade:true});
addProp({name:'plant',spr:sprPlant,hx:1270,hy:885,ax:190,ay:445,z:0,floats:true,sway:true});
addProp({name:'sofa',spr:sprSofa,hx:1110,hy:420,ax:0,ay:0,z:0,floats:true});
addProp({name:'table',spr:sprTable,hx:530,hy:937,ax:140,ay:204,z:1,floats:true});
addProp({name:'mug',spr:sprMug,hx:545,hy:779,ax:30,ay:64,z:1,floats:true,rattle:true});
addProp({name:'cush1',spr:sprCush,hx:1185,hy:645,ax:115,ay:84,z:0,floats:true});
addProp({name:'cush2',spr:sprCush,hx:1420,hy:645,ax:115,ay:84,z:0,floats:true});
const books=[],bookCols=['#a4553f','#5c6e52','#3f4a63','#c2953f','#d8cdb4','#734b3b','#4e6158'];
for(let i=0;i<7;i++){
const bw=26+(i*7)%9,bh=76+(i*13)%28;
const bcv=mkcv(bw+10,bh+8),bc=bcv.getContext('2d');
bc.fillStyle=bookCols[i];rr(bc,4,4,bw,bh,3);bc.fill();
bc.fillStyle='rgba(255,240,220,.35)';bc.fillRect(6,8+bh*.16,bw-4,4);bc.fillRect(6,8+bh*.7,bw-4,4);
bc.fillStyle='rgba(255,255,255,.16)';bc.fillRect(6,6,3,bh-4);
bc.strokeStyle='rgba(0,0,0,.25)';bc.lineWidth=1.5;rr(bc,4,4,bw,bh,3);bc.stroke();
books.push({spr:bcv,ax:(bw+10)/2,ay:bh+6,hx:592+i*40,hy:475,x:592+i*40,y:475,st:'shelf',vx:0,vy:0,rot:0,vr:0,seed:Math.random()*7,toppleAt:1e9,depth:1,ang:0,r:0,spd:0});
}
const leaves=[],shards=[],puffs=[];
const SHARDCOLS=['#8a6a4b','#a79a89','#5e7a70','#a75b3d','#ece1cb','#d9cfbc','#6d4c33','#9c7a55'];
const motes=[];
{const R=mulberry(5);for(let i=0;i<26;i++)motes.push({x:200+R()*760,y:480+R()*500,vx:6+R()*10,vy:3+R()*6,s:.8+R()*1.6,a:.04+R()*.09});}
/* ================= state ================= */
const STAGES=[
{h:0, rpm:0, rock:0, shake:0, zoom:1, ab:0},
{h:85, rpm:1.7,rock:.05, shake:1.6,zoom:1.01,ab:0},
{h:180,rpm:3.4,rock:.11, shake:4.5,zoom:1.03,ab:1},
{h:300,rpm:5.8,rock:.22, shake:8.5,zoom:1.06,ab:2},
{h:410,rpm:9.5,rock:.30, shake:13, zoom:.93, ab:3},
];
const cat={x:800,lift:0,bob:0,phi:0,omega:0};
const cam={x:0,y:0,zoom:1,roll:0};
let stage=0,extra=0,wind=0;const WIND0=.36;
let squash=1,phiKick=0,liftV=0,pupil=.22,blink=0,blinkAnim=0,blinkT=2.5;
let tailT=3,tailA=0,tailF=0,roomT=0,zk=0,kickShake=0,faceFlash=0,lastFlash=-1;
let holdUntil=0,nextOii=0,uttCount=0,puffAt=0,hintGone=false,muted=false;
let DT=.016,tNow=0;
const evq=[];
let capScale=1,capA=0,capRot=0,capX=0;
const capEl=document.getElementById('cap'),hintEl=document.getElementById('hint'),muteBtn=document.getElementById('mute');
function catY(){return 900-cat.lift+cat.bob;}
/* ================= audio: the "oiia" voice ================= */
let actx=null,master,bus,noiseBuf;
const VOW={O:[420,780,2500],I:[320,2200,2900],A:[780,1230,2650],U:[360,880,2350]};
const SHORTSEQ=[['O',.24],['I',.16],['I',.10],['A',.28],['A',.22]];
const LONGSEQ=[['O',.13],['I',.11],['A',.15],['U',.18],['U',.12],['I',.11],['A',.20]];
function ensureAudio(){
if(actx){actx.resume&&actx.resume();return;}
try{
actx=new(window.AudioContext||window.webkitAudioContext)();
master=actx.createGain();master.gain.value=muted?0:.85;
const comp=actx.createDynamicsCompressor();
comp.threshold.value=-16;comp.ratio.value=5;comp.attack.value=.004;comp.release.value=.24;
const peak=actx.createBiquadFilter();peak.type='peaking';peak.frequency.value=1900;peak.gain.value=3.5;peak.Q.value=.9;
const ws=actx.createWaveShaper();
const n=1024,curve=new Float32Array(n);
for(let i=0;i<n;i++){const x=i/(n-1)*2-1;curve[i]=Math.tanh(1.5*x)/Math.tanh(1.5);}
ws.curve=curve;ws.oversample='2x';
bus=actx.createGain();bus.gain.value=.9;
bus.connect(ws);ws.connect(peak);peak.connect(comp);comp.connect(master);master.connect(actx.destination);
noiseBuf=actx.createBuffer(1,actx.sampleRate*2,actx.sampleRate);
const d=noiseBuf.getChannelData(0);
for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1;
}catch(e){actx=null;}
}
/* one "oiia": sawtooth glottal source -> three parallel bandpass formants whose
center frequencies glide through the vowel sequence; amplitude pulses on the
segment boundaries; vibrato + breath noise make it a voice, not a tone. */
function scheduleUtterance(t,dur,f0,inten,long,gv){
const seq=long?LONGSEQ:SHORTSEQ;
const osc=actx.createOscillator();osc.type='sawtooth';
osc.frequency.setValueAtTime(f0*.9,t);
osc.frequency.linearRampToValueAtTime(f0,t+dur*.12);
osc.frequency.setValueAtTime(f0,t+dur*.75);
osc.frequency.linearRampToValueAtTime(f0*.93,t+dur);
const lfo=actx.createOscillator();lfo.frequency.value=4.6+Math.random()*1.8;
const lg=actx.createGain();lg.gain.value=f0*.018*(.4+inten);
lfo.connect(lg);lg.connect(osc.frequency);lfo.start(t);lfo.stop(t+dur+.05);
const vgain=actx.createGain();
vgain.gain.setValueAtTime(.0001,t);
vgain.gain.linearRampToValueAtTime(1,t+.03);
let tt=t;
for(const[v,fr]of seq){tt+=fr*dur;const amp=v==='I'?1.15:(v==='A'?1:.82);vgain.gain.linearRampToValueAtTime(amp,Math.min(tt,t+dur*.97));}
vgain.gain.linearRampToValueAtTime(.0001,t+dur);
const fmul=1+(f0-180)/1500;
const out=actx.createGain();out.gain.value=(.17+.11*inten)*gv;
const dg=actx.createGain();dg.gain.value=.12;osc.connect(dg);dg.connect(vgain);
const F0=VOW[seq[0][0]];
[[1,9,1],[2,11,.8],[3,13,.4]].forEach(ix=>{
const bp=actx.createBiquadFilter();bp.type='bandpass';bp.Q.value=ix[1];
bp.frequency.setValueAtTime(Math.min(F0[ix[0]-1]*fmul,18000),t);
let s=t;
for(const[v,fr]of seq){s+=fr*dur;bp.frequency.linearRampToValueAtTime(Math.min(VOW[v][ix[0]-1]*fmul,18000),s);}
const fg=actx.createGain();fg.gain.value=ix[2];
osc.connect(bp);bp.connect(fg);fg.connect(vgain);
});
vgain.connect(out);out.connect(bus);
const ns=actx.createBufferSource();ns.buffer=noiseBuf;ns.loop=true;
const nbp=actx.createBiquadFilter();nbp.type='bandpass';nbp.Q.value=3;
nbp.frequency.setValueAtTime(F0[1]*fmul,t);
let s2=t;
for(const[v,fr]of seq){s2+=fr*dur;nbp.frequency.linearRampToValueAtTime(VOW[v][1]*fmul*1.2,s2);}
const ng=actx.createGain();ng.gain.setValueAtTime(.0001,t);
ng.gain.linearRampToValueAtTime(.05+.07*inten,t+dur*.2);
ng.gain.linearRampToValueAtTime(.0001,t+dur);
ns.connect(nbp);nbp.connect(ng);ng.connect(vgain);
ns.start(t);ns.stop(t+dur+.05);
osc.start(t);osc.stop(t+dur+.05);
}
function whoosh(t,dur,inten){
const ns=actx.createBufferSource();ns.buffer=noiseBuf;ns.loop=true;
const bp=actx.createBiquadFilter();bp.type='bandpass';bp.Q.value=1.1;
bp.frequency.setValueAtTime(240,t);
bp.frequency.linearRampToValueAtTime(420+900*inten,t+dur*.6);
bp.frequency.linearRampToValueAtTime(260,t+dur);
const g=actx.createGain();g.gain.setValueAtTime(.0001,t);
g.gain.linearRampToValueAtTime(.05+.1*inten,t+dur*.45);
g.gain.linearRampToValueAtTime(.0001,t+dur);
ns.connect(bp);bp.connect(g);g.connect(bus);ns.start(t);ns.stop(t+dur+.02);
}
function thump(t,inten){
const o=actx.createOscillator();o.type='sine';
o.frequency.setValueAtTime(95,t);o.frequency.exponentialRampToValueAtTime(40,t+.14);
const g=actx.createGain();g.gain.setValueAtTime(.0001,t);
g.gain.linearRampToValueAtTime(.5*inten+.15,t+.015);
g.gain.exponentialRampToValueAtTime(.0001,t+.2);
o.connect(g);g.connect(bus);o.start(t);o.stop(t+.22);
}
function riser(){
const t=actx.currentTime;
const ns=actx.createBufferSource();ns.buffer=noiseBuf;ns.loop=true;
const bp=actx.createBiquadFilter();bp.type='bandpass';bp.Q.value=2;
bp.frequency.setValueAtTime(260,t);bp.frequency.exponentialRampToValueAtTime(2600,t+.42);
const g=actx.createGain();g.gain.setValueAtTime(.0001,t);
g.gain.linearRampToValueAtTime(.14,t+.3);g.gain.linearRampToValueAtTime(.0001,t+.46);
ns.connect(bp);bp.connect(g);g.connect(bus);ns.start(t);ns.stop(t+.5);
const o=actx.createOscillator();o.type='sawtooth';
o.frequency.setValueAtTime(150,t);o.frequency.exponentialRampToValueAtTime(520,t+.4);
const og=actx.createGain();og.gain.setValueAtTime(.0001,t);
og.gain.linearRampToValueAtTime(.06,t+.25);og.gain.linearRampToValueAtTime(.0001,t+.44);
o.connect(og);og.connect(bus);o.start(t);o.stop(t+.48);
}
function scheduleThud(t){
const o=actx.createOscillator();o.type='sine';
o.frequency.setValueAtTime(85,t);o.frequency.exponentialRampToValueAtTime(34,t+.16);
const g=actx.createGain();g.gain.setValueAtTime(.0001,t);
g.gain.linearRampToValueAtTime(.6,t+.012);g.gain.exponentialRampToValueAtTime(.0001,t+.24);
o.connect(g);g.connect(bus);o.start(t);o.stop(t+.26);
}
function scheduleSlam(t){
const o=actx.createOscillator();o.type='sine';
o.frequency.setValueAtTime(160,t);o.frequency.exponentialRampToValueAtTime(28,t+.5);
const g=actx.createGain();g.gain.setValueAtTime(.0001,t);
g.gain.linearRampToValueAtTime(.7,t+.02);g.gain.exponentialRampToValueAtTime(.0001,t+.6);
o.connect(g);g.connect(bus);o.start(t);o.stop(t+.65);
const ns=actx.createBufferSource();ns.buffer=noiseBuf;
const bp=actx.createBiquadFilter();bp.type='lowpass';bp.frequency.value=900;
const ng=actx.createGain();ng.gain.setValueAtTime(.35,t);ng.gain.exponentialRampToValueAtTime(.0001,t+.4);
ns.connect(bp);bp.connect(ng);ng.connect(bus);ns.start(t);ns.stop(t+.45);
}
function audioTick(){
if(!actx||stage<1||wind>0)return;
const now=actx.currentTime;
if(now<holdUntil)return;
if(nextOii<now-.25)nextOii=now+.06;
const revP=TAU/Math.max(cat.omega,.8);
while(nextOii<now+.2){
const dur=clamp(revP,.085,1.3);
const rpm=cat.omega/TAU;
const f0=Math.min(790,166*Math.pow(2,rpm/5.2))*(.98+Math.random()*.04);
const inten=clamp(rpm/9,.2,1);
const long=(uttCount%4===3)&&dur>.3;
scheduleUtterance(nextOii,dur*(long?.97:.9),f0,inten,long,1);
if(stage>=3){
scheduleUtterance(nextOii+.012,dur*.88,f0*1.005,inten,long,.45);
scheduleUtterance(nextOii+.023,dur*.88,f0*.995,inten,long,.45);
}
if(stage>=2)whoosh(nextOii,dur,inten);
if(stage>=3)thump(nextOii,inten);
evq.push({t:nextOii,txt:long?'oiiaiooooiai':'oiia',long});
uttCount++;nextOii+=dur;
}
while(evq.length&&evq[0].t<=now+.004){
const e=evq.shift();
if(tNow-lastFlash>.13){faceFlash=1;lastFlash=tNow;}
zk=Math.max(zk,e.long?1:.55);
popCaption(e.txt,e.long);
}
}
function popCaption(txt,long){
capEl.textContent=txt;
capEl.style.fontSize=long?'min(6.5vw,84px)':'min(10vw,120px)';
capScale=long?1.6:1.35;capRot=Math.random()*10-5;capX=Math.random()*56-28;capA=1;
}
/* ================= drawing ================= */
function applyCam(c){
c.setTransform(VS,0,0,VS,VOX,VOY);
c.translate(DW/2,DH/2);c.rotate(cam.roll);c.scale(cam.zoom,cam.zoom);
c.translate(-DW/2+cam.x,-DH/2+cam.y);
}
function drawVoid(t){
sc.fillStyle='#160f08';sc.fillRect(-1600,-1600,DW+3200,DH+3200);
const cx=cat.x,cy=catY()-100;
let g=sc.createRadialGradient(cx,cy,40,cx,cy,640);
g.addColorStop(0,'rgba(97,63,33,.6)');g.addColorStop(.5,'rgba(60,38,20,.26)');g.addColorStop(1,'rgba(60,38,20,0)');
sc.fillStyle=g;sc.fillRect(cx-700,cy-700,1400,1400);
sc.save();sc.translate(cx,cy);sc.rotate(t*.22);
sc.globalAlpha=.05;sc.fillStyle='#ffcf92';
for(let i=0;i<10;i++){sc.rotate(TAU/10);sc.beginPath();sc.moveTo(0,0);sc.lineTo(980,-70);sc.lineTo(980,70);sc.closePath();sc.fill();}
sc.restore();sc.globalAlpha=1;
sc.strokeStyle='rgba(255,220,170,.1)';sc.lineWidth=2;
for(let i=0;i<3;i++){sc.beginPath();sc.ellipse(cx,cy,210+i*130,(210+i*130)*.36,0,0,TAU);sc.stroke();}
}
function drawRoomFading(){
const cx=cat.x,cy=catY()-120;
sc.save();
sc.translate(cx,cy);sc.rotate(roomT*.55);const s=1+roomT*.85;sc.scale(s,s);sc.translate(-cx,-cy);
sc.globalAlpha=Math.max(0,1-roomT);
sc.drawImage(room,0,0);
sc.restore();sc.globalAlpha=1;
}
function drawShadow(){
if(stage>=4&&roomT>.25)return;
const k=Math.max(0,1-cat.lift/380);
if(k<=.02)return;
sc.save();sc.translate(cat.x,902);sc.scale(1,.2);
const g=sc.createRadialGradient(0,0,10,0,0,180*k+60);
g.addColorStop(0,'rgba(24,15,8,'+(.5*k).toFixed(3)+')');g.addColorStop(1,'rgba(24,15,8,0)');
sc.fillStyle=g;sc.beginPath();sc.arc(0,0,180*k+60,0,TAU);sc.fill();
sc.restore();
}
function drawPuffs(){
for(const p of puffs){
sc.globalAlpha=Math.max(0,p.l)*.5;
sc.fillStyle='#d8c4a8';
sc.beginPath();sc.arc(p.x,p.y,p.s,0,TAU);sc.fill();
}
sc.globalAlpha=1;
}
function drawMotes(){
for(const m of motes){
sc.globalAlpha=m.a;sc.fillStyle='#fff2dd';
sc.beginPath();sc.arc(m.x,m.y,m.s,0,TAU);sc.fill();
}
sc.globalAlpha=1;
}
function drawProp(p){
sc.save();
sc.translate(p.x+(p.jx||0),p.y+(p.jy||0));
sc.rotate(p.rot||0);
const d=p.depth||1;sc.scale(d,d);
sc.drawImage(p.spr,-p.ax,-p.ay);
sc.restore();
}
function drawBook(b){
sc.save();sc.translate(b.x,b.y);sc.rotate(b.rot);
const d=b.depth||1;sc.scale(d,d);
sc.drawImage(b.spr,-b.ax,-b.ay);sc.restore();
}
function drawLeaf(l){
sc.save();sc.translate(l.x,l.y);sc.rotate(l.rot);
const d=l.depth||1;sc.scale(d,d);
sc.drawImage(sprLeaf,-6,-24);sc.restore();
}
function drawShard(s){
const depth=Math.sin(s.ang+s.seed);
const x=cat.x+Math.cos(s.ang)*s.r,y=catY()-70+Math.sin(s.ang)*s.r*.42+s.yOff;
const d=.5+.5*(depth*.5+.5);
sc.save();sc.translate(x,y);sc.rotate(s.ang*1.7+s.seed);sc.scale(d,d);
sc.globalAlpha=.9;sc.fillStyle=s.col;
sc.fillRect(-s.size/2,-s.size/2,s.size,s.size*.72);
sc.globalAlpha=1;sc.restore();
}
function collect(back,front){
for(const p of props){
const f=()=>drawProp(p);
if(stage>=4&&p.orbit)(Math.sin(p.ang+p.seed)>0?front:back).push(f);
else(p.z===1?front:back).push(f);
}
for(const b of books){const f=()=>drawBook(b);
if(b.st==='orbit')(Math.sin(b.ang)>0?front:back).push(f);else back.push(f);}
for(const l of leaves){const f=()=>drawLeaf(l);
if(l.st==='orbit')(Math.sin(l.ang)>0?front:back).push(f);else back.push(f);}
if(stage>=4)for(const s of shards){const f=()=>drawShard(s);
(Math.sin(s.ang+s.seed)>0?front:back).push(f);}
}
function drawEye(c,px,py,bl,pu){
c.save();c.translate(px,py);
c.beginPath();c.ellipse(0,0,30,27,0,0,TAU);
c.save();c.clip();
let g=c.createRadialGradient(-6,-8,4,0,0,32);
g.addColorStop(0,'#d8e39a');g.addColorStop(.55,'#9cb33c');g.addColorStop(1,'#57721e');
c.fillStyle=g;c.fillRect(-32,-30,64,60);
const pr=9+pu*15;
c.fillStyle='#15100a';
c.beginPath();c.ellipse(0,2,pr*.82,pr,0,0,TAU);c.fill();
c.fillStyle='rgba(255,255,255,.92)';c.beginPath();c.arc(-9,-9,4.5,0,TAU);c.fill();
c.fillStyle='rgba(255,255,255,.5)';c.beginPath();c.arc(8,9,2.4,0,TAU);c.fill();
if(bl>0){
c.fillStyle='#e6dac2';c.fillRect(-32,-30,64,60*bl);
if(bl>.8){c.strokeStyle='#8f7c60';c.lineWidth=2.5;c.beginPath();c.moveTo(-28,-30+60*bl);c.lineTo(28,-30+60*bl);c.stroke();}
}
c.restore();
c.strokeStyle='rgba(90,70,48,.7)';c.lineWidth=2.5;
c.beginPath();c.ellipse(0,0,30,27,0,0,TAU);c.stroke();
c.strokeStyle='rgba(90,70,48,.9)';c.lineWidth=3.5;
c.beginPath();c.ellipse(0,-2,30,26,0,Math.PI*1.12,Math.PI*1.88);c.stroke();
c.restore();
}
/* turntable: four baked views crossfaded with cosine foreshortening */
function drawCatBody(phi,al,pawY,sy){
let sum=0;
for(const v of CATVIEWS){
const d=adist(phi,v.a);
v.d=d;v.w=d<Math.PI/2?Math.cos(d):0;sum+=v.w;
}
if(sum<.002)return;
CATVIEWS.sort((a,b)=>a.w-b.w);
for(const v of CATVIEWS){
if(v.w<.02)continue;
const k=Math.max(.09,v.w);
const a2=Math.min(1,al*(v.w/sum)*1.6);
sc.save();
sc.translate(cat.x,pawY);sc.scale(k,sy);
sc.globalAlpha=a2;
sc.drawImage(v.img,-280,-600);
sc.restore();
if(v.front&&a2>.04){
sc.save();
sc.translate(cat.x,pawY);sc.scale(k,sy);
sc.globalAlpha=a2;
drawEye(sc,-50,-397,blink,pupil);
drawEye(sc,50,-397,blink,pupil);
sc.restore();
}
}
}
function taper(pts,w0,w1,col){
sc.strokeStyle=col;sc.lineCap='round';
for(let i=0;i<pts.length-1;i++){
const tt=i/(pts.length-1);
sc.lineWidth=w0+(w1-w0)*tt;
sc.beginPath();sc.moveTo(pts[i][0],pts[i][1]);sc.lineTo(pts[i+1][0],pts[i+1][1]);sc.stroke();
}
}
function drawTail(t,pawY){
const bx=cat.x+76,by=pawY-96;
if(stage===0){
taper([[bx,by],[cat.x+152,pawY-58],[cat.x+142,pawY-16],[cat.x+112+tailF*16,pawY-8-tailF*20]],15,6,'#e2d5ba');
}else{
const ta=cat.phi+Math.PI/2;
const L=80+Math.min(150,cat.omega*13);
const w1=Math.sin(t*17)*.22;
const mx=bx+Math.cos(ta+w1)*L*.5,my=by+Math.sin(ta+w1)*L*.16-8;
const tx=bx+Math.cos(ta+w1)*L,ty=by+Math.sin(ta+w1)*L*.32;
taper([[bx,by],[mx,my],[tx,ty]],14,5,'#e2d5ba');
if(stage>=3){
sc.globalAlpha=.4;
taper([[bx,by],[bx-Math.cos(ta)*L*.45,by-Math.sin(ta)*L*.45*.16-8],[bx-Math.cos(ta)*L*.9,by-Math.sin(ta)*L*.9*.32]],10,4,'#c9b795');
sc.globalAlpha=1;
sc.globalAlpha=.12;sc.strokeStyle='#e2d5ba';sc.lineWidth=6;
sc.beginPath();sc.ellipse(cat.x,pawY-96,L+34,(L+34)*.3,0,0,TAU);sc.stroke();
sc.globalAlpha=1;
}
}
}
function drawCatAll(t,pawY){
const sy=(1+(stage===0?Math.sin(t*1.9)*.009:0))*squash;
const phiD=cat.phi+phiKick;
const n=(stage>=1&&wind<=0)?Math.min(5,1+Math.floor(cat.omega*DT/.055)):1;
for(let i=n-1;i>=0;i--){
const off=n>1?(i/(n-1))*cat.omega*DT*.8:0;
const al=n>1?Math.min(1,1.3/n):1;
drawCatBody(phiD-off,al,pawY,sy);
}
drawTail(t,pawY);
if(stage>=3&&faceFlash>.05){
sc.save();
sc.globalAlpha=Math.min(.6,faceFlash*.6);
sc.translate(cat.x,pawY);sc.scale(1,sy);
sc.drawImage(sprF,-280,-600);
sc.globalAlpha=Math.min(.85,faceFlash);
drawEye(sc,-50,-397,0,1);drawEye(sc,50,-397,0,1);
sc.restore();
}
}
function speedLines(){
sc.save();sc.globalCompositeOperation='lighter';
const cx=cat.x,cy=catY()-120;
for(let i=0;i<12;i++){
const a=Math.random()*TAU,r0=190+Math.random()*140,len=100+Math.random()*240;
sc.strokeStyle='rgba(255,238,206,'+(.04+Math.random()*.07).toFixed(3)+')';
sc.lineWidth=1+Math.random()*2;
sc.beginPath();
sc.moveTo(cx+Math.cos(a)*r0,cy+Math.sin(a)*r0*.62);
sc.lineTo(cx+Math.cos(a)*(r0+len),cy+Math.sin(a)*(r0+len)*.62);
sc.stroke();
}
sc.restore();
}
function lampGlow(t){
if(stage>=4&&roomT>.5)return;
const p=propByName.shade;if(!p)return;
sc.save();sc.globalCompositeOperation='lighter';
const a=.10+.05*Math.sin(t*11)+.03*Math.sin(t*23+1);
const g=sc.createRadialGradient(p.x,p.y+30,10,p.x,p.y+30,150);
g.addColorStop(0,'rgba(255,190,110,'+a.toFixed(3)+')');g.addColorStop(1,'rgba(255,190,110,0)');
sc.fillStyle=g;sc.beginPath();sc.arc(p.x,p.y+30,150,0,TAU);sc.fill();
sc.restore();
}
/* post: RGB split, grain, vignette */
function chroma(ab){
const w=cv.width,h=cv.height;
const sh=(1.4+ab*1.7+zk*5)*DPR;
fa.setTransform(1,0,0,1,0,0);fa.globalCompositeOperation='source-over';fa.drawImage(scene,0,0);
fa.globalCompositeOperation='multiply';fa.fillStyle='#ff0000';fa.fillRect(0,0,w,h);
fb.setTransform(1,0,0,1,0,0);fb.globalCompositeOperation='source-over';fb.drawImage(scene,0,0);
fb.globalCompositeOperation='multiply';fb.fillStyle='#00ffff';fb.fillRect(0,0,w,h);
ctx.globalCompositeOperation='lighter';
ctx.drawImage(fxA,-sh,0,w,h);
ctx.drawImage(fxB,sh,0,w,h);
ctx.globalCompositeOperation='source-over';
}
const grainPat=[];let grI=0,grT=0;
for(let i=0;i<3;i++){
const g=mkcv(160,160),gc=g.getContext('2d');
const id=gc.createImageData(160,160);
for(let p=0;p<id.data.length;p+=4){
const v=110+Math.random()*80|0;
id.data[p]=id.data[p+1]=id.data[p+2]=v;id.data[p+3]=26;
}
gc.putImageData(id,0,0);
grainPat.push(ctx.createPattern(g,'repeat'));
}
const vig=mkcv(512,512);
{const c=vig.getContext('2d');
const g=c.createRadialGradient(256,256,190,256,256,372);
g.addColorStop(0,'rgba(16,10,5,0)');g.addColorStop(1,'rgba(16,10,5,.55)');
c.fillStyle=g;c.fillRect(0,0,512,512);}
function grain(t){
if(t-grT>.085){grI=(grI+1)%3;grT=t;}
ctx.globalCompositeOperation='overlay';
ctx.globalAlpha=.5;
ctx.fillStyle=grainPat[grI];
ctx.fillRect(0,0,cv.width,cv.height);
ctx.globalCompositeOperation='source-over';
ctx.globalAlpha=1;
}
function render(t){
sc.setTransform(1,0,0,1,0,0);
sc.clearRect(0,0,scene.width,scene.height);
applyCam(sc);
sc.fillStyle='#141008';
sc.fillRect(-1600,-1600,DW+3200,DH+3200);
if(roomT>0){drawVoid(t);if(roomT<1)drawRoomFading();}
else sc.drawImage(room,0,0);
drawMotes();
drawShadow();
drawPuffs();
const back=[],front=[];
collect(back,front);
for(const f of back)f();
const pawY=catY();
drawCatAll(t,pawY);
for(const f of front)f();
if(stage>=3)speedLines();
lampGlow(t);
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,cv.width,cv.height);
const ab=STAGES[stage].ab;
if(ab>0)chroma(ab);else ctx.drawImage(scene,0,0);
grain(t);
ctx.drawImage(vig,0,0,cv.width,cv.height);
}
/* ================= simulation ================= */
function nz(t,s){return Math.sin(t*12.9+s)*.5+Math.sin(t*7.1+s*1.7)*.35+Math.sin(t*23.7+s*.31)*.15;}
function updateProps(dt,t){
for(const p of props){
if(stage>=4&&p.orbit){
p.ang+=p.spd*dt*(1+extra*.08);
p.r+=(p.rT-p.r)*Math.min(1,dt*.8);
p.x=cat.x+Math.cos(p.ang)*p.r;
p.y=catY()-70+Math.sin(p.ang)*p.r*.42+p.yOff;
p.rot+=p.vr*dt;
p.depth=.55+.45*Math.sin(p.ang+p.seed);
}else if(stage>=3&&p.floats){
p.fl=Math.min(1,p.fl+dt*.6);
const hover=p.hy-(120+(p.seed%1)*90);
p.y+=(hover-p.y)*Math.min(1,dt*1.4);
p.x+=Math.sin(t*.8+p.seed*3)*14*dt;
p.rot=Math.sin(t*.9+p.seed*3)*.14+(p.shade?t*6:0);
p.depth=1;
}else if(stage>=2){
if(p.shade)p.rot+=dt*(2.5+stage*1.5);
else p.rot=Math.sin(t*3+p.seed*3)*.05;
p.depth=1;
}else{
p.rot=p.sway?Math.sin(t*.75+p.seed)*.02:(p.shade?Math.sin(t*2.2)*.06:0);
p.depth=1;
}
if(p.rattle&&stage>=2&&stage<3){p.jx=(Math.random()-.5)*2.5;p.jy=(Math.random()-.5)*1.5;}
else{p.jx=0;p.jy=0;}
}
}
function updateBooks(dt,t){
for(const b of books){
if(b.st==='shelf'){
if(stage>=2&&tNow>b.toppleAt){b.st='fall';b.vx=Math.random()*90-45;b.vy=-40-Math.random()*70;b.vr=Math.random()*4-2;}
}else if(b.st==='fall'){
b.vy+=1900*dt;b.x+=b.vx*dt;b.y+=b.vy*dt;b.rot+=b.vr*dt;
const fy=668+(b.x-560)*.08;
if(b.y>fy){b.y=fy;b.vy*=-.32;b.vx*=.6;b.vr*=.5;
if(Math.abs(b.vy)<60){b.st='rest';b.rot=(Math.random()-.5)*.5;}}
}else if(b.st==='rest'){
if(stage>=3)b.st='float';
}else if(b.st==='float'){
b.y+=((b.hy-190)-b.y)*Math.min(1,dt*.9);
b.x+=Math.sin(t*.9+b.seed)*12*dt;
b.rot=Math.sin(t*1.1+b.seed)*.35;
}
if(stage>=4&&b.st!=='orbit'){
b.st='orbit';
b.ang=Math.atan2(b.y-catY(),b.x-cat.x);
b.r=Math.max(130,Math.hypot(b.x-cat.x,b.y-catY()));
b.spd=.8+Math.random()*1.4;
}
if(b.st==='orbit'){
b.ang+=b.spd*dt;b.r+=(250-b.r)*Math.min(1,dt*.6);
b.x=cat.x+Math.cos(b.ang)*b.r;
b.y=catY()-70+Math.sin(b.ang)*b.r*.42;
b.rot+=dt*2.2;b.depth=.55+.45*Math.sin(b.ang);
}
}
}
function spawnLeaves(){
for(let i=0;i<6;i++)leaves.push({x:1300+Math.random()*160-80,y:520+Math.random()*100,
vx:Math.random()*40-20,vy:-30-Math.random()*40,rot:Math.random()*TAU,seed:Math.random()*7,st:'fl',depth:1,ang:0,r:0,spd:0});
}
function updateLeaves(dt,t){
for(const l of leaves){
if(l.st==='fl'){
l.vy+=(-55-l.vy)*Math.min(1,dt*1.5);
l.x+=l.vx*dt+Math.sin(t*2+l.seed)*24*dt;
l.y+=l.vy*dt;
l.rot=Math.sin(t*2.2+l.seed)*.6+l.seed;
}
if(stage>=4&&l.st!=='orbit'){
l.st='orbit';l.ang=Math.atan2(l.y-catY(),l.x-cat.x);
l.r=Math.max(140,Math.hypot(l.x-cat.x,l.y-catY()));
l.spd=1+Math.random()*1.6;
}
if(l.st==='orbit'){
l.ang+=l.spd*dt;l.r+=(300-l.r)*Math.min(1,dt*.6);
l.x=cat.x+Math.cos(l.ang)*l.r;l.y=catY()-70+Math.sin(l.ang)*l.r*.42;
l.rot+=dt*3;l.depth=.55+.45*Math.sin(l.ang);
}
}
}
function spawnShards(n){
for(let i=0;i<n;i++){
const r=140+Math.random()*620;
shards.push({ang:Math.random()*TAU,r,spd:.6+(760-r)/760*2.4+Math.random()*.4,
yOff:Math.random()*160-80,size:7+Math.random()*20,
col:SHARDCOLS[(Math.random()*SHARDCOLS.length)|0],seed:Math.random()*7});
}
}
function updateShards(dt){
for(const s of shards){
s.ang+=s.spd*dt*(1+extra*.1);
s.r-=dt*(14+extra*4);
if(s.r<80){s.r=640+Math.random()*260;s.ang=Math.random()*TAU;}
}
}
function propsToOrbit(){
for(const p of props){
p.orbit=true;
p.ang=Math.atan2(p.y-catY(),p.x-cat.x);
p.r=Math.max(120,Math.hypot(p.x-cat.x,p.y-catY()));
p.rT=190+(p.seed%1)*260+(p.name==='sofa'?170:0);
p.spd=(.5+1.4*Math.random())*(1-p.r/1600)+.45;
p.vr=Math.random()*4-2;
p.yOff=Math.random()*120-60;
}
}
function updateMotes(dt){
for(const m of motes){
if(stage>=2){
const dx=m.x-cat.x,dy=m.y-(catY()-110);
const d=Math.max(40,Math.hypot(dx,dy));
m.vx+=(-dy/d)*60*dt*(stage-1);
m.vy+=(dx/d)*60*dt*(stage-1);
m.vx-=dx*.02*dt;m.vy-=dy*.02*dt;
const sp=Math.hypot(m.vx,m.vy);
if(sp>170){m.vx*=170/sp;m.vy*=170/sp;}
}
m.x+=m.vx*dt;m.y+=m.vy*dt;
if(m.x<80)m.x=1060;if(m.x>1080)m.x=90;
if(m.y<430)m.y=1000;if(m.y>1010)m.y=440;
}
}
function updatePuffs(dt){
for(let i=puffs.length-1;i>=0;i--){
const p=puffs[i];
p.l-=dt;p.x+=p.vx*dt;p.y+=p.vy*dt;p.vy+=60*dt;
if(p.l<=0)puffs.splice(i,1);
}
}
function update(dt,t){
const st=STAGES[stage];
if(wind>0){
wind-=dt;
const p=clamp(1-Math.max(0,wind)/WIND0,0,1);
squash=.93+.07*Math.sin(p*Math.PI);
phiKick=-.55*Math.sin(p*Math.PI);
}else{squash+=(1-squash)*Math.min(1,dt*8);phiKick*=Math.exp(-dt*6);}
if(stage>=1&&wind<=0){
const tgt=st.rpm*(1+extra*.16)*TAU;
cat.omega+=(tgt-cat.omega)*Math.min(1,dt*(stage>=4?2.4:1.5));
cat.phi+=cat.omega*dt;
const hT=st.h*(1+extra*.05);
liftV+=(hT-cat.lift)*26*dt-liftV*5.5*dt;
cat.lift+=liftV*dt;
cat.bob=Math.sin(t*3.1)*(2+stage*1.6);
}else if(stage===0){
cat.omega*=Math.exp(-dt*4);cat.phi*=Math.exp(-dt*3);
cat.lift+=(0-cat.lift)*Math.min(1,dt*3);
cat.bob=Math.sin(t*1.4)*1.5;
}
cat.x=800+(stage>=2?Math.sin(cat.phi)*6:0)+(stage>=4?Math.sin(t*.7)*26:0);
const pT=stage>=1?1:.22+.08*Math.sin(t*.6);
pupil+=(pT-pupil)*Math.min(1,dt*(stage>=1?6:2));
if(stage===0){blinkT-=dt;if(blinkT<=0){blinkAnim=.24;blinkT=2.2+Math.random()*3.5;}}
if(blinkAnim>0){blinkAnim-=dt;blink=Math.sin(clamp(1-blinkAnim/.24,0,1)*Math.PI);if(blinkAnim<=0)blink=0;}
else blink=0;
if(stage===0){tailT-=dt;if(tailT<=0){tailA=.5;tailT=2.5+Math.random()*4;}}
if(tailA>0){tailA-=dt;tailF=Math.sin(clamp(1-tailA/.5,0,1)*Math.PI);}else tailF=0;
const rpmNow=cat.omega/TAU;
const shAmp=st.shake*(.35+.65*Math.min(1,rpmNow/6))+kickShake*9;
cam.x=nz(t,1.3)*shAmp+Math.sin(t*.4)*1.1;
cam.y=nz(t,5.7)*shAmp*.7+Math.cos(t*.33)*.9;
cam.roll=st.rock*(.3*Math.sin(t*.6)+(stage>=2?.75*Math.sin(cat.phi*2):0))
+(stage>=4?.45*Math.sin(t*.19):0)
+kickShake*.05*Math.sin(t*37);
zk*=Math.exp(-dt*5.5);kickShake*=Math.exp(-dt*3.2);
const zt=st.zoom*(1+.05*zk+.012*Math.sin(t*.5));
cam.zoom+=(zt-cam.zoom)*Math.min(1,dt*5);
updateProps(dt,t);updateBooks(dt,t);updateLeaves(dt,t);
updateShards(dt);updateMotes(dt);updatePuffs(dt);
if(stage>=4)roomT=Math.min(1,roomT+dt/1.5);
faceFlash*=Math.exp(-dt*9);
audioTick();
capScale+=(1-capScale)*Math.min(1,dt*9);
if(capA>0){
capA-=dt*(stage>=4?1.05:1.5);
if(capA<=0)capEl.style.opacity=0;
else{
capEl.style.opacity=capA.toFixed(3);
capEl.style.transform='translateX(calc(-50% + '+capX.toFixed(0)+'px)) rotate('+capRot.toFixed(1)+'deg) scale('+capScale.toFixed(3)+')';
}
}
}
/* ================= escalation ================= */
function escalate(){
ensureAudio();
if(stage<4){stage++;extra=0;}else extra++;
wind=WIND0;zk=1;kickShake=1;
cv.style.filter=stage>=3?'saturate('+(1+(stage-2)*.09).toFixed(2)+') contrast('+(1+(stage-2)*.035).toFixed(2)+')':'none';
if(stage===2)books.forEach((b,i)=>b.toppleAt=tNow+.5+i*.32);
if(stage===3)spawnLeaves();
if(stage===4&&roomT===0){
spawnShards(70);propsToOrbit();
if(actx){const n=actx.currentTime;holdUntil=n+.5;scheduleSlam(n+.42);nextOii=n+.62;}
}else if(stage===4){
spawnShards(14);
if(actx)nextOii=Math.max(nextOii,actx.currentTime+.12);
}else if(actx)nextOii=actx.currentTime+.42;
if(stage===1&&actx)scheduleThud(actx.currentTime+.34);
if(stage===1)puffAt=tNow+.34;
if(actx)riser();
if(!hintGone){hintGone=true;hintEl.style.opacity=0;}
}
cv.addEventListener('pointerdown',escalate);
addEventListener('keydown',e=>{
if((e.code==='Space'||e.code==='Enter')&&!e.repeat){e.preventDefault();escalate();}
});
muteBtn.addEventListener('click',e=>{
e.stopPropagation();
muted=!muted;
muteBtn.classList.toggle('muted',muted);
if(actx&&master)master.gain.setTargetAtTime(muted?0:.85,actx.currentTime,.02);
});
/* ================= loop ================= */
let lastT=0;
function frame(ts){
const t=ts/1000;
const dt=Math.min(.05,lastT?t-lastT:.016);
lastT=t;tNow=t;DT=dt;
if(puffAt&&tNow>=puffAt){
for(let i=0;i<14;i++)puffs.push({x:cat.x+Math.random()*140-70,y:898,
vx:Math.random()*120-60,vy:-(20+Math.random()*50),l:.7+Math.random()*.3,s:3+Math.random()*5});
puffAt=0;
}
update(dt,t);
render(t);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>
The opening frame is a baked one-point-perspective room: a window with a sun gradient, blurred hedge and sheer curtain throwing a light beam and a bright pool across perspective-correct floorboards and a worn rug; every piece of furniture is pre-rendered with its depth-of-field blur baked in (sharp cat, soft mid-ground, softer foreground), plus film grain, vignette, drifting dust motes in the beam, a flickering lamp, and a cat built from layered gradients, ~500 individual fur strokes, whiskers, rim light from the window side, a contact shadow, breathing, blinking and tail flicks. The realism gives out at the fur (thousands of strokes, not strands), the immobile ears and whiskers, and the spin being a four-view turntable rather than true 3D — grain and motion carry it right up until someone leans in.
The Oiia read comes from the specifics: cream cat, square to camera, huge staring eyes with live pupils that dilate to saucers on click. The spin is a vertical-axis turntable — front, profile and back-of-head views crossfading with cosine foreshortening, so the face squashes to a sliver, the ears-from-behind appear, and temporal motion-blur samples smear the face past readability at speed. The tail streams out and becomes a propeller, the face flashes back for single frames on vocal beats, and the meme's written loop appears as Impact-font captions: "oiia… oiiaiooooiai".
The stages: (1) Liftoff — crouch, dilated pupils, a floor thud, dust puff, slow low "oiia" per revolution, lamp shade swaying. (2) The room notices — camera rolls in sync with the spin, zoom pulses per syllable, the picture frame wobbles, books topple off the shelf and bounce, the mug rattles, captions begin, RGB split fringes appear. (3) Gravity quits — higher and faster, the lamp, table, cushions and shelf all float, leaves tear off the plant, books orbit, anime speed lines, and the voice becomes three detuned singers with a bass thump per revolution. (4) Singularity — half a second of silence, then a slam: the walls and floor fling themselves apart into an orbiting vortex of room-colored shards, every piece of furniture (sofa included) spirals around the cat passing in front of and behind it, god rays rotate in the void, and every further click adds spin, shards and pitch.
The voice is formant synthesis: a sawtooth "glottal source" fed through three parallel bandpass filters whose center frequencies glide through the vowel sequence O→I→A (plus U for the "oooo" in the long call), with amplitude pulses on each vowel boundary, a vibrato LFO, breath noise tracking the second formant, and a soft-clip/compressor/presence-EQ chain — the moving formants are what make it read as a mouth, not a tone. It's locked to the spin mechanically: one utterance per revolution with duration equal to the measured revolution period, and f0 = 166·2^(rpm/5.2), so rate and pitch are literally functions of RPM; a whoosh per rev keeps the air attached to the cat.
With more time I'd give the head its own yaw so it leads the body and the ears whip independently, rebuild the fur as strand-level curves with wind response, add a proper meow layer under the chant, and add adaptive quality — dropping motion-blur samples and the RGB split when frame time climbs — so stage four stays at 60fps on weak machines.