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, maximum-scale=1, user-scalable=no">
<title>oiia</title>
<style>
html,body{margin:0;height:100%;background:#0a0806;overflow:hidden;}
canvas{display:block;width:100vw;height:100vh;cursor:pointer;touch-action:none;}
#ui{position:fixed;right:14px;top:14px;display:flex;gap:8px;z-index:10;
font-family:ui-sans-serif,system-ui,-apple-system,sans-serif;}
#ui button{
background:rgba(20,16,12,0.55);color:#e8dcc8;border:1px solid rgba(232,220,200,0.22);
padding:7px 13px;border-radius:999px;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;
cursor:pointer;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
transition:background .2s,color .2s,border-color .2s;
}
#ui button:hover{background:rgba(46,36,26,0.85);border-color:rgba(232,220,200,0.45);}
#hint{
position:fixed;left:50%;bottom:26px;transform:translateX(-50%);
color:rgba(240,230,214,0.5);font-family:ui-sans-serif,system-ui,sans-serif;
font-size:11px;letter-spacing:0.26em;text-transform:uppercase;pointer-events:none;
transition:opacity .8s;z-index:10;white-space:nowrap;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="ui">
<button id="mute">sound on</button>
<button id="reset">reset</button>
</div>
<div id="hint">click the cat</div>
<script>
(function(){
'use strict';
/* ============================================================
CANVAS / SCENE SETUP
============================================================ */
const cv = document.getElementById('c');
const ctx = cv.getContext('2d', {alpha:false});
const hintEl = document.getElementById('hint');
let W = 1, H = 1, DPR = 1, fitScale = 1;
function resize(){
DPR = Math.min(2, window.devicePixelRatio || 1);
W = Math.max(1, window.innerWidth);
H = Math.max(1, window.innerHeight);
cv.width = Math.floor(W * DPR);
cv.height = Math.floor(H * DPR);
cv.style.width = W + 'px';
cv.style.height = H + 'px';
fitScale = Math.max(W / 1600, H / 900);
}
window.addEventListener('resize', resize);
resize();
/* ============================================================
UTILITIES
============================================================ */
function mulberry32(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 mk(w,h){
const c = document.createElement('canvas');
c.width = w; c.height = h;
return c;
}
function blurCanvas(src, px){
const o = mk(src.width, src.height);
const g = o.getContext('2d');
g.filter = 'blur(' + px + 'px)';
g.drawImage(src, -px*2, -px*2, src.width + px*4, src.height + px*4);
return o;
}
/* ============================================================
ROOM SPRITES
============================================================ */
const FLOOR_Y = 586;
function drawWindow(g, x, y, w, h){
g.save();
// glow spill
const gl = g.createRadialGradient(x+w/2, y+h/2, 20, x+w/2, y+h/2, w*1.5);
gl.addColorStop(0, 'rgba(255,250,235,0.85)');
gl.addColorStop(0.5, 'rgba(255,246,225,0.28)');
gl.addColorStop(1, 'rgba(255,246,225,0)');
g.fillStyle = gl;
g.fillRect(x-260, y-260, w+520, h+520);
// glass
const gg = g.createLinearGradient(x, y, x, y+h);
gg.addColorStop(0, '#eef6fa');
gg.addColorStop(0.55,'#dbe9f0');
gg.addColorStop(1, '#c9dbe4');
g.fillStyle = gg;
g.fillRect(x, y, w, h);
// blurred outside foliage
g.save();
g.beginPath(); g.rect(x, y, w, h); g.clip();
g.filter = 'blur(7px)';
const rnd = mulberry32(77);
for(let i=0;i<18;i++){
const bx = x + rnd()*w;
const by = y + h*0.35 + rnd()*h*0.7;
g.fillStyle = 'rgba(' + (80+rnd()*50|0) + ',' + (110+rnd()*50|0) + ',' + (70+rnd()*40|0) + ',0.55)';
g.beginPath();
g.ellipse(bx, by, 26+rnd()*60, 18+rnd()*46, rnd()*3, 0, 7);
g.fill();
}
g.filter = 'none';
g.restore();
// frame + mullions
g.strokeStyle = '#efe6d6';
g.lineWidth = 11;
g.strokeRect(x, y, w, h);
g.beginPath();
g.moveTo(x+w/2, y); g.lineTo(x+w/2, y+h);
g.moveTo(x, y+h*0.44); g.lineTo(x+w, y+h*0.44);
g.stroke();
g.strokeStyle = 'rgba(110,90,70,0.35)';
g.lineWidth = 2;
g.strokeRect(x, y, w, h);
// sill
g.fillStyle = '#e6dcc8';
g.fillRect(x-14, y+h, w+28, 14);
g.fillStyle = 'rgba(90,70,50,0.25)';
g.fillRect(x-14, y+h+12, w+28, 5);
g.restore();
}
function drawFloor(g){
const y0 = FLOOR_Y, y1 = 900;
const fg = g.createLinearGradient(0, y0, 0, y1);
fg.addColorStop(0, '#6a472c');
fg.addColorStop(0.35,'#875c39');
fg.addColorStop(0.75,'#9c6d45');
fg.addColorStop(1, '#a97a50');
g.fillStyle = fg;
g.fillRect(0, y0, 1600, y1-y0);
g.save();
g.beginPath(); g.rect(0, y0, 1600, y1-y0); g.clip();
// plank seams converging to a vanishing point
const vpx = 800, vpy = 300;
for(let i=-15;i<=15;i++){
const xb = 800 + i*128;
g.beginPath();
g.moveTo(vpx + (xb-vpx)*0.02, vpy);
g.lineTo(xb, y1+60);
g.strokeStyle = 'rgba(42,26,14,0.30)';
g.lineWidth = 2.2;
g.stroke();
}
// horizontal board seams
const ys = [598, 618, 646, 684, 734, 800, 884];
for(const y of ys){
g.beginPath(); g.moveTo(0,y); g.lineTo(1600,y);
g.strokeStyle = 'rgba(42,26,14,0.20)';
g.lineWidth = 1.6;
g.stroke();
}
// grain
const rnd = mulberry32(7);
for(let i=0;i<1100;i++){
const y = y0 + rnd()*(y1-y0);
const x = rnd()*1600;
const len = 18 + rnd()*130;
g.beginPath();
g.moveTo(x, y);
g.quadraticCurveTo(x+len*0.5, y + (rnd()-0.5)*4, x+len, y + (rnd()-0.5)*7);
g.strokeStyle = 'rgba(' + (55+rnd()*45|0) + ',' + (32+rnd()*26|0) + ',' + (16+rnd()*16|0) + ',' + (0.05+rnd()*0.13) + ')';
g.lineWidth = 0.7 + rnd()*1.7;
g.stroke();
}
// window light reflection on the boards
const rg = g.createLinearGradient(0, 600, 0, 900);
rg.addColorStop(0, 'rgba(255,240,210,0.24)');
rg.addColorStop(1, 'rgba(255,240,210,0)');
g.fillStyle = rg;
g.beginPath();
g.moveTo(120, 600); g.lineTo(430, 600); g.lineTo(580, 900); g.lineTo(40, 900);
g.closePath(); g.fill();
g.restore();
}
function buildBase(){
const c = mk(1600, 900), g = c.getContext('2d');
// wall
const wg = g.createLinearGradient(0, 0, 0, FLOOR_Y);
wg.addColorStop(0, '#b6a389');
wg.addColorStop(0.35, '#cdbb9f');
wg.addColorStop(0.8, '#c2ad90');
wg.addColorStop(1, '#ab9578');
g.fillStyle = wg;
g.fillRect(0, 0, 1600, FLOOR_Y);
// wall micro-texture
const rnd = mulberry32(11);
for(let i=0;i<3200;i++){
const x = rnd()*1600, y = rnd()*FLOOR_Y;
const light = rnd() < 0.5;
g.fillStyle = 'rgba(' + (light?255:60) + ',' + (light?246:50) + ',' + (light?226:38) + ',' + (0.010+rnd()*0.020) + ')';
g.fillRect(x, y, 1+rnd()*2, 1+rnd()*2);
}
// warm pool from the window
const lp = g.createRadialGradient(230, 300, 40, 230, 300, 780);
lp.addColorStop(0, 'rgba(255,241,214,0.55)');
lp.addColorStop(0.5, 'rgba(255,238,208,0.18)');
lp.addColorStop(1, 'rgba(255,238,208,0)');
g.fillStyle = lp;
g.fillRect(0, 0, 1600, FLOOR_Y);
// cool falloff to the right
const cp = g.createLinearGradient(880, 0, 1600, 0);
cp.addColorStop(0, 'rgba(40,50,62,0)');
cp.addColorStop(1, 'rgba(28,36,48,0.38)');
g.fillStyle = cp;
g.fillRect(0, 0, 1600, FLOOR_Y);
drawWindow(g, 70, 110, 320, 390);
// baseboard
g.fillStyle = '#a8906f';
g.fillRect(0, 556, 1600, 30);
g.fillStyle = 'rgba(255,240,215,0.28)';
g.fillRect(0, 556, 1600, 4);
g.fillStyle = 'rgba(48,33,20,0.38)';
g.fillRect(0, 584, 1600, 8);
drawFloor(g);
return c;
}
function buildSofa(){
const c = mk(700, 400), g = c.getContext('2d');
const rnd = mulberry32(3);
function rr(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();
}
// back
rr(30, 20, 640, 220, 46);
let bg = g.createLinearGradient(0, 20, 0, 240);
bg.addColorStop(0, '#4f7273');
bg.addColorStop(0.5, '#3c5a5c');
bg.addColorStop(1, '#2a4143');
g.fillStyle = bg; g.fill();
// back cushions
rr(62, 52, 268, 168, 34); g.fillStyle = '#466769'; g.fill();
rr(370, 52, 268, 168, 34); g.fillStyle = '#416263'; g.fill();
// arms
rr(0, 120, 122, 250, 44);
let ag = g.createLinearGradient(0, 120, 122, 370);
ag.addColorStop(0, '#567a7b'); ag.addColorStop(1, '#2b4345');
g.fillStyle = ag; g.fill();
rr(578, 120, 122, 250, 44);
g.fillStyle = ag; g.fill();
// seat
rr(100, 200, 500, 122, 30);
let sg = g.createLinearGradient(0, 200, 0, 322);
sg.addColorStop(0, '#4b6d6e'); sg.addColorStop(1, '#324f4e');
g.fillStyle = sg; g.fill();
g.strokeStyle = 'rgba(18,32,32,0.55)';
g.lineWidth = 3;
g.beginPath(); g.moveTo(350, 206); g.lineTo(350, 316); g.stroke();
// legs
g.fillStyle = '#2a1d12';
g.fillRect(62, 360, 26, 32);
g.fillRect(612, 360, 26, 32);
// fabric noise
for(let i=0;i<3600;i++){
const x = rnd()*700, y = rnd()*400;
const light = rnd() < 0.5;
g.fillStyle = 'rgba(' + (light?255:0) + ',' + (light?255:0) + ',' + (light?255:0) + ',' + (0.010+rnd()*0.020) + ')';
g.fillRect(x, y, 1.6, 1.6);
}
// top light / bottom shade
const tl = g.createLinearGradient(0, 0, 0, 400);
tl.addColorStop(0, 'rgba(255,240,215,0.20)');
tl.addColorStop(0.4, 'rgba(255,240,215,0)');
tl.addColorStop(1, 'rgba(0,0,0,0.28)');
g.fillStyle = tl;
g.fillRect(0, 0, 700, 400);
return c;
}
function buildPlant(){
const c = mk(280, 440), g = c.getContext('2d');
// pot
g.beginPath();
g.moveTo(80, 300); g.lineTo(200, 300); g.lineTo(186, 420); g.lineTo(94, 420);
g.closePath();
const pg = g.createLinearGradient(80, 300, 200, 420);
pg.addColorStop(0, '#b06a45');
pg.addColorStop(0.5, '#8f4f31');
pg.addColorStop(1, '#6b3a23');
g.fillStyle = pg; g.fill();
g.fillStyle = '#c07a52';
g.beginPath(); g.ellipse(140, 300, 62, 16, 0, 0, 7); g.fill();
g.fillStyle = '#5a2f1c';
g.beginPath(); g.ellipse(140, 300, 52, 11, 0, 0, 7); g.fill();
// leaves
const rnd = mulberry32(9);
for(let i=0;i<30;i++){
const a = -Math.PI/2 + (rnd()-0.5)*2.5;
const len = 85 + rnd()*140;
const bx = 140 + (rnd()-0.5)*40, by = 300;
const ex = bx + Math.cos(a)*len, ey = by + Math.sin(a)*len;
const nx = -Math.sin(a)*30, ny = Math.cos(a)*30;
g.beginPath();
g.moveTo(bx, by);
g.quadraticCurveTo(bx + Math.cos(a)*len*0.5 + nx, by + Math.sin(a)*len*0.5 + ny, ex, ey);
g.quadraticCurveTo(bx + Math.cos(a)*len*0.5 - nx, by + Math.sin(a)*len*0.5 - ny, bx, by);
const hue = 96 + rnd()*44;
const lg = g.createLinearGradient(bx, by, ex, ey);
lg.addColorStop(0, 'hsl(' + hue + ',30%,20%)');
lg.addColorStop(1, 'hsl(' + hue + ',36%,' + (32+rnd()*16) + '%)');
g.fillStyle = lg;
g.fill();
}
return c;
}
function buildLamp(){
const c = mk(220, 340), g = c.getContext('2d');
// cord
g.strokeStyle = '#2a2620';
g.lineWidth = 4;
g.beginPath(); g.moveTo(110, 0); g.lineTo(110, 120); g.stroke();
// shade
g.beginPath();
g.moveTo(58, 120); g.lineTo(162, 120); g.lineTo(196, 200); g.lineTo(24, 200);
g.closePath();
const sg = g.createLinearGradient(24, 120, 196, 200);
sg.addColorStop(0, '#e8d3a8');
sg.addColorStop(0.45,'#d8bd8a');
sg.addColorStop(1, '#a98c5c');
g.fillStyle = sg; g.fill();
// inner glow
const ig = g.createRadialGradient(110, 200, 4, 110, 200, 130);
ig.addColorStop(0, 'rgba(255,236,190,0.95)');
ig.addColorStop(0.35,'rgba(255,220,150,0.35)');
ig.addColorStop(1, 'rgba(255,220,150,0)');
g.fillStyle = ig;
g.beginPath(); g.ellipse(110, 202, 120, 70, 0, 0, 7); g.fill();
// bulb
g.beginPath(); g.arc(110, 198, 16, 0, 7);
g.fillStyle = '#fff4d6'; g.fill();
return c;
}
function buildPicture(){
const c = mk(200, 180), g = c.getContext('2d');
g.fillStyle = '#3a2c1e';
g.fillRect(0, 0, 200, 180);
g.fillStyle = '#6b5334';
g.fillRect(6, 6, 188, 168);
// abstract art
const rnd = mulberry32(31);
g.save();
g.beginPath(); g.rect(18, 18, 164, 144); g.clip();
g.fillStyle = '#d9cbb0';
g.fillRect(18, 18, 164, 144);
for(let i=0;i<9;i++){
g.fillStyle = 'rgba(' + (60+rnd()*120|0) + ',' + (50+rnd()*90|0) + ',' + (40+rnd()*70|0) + ',' + (0.35+rnd()*0.4) + ')';
g.beginPath();
g.ellipse(18+rnd()*164, 18+rnd()*144, 12+rnd()*46, 10+rnd()*40, rnd()*3, 0, 7);
g.fill();
}
g.restore();
// glass sheen
const sh = g.createLinearGradient(0, 0, 200, 180);
sh.addColorStop(0, 'rgba(255,255,255,0.22)');
sh.addColorStop(0.4, 'rgba(255,255,255,0.02)');
sh.addColorStop(1, 'rgba(255,255,255,0)');
g.fillStyle = sh;
g.fillRect(18, 18, 164, 144);
return c;
}
function buildRug(){
const c = mk(820, 240), g = c.getContext('2d');
g.save();
g.beginPath(); g.ellipse(410, 120, 400, 110, 0, 0, 7); g.clip();
const rg = g.createRadialGradient(410, 120, 20, 410, 120, 400);
rg.addColorStop(0, '#8f5c4b');
rg.addColorStop(0.6, '#7a4a3c');
rg.addColorStop(1, '#5c342a');
g.fillStyle = rg;
g.fillRect(0, 0, 820, 240);
for(let i=1;i<=5;i++){
g.beginPath();
g.ellipse(410, 120, 400*i/5.6, 110*i/5.6, 0, 0, 7);
g.strokeStyle = 'rgba(232,208,172,' + (0.09 + 0.03*i) + ')';
g.lineWidth = 6;
g.stroke();
}
const rnd = mulberry32(21);
for(let i=0;i<5000;i++){
const x = rnd()*820, y = rnd()*240;
const light = rnd() < 0.5;
g.fillStyle = 'rgba(' + (light?255:0) + ',' + (light?228:0) + ',' + (light?198:0) + ',' + (0.018+rnd()*0.030) + ')';
g.fillRect(x, y, 2, 2);
}
g.restore();
return c;
}
/* ============================================================
CAT SPRITE
============================================================ */
function furRing(g, rnd, cx, cy, rx, ry, n){
for(let i=0;i<n;i++){
const a = rnd()*Math.PI*2;
const rr = 0.92 + rnd()*0.13;
const x = cx + Math.cos(a)*rx*rr;
const y = cy + Math.sin(a)*ry*rr;
const len = 5 + rnd()*15;
const dir = a + (rnd()-0.5)*0.9;
g.beginPath();
g.moveTo(x, y);
g.quadraticCurveTo(
x + Math.cos(dir)*len*0.5, y + Math.sin(dir)*len*0.5,
x + Math.cos(dir)*len, y + Math.sin(dir)*len
);
g.strokeStyle = rnd() < 0.6
? 'rgba(255,251,240,' + (0.06 + rnd()*0.14) + ')'
: 'rgba(188,163,128,' + (0.05 + rnd()*0.10) + ')';
g.lineWidth = 0.7 + rnd()*1.5;
g.stroke();
}
}
function drawEye(g, ex, ey, r){
// socket shadow
g.save();
g.beginPath();
g.ellipse(ex, ey, r*1.12, r*1.16, 0, 0, 7);
g.fillStyle = 'rgba(118,90,60,0.22)';
g.filter = 'blur(8px)';
g.fill();
g.filter = 'none';
g.restore();
// sclera
g.beginPath();
g.ellipse(ex, ey, r, r*1.06, 0, 0, 7);
const sg = g.createRadialGradient(ex - r*0.3, ey - r*0.35, r*0.1, ex, ey, r*1.25);
sg.addColorStop(0, '#fffdf8');
sg.addColorStop(0.65, '#f3ebdb');
sg.addColorStop(1, '#d5c6ac');
g.fillStyle = sg;
g.fill();
// iris
const ir = r*0.80;
g.beginPath();
g.arc(ex, ey, ir, 0, 7);
const ig = g.createRadialGradient(ex, ey - ir*0.2, ir*0.15, ex, ey, ir);
ig.addColorStop(0, '#d8b25c');
ig.addColorStop(0.55, '#a87c2e');
ig.addColorStop(1, '#4e3614');
g.fillStyle = ig;
g.fill();
// pupil
g.beginPath();
g.arc(ex, ey, ir*0.60, 0, 7);
g.fillStyle = '#0b0908';
g.fill();
// specular
g.beginPath();
g.arc(ex - r*0.30, ey - r*0.34, r*0.21, 0, 7);
g.fillStyle = 'rgba(255,255,255,0.95)';
g.fill();
g.beginPath();
g.arc(ex + r*0.24, ey + r*0.30, r*0.085, 0, 7);
g.fillStyle = 'rgba(255,255,255,0.65)';
g.fill();
// rim
g.beginPath();
g.ellipse(ex, ey, r, r*1.06, 0, 0, 7);
g.strokeStyle = 'rgba(70,50,32,0.55)';
g.lineWidth = 3;
g.stroke();
// upper lid shadow
g.save();
g.beginPath();
g.ellipse(ex, ey, r, r*1.06, 0, 0, 7);
g.clip();
const lg = g.createLinearGradient(0, ey - r*1.1, 0, ey - r*0.2);
lg.addColorStop(0, 'rgba(90,65,42,0.35)');
lg.addColorStop(1, 'rgba(90,65,42,0)');
g.fillStyle = lg;
g.fillRect(ex - r*1.2, ey - r*1.2, r*2.4, r*1.2);
g.restore();
}
function buildCat(front){
const SW = 800, SH = 1000;
const c = mk(SW, SH), g = c.getContext('2d');
const rnd = mulberry32(front ? 1337 : 4242);
const cx = 400;
/* ---- tail ---- */
g.save();
g.beginPath();
g.moveTo(560, 900);
g.bezierCurveTo(720, 910, 762, 760, 700, 690);
g.bezierCurveTo(680, 665, 645, 668, 638, 700);
g.bezierCurveTo(668, 770, 650, 860, 545, 860);
g.closePath();
const tg = g.createLinearGradient(560, 690, 740, 900);
tg.addColorStop(0, '#f3e4c8');
tg.addColorStop(0.6, '#e6d2ae');
tg.addColorStop(1, '#c9ae86');
g.fillStyle = tg;
g.fill();
g.restore();
/* ---- body ---- */
const body = new Path2D();
body.moveTo(255, 570);
body.bezierCurveTo(185, 670, 178, 800, 205, 880);
body.bezierCurveTo(245, 945, 555, 945, 595, 880);
body.bezierCurveTo(622, 800, 615, 670, 545, 570);
body.closePath();
const bg = g.createLinearGradient(200, 560, 600, 940);
bg.addColorStop(0, '#f7ecd6');
bg.addColorStop(0.45, '#eddfc2');
bg.addColorStop(1, '#d3bb95');
g.fillStyle = bg;
g.fill(body);
g.save();
g.clip(body);
// directional shading
const sh = g.createLinearGradient(200, 0, 620, 0);
sh.addColorStop(0, 'rgba(255,248,232,0.35)');
sh.addColorStop(0.45, 'rgba(255,248,232,0)');
sh.addColorStop(1, 'rgba(118,90,58,0.32)');
g.fillStyle = sh;
g.fillRect(150, 500, 520, 480);
// white chest
const cg = g.createRadialGradient(400, 720, 20, 400, 740, 230);
cg.addColorStop(0, 'rgba(255,253,247,1)');
cg.addColorStop(0.55, 'rgba(252,246,234,0.95)');
cg.addColorStop(1, 'rgba(252,246,234,0)');
g.fillStyle = cg;
g.beginPath(); g.ellipse(400, 740, 155, 200, 0, 0, 7); g.fill();
// occlusion under the chin
const ao = g.createRadialGradient(400, 560, 10, 400, 560, 185);
ao.addColorStop(0, 'rgba(108,78,48,0.38)');
ao.addColorStop(1, 'rgba(108,78,48,0)');
g.fillStyle = ao;
g.fillRect(200, 420, 400, 260);
g.restore();
/* ---- front legs ---- */
function leg(x){
g.save();
g.beginPath();
g.moveTo(x-52, 700);
g.bezierCurveTo(x-58, 800, x-56, 880, x-50, 915);
g.bezierCurveTo(x-20, 935, x+20, 935, x+50, 915);
g.bezierCurveTo(x+56, 880, x+58, 800, x+52, 700);
g.closePath();
const lg = g.createLinearGradient(x-60, 700, x+60, 930);
lg.addColorStop(0, '#f8efdb');
lg.addColorStop(0.6, '#eee0c4');
lg.addColorStop(1, '#d8c19c');
g.fillStyle = lg;
g.fill();
g.restore();
}
leg(320); leg(480);
function paw(x){
g.beginPath();
g.ellipse(x, 918, 58, 30, 0, 0, 7);
const pg = g.createRadialGradient(x-15, 905, 5, x, 918, 70);
pg.addColorStop(0, '#fffdf7');
pg.addColorStop(1, '#e8d9bd');
g.fillStyle = pg;
g.fill();
g.strokeStyle = 'rgba(158,128,94,0.35)';
g.lineWidth = 2;
for(let i=-1;i<=1;i++){
g.beginPath();
g.moveTo(x + i*20, 900);
g.lineTo(x + i*20, 928);
g.stroke();
}
}
paw(320); paw(480);
/* ---- ears ---- */
function ear(sign){
g.save();
g.beginPath();
g.moveTo(cx + sign*150, 285);
g.quadraticCurveTo(cx + sign*185, 110, cx + sign*120, 95);
g.quadraticCurveTo(cx + sign*70, 90, cx + sign*55, 235);
g.closePath();
const eg = g.createLinearGradient(cx + sign*60, 100, cx + sign*160, 290);
eg.addColorStop(0, '#f6ead2');
eg.addColorStop(1, '#dcc7a2');
g.fillStyle = eg;
g.fill();
if(front){
g.beginPath();
g.moveTo(cx + sign*128, 265);
g.quadraticCurveTo(cx + sign*150, 150, cx + sign*112, 140);
g.quadraticCurveTo(cx + sign*85, 145, cx + sign*78, 245);
g.closePath();
const ig = g.createLinearGradient(cx + sign*80, 140, cx + sign*140, 270);
ig.addColorStop(0, '#e8b3a8');
ig.addColorStop(1, '#c98a80');
g.fillStyle = ig;
g.fill();
}
g.restore();
}
ear(-1); ear(1);
/* ---- head ---- */
g.save();
g.beginPath();
g.ellipse(400, 385, 198, 178, 0, 0, 7);
const hg = g.createRadialGradient(330, 300, 30, 400, 400, 260);
hg.addColorStop(0, '#fbf2df');
hg.addColorStop(0.55, '#f0e2c6');
hg.addColorStop(1, '#d9c29c');
g.fillStyle = hg;
g.fill();
g.beginPath(); g.ellipse(255, 455, 80, 72, 0, 0, 7); g.fillStyle = '#f3e6cc'; g.fill();
g.beginPath(); g.ellipse(545, 455, 80, 72, 0, 0, 7); g.fillStyle = '#f3e6cc'; g.fill();
g.clip();
const hs = g.createLinearGradient(200, 200, 600, 560);
hs.addColorStop(0, 'rgba(255,250,236,0.45)');
hs.addColorStop(0.5, 'rgba(255,250,236,0)');
hs.addColorStop(1, 'rgba(118,90,58,0.30)');
g.fillStyle = hs;
g.fillRect(180, 180, 440, 420);
g.restore();
/* ---- face ---- */
if(front){
drawEye(g, 318, 392, 60);
drawEye(g, 482, 392, 60);
// nose
g.beginPath();
g.moveTo(400, 462);
g.quadraticCurveTo(382, 462, 382, 474);
g.quadraticCurveTo(400, 496, 418, 474);
g.quadraticCurveTo(418, 462, 400, 462);
g.closePath();
const ng = g.createLinearGradient(382, 462, 418, 496);
ng.addColorStop(0, '#e0a49a');
ng.addColorStop(1, '#b9706a');
g.fillStyle = ng;
g.fill();
g.beginPath(); g.ellipse(394, 468, 7, 4, 0, 0, 7);
g.fillStyle = 'rgba(255,255,255,0.5)'; g.fill();
// mouth
g.strokeStyle = 'rgba(118,84,58,0.55)';
g.lineWidth = 3.5;
g.lineCap = 'round';
g.beginPath();
g.moveTo(400, 494);
g.quadraticCurveTo(388, 510, 374, 504);
g.moveTo(400, 494);
g.quadraticCurveTo(412, 510, 426, 504);
g.stroke();
// whiskers
g.strokeStyle = 'rgba(255,252,244,0.55)';
g.lineWidth = 2;
for(let i=0;i<4;i++){
const y = 440 + i*16;
g.beginPath();
g.moveTo(300, y);
g.quadraticCurveTo(230, y - 14 + i*6, 170, y - 30 + i*14);
g.stroke();
g.beginPath();
g.moveTo(500, y);
g.quadraticCurveTo(570, y - 14 + i*6, 630, y - 30 + i*14);
g.stroke();
}
} else {
g.strokeStyle = 'rgba(178,148,108,0.25)';
g.lineWidth = 3;
g.beginPath(); g.moveTo(400, 230); g.lineTo(400, 330); g.stroke();
}
/* ---- fur detail ---- */
furRing(g, rnd, 400, 385, 198, 178, 950);
furRing(g, rnd, 400, 760, 210, 180, 750);
for(let i=0;i<650;i++){
const x = 400 + (rnd()-0.5)*260;
const y = 620 + rnd()*260;
const len = 8 + rnd()*18;
g.beginPath();
g.moveTo(x, y);
g.quadraticCurveTo(x + (rnd()-0.5)*8, y + len*0.6, x + (rnd()-0.5)*14, y + len);
g.strokeStyle = rnd() < 0.5
? 'rgba(255,253,246,' + (0.05 + rnd()*0.12) + ')'
: 'rgba(200,175,140,' + (0.04 + rnd()*0.08) + ')';
g.lineWidth = 0.8 + rnd()*1.4;
g.stroke();
}
for(let i=0;i<420;i++){
const side = rnd() < 0.5 ? -1 : 1;
const x = 400 + side*(150 + rnd()*70);
const y = 400 + (rnd()-0.5)*150;
const len = 6 + rnd()*16;
g.beginPath();
g.moveTo(x, y);
g.lineTo(x + side*len, y + (rnd()-0.5)*len);
g.strokeStyle = 'rgba(255,250,238,' + (0.05 + rnd()*0.10) + ')';
g.lineWidth = 0.8 + rnd()*1.2;
g.stroke();
}
for(let i=0;i<2600;i++){
const x = rnd()*SW, y = rnd()*SH;
const light = rnd() < 0.5;
g.fillStyle = 'rgba(' + (light?255:90) + ',' + (light?245:70) + ',' + (light?225:50) + ',' + (0.010+rnd()*0.020) + ')';
g.fillRect(x, y, 1.5, 1.5);
}
return c;
}
function makeSmear(src){
const c = mk(src.width*2, src.height);
const g = c.getContext('2d');
g.filter = 'blur(9px)';
g.globalAlpha = 0.55;
g.drawImage(src, src.width*0.5, 0);
g.globalAlpha = 0.40;
g.drawImage(src, src.width*0.28, 0);
g.drawImage(src, src.width*0.72, 0);
g.globalAlpha = 0.25;
g.drawImage(src, src.width*0.10, 0);
g.drawImage(src, src.width*0.90, 0);
return c;
}
/* ============================================================
BUILD EVERYTHING
============================================================ */
const baseC = blurCanvas(buildBase(), 0.9);
const sofaC = blurCanvas(buildSofa(), 1.4);
const plantC = blurCanvas(buildPlant(), 2.0);
const lampC = blurCanvas(buildLamp(), 1.8);
const pictureC = blurCanvas(buildPicture(), 1.6);
const rugC = blurCanvas(buildRug(), 0.6);
const catFront = buildCat(true);
const catBack = buildCat(false);
const catSmear = makeSmear(catFront);
const FURN = [
{img: pictureC, x: 470, y: 180, w: 200, h: 180, phase: 1.2, dx: -30, dy: -20, rot: -0.12, orbit: 1.5},
{img: plantC, x: 20, y: 330, w: 280, h: 440, phase: 2.1, dx: 40, dy: -30, rot: 0.10, orbit: 1.3},
{img: sofaC, x: 960, y: 440, w: 660, h: 380, phase: 0.0, dx: -70, dy: 20, rot: -0.06, orbit: 1.0},
{img: lampC, x: 1080, y: 120, w: 220, h: 340, phase: 4.0, dx: 0, dy: -40, rot: 0.14, orbit: 0.8}
];
/* ============================================================
STATE
============================================================ */
const S = {
stage: 0,
tStage: 0,
time: 0,
spin: 0,
spinVel: 0,
spinTarget: 0,
lift: 0,
liftTarget: 0,
camRot: 0,
camRotTarget: 0,
shake: 0,
shakeTarget: 0,
flash: 0
};
let blinkTimer = 2.5 + Math.random()*3;
let blinkPhase = -1;
let blink = 0;
function stageParams(n){
const k = Math.min(n, 4);
const over = Math.max(0, n - 4);
return {
spin: [0, 11, 24, 40, 62][k] + over*10,
lift: [0, 80, 210, 350, 460][k] + over*15,
camRot: [0, 0, 0.03, 0.12, 0.26][k],
shake: [0, 1.0, 2.4, 5.0, 8.5][k] + over*2
};
}
/* ============================================================
AUDIO — synthesized "oiia" voice
============================================================ */
let AC = null, master = null, windGain = null, windFilter = null, noiseBuf = null;
let nextPhrase = 0, audioOn = false, muted = false;
const VOWELS = {
o: {f: [450, 800, 2600], g: [1.00, 0.55, 0.25]},
i: {f: [300, 2250, 3000], g: [1.00, 0.70, 0.35]},
a: {f: [720, 1200, 2600], g: [1.00, 0.60, 0.30]},
u: {f: [350, 700, 2400], g: [1.00, 0.50, 0.20]}
};
const PHRASE = [
['o', 0.10, 0.92], ['i', 0.09, 1.00], ['a', 0.13, 1.06],
['o', 0.09, 0.96], ['i', 0.08, 1.04], ['a', 0.12, 1.10],
['i', 0.07, 1.02], ['o', 0.08, 0.94], ['o', 0.09, 0.98],
['i', 0.08, 1.08], ['a', 0.14, 1.14], ['i', 0.10, 1.00]
];
const PHRASE_LEN = PHRASE.reduce(function(s, p){ return s + p[1]; }, 0);
function initAudio(){
if(AC) return;
const Ctx = window.AudioContext || window.webkitAudioContext;
if(!Ctx) return;
AC = new Ctx();
master = AC.createGain();
master.gain.value = 0;
const comp = AC.createDynamicsCompressor();
comp.threshold.value = -18;
comp.knee.value = 24;
comp.ratio.value = 8;
comp.attack.value = 0.004;
comp.release.value = 0.20;
master.connect(comp);
comp.connect(AC.destination);
noiseBuf = AC.createBuffer(1, Math.floor(AC.sampleRate*2), AC.sampleRate);
const d = noiseBuf.getChannelData(0);
for(let i=0;i<d.length;i++) d[i] = Math.random()*2 - 1;
const wn = AC.createBufferSource();
wn.buffer = noiseBuf;
wn.loop = true;
windFilter = AC.createBiquadFilter();
windFilter.type = 'bandpass';
windFilter.frequency.value = 500;
windFilter.Q.value = 1.1;
windGain = AC.createGain();
windGain.gain.value = 0;
wn.connect(windFilter);
windFilter.connect(windGain);
windGain.connect(master);
wn.start();
nextPhrase = AC.currentTime + 0.08;
}
function sing(t0, dur, vowel, f0, amp, bright){
const v = VOWELS[vowel];
const out = AC.createGain();
out.gain.setValueAtTime(0.0001, t0);
out.gain.exponentialRampToValueAtTime(amp, t0 + 0.014);
out.gain.setTargetAtTime(amp*0.72, t0 + 0.02, 0.05);
out.gain.setTargetAtTime(0.0001, t0 + dur*0.78, dur*0.14);
const am = AC.createGain();
am.gain.value = 1;
out.connect(am);
am.connect(master);
if(bright > 0.35){
const gl = AC.createOscillator();
gl.type = 'sine';
gl.frequency.value = 34 + bright*40;
const gg = AC.createGain();
gg.gain.value = 0.30 * bright;
gl.connect(gg);
gg.connect(am.gain);
gl.start(t0);
gl.stop(t0 + dur + 0.1);
}
const src = AC.createOscillator();
src.type = 'sawtooth';
src.frequency.setValueAtTime(f0*1.06, t0);
src.frequency.linearRampToValueAtTime(f0*1.16, t0 + dur*0.30);
src.frequency.linearRampToValueAtTime(f0*0.94, t0 + dur);
const lfo = AC.createOscillator();
lfo.frequency.value = 6 + Math.random()*2;
const lg = AC.createGain();
lg.gain.value = f0*0.04;
lfo.connect(lg);
lg.connect(src.frequency);
lfo.start(t0);
lfo.stop(t0 + dur + 0.1);
const pre = AC.createGain();
pre.gain.value = 0.6;
src.connect(pre);
const src2 = AC.createOscillator();
src2.type = 'square';
src2.frequency.setValueAtTime(f0*0.5, t0);
src2.frequency.linearRampToValueAtTime(f0*0.5, t0 + dur);
const g2 = AC.createGain();
g2.gain.value = 0.10;
src2.connect(g2);
g2.connect(pre);
src2.start(t0);
src2.stop(t0 + dur + 0.1);
const fs = 1 + (f0/400 - 1)*0.30;
for(let i=0;i<3;i++){
const bp = AC.createBiquadFilter();
bp.type = 'bandpass';
bp.Q.value = [6, 9, 11][i];
bp.frequency.setValueAtTime(v.f[i]*fs, t0);
const fg = AC.createGain();
fg.gain.value = v.g[i] * (i === 2 ? (0.40 + bright*0.80) : 1);
pre.connect(bp);
bp.connect(fg);
fg.connect(out);
}
// breath / consonant
const n = AC.createBufferSource();
n.buffer = noiseBuf;
const nf = AC.createBiquadFilter();
nf.type = 'bandpass';
nf.frequency.value = 2200;
nf.Q.value = 1.5;
const ng = AC.createGain();
ng.gain.setValueAtTime(0.0001, t0);
ng.gain.exponentialRampToValueAtTime(0.05 + bright*0.05, t0 + 0.010);
ng.gain.exponentialRampToValueAtTime(0.0001, t0 + dur*0.5);
n.connect(nf);
nf.connect(ng);
ng.connect(out);
n.start(t0);
n.stop(t0 + dur + 0.05);
src.start(t0);
src.stop(t0 + dur + 0.1);
}
function schedulePhrase(t0, rate, pitch, bright){
let t = t0;
const base = 360 * pitch;
for(let i=0;i<PHRASE.length;i++){
const v = PHRASE[i][0], d = PHRASE[i][1], pm = PHRASE[i][2];
const dur = d / rate;
sing(t, dur, v, base * pm * (0.97 + Math.random()*0.06), 0.16, bright);
t += dur;
}
}
function updateAudio(){
if(!AC || !audioOn) return;
const norm = Math.min(1, S.spinVel / 70);
const rate = 1 + norm*2.6;
const pitch = 1 + norm*1.6;
const now = AC.currentTime;
if(nextPhrase < now) nextPhrase = now + 0.02;
let guard = 0;
while(nextPhrase < now + 0.25 && guard++ < 8){
schedulePhrase(nextPhrase, rate, pitch, norm);
nextPhrase += PHRASE_LEN / rate;
}
windGain.gain.setTargetAtTime(0.015 + norm*0.09, now, 0.12);
windFilter.frequency.setTargetAtTime(420 + norm*2400, now, 0.12);
}
/* ============================================================
UPDATE
============================================================ */
function update(dt){
S.time += dt;
S.tStage += dt;
const p = stageParams(S.stage);
S.spinTarget = Math.min(100, p.spin);
S.liftTarget = p.lift;
S.camRotTarget = p.camRot;
S.shakeTarget = p.shake;
S.spinVel += (S.spinTarget - S.spinVel) * (1 - Math.exp(-dt*1.6));
S.spin += S.spinVel * dt;
S.lift += (S.liftTarget - S.lift) * (1 - Math.exp(-dt*2.2));
S.camRot += (S.camRotTarget - S.camRot) * (1 - Math.exp(-dt*1.2));
S.shake += (S.shakeTarget - S.shake) * (1 - Math.exp(-dt*3.0));
S.flash *= Math.exp(-dt*4.5);
// blink
if(S.stage === 0){
blinkTimer -= dt;
if(blinkTimer <= 0 && blinkPhase < 0){
blinkPhase = 0;
blinkTimer = 2.5 + Math.random()*4;
}
}
if(blinkPhase >= 0){
blinkPhase += dt / 0.16;
if(blinkPhase >= 1){ blinkPhase = -1; blink = 0; }
else blink = Math.sin(blinkPhase * Math.PI);
} else {
blink = 0;
}
}
/* ============================================================
DRAW HELPERS
============================================================ */
function drawBase(t){
ctx.save();
if(S.stage >= 3){
const m = Math.min(1, (S.stage - 2) * 0.5);
ctx.translate(700, 450);
ctx.rotate(Math.sin(t*0.6)*0.02*m + (S.stage >= 4 ? t*0.05 : 0));
ctx.scale(1 + 0.03*m, 1 + 0.03*m);
ctx.translate(-700, -450);
}
ctx.drawImage(baseC, 0, 0);
ctx.restore();
if(S.stage >= 4){
ctx.save();
ctx.globalAlpha = 0.18;
ctx.translate(700, 450);
ctx.rotate(t*0.09);
ctx.translate(-700, -450);
ctx.drawImage(baseC, 0, 0);
ctx.restore();
}
}
function drawShaft(){
ctx.save();
ctx.globalCompositeOperation = 'lighter';
const g = ctx.createLinearGradient(200, 150, 700, 800);
g.addColorStop(0, 'rgba(255,244,215,0.10)');
g.addColorStop(1, 'rgba(255,244,215,0)');
ctx.fillStyle = g;
ctx.beginPath();
ctx.moveTo(90, 120); ctx.lineTo(390, 120);
ctx.lineTo(760, 900); ctx.lineTo(180, 900);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function drawRug(t){
ctx.save();
if(S.stage >= 4){
const a = t*0.5;
const cx = 700, cy = 850 - S.lift;
const px = cx + Math.cos(a)*300;
const py = cy + Math.sin(a)*140;
ctx.translate(px, py);
ctx.rotate(a*1.4);
ctx.globalAlpha = 0.75;
ctx.drawImage(rugC, -410, -120, 820, 240);
} else {
if(S.stage >= 3){
const m = Math.min(1, (S.stage - 2) * 0.5);
ctx.translate(730, 860);
ctx.rotate(Math.sin(t*0.9)*0.05*m);
ctx.translate(-730, -860);
ctx.globalAlpha = 1 - 0.25*m;
}
ctx.drawImage(rugC, 320, 740, 820, 240);
}
ctx.restore();
}
function drawFurniture(t){
const st = S.stage;
const catX = 700, catY = 850 - S.lift;
for(let fi=0; fi<FURN.length; fi++){
const f = FURN[fi];
let x = f.x, y = f.y, rot = 0, sc = 1, alpha = 1;
const k = Math.min(1, S.tStage / 1.8);
if(st >= 2){
const m = Math.min(1, (st - 1) * 0.5);
x += f.dx * k * m;
y += f.dy * k * m;
rot += f.rot * k * m;
}
if(st >= 3){
const m = Math.min(1, (st - 2) * 0.6);
y -= (30 + Math.sin(t*1.1 + f.phase)*14) * m;
rot += Math.sin(t*0.7 + f.phase) * 0.12 * m;
}
if(st >= 4){
const a = t*0.55*f.orbit + f.phase;
const R = 380 + f.orbit*120;
x = catX + Math.cos(a)*R - f.w/2;
y = catY + Math.sin(a)*R*0.42 - f.h/2;
rot = a*1.2;
sc = 0.75;
alpha = 0.9;
// motion trail
ctx.save();
ctx.globalAlpha = 0.16;
const a2 = a - 0.22;
ctx.translate(catX + Math.cos(a2)*R, catY + Math.sin(a2)*R*0.42);
ctx.rotate(a2*1.2);
ctx.scale(sc, sc);
ctx.drawImage(f.img, -f.w/2, -f.h/2, f.w, f.h);
ctx.restore();
}
ctx.save();
ctx.globalAlpha = alpha;
ctx.translate(x + f.w/2, y + f.h/2);
ctx.rotate(rot);
ctx.scale(sc, sc);
ctx.drawImage(f.img, -f.w/2, -f.h/2, f.w, f.h);
ctx.restore();
}
}
/* ---- dust motes ---- */
const motes = [];
for(let i=0;i<150;i++){
motes.push({
x: Math.random()*1600,
y: Math.random()*900,
z: 0.3 + Math.random()*0.7,
ph: Math.random()*Math.PI*2,
sp: 0.3 + Math.random()*0.8
});
}
function drawMotes(t){
const st = S.stage;
const catX = 700, catY = 850 - S.lift - 200;
const blend = Math.min(1, Math.max(0, (st - 1) / 2));
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for(let i=0;i<motes.length;i++){
const m = motes[i];
let dx = m.x + Math.sin(t*0.3*m.sp + m.ph)*30;
let dy = m.y - ((t*8*m.sp) % 900);
if(dy < -20) dy += 920;
const a = t*0.8*m.sp + m.ph;
const R = 200 + m.z*400;
const ox = catX + Math.cos(a)*R;
const oy = catY + Math.sin(a)*R*0.5;
const x = dx + (ox - dx)*blend;
const y = dy + (oy - dy)*blend;
const s = 1 + m.z*2.5;
const al = 0.10 + m.z*0.22;
ctx.beginPath();
ctx.arc(x, y, s, 0, 7);
ctx.fillStyle = 'rgba(255,240,210,' + al + ')';
ctx.fill();
}
ctx.restore();
}
/* ---- cat ---- */
function drawLid(g, ex, ey, r, b){
g.save();
g.beginPath();
g.ellipse(ex, ey, r*1.06, r*1.10, 0, 0, 7);
g.clip();
g.fillStyle = '#efe1c5';
g.fillRect(ex - r*1.2, ey - r*1.2, r*2.4, r*2.4*b + r*0.08);
g.strokeStyle = 'rgba(90,65,45,0.5)';
g.lineWidth = 3;
g.beginPath();
const ly = ey - r*1.1 + r*2.2*b;
g.moveTo(ex - r*1.05, ly);
g.quadraticCurveTo(ex, ly + 8, ex + r*1.05, ly);
g.stroke();
g.restore();
}
function drawCatSprite(angle, x, feetY, scale, alpha, breath){
const c = Math.cos(angle);
const front = c >= 0;
const sx = Math.max(0.045, Math.abs(c));
const img = front ? catFront : catBack;
const w = img.width * scale;
const h = img.height * scale;
ctx.save();
ctx.globalAlpha = alpha;
ctx.translate(x, feetY);
ctx.scale(sx, 1 + (breath || 0));
ctx.drawImage(img, -w/2, -h + 60*scale);
if(front && blink > 0.01 && alpha > 0.5){
ctx.translate(-400*scale, -940*scale);
ctx.scale(scale, scale);
drawLid(ctx, 318, 392, 60, blink);
drawLid(ctx, 482, 392, 60, blink);
}
ctx.restore();
}
function drawCat(t){
const lift = S.lift;
const cx = 700;
const feetY = 850 - lift;
const scale = 0.52;
const bob = Math.sin(t*1.8)*1.5 + Math.sin(t*0.7)*2.5;
const breath = S.stage === 0 ? Math.sin(t*1.3)*0.005 : 0;
// contact shadow
const shk = Math.max(0, 1 - lift/560);
if(shk > 0.01){
ctx.save();
const rw = 210*shk + 40;
const rh = 52*shk + 12;
const sg = ctx.createRadialGradient(cx, 862, 4, cx, 862, rw);
sg.addColorStop(0, 'rgba(24,13,7,' + (0.55*shk) + ')');
sg.addColorStop(0.6, 'rgba(24,13,7,' + (0.22*shk) + ')');
sg.addColorStop(1, 'rgba(24,13,7,0)');
ctx.fillStyle = sg;
ctx.beginPath();
ctx.ellipse(cx, 862, rw, rh, 0, 0, 7);
ctx.fill();
ctx.restore();
}
// glow
if(S.stage >= 3){
const gr = 260 + Math.sin(t*4)*30;
const gg = ctx.createRadialGradient(cx, feetY - 220, 20, cx, feetY - 220, gr);
gg.addColorStop(0, 'rgba(255,225,170,' + (0.10 + 0.05*Math.sin(t*6)) + ')');
gg.addColorStop(1, 'rgba(255,225,170,0)');
ctx.save();
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = gg;
ctx.beginPath();
ctx.arc(cx, feetY - 220, gr, 0, 7);
ctx.fill();
ctx.restore();
}
// motion smear
if(S.spinVel > 9){
const a = Math.min(0.55, (S.spinVel - 9) / 90);
ctx.save();
ctx.globalAlpha = a;
ctx.translate(cx, feetY + bob);
ctx.drawImage(catSmear, -catSmear.width*scale/2, -catSmear.height*scale + 60*scale, catSmear.width*scale, catSmear.height*scale);
ctx.restore();
}
// ghosts
const ghosts = Math.min(6, Math.floor(S.spinVel / 3.2));
for(let i=ghosts;i>=1;i--){
const a = S.spin - i * S.spinVel * 0.006;
drawCatSprite(a, cx, feetY + bob, scale, 0.10 * (1 - i/(ghosts+1)), breath);
}
drawCatSprite(S.spin, cx, feetY + bob, scale, 1, breath);
// ring of cats
if(S.stage >= 4){
const n = 6;
for(let i=0;i<n;i++){
const a = t*1.6 + i*Math.PI*2/n;
const px = cx + Math.cos(a)*330;
const py = feetY - 220 + Math.sin(a)*130;
drawCatSprite(S.spin*1.8 + i*1.3, px, py + 110, 0.20, 0.55, 0);
}
}
}
/* ---- vortex ---- */
function drawVortex(t){
const cx = 700, cy = 850 - S.lift - 220;
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for(let i=0;i<26;i++){
const a0 = t*1.4 + i*0.24;
ctx.beginPath();
for(let j=0;j<24;j++){
const a = a0 + j*0.06;
const r = 60 + j*14 + i*8;
const x = cx + Math.cos(a)*r;
const y = cy + Math.sin(a)*r*0.55;
if(j === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.strokeStyle = 'rgba(255,200,150,' + (0.03 + 0.02*Math.abs(Math.sin(t*3 + i))) + ')';
ctx.lineWidth = 2;
ctx.stroke();
}
ctx.restore();
}
/* ============================================================
POST
============================================================ */
const grainC = (function(){
const c = mk(256, 256), g = c.getContext('2d');
const id = g.createImageData(256, 256);
for(let i=0;i<id.data.length;i+=4){
const v = 128 + (Math.random()-0.5)*120;
id.data[i] = v; id.data[i+1] = v; id.data[i+2] = v; id.data[i+3] = 255;
}
g.putImageData(id, 0, 0);
return c;
})();
let grainPattern = null;
function post(){
// vignette
const vg = ctx.createRadialGradient(W/2, H/2, Math.min(W,H)*0.25, W/2, H/2, Math.max(W,H)*0.78);
vg.addColorStop(0, 'rgba(0,0,0,0)');
vg.addColorStop(1, 'rgba(0,0,0,0.58)');
ctx.fillStyle = vg;
ctx.fillRect(0, 0, W, H);
// warm / cool grade
ctx.globalCompositeOperation = 'soft-light';
const gg = ctx.createLinearGradient(0, 0, 0, H);
gg.addColorStop(0, 'rgba(255,200,140,0.20)');
gg.addColorStop(1, 'rgba(40,60,90,0.20)');
ctx.fillStyle = gg;
ctx.fillRect(0, 0, W, H);
// grain
if(!grainPattern) grainPattern = ctx.createPattern(grainC, 'repeat');
ctx.globalCompositeOperation = 'overlay';
ctx.globalAlpha = 0.055;
ctx.save();
ctx.translate((Math.random()*256)|0, (Math.random()*256)|0);
ctx.fillStyle = grainPattern;
ctx.fillRect(-256, -256, W + 512, H + 512);
ctx.restore();
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
// flash
if(S.flash > 0.01){
ctx.fillStyle = 'rgba(255,240,215,' + (S.flash*0.35) + ')';
ctx.fillRect(0, 0, W, H);
}
}
/* ============================================================
RENDER
============================================================ */
function render(){
const t = S.time;
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
ctx.fillStyle = '#0a0806';
ctx.fillRect(0, 0, W, H);
const shx = (Math.random()-0.5) * S.shake;
const shy = (Math.random()-0.5) * S.shake;
ctx.save();
ctx.translate(W/2 + shx, H/2 + shy);
ctx.rotate(
S.camRot +
(S.stage >= 4 ? t*0.22 : 0) +
Math.sin(t*0.8) * 0.015 * Math.max(0, S.stage - 2)
);
ctx.scale(fitScale, fitScale);
ctx.translate(-760, -470);
drawBase(t);
drawShaft();
drawRug(t);
drawFurniture(t);
drawCat(t);
drawMotes(t);
if(S.stage >= 3) drawVortex(t);
ctx.restore();
post();
}
/* ============================================================
LOOP
============================================================ */
let last = performance.now();
function loop(now){
const dt = Math.min(0.05, (now - last) / 1000);
last = now;
update(dt);
render();
updateAudio();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
/* ============================================================
INPUT
============================================================ */
function activate(){
initAudio();
if(AC && AC.state === 'suspended') AC.resume();
audioOn = true;
if(master && AC) master.gain.setTargetAtTime(muted ? 0 : 0.85, AC.currentTime, 0.12);
S.stage = Math.min(S.stage + 1, 6);
S.tStage = 0;
S.flash = 1;
hintEl.style.opacity = 0;
}
cv.addEventListener('pointerdown', function(e){
const r = cv.getBoundingClientRect();
const px = (e.clientX - r.left) * (W / r.width);
const py = (e.clientY - r.top) * (H / r.height);
const sx = (px - W/2) / fitScale + 760;
const sy = (py - H/2) / fitScale + 470;
const feet = 850 - S.lift;
if(sx > 470 && sx < 930 && sy > feet - 470 && sy < feet + 40){
activate();
}
});
const muteBtn = document.getElementById('mute');
muteBtn.addEventListener('click', function(e){
e.stopPropagation();
muted = !muted;
muteBtn.textContent = muted ? 'sound off' : 'sound on';
if(master && AC) master.gain.setTargetAtTime(muted ? 0 : 0.85, AC.currentTime, 0.08);
});
document.getElementById('reset').addEventListener('click', function(e){
e.stopPropagation();
S.stage = 0;
S.tStage = 0;
S.spin = 0;
S.spinVel = 0;
S.lift = 0;
S.camRot = 0;
S.shake = 0;
S.flash = 0;
audioOn = false;
if(master && AC) master.gain.setTargetAtTime(0, AC.currentTime, 0.2);
hintEl.style.opacity = 1;
});
})();
</script>
</body>
</html>
Opening-frame realism. The room is built as separate pre-rendered sprites (wall/window/floor, sofa, plant, pendant lamp, picture, rug), each blurred by a different amount to fake depth of field, then composited under a warm window key light, a cool right-side falloff, a soft light shaft, a contact shadow under the cat, a vignette, a warm/cool grade and animated film grain. The cat itself is a 800×1000 offscreen canvas with layered gradients, ambient occlusion under the chin and between the legs, ~3,000 individual fur strokes along the silhouette and chest, and a specular eye with a socket shadow. It gives out at the silhouette edge — the fur strokes are 2D strokes, not geometry, so a very close look at the ear tips and the tail curve reveals the drawing. The floor planks also converge to a single vanishing point, which is correct for a flat wall but not for a room with a real corner.
Reading as the Oiia cat. The cat is square to the camera, sitting, with oversized round eyes, a tiny pink nose, a "w" mouth, whiskers, and a completely blank stare — no expression, no head turn. The spin is a true vertical-axis rotation: the sprite is squashed with scaleX = cos(angle) and swapped to a separately drawn back-of-head sprite when the cosine goes negative, so you see the back of the ears and the fur part rather than a mirrored face. Motion blur is a pre-blurred, horizontally smeared copy of the sprite plus a few angular ghosts, so the face becomes unreadable exactly the way the meme does.
The four stages. (1) The cat lifts ~80px off the rug, starts a slow accelerating spin, the lamp flickers, the room dims, and the "oiia" loop begins low and slow. (2) It rises to ~210px, the spin doubles, the camera tilts, the furniture slides and rotates outward, dust motes break from their drift into orbit, and the voice climbs in rate and pitch. (3) It rises to ~350px, the whole room rolls and scales, gravity inverts — the sofa, plant, lamp and picture float upward and tumble, the rug lifts, spiral light streaks appear, and the cat throws a ring of afterimages. (4) The room becomes a vortex: furniture orbits the cat on elliptical paths with motion trails, the rug flies, a ring of six smaller cats orbits the main one, the background smears into a second rotating copy, the camera rolls continuously, and the voice gains a growl AM layer and a wind bed. Each stage changes what the room does, not just how fast it goes.
The voice. Each syllable is a sawtooth plus a sub-octave square, pushed through three parallel bandpass filters tuned to vowel formants (F1/F2/F3) that morph between "o", "i" and "a" targets, with a pitch contour that rises then falls inside every syllable, a 6–8 Hz vibrato, and a short filtered-noise breath attack. The phrase is a fixed 12-syllable "oiia oiia oiiaiooooiai" pattern. The spin velocity sets both the phrase rate (1 + norm*2.6) and the base pitch (1 + norm*1.6), and the same normalized value drives the wind-noise bandpass and gain, so the sound and the rotation are literally the same number.
What I'd fix with more time. The cat's silhouette is still a 2D cutout — I'd build a small 3D-ish mesh or a set of pre-rendered rotation frames so the spin shows real volume instead of a squash, and I'd give the tail and ears independent secondary motion. I'd also add proper per-object depth sorting and a real chromatic-aberration pass on the whole frame at stage 4, rather than the current glow-and-streak approximation.