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, in the living room</title>
<style>
* { box-sizing: border-box; }
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
body { background: #342e2b; font-family: system-ui, sans-serif; }
canvas { display: block; width: 100%; height: 100%; cursor: pointer; }
.ui {
position: fixed; inset: 0; pointer-events: none;
color: #fff8ee; text-shadow: 0 2px 12px #241912bb;
}
.hint {
position: absolute; left: 50%; bottom: 5%;
transform: translateX(-50%); white-space: nowrap;
padding: 9px 15px; border-radius: 30px;
background: #332d298a; backdrop-filter: blur(8px);
font-size: 13px; letter-spacing: .035em;
transition: opacity .5s;
}
.hint.hidden { opacity: 0; }
.label {
position: absolute; top: 22px; left: 25px;
font-size: 11px; letter-spacing: .2em; text-transform: uppercase;
opacity: .65;
}
button {
pointer-events: auto; position: absolute; top: 17px; right: 18px;
color: #fff8ee; background: #332d2999;
border: 1px solid #ffffff55; border-radius: 30px;
padding: 9px 13px; cursor: pointer; font: inherit; font-size: 12px;
backdrop-filter: blur(8px);
}
button:hover { background: #51423bcc; }
</style>
</head>
<body>
<canvas id="scene" aria-label="A cream-and-white cat sitting in a living room. Click the cat to make it spin."></canvas>
<div class="ui">
<div class="label">A perfectly ordinary afternoon</div>
<div class="hint" id="hint">click the cat</div>
<button id="mute" type="button" aria-label="Mute sound">Sound on</button>
</div>
<script>
(() => {
'use strict';
const canvas = document.querySelector('#scene');
const ctx = canvas.getContext('2d', { alpha: false });
const hint = document.querySelector('#hint');
const muteButton = document.querySelector('#mute');
const W = 1100, H = 740, TAU = Math.PI * 2;
let cw, ch, dpr, fit, ox, oy;
let stage = 0, extra = 0, muted = false, angle = 0, speed = 0;
let lift = 0, clickTime = 0, last = performance.now(), time = 0;
let audio, voice, voice2, volume, filter;
function rng(seed) {
return () => {
seed = (Math.imul(seed, 1664525) + 1013904223) | 0;
return (seed >>> 0) / 4294967296;
};
}
const random = rng(90317);
const lerp = (a,b,t) => a+(b-a)*t;
const clamp = (x,a,b) => Math.max(a,Math.min(b,x));
const ease = x => x*x*(3-2*x);
function ellipse(c,x,y,rx,ry,color) {
c.beginPath(); c.ellipse(x,y,rx,ry,0,0,TAU);
if (color) { c.fillStyle=color; c.fill(); }
}
function line(c,x1,y1,x2,y2,color,width=1) {
c.strokeStyle=color; c.lineWidth=width;
c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke();
}
function grad(c,x,y,r,stops) {
const g=c.createRadialGradient(x,y,1,x,y,r);
stops.forEach(([at,col])=>g.addColorStop(at,col));
return g;
}
// The still room is painted once, including its fine-grained surfaces.
const room=document.createElement('canvas');
room.width=W; room.height=H;
const r=room.getContext('2d');
function drawRoom() {
let g=r.createLinearGradient(0,0,W,H);
g.addColorStop(0,'#b9b4a8'); g.addColorStop(.48,'#d4cabe');
g.addColorStop(1,'#a99b8d');
r.fillStyle=g; r.fillRect(0,0,W,H);
// Recessed window, late-afternoon light from camera left.
r.fillStyle='#a69b8d'; r.fillRect(48,65,319,326);
r.fillStyle='#ede8df'; r.fillRect(55,70,303,313);
g=r.createLinearGradient(80,77,310,362);
g.addColorStop(0,'#d7e7e4'); g.addColorStop(.65,'#b8c9c2');
g.addColorStop(1,'#e7d5ae');
r.fillStyle=g; r.fillRect(72,86,267,281);
r.fillStyle='#87938a';
r.beginPath(); r.moveTo(72,323); r.lineTo(151,248); r.lineTo(210,304);
r.lineTo(269,234); r.lineTo(339,302); r.lineTo(339,367);
r.lineTo(72,367); r.fill();
r.fillStyle='#aab3a2';
r.beginPath(); r.moveTo(72,343); r.lineTo(158,294); r.lineTo(222,339);
r.lineTo(295,281); r.lineTo(339,324); r.lineTo(339,367);
r.lineTo(72,367); r.fill();
r.fillStyle='#f4eee1';
r.fillRect(66,80,9,296); r.fillRect(202,80,9,296);
r.fillRect(333,80,10,296); r.fillRect(66,219,276,9);
g=r.createLinearGradient(52,0,168,0);
g.addColorStop(0,'#c9bdae'); g.addColorStop(.7,'#eee5d6');
g.addColorStop(1,'#d6cabb');
r.fillStyle=g;
r.beginPath(); r.moveTo(44,65); r.bezierCurveTo(98,93,96,323,38,406);
r.lineTo(113,403); r.bezierCurveTo(153,257,115,119,93,65);
r.fill();
r.beginPath(); r.moveTo(361,65); r.bezierCurveTo(324,126,332,319,374,404);
r.lineTo(330,400); r.bezierCurveTo(293,254,326,109,308,65); r.fill();
// Wall molding, art, and the soft spill of the window.
r.fillStyle='#ded3c3'; r.fillRect(0,409,W,9);
r.fillStyle='#9e8b77'; r.fillRect(0,418,W,4);
r.fillStyle='#dfd5c8'; r.fillRect(0,422,W,20);
r.fillStyle='#786d5f'; r.fillRect(690,112,180,141);
r.fillStyle='#ede7d9'; r.fillRect(698,120,164,125);
r.fillStyle='#adac98'; r.fillRect(710,132,140,101);
r.fillStyle='#697a70';
r.beginPath(); r.ellipse(790,185,51,27,-.5,0,TAU); r.fill();
r.fillStyle='#d8c3a4';
r.beginPath(); r.ellipse(789,184,28,43,.55,0,TAU); r.fill();
// Perspective floorboards.
g=r.createLinearGradient(0,440,0,H);
g.addColorStop(0,'#9a7860'); g.addColorStop(.5,'#b28c6e');
g.addColorStop(1,'#70543f');
r.fillStyle=g; r.fillRect(0,442,W,H-442);
for(let i=-8;i<=13;i++) {
line(r,550+(i-5)*43,442,550+(i-5)*136,H,'#563d30a0',1.3);
line(r,550+(i-5)*43+3,442,550+(i-5)*136+5,H,'#ead0a045',1);
}
for(const y of [486,553,655]) {
line(r,0,y,W,y,'#604a3880',2);
line(r,0,y+3,W,y+3,'#e6c59735',1);
}
// Rug: its back edge is tucked behind the cat.
r.save(); r.translate(531,622); r.scale(1,.29);
ellipse(r,0,0,391,295,'#b1a59a');
ellipse(r,0,0,371,277,'#e0d2bb');
ellipse(r,0,0,349,256,'#b9a898');
ellipse(r,0,0,337,245,'#d6c5ad');
r.restore();
// Sofa, with separated cushions and contact shadows.
r.fillStyle='#473d36aa';
ellipse(r,926,520,197,39);
g=r.createLinearGradient(0,277,0,530);
g.addColorStop(0,'#927b68'); g.addColorStop(.5,'#746354');
g.addColorStop(1,'#53483d');
r.fillStyle=g;
r.beginPath(); r.roundRect(769,277,351,231,34); r.fill();
r.fillStyle='#a28b74';
r.beginPath(); r.roundRect(783,295,145,145,20); r.fill();
r.fillStyle='#ae957e';
r.beginPath(); r.roundRect(934,295,171,145,20); r.fill();
r.fillStyle='#69594d';
r.beginPath(); r.roundRect(775,430,347,79,19); r.fill();
r.fillStyle='#8f7865';
r.beginPath(); r.roundRect(785,436,155,60,14); r.fill();
r.beginPath(); r.roundRect(946,436,165,60,14); r.fill();
r.fillStyle='#453c34';
r.fillRect(797,495,14,42); r.fillRect(1076,495,14,42);
r.fillStyle='#bc997c';
r.beginPath(); r.roundRect(1007,368,70,75,9); r.fill();
for(let i=0;i<50;i++){
const x=783+random()*330,y=287+random()*214;
line(r,x,y,x+random()*13,y+2,'#f4dfb112',1);
}
// Lamp and plant supply small, recognizable room-scale details.
r.fillStyle='#4b4238'; r.fillRect(721,339,8,147);
r.fillStyle='#52473b'; ellipse(r,725,490,41,9);
r.fillStyle='#e4d2b6';
r.beginPath(); r.moveTo(688,312); r.lineTo(763,312);
r.lineTo(745,374); r.lineTo(705,374); r.closePath(); r.fill();
r.fillStyle='#51483e'; r.fillRect(690,310,71,4);
r.fillStyle='#b89470';
r.beginPath(); r.moveTo(43,492); r.lineTo(113,492);
r.lineTo(104,563); r.lineTo(58,563); r.closePath(); r.fill();
for(let i=0;i<14;i++){
const a=i*2.399, length=37+random()*46;
r.save(); r.translate(80,491); r.rotate(a);
r.fillStyle=i%3?'#4d6851':'#688165';
r.beginPath(); r.ellipse(0,-length/2,10,length/2,0,0,TAU);
r.fill(); r.restore();
}
// Grain, uneven light, and a restrained photographic vignette.
for(let i=0;i<19500;i++){
const x=random()*W,y=random()*H;
r.fillStyle=random()<.5?'#ffffff0c':'#261b150c';
r.fillRect(x,y,1+random()*2,1+random()*2);
}
r.fillStyle=grad(r,165,210,1050,[
[0,'#fff6d300'],[.55,'#fff6d306'],[1,'#261b1b75']
]);
r.fillRect(0,0,W,H);
r.fillStyle=grad(r,117,147,390,[
[0,'#fff6d244'],[1,'#fff6d200']
]);
r.fillRect(0,0,550,590);
}
drawRoom();
function makeCat(back=false) {
const cv=document.createElement('canvas'); cv.width=520; cv.height=640;
const c=cv.getContext('2d'), q=rng(back?8721:2934);
c.lineCap='round';
// Tail peeks out along the floor.
c.strokeStyle='#805e45'; c.lineWidth=61;
c.beginPath(); c.moveTo(336,527); c.bezierCurveTo(408,563,431,532,430,481); c.stroke();
c.strokeStyle='#c79a71'; c.lineWidth=47; c.stroke();
for(let i=0;i<12;i++){
let x=352+i*6;
line(c,x,537-i*.8,x+11,547-i*1.8,'#9e715171',4);
}
// Seated, rather than standing: broad haunches, a narrow neck.
const bodyPath=()=>{
c.beginPath();
c.moveTo(260,288);
c.bezierCurveTo(192,282,143,327,119,423);
c.bezierCurveTo(92,538,144,574,260,574);
c.bezierCurveTo(376,574,428,538,401,423);
c.bezierCurveTo(377,327,328,282,260,288);
c.closePath();
};
bodyPath();
c.fillStyle=grad(c,223,367,231,[
[0,'#f4dec0'],[.53,'#dfb98f'],[.83,'#b78a68'],[1,'#715641']
]); c.fill();
c.save(); bodyPath(); c.clip();
ellipse(c,255,465,89,130,grad(c,233,430,157,[
[0,'#fffaf0'],[.7,'#efe8d8'],[1,'#cbbca9']
]));
if(back) {
c.fillStyle='#bc875f';
for(let i=0;i<7;i++){
c.beginPath(); c.ellipse(259,322+i*34,82-i*3,8,0,0,TAU); c.fill();
}
}
// Directional individual hairs catch the window light.
for(let i=0;i<4400;i++){
const x=106+q()*308,y=293+q()*286;
const a=(x-260)/210;
c.strokeStyle=q()<.55?'#fff3d047':'#785b4530';
c.lineWidth=.35+q()*1.4;
line(c,x,y,x+a*2+(q()-.5)*3,y+3+q()*12,c.strokeStyle,c.lineWidth);
}
c.restore();
// White paws have their own weight and small toe divisions.
for(const x of [197,323]){
ellipse(c,x,549,51,54,grad(c,x-12,526,74,[
[0,'#fffdf4'],[.7,'#e9e4d8'],[1,'#a89e90']
]));
ellipse(c,x,574,48,22,'#f2eee3');
for(let t=-1;t<=1;t++)
line(c,x+t*19,566,x+t*19-3,583,'#a89e9080',1.4);
}
// Ears are behind the face; inner ear skin shows through fine fur.
function ear(x,flip) {
c.save(); c.translate(x,131); c.scale(flip,1);
c.beginPath(); c.moveTo(-56,30); c.quadraticCurveTo(-51,-49,-34,-89);
c.quadraticCurveTo(-21,-100,32,-27); c.lineTo(54,33); c.closePath();
c.fillStyle='#c7956d'; c.fill();
c.beginPath(); c.moveTo(-38,10); c.lineTo(-29,-67);
c.quadraticCurveTo(-24,-70,21,-11); c.closePath();
c.fillStyle=grad(c,-13,-12,67,[
[0,'#bd7772'],[.6,'#dcaaa0'],[1,'#ecd0bc']
]); c.fill();
for(let i=0;i<150;i++){
let px=-35+q()*70,py=-49+q()*87;
line(c,px,py,px+(q()-.5)*11,py+q()*14,
q()<.55?'#ffe9ce76':'#78564538',.5+q());
}
c.restore();
}
ear(177,1); ear(343,-1);
const headPath=()=>{
c.beginPath();
c.moveTo(259,109);
c.bezierCurveTo(172,103,118,160,132,250);
c.bezierCurveTo(140,303,194,337,260,338);
c.bezierCurveTo(326,337,380,303,388,250);
c.bezierCurveTo(402,160,347,103,259,109);
c.closePath();
};
headPath();
c.fillStyle=grad(c,223,171,196,[
[0,'#f5e4c6'],[.5,'#e9c5a0'],[.78,'#ca9d77'],[1,'#765842']
]); c.fill();
c.save(); headPath(); c.clip();
// Ginger crown and asymmetrical markings over a creamy face.
ellipse(c,258,120,110,68,'#c58e64');
ellipse(c,168,209,39,91,'#ca966e');
ellipse(c,352,204,39,89,'#d2a078');
if(back) {
for(let i=0;i<8;i++){
line(c,173+i*25,120,186+i*20,292,
i%2?'#a6775670':'#f9dbaf50',8);
}
} else {
ellipse(c,262,281,91,65,grad(c,242,262,99,[
[0,'#fffdf5'],[.75,'#f2ecde'],[1,'#d5c4ac']
]));
for(let i=0;i<4;i++){
let x=211+i*32;
c.strokeStyle='#a8714b5a'; c.lineWidth=7-i*.8;
c.beginPath(); c.moveTo(x,133); c.quadraticCurveTo(x+8,154,x+3,170);
c.stroke();
}
for(const side of [-1,1]){
for(let i=0;i<2;i++){
line(c,260+side*81,181+i*19,
260+side*(111-i*5),174+i*22,'#9d694b66',6);
}
}
}
for(let i=0;i<6100;i++){
let x=130+q()*260,y=105+q()*235;
let dir=(x-260)*.025;
line(c,x,y,x+dir+(q()-.5)*4,y+2+q()*9,
q()<.55?'#fff2d452':'#76533c31',.35+q()*1.15);
}
c.restore();
if(!back) {
// Dark rims and glassy, camera-facing eyes.
for(const ex of [202,318]){
ellipse(c,ex,222,30,34,'#775c4b');
ellipse(c,ex,220,26,30,'#eee1cb');
ellipse(c,ex,220,22,27,'#927e51');
ellipse(c,ex,220,17,25,'#24261f');
ellipse(c,ex-7,210,7,9,'#fffdf0');
ellipse(c,ex+8,231,2.5,3,'#ffffff9c');
c.strokeStyle='#b38360'; c.lineWidth=5;
c.beginPath(); c.moveTo(ex-29,203);
c.quadraticCurveTo(ex,191,ex+28,203); c.stroke();
}
ellipse(c,231,278,33,23,'#f9f6ea');
ellipse(c,288,278,33,23,'#f9f6ea');
c.fillStyle='#ad756a';
c.beginPath(); c.moveTo(246,267); c.quadraticCurveTo(260,258,274,267);
c.quadraticCurveTo(263,283,260,282);
c.quadraticCurveTo(256,281,246,267); c.fill();
line(c,260,280,260,295,'#795e51',1.8);
c.strokeStyle='#796558'; c.lineWidth=1.7;
c.beginPath(); c.moveTo(260,293);
c.quadraticCurveTo(248,305,239,295); c.moveTo(260,293);
c.quadraticCurveTo(272,305,281,295); c.stroke();
for(let side of [-1,1]){
for(let i=0;i<4;i++){
ellipse(c,260+side*(29+i*6),281+i%2*7,1.5,1.2,'#977d6c');
c.strokeStyle=i%2?'#e9e3d8ba':'#f9f5ebdd';
c.lineWidth=1.4-i*.17; c.beginPath();
c.moveTo(260+side*31,284+i*5);
c.bezierCurveTo(260+side*91,269+i*9,
260+side*119,269+i*14,260+side*150,280+i*16);
c.stroke();
}
}
}
// Wispy silhouette hairs stop the outline looking like a sticker.
for(let i=0;i<610;i++){
const a=q()*TAU, x=260+Math.cos(a)*126,y=221+Math.sin(a)*112;
if(y<104||y>329) continue;
line(c,x,y,x+Math.cos(a)*(2+q()*8),
y+Math.sin(a)*(2+q()*8),
q()<.5?'#f6dfbb85':'#a87b5960',.4+q()*.6);
}
return cv;
}
const front=makeCat(), back=makeCat(true);
// A short, vowel-shaped vocal recording made entirely from synthesized
// harmonics. Playback rate is the shared control for tempo AND pitch.
function makeVocalBuffer(ac) {
const sr=ac.sampleRate, seconds=1.48, n=Math.floor(sr*seconds);
const buffer=ac.createBuffer(1,n,sr), out=buffer.getChannelData(0);
const vowels={o:[480,920,2750], i:[305,2250,3100], a:[830,1450,2750]};
const seq=[
[0,'o'],[.12,'i'],[.25,'a'],[.39,'a'],
[.47,'o'],[.59,'i'],[.73,'a'],[.86,'a'],
[.95,'o'],[1.08,'i'],[1.20,'a'],[1.36,'a'],[1.48,'o']
];
let phase=0;
for(let s=0;s<n;s++){
const t=s/sr;
let k=0; while(k<seq.length-2 && seq[k+1][0]<t) k++;
const u=ease(clamp((t-seq[k][0])/(seq[k+1][0]-seq[k][0]),0,1));
const form=[0,1,2].map(j=>lerp(vowels[seq[k][1]][j],
vowels[seq[k+1][1]][j],u));
// Gentle natural wobble under the glissando supplied by playbackRate.
const fundamental=153+4*Math.sin(t*TAU*5.1)+2*Math.sin(t*TAU*8);
phase+=TAU*fundamental/sr;
let sample=0;
for(let h=1;h<=30;h++){
const hz=h*fundamental;
const peak=(f,width)=>Math.exp(-.5*((hz-f)/width)**2);
const strength=(.085+peak(form[0],145)*1.4+
peak(form[1],240)*.9+peak(form[2],310)*.38) /
Math.pow(h,.78);
sample+=Math.sin(phase*h+.25*h)*strength;
}
const syllable=t% .47;
const envelope=.68+.32*Math.sin(Math.PI*clamp(syllable/.47,0,1));
out[s]=Math.tanh(sample*.66)*envelope*.54;
}
return buffer;
}
function startAudio() {
if(audio) { audio.resume(); return; }
const AC=window.AudioContext||window.webkitAudioContext;
if(!AC) return;
audio=new AC();
volume=audio.createGain(); volume.gain.value=muted?0:.52;
filter=audio.createBiquadFilter(); filter.type='lowpass'; filter.frequency.value=3900;
const compressor=audio.createDynamicsCompressor();
compressor.threshold.value=-21; compressor.ratio.value=3;
filter.connect(compressor); compressor.connect(volume);
volume.connect(audio.destination);
const buffer=makeVocalBuffer(audio);
voice=audio.createBufferSource(); voice.buffer=buffer; voice.loop=true;
voice.connect(filter); voice.playbackRate.value=.83; voice.start();
voice2=audio.createBufferSource(); voice2.buffer=buffer; voice2.loop=true;
const harmony=audio.createGain(); harmony.gain.value=0;
voice2.connect(harmony); harmony.connect(filter);
voice2.playbackRate.value=.84; voice2.start(audio.currentTime+.09);
voice2.harmonyGain=harmony;
}
function resize() {
dpr=Math.min(devicePixelRatio||1,2);
cw=innerWidth; ch=innerHeight;
canvas.width=Math.ceil(cw*dpr); canvas.height=Math.ceil(ch*dpr);
fit=Math.max(cw/W,ch/H);
ox=(cw-W*fit)/2; oy=(ch-H*fit)/2;
}
addEventListener('resize',resize); resize();
function drawSprite(c,x,y,scale,turn,alpha=1,blur=0) {
c.save(); c.globalAlpha=alpha;
c.translate(x,y); c.scale(scale,scale);
const cosine=Math.cos(turn);
// Narrow edge-on silhouettes, with the back actually replacing the face.
const width=Math.max(.075,Math.abs(cosine));
c.scale(width,1);
if(cosine<0) c.scale(-1,1);
if(blur) c.filter=`blur(${blur}px)`;
c.drawImage(cosine<0?back:front,-260,-590);
c.restore();
}
function furnitureOrbit(c,t,amount) {
const items=[
['lamp',0],['frame',1],['plant',2],['cushion',3],
['lamp',4],['frame',5],['cushion',6]
];
items.slice(0,stage===2?3:items.length).forEach(([type,i])=>{
let a=t*(.53+i*.04)+i*TAU/items.length;
let radius=(stage===2?245:315)+Math.sin(t+i)*22;
let x=550+Math.cos(a)*radius, y=370+Math.sin(a)*radius*.43;
c.save(); c.translate(x,y); c.rotate(a+t*(i%2?-.4:.6));
c.globalAlpha=.36+amount*.3;
c.shadowColor='#ffe0a7'; c.shadowBlur=13;
if(type==='frame') {
c.fillStyle='#5c4b40'; c.fillRect(-28,-35,56,70);
c.fillStyle='#ddd2bd'; c.fillRect(-23,-30,46,60);
c.fillStyle='#8b9981'; c.fillRect(-18,-25,36,50);
} else if(type==='plant') {
c.fillStyle='#b48261'; c.fillRect(-13,7,26,23);
c.strokeStyle='#465f4b'; c.lineWidth=7;
for(let j=-2;j<=2;j++){
c.beginPath(); c.moveTo(0,8);
c.quadraticCurveTo(j*19,-42,j*23,-33); c.stroke();
}
} else if(type==='lamp') {
c.fillStyle='#e6cfab'; c.beginPath();
c.moveTo(-21,-26); c.lineTo(21,-26); c.lineTo(13,3);
c.lineTo(-13,3); c.fill();
line(c,0,3,0,34,'#655345',4);
} else {
c.fillStyle='#ac8a72';
c.beginPath(); c.roundRect(-28,-23,56,46,9); c.fill();
line(c,-24,-16,24,16,'#d6b798',2);
}
c.restore();
});
}
function draw(now) {
const dt=Math.min((now-last)/1000,.045); last=now; time+=dt;
const target=[0,15,24,34,46][stage]+Math.min(extra*3,12);
speed=lerp(speed,target,1-Math.exp(-dt*(stage?1.15:3)));
angle+=speed*dt;
const height=[0,94,171,244,298][stage]+Math.min(extra*9,55);
lift=lerp(lift,height,1-Math.exp(-dt*1.7));
if(audio) {
const rate=clamp(.78+speed*.073,.78,4.4);
voice.playbackRate.setTargetAtTime(rate,audio.currentTime,.12);
voice2.playbackRate.setTargetAtTime(rate*1.012,audio.currentTime,.15);
voice2.harmonyGain.gain.setTargetAtTime(stage>=3?.19:0,audio.currentTime,.2);
filter.frequency.setTargetAtTime(2600+stage*900,audio.currentTime,.2);
}
const c=ctx;
c.setTransform(dpr,0,0,dpr,0,0);
c.fillStyle='#302923'; c.fillRect(0,0,cw,ch);
c.translate(ox,oy); c.scale(fit,fit);
c.save(); c.beginPath(); c.rect(0,0,W,H); c.clip();
// The room first sways, then loses its claim to being a fixed reference.
if(stage<2) {
c.save();
if(stage===1) {
c.translate(550,365); c.rotate(Math.sin(time*1.5)*.006);
c.scale(1.013,1.013); c.translate(-550,-365);
}
c.drawImage(room,0,0); c.restore();
} else {
const strength=stage===2?.022:stage===3?.11:.27;
c.fillStyle=stage===4?'#32263b':'#483b36'; c.fillRect(0,0,W,H);
c.save(); c.translate(550,370);
c.rotate(Math.sin(time*(stage===4?.76:.44))*strength);
let s=stage===2?1.07:stage===3?1.18:1.3;
c.scale(s,s); c.translate(-550,-370);
c.drawImage(room,0,0); c.restore();
if(stage>=3) {
// Receding, translucent copies of the room make a rotating tunnel.
for(let i=5;i>=1;i--){
c.save(); c.translate(550,357);
const s=.18+i*.125;
c.rotate(time*(.15+i*.038)*(i%2?-1:1)+i*.19);
c.scale(s,s); c.translate(-550,-357);
c.globalAlpha=stage===4?.105:.065;
c.drawImage(room,0,0); c.restore();
}
}
}
if(stage>=2) furnitureOrbit(c,time,clamp((time-clickTime)*2,0,1));
// Ground contact remains visible as the cat rises.
const shadow=grad(c,550,617,190+lift*.35,[
[0,`rgba(43,30,24,${.48-lift*.0008})`],
[.65,'#39271a21'],[1,'#39271a00']
]);
c.save(); c.translate(550,616); c.scale(1,.24);
ellipse(c,0,0,190+lift*.32,145,shadow); c.restore();
const bob=stage?Math.sin(time*(2+stage*.7))*Math.min(5+stage*3,19):0;
const cy=625-lift+bob;
if(stage) {
// Warm motion arcs, not white flashes.
c.save(); c.translate(550,cy-205);
for(let i=0;i<Math.min(stage+1,5);i++){
let radius=75+i*38+stage*9;
c.beginPath();
c.ellipse(0,0,radius,radius*.52,
time*(i%2?-.8:.7),time*1.1+i,time*1.1+i+Math.PI*1.35);
c.strokeStyle=['#ffd99c88','#f6b18466','#f7ead280','#b3c6d37a','#efb5ca80'][i];
c.lineWidth=1.5+i*.55; c.stroke();
}
c.restore();
// Multiple angular samples make the cat unreadable at full speed.
const count=stage===1?6:stage===2?9:13;
for(let i=count;i>=1;i--){
const p=i/count;
const a=angle-speed*p*.095;
const x=550+Math.sin(a)*((stage>=3?20:8)*p);
drawSprite(c,x,cy+Math.sin(a*2)*p*4,.78,a,
(.035+(1-p)*.045)*(stage>=3?1.5:1),stage>=3?2:1);
}
}
drawSprite(c,550,cy,.78,angle,stage? .83:1,stage>=3?1.4:0);
if(stage>=3) {
// The room's objects begin orbiting the cat instead of the other way around.
furnitureOrbit(c,time+Math.PI,1);
if(stage===4) {
// Stage four: a little impossible procession of synchronized cats.
for(let i=0;i<7;i++){
const a=time*.62+i*TAU/7;
const radius=220+32*Math.sin(time*1.3+i);
const depth=(Math.sin(a)+1)/2;
c.save();
c.globalAlpha=.4+depth*.38;
drawSprite(c,550+Math.cos(a)*radius,
350+Math.sin(a)*105, .19+depth*.13,
angle+i*.8, .55+depth*.3,1);
c.restore();
}
c.save(); c.translate(550,350);
for(let i=0;i<3;i++){
c.beginPath();
c.ellipse(0,0,295+i*63,95+i*23,
time*(i%2?-1:1)*.38,0,TAU);
c.strokeStyle=['#ffe6b777','#b8ced280','#e3b8a879'][i];
c.lineWidth=3; c.stroke();
}
c.restore();
}
}
c.restore();
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
canvas.addEventListener('click', e=>{
const x=(e.clientX-ox)/fit, y=(e.clientY-oy)/fit;
// The first click must find the cat; afterward the whole unruly room responds.
if(stage===0 && (x<393||x>707||y<175||y>639)) return;
if(stage<4) stage++;
else extra++;
clickTime=time;
hint.classList.add('hidden');
startAudio();
});
muteButton.addEventListener('click', e=>{
e.stopPropagation();
muted=!muted;
muteButton.textContent=muted?'Sound off':'Sound on';
muteButton.setAttribute('aria-label',muted?'Unmute sound':'Mute sound');
if(audio) volume.gain.setTargetAtTime(muted?0:.52,audio.currentTime,.04);
});
})();
</script>
</body>
</html>