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.
Here's a single HTML file that turns a realistic living-room cat into a spinning, sound-making meme. Click the cat to trigger four escalating stages of lift, spin, and chaos.
<!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 Cat</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0d0b09; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
canvas { display: block; width: 100vw; height: 100vh; touch-action: none; cursor: pointer; }
#hud {
position: fixed; top: 14px; right: 14px; z-index: 10;
display: flex; align-items: center; gap: 9px;
font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
color: rgba(255,255,255,0.5);
background: rgba(20,14,8,0.35);
border: 1px solid rgba(255,255,255,0.12);
padding: 8px 13px; border-radius: 999px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
user-select: none;
transition: opacity 0.6s;
}
#mute {
width: 30px; height: 30px; border-radius: 50%;
border: 1px solid rgba(255,255,255,0.25);
background: rgba(255,255,255,0.08);
color: #fff; font-size: 13px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: background 0.15s, transform 0.15s;
}
#mute:hover { background: rgba(255,255,255,0.2); }
#mute.on { background: rgba(255,90,90,0.28); }
#hint { opacity: 1; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hud">
<button id="mute" title="Mute">🔊</button>
<span id="hint">click the cat</span>
</div>
<script>
(function () {
'use strict';
/* ============================================================
SETUP
============================================================ */
const cvs = document.getElementById('c');
const ctx = cvs.getContext('2d');
const muteBtn = document.getElementById('mute');
const hint = document.getElementById('hint');
let W = 0, H = 0, DPR = 1;
let noiseTex = null;
let roomCanvas = null;
let catBuffer = null, catBufCtx = null;
const CATBUF = 360; // logical size of the cat offscreen buffer
const HAS_FILTER = 'filter' in ctx;
/* ============================================================
UTILITIES
============================================================ */
function makeNoise(w, h) {
const c = document.createElement('canvas');
c.width = w; c.height = h;
const x = c.getContext('2d');
const img = x.createImageData(w, h);
const d = img.data;
for (let i = 0; i < d.length; i += 4) {
const v = (Math.random() * 255) | 0;
d[i] = d[i + 1] = d[i + 2] = v;
d[i + 3] = 255;
}
x.putImageData(img, 0, 0);
return c;
}
function rr(g, x, y, w, h, r) {
r = Math.max(0, Math.min(r, Math.abs(w) / 2, Math.abs(h) / 2));
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();
}
/* ============================================================
ROOM (baked once, reused with transforms)
============================================================ */
function buildRoom() {
const b = document.createElement('canvas');
b.width = W; b.height = H;
const g = b.getContext('2d');
const floorY = H * 0.63;
/* ---- wall ---- */
let grad = g.createLinearGradient(0, 0, 0, floorY);
grad.addColorStop(0, '#f3e9d9');
grad.addColorStop(0.45, '#e9dbc1');
grad.addColorStop(1, '#dcc39a');
g.fillStyle = grad;
g.fillRect(0, 0, W, floorY);
g.save();
g.imageSmoothingEnabled = false;
g.globalAlpha = 0.055;
g.drawImage(noiseTex, 0, 0, W, floorY);
g.restore();
/* ---- window light (left) ---- */
const wx = W * 0.07, wy = H * 0.09, ww = W * 0.20, wh = H * 0.27;
const light = g.createRadialGradient(wx + ww / 2, wy + wh / 2, 20,
wx + ww / 2, wy + wh / 2, Math.max(ww, wh) * 1.15);
light.addColorStop(0, 'rgba(255,246,216,0.80)');
light.addColorStop(0.45,'rgba(255,246,216,0.24)');
light.addColorStop(1, 'rgba(255,246,216,0)');
g.fillStyle = light;
rr(g, wx - 70, wy - 70, ww + 140, wh + 140, 55);
g.fill();
/* ---- window frame ---- */
g.fillStyle = '#7d6a52';
rr(g, wx, wy, ww, wh, 7);
g.fill();
g.fillStyle = 'rgba(190,225,255,0.32)';
rr(g, wx + 9, wy + 9, ww - 18, wh - 18, 4);
g.fill();
g.strokeStyle = '#5f4d3a';
g.lineWidth = Math.max(5, W * 0.007);
g.beginPath();
g.moveTo(wx + ww / 2, wy + 9); g.lineTo(wx + ww / 2, wy + wh - 9);
g.moveTo(wx + 9, wy + wh / 2); g.lineTo(wx + ww - 9, wy + wh / 2);
g.stroke();
/* ---- sofa (right) ---- */
const sx = W * 0.60, sy = floorY - H * 0.17;
const sw = W * 0.32, sh = H * 0.17;
const sg = g.createLinearGradient(sx, sy, sx + sw, sy + sh);
sg.addColorStop(0, '#4d3b2c');
sg.addColorStop(1, '#2b1d14');
g.fillStyle = sg;
rr(g, sx, sy, sw, sh, Math.min(sw, sh) * 0.45);
g.fill();
g.fillStyle = '#3a2a20';
rr(g, sx - 20, sy + sh * 0.32, 20, sh * 0.55, 9);
g.fill();
/* ---- plant (far wall, left) ---- */
const px = W * 0.20, py = floorY - H * 0.30;
g.fillStyle = '#2f3a26';
g.beginPath();
g.ellipse(px, py + H * 0.06, W * 0.05, H * 0.09, 0, 0, Math.PI * 2);
g.fill();
g.fillStyle = '#3d5a34';
g.beginPath();
g.ellipse(px, py, W * 0.075, H * 0.16, 0, 0, Math.PI * 2);
g.fill();
/* ---- floor ---- */
const fg = g.createLinearGradient(0, floorY, 0, H);
fg.addColorStop(0, '#c9a878');
fg.addColorStop(1, '#7a5a3a');
g.fillStyle = fg;
g.fillRect(0, floorY, W, H - floorY);
/* ---- floorboards ---- */
g.save();
g.strokeStyle = 'rgba(0,0,0,0.075)';
g.lineWidth = 1;
const vpX = W * 0.42, vpY = floorY;
for (let i = -20; i <= 20; i++) {
const bx = W * 0.5 + i * W * 0.082;
g.beginPath();
g.moveTo(vpX, vpY);
g.lineTo(bx, H);
g.stroke();
}
g.restore();
/* ---- vignette ---- */
const vg = g.createRadialGradient(W / 2, H * 0.5, Math.min(W, H) * 0.28,
W / 2, H * 0.5, Math.max(W, H) * 0.78);
vg.addColorStop(0, 'rgba(0,0,0,0)');
vg.addColorStop(1, 'rgba(18,8,0,0.5)');
g.fillStyle = vg;
g.fillRect(0, 0, W, H);
roomCanvas = b;
}
/* ============================================================
CAT (drawn centred at origin, scale 1)
============================================================ */
function drawCatBody(g, s) {
const FUR = '#f2e5d3';
const FURD = '#dfc7a4';
const FURL = '#fcf5ec';
const PINK = '#e79ba3';
const PINKD = '#c47f87';
const DARK = '#2b2119';
const DARK2 = '#3e342b';
function grain(pathFn, fill) {
g.save();
pathFn();
g.clip();
g.fillStyle = fill;
g.fill();
g.save();
g.imageSmoothingEnabled = false;
g.globalAlpha = 0.055;
g.globalCompositeOperation = 'multiply';
g.drawImage(noiseTex, -60 * s, -60 * s, 300 * s, 300 * s);
g.restore();
g.save();
g.globalAlpha = 0.035;
g.globalCompositeOperation = 'screen';
g.drawImage(noiseTex, -60 * s + 47, -60 * s + 29, 300 * s, 300 * s);
g.restore();
g.restore();
}
/* ---- tail ---- */
g.save();
g.beginPath();
g.moveTo(46 * s, 22 * s);
g.bezierCurveTo(104 * s, 12 * s, 134 * s, -38 * s, 112 * s, -92 * s);
g.bezierCurveTo(100 * s, -132 * s, 70 * s, -142 * s, 56 * s, -120 * s);
g.bezierCurveTo(45 * s, -102 * s, 48 * s, -76 * s, 54 * s, -56 * s);
g.closePath();
g.fillStyle = FURD;
g.fill();
g.save();
g.clip();
g.strokeStyle = 'rgba(110,72,36,0.22)';
g.lineWidth = 3 * s;
g.beginPath();
g.moveTo(76 * s, -18 * s);
g.quadraticCurveTo(104 * s, -46 * s, 94 * s, -76 * s);
g.moveTo(70 * s, -52 * s);
g.quadraticCurveTo(86 * s, -78 * s, 76 * s, -104 * s);
g.stroke();
g.restore();
g.restore();
/* ---- body ---- */
function bodyPath() {
g.beginPath();
g.moveTo(-66 * s, -28 * s);
g.bezierCurveTo(-100 * s, 14 * s, -94 * s, 78 * s, -58 * s, 100 * s);
g.bezierCurveTo(-32 * s, 118 * s, 32 * s, 118 * s, 58 * s, 100 * s);
g.bezierCurveTo(94 * s, 78 * s, 100 * s, 14 * s, 66 * s, -28 * s);
g.closePath();
}
grain(bodyPath, FUR);
/* ---- chest ---- */
function chestPath() {
g.beginPath();
g.ellipse(0, 12 * s, 62 * s, 74 * s, 0, 0, Math.PI * 2);
}
grain(chestPath, FURL);
/* ---- front paws ---- */
for (const sx of [-1, 1]) {
(function (sx) {
function pawPath() {
g.beginPath();
g.ellipse(sx * 44 * s, 96 * s, 27 * s, 15 * s, 0, 0, Math.PI * 2);
}
grain(pawPath, FURL);
})(sx);
}
/* ---- head ---- */
function headPath() {
g.beginPath();
g.ellipse(0, -96 * s, 58 * s, 56 * s, 0, 0, Math.PI * 2);
}
grain(headPath, FURL);
/* ---- ears ---- */
for (const sx of [-1, 1]) {
(function (sx) {
function earPath() {
g.beginPath();
g.moveTo(sx * 30 * s, -138 * s);
g.lineTo(sx * 52 * s, -184 * s);
g.lineTo(sx * 70 * s, -124 * s);
g.closePath();
}
grain(earPath, FUR);
function innerPath() {
g.beginPath();
g.moveTo(sx * 36 * s, -146 * s);
g.lineTo(sx * 46 * s, -172 * s);
g.lineTo(sx * 60 * s, -132 * s);
g.closePath();
}
grain(innerPath, PINK);
})(sx);
}
/* ---- muzzle ---- */
function muzzlePath() {
g.beginPath();
g.ellipse(0, -74 * s, 30 * s, 22 * s, 0, 0, Math.PI * 2);
}
grain(muzzlePath, FURL);
/* ---- nose ---- */
g.save();
g.beginPath();
g.moveTo(-8 * s, -80 * s);
g.lineTo(8 * s, -80 * s);
g.lineTo(0, -68 * s);
g.closePath();
g.fillStyle = PINK;
g.fill();
g.restore();
/* ---- mouth ---- */
g.save();
g.strokeStyle = DARK2;
g.lineWidth = 2.2 * s;
g.lineCap = 'round';
g.beginPath();
g.moveTo(0, -68 * s);
g.quadraticCurveTo(-9 * s, -62 * s, -13 * s, -56 * s);
g.moveTo(0, -68 * s);
g.quadraticCurveTo(9 * s, -62 * s, 13 * s, -56 * s);
g.stroke();
g.restore();
/* ---- eyes ---- */
for (const sx of [-1, 1]) {
(function (sx) {
g.save();
g.beginPath();
g.ellipse(sx * 30 * s, -100 * s, 17 * s, 21 * s, 0, 0, Math.PI * 2);
g.fillStyle = '#ffffff';
g.fill();
g.restore();
g.save();
g.beginPath();
g.ellipse(sx * 30 * s, -100 * s, 6.5 * s, 15 * s, 0, 0, Math.PI * 2);
g.fillStyle = DARK;
g.fill();
g.restore();
g.save();
g.beginPath();
g.arc(sx * 26 * s, -107 * s, 4 * s, 0, Math.PI * 2);
g.fillStyle = 'rgba(255,255,255,0.92)';
g.fill();
g.restore();
})(sx);
}
/* ---- whiskers ---- */
g.save();
g.strokeStyle = 'rgba(190,165,140,0.55)';
g.lineWidth = 1.4 * s;
g.lineCap = 'round';
for (const sx of [-1, 1]) {
g.beginPath();
g.moveTo(sx * 52 * s, -84 * s);
g.lineTo(sx * 84 * s, -92 * s);
g.moveTo(sx * 52 * s, -96 * s);
g.lineTo(sx * 86 * s, -96 * s);
g.moveTo(sx * 52 * s, -108 * s);
g.lineTo(sx * 82 * s, -104 * s);
g.stroke();
}
g.restore();
}
function renderCatBuffer(headTilt) {
const g = catBufCtx;
g.setTransform(1, 0, 0, 1, 0, 0);
g.clearRect(0, 0, CATBUF * DPR, CATBUF * DPR);
g.save();
g.setTransform(DPR, 0, 0, DPR, 0, 0);
g.translate(CATBUF / 2, CATBUF / 2);
g.rotate(headTilt);
drawCatBody(g, 1);
g.restore();
}
/* ============================================================
AUDIO — "oiia" voice synthesizer
============================================================ */
let audioCtx = null, masterGain = null, comp = null, noiseBuf = null;
let muted = false;
let nextSchedTime = 0, schedCount = 0, schedTimer = null;
const SEQ = ['oi', 'oi', 'a', 'i', 'oi', 'oi', 'a', 'i', 'o'];
const VOW = {
oi: { f0: 1.00, f1: 1.78, fm: 820, f1m: 1950, d: 0.20 },
a: { f0: 0.85, f1: 1.05, fm: 1160, f1m: 960, d: 0.16 },
i: { f0: 1.55, f1: 2.12, fm: 2060, f1m: 2480, d: 0.13 },
o: { f0: 0.90, f1: 1.16, fm: 660, f1m: 870, d: 0.26 }
};
function initAudio() {
if (audioCtx) {
if (audioCtx.state === 'suspended') audioCtx.resume();
return;
}
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return;
audioCtx = new AC();
masterGain = audioCtx.createGain();
masterGain.gain.value = muted ? 0 : 0.9;
comp = audioCtx.createDynamicsCompressor();
comp.threshold.value = -14;
comp.knee.value = 28;
comp.ratio.value = 5;
comp.attack.value = 0.004;
comp.release.value = 0.25;
masterGain.connect(comp);
comp.connect(audioCtx.destination);
noiseBuf = audioCtx.createBuffer(1, Math.floor(audioCtx.sampleRate * 0.4), audioCtx.sampleRate);
const nd = noiseBuf.getChannelData(0);
for (let i = 0; i < nd.length; i++) nd[i] = Math.random() * 2 - 1;
nextSchedTime = audioCtx.currentTime + 0.12;
schedTimer = setInterval(scheduler, 60);
}
function playSyllable(t0, f0, f1, fm, f1m, dur, peak) {
const ctx = audioCtx;
const t1 = t0 + dur;
const stopAt = t1 + 0.035;
/* carrier */
const osc = ctx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(f0, t0);
osc.frequency.exponentialRampToValueAtTime(f1, t1);
const osc2 = ctx.createOscillator();
osc2.type = 'triangle';
osc2.frequency.setValueAtTime(f0 * 2, t0);
osc2.frequency.exponentialRampToValueAtTime(f1 * 2, t1);
const g2 = ctx.createGain();
g2.gain.value = 0.22;
/* vibrato for organic pitch wobble */
const lfo = ctx.createOscillator();
lfo.type = 'sine';
lfo.frequency.value = 20 + Math.random() * 20;
const lfoG = ctx.createGain();
lfoG.gain.value = f0 * 0.014;
lfo.connect(lfoG);
lfoG.connect(osc.frequency);
/* breath noise */
const nz = ctx.createBufferSource();
nz.buffer = noiseBuf;
nz.loop = true;
const gn = ctx.createGain();
gn.gain.value = 0.10;
/* formant shaping — this is what makes it a vowel, not a tone */
const bp = ctx.createBiquadFilter();
bp.type = 'bandpass';
bp.Q.value = 6;
bp.frequency.setValueAtTime(fm, t0);
bp.frequency.exponentialRampToValueAtTime(f1m, t1);
const lp = ctx.createBiquadFilter();
lp.type = 'lowpass';
lp.frequency.setValueAtTime(5200, t0);
lp.frequency.exponentialRampToValueAtTime(3100, t1);
/* amplitude envelope */
const env = ctx.createGain();
env.gain.setValueAtTime(0.0001, t0);
env.gain.exponentialRampToValueAtTime(1, t0 + 0.010);
env.gain.setValueAtTime(1, t0 + 0.010);
env.gain.exponentialRampToValueAtTime(0.0001, t1);
const out = ctx.createGain();
out.gain.value = peak;
osc.connect(bp);
osc2.connect(g2).connect(bp);
nz.connect(gn).connect(bp);
bp.connect(lp).connect(env).connect(out).connect(masterGain);
osc.start(t0); osc.stop(stopAt);
osc2.start(t0); osc2.stop(stopAt);
nz.start(t0); nz.stop(stopAt);
lfo.start(t0); lfo.stop(stopAt);
}
function scheduler() {
if (!audioCtx) return;
const now = audioCtx.currentTime;
if (nextSchedTime < now) nextSchedTime = now + 0.02;
const stage = state.stage;
const rev = Math.max(0, state.speed) / (2 * Math.PI);
const rate = Math.min(14, 1.6 + rev * 2.2);
const interval = 1 / rate;
const pitchMul = Math.min(4.0, 1 + rev * 1.4);
const speedMul = Math.min(1.8, 1 + rev * 0.5);
while (nextSchedTime < now + 0.2) {
const key = SEQ[schedCount % SEQ.length];
const v = VOW[key];
const dur = v.d * speedMul;
const f0 = 190 * pitchMul * v.f0;
const f1 = 190 * pitchMul * v.f1;
const peak = 0.30 * (0.55 + 0.45 * Math.min(1, rev / 2.5));
playSyllable(nextSchedTime, f0, f1, v.fm, v.f1m, dur, peak);
nextSchedTime += interval * (0.75 + Math.random() * 0.5);
schedCount++;
}
}
/* ============================================================
STATE
============================================================ */
const STAGES = [
{ speed: 1.6, lift: 0.30, chaos: 0.00, copies: 0 },
{ speed: 3.4, lift: 0.55, chaos: 0.22, copies: 1 },
{ speed: 8.0, lift: 0.82, chaos: 0.55, copies: 3 },
{ speed: 16, lift: 1.00, chaos: 1.00, copies: 6 }
];
const state = {
stage: 0,
angle: 0,
speed: 0,
targetSpeed: 0,
lift: 0,
targetLift: 0,
chaos: 0,
t: 0,
headTilt: 0,
headTiltTarget: 0
};
const trail = [];
const particles = [];
/* ============================================================
PARTICLES
============================================================ */
function spawnParticles(n, cx, cy) {
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2;
const sp = 40 + Math.random() * 320;
particles.push({
x: cx + (Math.random() - 0.5) * 50,
y: cy + (Math.random() - 0.5) * 50,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp,
life: 1,
decay: 0.5 + Math.random() * 1.5,
size: 1 + Math.random() * 3,
hue: 25 + Math.random() * 45
});
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 70 * dt;
p.vx *= (1 - 1.3 * dt);
p.life -= p.decay * dt;
if (p.life <= 0) particles.splice(i, 1);
}
}
function drawParticles() {
if (!particles.length) return;
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
const a = Math.max(0, p.life);
ctx.globalAlpha = a * 0.7;
ctx.fillStyle = 'hsl(' + p.hue + ',90%,66%)';
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * (0.4 + p.life * 0.6), 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
/* ============================================================
UPDATE
============================================================ */
function update(dt) {
state.t += dt;
if (state.stage > 0) {
const st = STAGES[state.stage - 1];
/* accelerate — the spin winds up */
state.speed += dt * (1.2 + state.stage * 1.6);
state.speed = Math.min(state.speed, st.speed * 1.9);
state.angle += state.speed * dt;
/* rise */
state.lift += (state.targetLift - state.lift) * Math.min(1, dt * 3.5);
/* head settles to face the lens */
state.headTilt += (state.headTiltTarget - state.headTilt) * Math.min(1, dt * 8);
} else {
state.speed *= Math.pow(0.02, dt);
state.angle += state.speed * dt;
state.lift = 0;
}
updateParticles(dt);
}
/* ============================================================
DRAW HELPERS
============================================================ */
function drawCatBuf(cx, cy, s, angle, alpha, blur) {
const c = Math.cos(angle);
ctx.save();
ctx.globalAlpha = alpha;
if (blur > 0.2 && HAS_FILTER) ctx.filter = 'blur(' + blur + 'px)';
ctx.translate(cx, cy);
ctx.scale(c * s, s);
ctx.drawImage(catBuffer, -CATBUF / 2, -CATBUF / 2);
ctx.restore();
}
function catScreenPos() {
const floorY = H * 0.63;
const s = Math.min(W / 720, H / 620);
const liftPx = state.lift * (H * 0.32 + 40);
return {
cx: W / 2,
cy: floorY - 30 - liftPx,
s: s,
floorY: floorY
};
}
/* ============================================================
SCENE DRAW
============================================================ */
function drawRoom() {
const spinAmt = state.stage * state.chaos * 0.9;
const rot = spinAmt * Math.sin(state.t * (0.7 + state.stage * 0.35));
ctx.save();
ctx.translate(W / 2, H * 0.63);
ctx.rotate(rot * 0.12);
ctx.translate(-W / 2, -H * 0.63);
ctx.drawImage(roomCanvas, 0, 0);
ctx.restore();
/* motion smear */
if (state.stage >= 2) {
const n = 8 + state.stage * 5;
ctx.save();
ctx.globalAlpha = 0.016;
for (let i = 0; i < n; i++) {
const f = i / n;
const off = (f - 0.5) * W * (0.25 + state.stage * 0.12);
ctx.drawImage(roomCanvas, off, 0);
}
ctx.restore();
}
/* stage 4 — the room tears into horizontal streaks */
if (state.stage >= 4) {
ctx.save();
ctx.globalAlpha = 0.10;
const strips = 16;
const h = H / strips;
for (let i = 0; i < strips; i++) {
const y = i * h;
const f = i / strips;
const off = Math.sin(state.t * 2.5 + f * 10) * W * 0.10;
ctx.save();
ctx.beginPath();
ctx.rect(0, y, W, h);
ctx.clip();
ctx.drawImage(roomCanvas, off, 0);
ctx.restore();
}
ctx.restore();
}
}
function drawCatShadow(cx, floorY, s) {
const shadowScale = 1 + state.lift * 0.35;
ctx.save();
ctx.globalCompositeOperation = 'multiply';
const sg = ctx.createRadialGradient(cx, floorY + 8, 4, cx, floorY + 8, 95 + s * 62 * shadowScale);
sg.addColorStop(0, 'rgba(58,32,12,0.38)');
sg.addColorStop(1, 'rgba(58,32,12,0)');
ctx.fillStyle = sg;
ctx.beginPath();
ctx.ellipse(cx, floorY + 8, 95 + s * 62 * shadowScale, 24 + s * 13 * shadowScale, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawCatScene() {
const pos = catScreenPos();
const cx = pos.cx, cy = pos.cy, s = pos.s, floorY = pos.floorY;
renderCatBuffer(state.headTilt);
/* contact shadow */
drawCatShadow(cx, floorY, s);
/* spin glow */
const glowR = 130 + state.speed * 7 + state.stage * 45;
const gg = ctx.createRadialGradient(cx, cy, 10, cx, cy, glowR);
gg.addColorStop(0, 'rgba(255,222,150,' + (0.09 + state.speed * 0.004) + ')');
gg.addColorStop(1, 'rgba(255,222,150,0)');
ctx.fillStyle = gg;
ctx.beginPath();
ctx.arc(cx, cy, glowR, 0, Math.PI * 2);
ctx.fill();
/* trail */
trail.push({ a: state.angle, y: cy });
if (trail.length > 26) trail.shift();
const blurAmt = Math.min(16, state.speed * 1.1);
for (let i = 0; i < trail.length; i++) {
const p = trail[i];
const f = i / trail.length;
const a = Math.pow(f, 1.6) * 0.30 * Math.min(1, state.speed / 2.5);
if (a < 0.004) continue;
drawCatBuf(cx, p.y, s, p.a, a, Math.min(6, f * 8));
}
/* main cat */
drawCatBuf(cx, cy, s, state.angle, 1, blurAmt);
/* orbiting copies */
const st = state.stage > 0 ? STAGES[state.stage - 1] : null;
const copies = st ? st.copies : 0;
if (copies > 0) {
for (let i = 0; i < copies; i++) {
const off = (i / copies) * Math.PI * 2 + state.t * 1.5;
const r = 95 + i * 58;
const ox = cx + Math.cos(off) * r;
const oy = cy + Math.sin(off) * r * 0.5;
const a = 0.09 + 0.07 * Math.sin(state.t * 2 + i);
drawCatBuf(ox, oy, s * 0.72, state.angle + off * 0.5, Math.max(0.02, a), 3);
}
}
}
/* ============================================================
RENDER
============================================================ */
function render() {
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
ctx.fillStyle = '#0d0b09';
ctx.fillRect(0, 0, W, H);
const shake = state.stage * state.chaos * 1.7;
const sx = (Math.random() - 0.5) * shake;
const sy = (Math.random() - 0.5) * shake;
ctx.save();
ctx.translate(sx, sy);
drawRoom();
drawCatScene();
drawParticles();
ctx.restore();
/* stage tint */
const tints = [
'rgba(255,236,200,0)',
'rgba(255,214,150,0.05)',
'rgba(255,170,90,0.09)',
'rgba(255,110,60,0.14)',
'rgba(120,20,90,0.16)'
];
if (state.stage > 0) {
const t = tints[state.stage];
if (t) {
ctx.save();
ctx.globalAlpha = 0.55;
ctx.fillStyle = t;
ctx.fillRect(0, 0, W, H);
ctx.restore();
}
}
}
/* ============================================================
RESIZE
============================================================ */
function resize() {
DPR = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth;
H = window.innerHeight;
cvs.width = Math.floor(W * DPR);
cvs.height = Math.floor(H * DPR);
cvs.style.width = W + 'px';
cvs.style.height = H + 'px';
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
noiseTex = makeNoise(256, 256);
if (!catBuffer) {
catBuffer = document.createElement('canvas');
catBufCtx = catBuffer.getContext('2d');
}
catBuffer.width = CATBUF * DPR;
catBuffer.height = CATBUF * DPR;
buildRoom();
renderCatBuffer(state.headTilt);
}
/* ============================================================
INTERACTION
============================================================ */
function activate() {
if (state.stage === 0) {
state.stage = 1;
state.speed = 0.4;
state.targetLift = STAGES[0].lift;
state.chaos = 0;
state.headTilt = 0.16;
state.headTiltTarget = 0;
hint.style.opacity = 0;
} else {
state.stage = Math.min(4, state.stage + 1);
const st = STAGES[state.stage - 1];
state.speed = st.speed * 0.45;
state.targetLift = st.lift;
state.chaos = st.chaos;
state.headTilt = 0.22;
state.headTiltTarget = 0;
}
const pos = catScreenPos();
spawnParticles(10 + state.stage * 9, pos.cx, pos.cy);
}
cvs.addEventListener('pointerdown', function (e) {
e.preventDefault();
initAudio();
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
activate();
});
muteBtn.addEventListener('click', function () {
muted = !muted;
muteBtn.classList.toggle('on', muted);
muteBtn.textContent = muted ? '🔇' : '🔊';
if (masterGain && audioCtx) {
masterGain.gain.setTargetAtTime(muted ? 0 : 0.9, audioCtx.currentTime, 0.02);
}
});
/* ============================================================
MAIN LOOP
============================================================ */
let last = performance.now();
function frame(now) {
const dt = Math.min((now - last) / 1000, 0.05);
last = now;
update(dt);
render();
requestAnimationFrame(frame);
}
/* ============================================================
GO
============================================================ */
window.addEventListener('resize', resize);
resize();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Clicking the cat kicks off a four-stage escalation, each with its own speed, lift, and visual chaos.
Optimization Tip: You can adjust the spin speed, lift height, and chaos for each stage by editing the STAGES array near the top of the script. The VOW object controls the vowel formants and durations for the "oiia" voice.