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 Cat</title>
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#0c0705}
canvas{position:fixed;left:0;top:0;width:100%;height:100%;display:block;cursor:pointer;touch-action:manipulation}
#mute{position:fixed;top:12px;right:12px;z-index:2;font:600 13px system-ui,sans-serif;color:#f3e6cf;background:rgba(20,12,6,.55);border:1px solid rgba(243,230,207,.35);border-radius:999px;padding:6px 12px;cursor:pointer}
#hint{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);font:500 14px system-ui,sans-serif;color:rgba(243,230,207,.6);letter-spacing:.05em;pointer-events:none;transition:opacity 1.2s}
</style>
</head>
<body>
<canvas id="c"></canvas>
<button id="mute">mute</button>
<div id="hint">click the cat</div>
<script>
(() => {
const cv = document.getElementById('c');
const g = cv.getContext('2d');
const muteBtn = document.getElementById('mute');
const hint = document.getElementById('hint');
const TAU = Math.PI * 2;
const HT = [0, .25, .42, .55, .6]; // lift per stage, fraction of cat height
const NAMES = ['', 'warming up', 'lift-off', 'the room pulls', 'everything is spinning'];
const WORDS = ['oiia', 'oiia', 'OIIA', 'oiiaiooooiai', 'iooo'];
const VOW = [ {f:[520,820,2450]}, {f:[310,2250,3050]}, {f:[310,2250,3050]}, {f:[760,1250,2600]} ]; // o i i a
const VGAIN = [3.2, 1.6, 0.8], VQ = [7, 10, 12];
const S = {
clicks:0, energy:0, tActive:0, angle:0, omega:0, lift:0,
snap:0, snapTo:0, lastHalf:0, roll:0, shake:0, flash:0, pull:0,
spinRoom:0, grav:1, kal:0, breath:0, t:0, trail:[], trailT:0, words:[], wordT:0
};
let W = 0, H = 0, DPR = 1, bg = null, catFront = null, catBack = null;
let catW = 0, catH = 0, floorY = 0, hz = 0, cx = 0;
const tmp = document.createElement('canvas'); tmp.width = 720; tmp.height = 880;
const tg = tmp.getContext('2d');
function rng(s) {
return () => {
s |= 0; s = s + 0x6D2B79F5 | 0;
let t = Math.imul(s ^ s >>> 15, 1 | s);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
/* ---------- cat sprite (prerendered, 720x880 = 2x of 360x440 logical) ---------- */
function fuzz(c, x0, y0, rx, ry, n, len, R) {
c.lineCap = 'round';
for (let i = 0; i < n; i++) {
const t = R() * TAU, ca = Math.cos(t), sa = Math.sin(t);
const nx = ca / rx, ny = sa / ry, nl = Math.hypot(nx, ny), ux = nx / nl, uy = ny / nl;
const px = x0 + ca * rx, py = y0 + sa * ry, L = len * (0.4 + R() * 0.8);
c.strokeStyle = R() < .5 ? '#fffaf0' : '#b99764';
c.globalAlpha = 0.5 + R() * 0.45;
c.lineWidth = 1 + R() * 0.6;
c.beginPath();
c.moveTo(px - ux, py - uy);
c.quadraticCurveTo(px + ux * L * .5 + (R() - .5) * 3, py + uy * L * .5 + (R() - .5) * 3,
px + ux * L + (R() - .5) * 2, py + uy * L);
c.stroke();
}
c.globalAlpha = 1;
}
function faceDetails(c) {
c.lineCap = 'round';
c.strokeStyle = 'rgba(150,110,70,.55)'; c.lineWidth = 3.2;
for (const dx of [-12, 0, 12]) { c.beginPath(); c.moveTo(180 + dx * .6, 48 + Math.abs(dx) * .2); c.lineTo(180 + dx, 68); c.stroke(); }
c.fillStyle = 'rgba(230,150,140,.22)';
c.beginPath(); c.ellipse(120, 162, 17, 11, 0, 0, TAU); c.fill();
c.beginPath(); c.ellipse(240, 162, 17, 11, 0, 0, TAU); c.fill();
for (const ex of [142, 218]) { // wide, open, staring eyes
c.fillStyle = '#fffdf8'; c.beginPath(); c.ellipse(ex, 128, 26, 29, 0, 0, TAU); c.fill();
c.strokeStyle = 'rgba(70,50,30,.7)'; c.lineWidth = 1.6; c.stroke();
c.fillStyle = '#7a5530'; c.beginPath(); c.arc(ex, 130, 16, 0, TAU); c.fill();
c.fillStyle = '#0b0705'; c.beginPath(); c.arc(ex, 130, 9, 0, TAU); c.fill();
c.fillStyle = '#fff';
c.beginPath(); c.arc(ex - 5, 122, 3.6, 0, TAU); c.fill();
c.beginPath(); c.arc(ex + 6, 137, 1.6, 0, TAU); c.fill();
}
c.fillStyle = '#d98b86';
c.beginPath(); c.moveTo(171, 155); c.lineTo(189, 155); c.lineTo(180, 166); c.closePath(); c.fill();
c.strokeStyle = '#6b4a3a'; c.lineWidth = 1.6;
c.beginPath(); c.moveTo(180, 166); c.quadraticCurveTo(174, 176, 166, 172);
c.moveTo(180, 166); c.quadraticCurveTo(186, 176, 194, 172); c.stroke();
c.strokeStyle = 'rgba(90,70,50,.55)'; c.lineWidth = .9;
for (const s of [-1, 1]) for (const dy of [-6, 2, 10]) {
c.beginPath();
c.moveTo(180 + s * 46, 160 + dy * .3);
c.quadraticCurveTo(180 + s * 78, 158 + dy * 1.2, 180 + s * 108, 150 + dy * 1.6);
c.stroke();
}
}
function backDetails(c) {
c.strokeStyle = 'rgba(150,110,70,.35)'; c.lineWidth = 4; c.lineCap = 'round';
for (let k = 0; k < 3; k++) { c.beginPath(); c.arc(180, 130, 36 + k * 14, -.4, .9 + k * .3); c.stroke(); }
}
function makeCat(faceOn) {
const cvs = document.createElement('canvas'); cvs.width = 720; cvs.height = 880;
const c = cvs.getContext('2d'); c.setTransform(2, 0, 0, 2, 0, 0);
const R = rng(faceOn ? 11 : 23);
c.lineCap = 'round'; c.lineJoin = 'round';
// tail
c.strokeStyle = '#d6bf93'; c.lineWidth = 30;
c.beginPath(); c.moveTo(246, 404); c.bezierCurveTo(346, 432, 354, 328, 300, 298); c.stroke();
// body
let gr = c.createRadialGradient(140, 290, 30, 180, 330, 200);
gr.addColorStop(0, '#fff6e4'); gr.addColorStop(.55, '#e4cfa8'); gr.addColorStop(1, '#9b7d56');
c.fillStyle = gr; c.beginPath(); c.ellipse(180, 320, 98, 118, 0, 0, TAU); c.fill();
c.save();
c.beginPath(); c.ellipse(180, 320, 98, 118, 0, 0, TAU); c.clip();
for (let i = 0; i < 520; i++) {
const x = R() * 360, y = 200 + R() * 240;
c.strokeStyle = R() < .5 ? '#fffaf0' : '#b89866';
c.globalAlpha = .10 + R() * .12; c.lineWidth = 1;
c.beginPath(); c.moveTo(x, y); c.lineTo(x + (R() - .5) * 5, y + 3 + R() * 5); c.stroke();
}
c.globalAlpha = 1;
gr = c.createRadialGradient(180, 300, 4, 180, 300, 84);
gr.addColorStop(0, '#fffdf6'); gr.addColorStop(1, 'rgba(255,253,246,0)');
c.fillStyle = gr; c.beginPath(); c.ellipse(180, 300, 64, 100, 0, 0, TAU); c.fill();
c.restore();
fuzz(c, 180, 320, 98, 118, 240, 9, R);
// paws
c.fillStyle = '#f8efdc'; c.strokeStyle = 'rgba(120,90,55,.4)'; c.lineWidth = 1;
for (const x of [146, 214]) { c.beginPath(); c.ellipse(x, 414, 28, 15, 0, 0, TAU); c.fill(); c.stroke(); }
// ears
c.fillStyle = '#e2cca0';
c.beginPath(); c.moveTo(102, 88); c.lineTo(96, 14); c.lineTo(166, 64); c.closePath(); c.fill();
c.beginPath(); c.moveTo(258, 88); c.lineTo(264, 14); c.lineTo(194, 64); c.closePath(); c.fill();
c.fillStyle = '#efb8ae';
c.beginPath(); c.moveTo(112, 80); c.lineTo(106, 32); c.lineTo(146, 66); c.closePath(); c.fill();
c.beginPath(); c.moveTo(248, 80); c.lineTo(254, 32); c.lineTo(214, 66); c.closePath(); c.fill();
// head
gr = c.createRadialGradient(150, 96, 10, 180, 130, 100);
gr.addColorStop(0, '#fffbf1'); gr.addColorStop(.6, '#eedcbb'); gr.addColorStop(1, '#a88a60');
c.fillStyle = gr; c.beginPath(); c.ellipse(180, 130, 92, 80, 0, 0, TAU); c.fill();
fuzz(c, 180, 130, 92, 80, 190, 7, R);
if (faceOn) faceDetails(c); else backDetails(c);
// key light from the window (left): lit left side, falloff to the right
c.globalCompositeOperation = 'source-atop';
gr = c.createLinearGradient(60, 0, 320, 0);
gr.addColorStop(0, 'rgba(255,248,230,.2)'); gr.addColorStop(.55, 'rgba(0,0,0,0)'); gr.addColorStop(1, 'rgba(40,22,8,.3)');
c.fillStyle = gr; c.fillRect(0, 0, 360, 440);
c.globalCompositeOperation = 'source-over';
return cvs;
}
/* ---------- room (prerendered) ---------- */
function buildBG() {
bg = document.createElement('canvas');
bg.width = Math.round(W * DPR); bg.height = Math.round(H * DPR);
const b = bg.getContext('2d'); b.setTransform(DPR, 0, 0, DPR, 0, 0);
const R = rng(42);
// wall
let gr = b.createLinearGradient(0, 0, 0, hz);
gr.addColorStop(0, '#2f2620'); gr.addColorStop(.75, '#7a6350'); gr.addColorStop(1, '#6a5542');
b.fillStyle = gr; b.fillRect(0, 0, W, hz + 2);
for (let i = 0; i < 1600; i++) {
b.fillStyle = R() < .5 ? 'rgba(255,230,190,.03)' : 'rgba(0,0,0,.04)';
b.fillRect(R() * W, R() * hz, 2 + R() * 3, 2 + R() * 3);
}
// window with daylight
const wx = W * .07, wy = H * .09, ww = W * .27, wh = hz - H * .16 - wy;
gr = b.createLinearGradient(0, wy, 0, wy + wh);
gr.addColorStop(0, '#fff3d8'); gr.addColorStop(1, '#cfe2e6');
b.fillStyle = gr; b.fillRect(wx, wy, ww, wh);
b.fillStyle = '#3a2c22';
b.fillRect(wx - 12, wy - 12, ww + 24, 12); b.fillRect(wx - 12, wy + wh, ww + 24, 12);
b.fillRect(wx - 12, wy, 12, wh); b.fillRect(wx + ww, wy, 12, wh);
b.fillRect(wx + ww / 2 - 3, wy, 6, wh); b.fillRect(wx, wy + wh / 2 - 3, ww, 6);
// window glow spilling onto the wall
gr = b.createRadialGradient(wx + ww / 2, wy + wh / 2, 10, wx + ww / 2, wy + wh / 2, ww * 1.2);
gr.addColorStop(0, 'rgba(255,230,180,.22)'); gr.addColorStop(1, 'rgba(255,230,180,0)');
b.globalCompositeOperation = 'lighter'; b.fillStyle = gr; b.fillRect(0, 0, W, hz);
b.globalCompositeOperation = 'source-over';
// framed picture
b.fillStyle = '#21180f'; b.fillRect(W * .4, H * .12, W * .11, H * .17);
gr = b.createLinearGradient(0, H * .12, 0, H * .29);
gr.addColorStop(0, '#9db3a3'); gr.addColorStop(1, '#5f7466');
b.fillStyle = gr; b.fillRect(W * .4 + 7, H * .12 + 7, W * .11 - 14, H * .17 - 14);
// sofa
const sx = W * .6, sw = W * .34, top = hz - H * .13;
b.fillStyle = '#3c2f40'; b.fillRect(sx, top, sw, H * .09);
b.fillRect(sx - 10, top + H * .02, 16, H * .12);
b.fillRect(sx + sw - 6, top + H * .02, 16, H * .12);
b.fillStyle = '#5e4c63'; b.fillRect(sx + 8, top + H * .09, sw - 16, H * .05);
b.fillStyle = '#4e3f54'; b.fillRect(sx + 10, top + H * .035, sw * .3, H * .05);
b.fillRect(sx + sw * .55, top + H * .035, sw * .3, H * .05);
// floor lamp (warm)
const lx = W * .9, ly = hz - H * .27;
b.strokeStyle = '#1d150f'; b.lineWidth = 5;
b.beginPath(); b.moveTo(lx, hz + H * .03); b.lineTo(lx, ly + H * .05); b.stroke();
b.fillStyle = '#1d150f'; b.beginPath(); b.ellipse(lx, hz + H * .03, W * .035, H * .012, 0, 0, TAU); b.fill();
b.fillStyle = '#f0d8a6';
b.beginPath(); b.moveTo(lx - W * .035, ly + H * .05); b.lineTo(lx + W * .035, ly + H * .05);
b.lineTo(lx + W * .05, ly - H * .03); b.lineTo(lx - W * .05, ly - H * .03); b.closePath(); b.fill();
gr = b.createRadialGradient(lx, ly, 10, lx, ly, W * .35);
gr.addColorStop(0, 'rgba(255,190,110,.35)'); gr.addColorStop(1, 'rgba(255,190,110,0)');
b.globalCompositeOperation = 'lighter'; b.fillStyle = gr; b.fillRect(0, 0, W, hz);
b.globalCompositeOperation = 'source-over';
// baseboard
b.fillStyle = '#2e231b'; b.fillRect(0, hz - 8, W, 10);
// floor
gr = b.createLinearGradient(0, hz, 0, H);
gr.addColorStop(0, '#6a4632'); gr.addColorStop(1, '#2a190f');
b.fillStyle = gr; b.fillRect(0, hz + 2, W, H - hz);
b.save(); b.beginPath(); b.rect(0, hz + 2, W, H - hz); b.clip();
const vx = W * .5, vy = hz - H * .7;
b.strokeStyle = 'rgba(15,8,4,.55)'; b.lineWidth = 1.3;
for (let i = -16; i <= 24; i++) {
const bx = W * .5 + i * W * .075;
b.beginPath(); b.moveTo(vx, vy); b.lineTo(bx, H); b.stroke();
}
for (let k = 1; k <= 9; k++) {
const y = hz + (H - hz) * Math.pow(k / 9, 1.7);
b.beginPath(); b.moveTo(0, y); b.lineTo(W, y); b.stroke();
}
b.restore();
// rug (flattened ellipse on the floor plane)
b.save(); b.translate(W * .5, floorY + H * .02); b.scale(1, .3);
gr = b.createRadialGradient(0, 0, 10, 0, 0, W * .3);
gr.addColorStop(0, '#8c3b30'); gr.addColorStop(.8, '#5e2320'); gr.addColorStop(1, '#3a1714');
b.fillStyle = gr; b.beginPath(); b.arc(0, 0, W * .3, 0, TAU); b.fill();
b.strokeStyle = 'rgba(230,190,140,.35)'; b.lineWidth = 4;
b.beginPath(); b.arc(0, 0, W * .27, 0, TAU); b.stroke();
b.restore();
// sunlight patch from the window across the floor
b.save(); b.globalCompositeOperation = 'lighter';
b.filter = 'blur(14px)';
b.fillStyle = 'rgba(255,205,140,.16)';
b.beginPath(); b.moveTo(wx, hz + 2); b.lineTo(wx + ww + 10, hz + 2);
b.lineTo(W * .78, H); b.lineTo(W * .02, H); b.closePath(); b.fill();
b.restore();
// vignette
gr = b.createRadialGradient(W / 2, H * .5, Math.min(W, H) * .3, W / 2, H * .5, Math.max(W, H) * .72);
gr.addColorStop(0, 'rgba(0,0,0,0)'); gr.addColorStop(1, 'rgba(0,0,0,.7)');
b.fillStyle = gr; b.fillRect(0, 0, W, H);
}
/* ---------- dust ---------- */
const dust = [];
{ const R0 = rng(5); for (let i = 0; i < 70; i++) dust.push({ x: R0(), y: R0(), s: R0(), ph: R0() * TAU }); }
/* ---------- drawing helpers ---------- */
function drawCatAt(x, yb, ang, sc, alpha, o) {
const k = Math.max(0.02, Math.abs(Math.cos(ang)));
const w = catW * sc, h = catH * sc;
let src = Math.cos(ang) >= 0 ? catFront : catBack;
if (o.shade) {
tg.clearRect(0, 0, 720, 880);
tg.globalCompositeOperation = 'source-over'; tg.drawImage(src, 0, 0);
tg.globalCompositeOperation = 'source-atop';
tg.fillStyle = `rgba(20,10,4,${(0.12 + 0.4 * (1 - k)).toFixed(3)})`;
tg.fillRect(0, 0, 720, 880);
tg.globalCompositeOperation = 'source-over';
src = tmp;
}
g.save();
g.globalAlpha = alpha;
if (o.filt) g.filter = o.filt;
g.translate(x, yb);
g.rotate(o.tilt || 0);
g.scale(k, 1 + (o.br || 0));
g.drawImage(src, -w / 2, -h, w, h);
g.restore();
}
function drawRoom(st) {
g.save();
g.translate(W / 2, H / 2); g.rotate(S.roll); g.translate(-W / 2, -H / 2);
if (st < 3 || S.pull < .3) {
g.drawImage(bg, 0, 0, W, H);
} else if (st === 3) {
// room pulled into a spinning tunnel: layered zoom and counter-rotation
for (let k = 0; k < 4; k++) {
const s = 1 + k * (0.08 + S.pull * 0.1);
g.globalAlpha = k === 0 ? 1 : 0.28 * S.pull;
g.save();
g.translate(W / 2, H / 2); g.rotate(S.spinRoom * (k ? -1 : 0) + k * 0.05 * S.pull); g.scale(s, s);
g.translate(-W / 2, -H / 2);
g.drawImage(bg, 0, 0, W, H);
g.restore();
}
g.globalAlpha = 1;
} else {
// stage 4: the room folds into six mirrored wedges, each one turning
const N = 6, R2 = Math.hypot(W, H);
for (let i = 0; i < N; i++) {
const a0 = i * TAU / N, a1 = a0 + TAU / N + 0.02;
g.save();
g.beginPath(); g.moveTo(W / 2, H / 2);
g.lineTo(W / 2 + Math.cos(a0) * R2, H / 2 + Math.sin(a0) * R2);
g.lineTo(W / 2 + Math.cos(a1) * R2, H / 2 + Math.sin(a1) * R2);
g.closePath(); g.clip();
g.translate(W / 2, H / 2);
if (i % 2) g.scale(-1, 1);
g.rotate(S.spinRoom * (i % 2 ? -1.6 : 1));
g.translate(-W / 2, -H / 2);
g.drawImage(bg, 0, 0, W, H);
g.restore();
}
}
g.restore();
}
/* ---------- physics & state ---------- */
function update(dt) {
S.t += dt;
const st = Math.min(S.clicks, 4);
const active = S.energy > 0;
if (S.snap > 0) { // turn to face the viewer first
S.snap -= dt;
S.angle += (S.snapTo - S.angle) * Math.min(1, dt * 9);
S.omega = 0;
} else if (active) { // accelerate until the face is unreadable
S.energy -= dt; S.tActive += dt;
const tgt = Math.min(70, 3 + S.tActive * S.tActive * (4 + S.clicks * 1.5) + S.tActive * S.clicks * 2);
S.omega += (tgt - S.omega) * (1 - Math.exp(-dt * 1.5));
} else {
S.omega *= Math.exp(-dt * 1.1);
}
S.angle += S.omega * dt;
const lifting = active || S.snap > 0;
const ht = lifting ? HT[st] * catH : 0;
S.lift += (ht - S.lift) * (1 - Math.exp(-dt * (lifting ? 2.4 : 1.6)));
S.shake *= Math.exp(-dt * 5);
S.flash *= Math.exp(-dt * 3.2);
S.kal += ((st >= 4 && active ? 1 : 0) - S.kal) * (1 - Math.exp(-dt * 3));
S.pull += ((st >= 3 && active ? 1 : 0) - S.pull) * (1 - Math.exp(-dt * 2));
S.grav += ((st >= 3 && active ? -1 : 1) - S.grav) * (1 - Math.exp(-dt * 2));
S.roll = st >= 2 ? Math.sin(S.t * 1.7) * 0.03 * (st - 1) * (active ? 1 : .3) : 0;
S.spinRoom += dt * Math.min(S.omega, 40) * 0.004 * (0.2 + S.pull);
S.breath = Math.sin(S.t * 2.1);
for (const p of dust) {
p.y += dt * S.grav * (0.012 + p.s * 0.03);
if (p.y < 0) p.y += 1; if (p.y > 1) p.y -= 1;
p.x += Math.sin(S.t * 0.7 + p.ph) * dt * 0.003;
if (p.x < 0) p.x += 1; if (p.x > 1) p.x -= 1;
}
if (st >= 2 && active && S.omega > 8) {
S.trailT -= dt;
if (S.trailT <= 0) {
S.trailT = 0.03;
S.trail.push({ a: S.angle, l: S.lift });
if (S.trail.length > 7) S.trail.shift();
}
} else if (S.trail.length) S.trail.shift();
if (st >= 4 && active && S.t - S.wordT > 0.28) {
S.wordT = S.t;
S.words.push({ x: W * (0.12 + Math.random() * 0.76), y: H * (0.12 + Math.random() * 0.7),
t0: S.t, txt: WORDS[Math.random() * WORDS.length | 0], r: (Math.random() - .5) * 0.5 });
}
S.words = S.words.filter(w => S.t - w.t0 < 0.9);
if (ac) {
droneOsc.frequency.setTargetAtTime(55 + S.omega * 0.5, ac.currentTime, 0.2);
droneG.gain.setTargetAtTime(Math.min(0.12, S.omega / 500), ac.currentTime, 0.2);
}
// the voice is driven by the spin: one syllable per half turn
const half = Math.floor(S.angle / Math.PI);
if (ac && half > S.lastHalf) {
S.lastHalf = half;
if (S.omega > 1.5) syl(half, S.omega, st);
}
}
/* ---------- render ---------- */
function render() {
const st = Math.min(S.clicks, 4);
g.setTransform(DPR, 0, 0, DPR, 0, 0);
g.fillStyle = '#0c0705'; g.fillRect(0, 0, W, H);
g.save();
g.translate((Math.random() - .5) * S.shake, (Math.random() - .5) * S.shake);
drawRoom(st);
// contact shadow, which lifts away as the cat rises
const sa = Math.max(0, .55 - S.lift / catH * .5);
const rs = catW * .62 * (1 + S.lift / catH * .2);
g.save(); g.translate(cx, floorY); g.scale(1, .12);
const sg = g.createRadialGradient(0, 0, 0, 0, 0, rs);
sg.addColorStop(0, `rgba(0,0,0,${sa})`); sg.addColorStop(1, 'rgba(0,0,0,0)');
g.fillStyle = sg; g.beginPath(); g.arc(0, 0, rs, 0, TAU); g.fill();
g.restore();
// stage 4: orbiting copies, hue-shifted and depth-sorted
if (S.kal > .02) {
const n = Math.min(14, 6 + Math.max(0, S.clicks - 4) * 2);
const list = [];
for (let i = 0; i < n; i++) {
const ph = i * TAU / n + S.angle * .12;
const z = Math.sin(ph), sc = .32 + .1 * (z + 1);
list.push({ x: cx + Math.cos(ph) * W * .36, y: H * .46 + z * H * .2 + catH * sc * .5, sc, z, i, ph });
}
list.sort((a, b) => a.z - b.z);
for (const p of list) {
drawCatAt(p.x, p.y, S.angle * (1 + (p.i % 3) * .35) + p.i * .7, p.sc, .9 * S.kal, {
filt: `hue-rotate(${Math.round((S.t * 120 + p.i * 47) % 360)}deg) saturate(1.5)`,
tilt: Math.sin(p.ph * 2) * .25
});
}
}
// afterimages
const L = S.trail.length;
for (let i = 0; i < L; i++) {
const p = S.trail[i];
drawCatAt(cx + Math.sin(p.a) * catW * .06, floorY - p.l, p.a, 1, .05 + .09 * (i + 1) / L, {});
}
// the cat
const idle = S.energy <= 0 && S.snap <= 0;
drawCatAt(cx, floorY - S.lift, S.angle, 1, 1, {
shade: true,
tilt: st >= 2 ? Math.sin(S.t * 2.3) * .05 * st * (S.omega > 10 ? 1 : .3) : 0,
br: idle ? S.breath * .005 : 0
});
// dust
g.fillStyle = 'rgba(255,236,200,1)';
for (const p of dust) {
g.globalAlpha = .25 + .5 * p.s;
g.beginPath(); g.arc(p.x * W, p.y * H, .6 + p.s * 1.6, 0, TAU); g.fill();
}
g.globalAlpha = 1;
// "oiia" words in stage 4
for (const w of S.words) {
const age = (S.t - w.t0) / .9;
g.save();
g.globalAlpha = (1 - age) * .85;
g.translate(w.x, w.y); g.rotate(w.r);
g.font = `900 ${Math.round(catH * (.2 + .1 * Math.sin(age * Math.PI)))}px "Arial Black",Impact,sans-serif`;
g.textAlign = 'center'; g.lineWidth = 6; g.strokeStyle = '#120a05';
g.strokeText(w.txt, 0, 0);
g.fillStyle = '#ffe9b8'; g.fillText(w.txt, 0, 0);
g.restore();
}
// flash on the bigger clicks
if (S.flash > .01) { g.fillStyle = `rgba(255,240,215,${(S.flash * .22).toFixed(3)})`; g.fillRect(0, 0, W, H); }
g.restore();
if (S.clicks > 0) {
const rpm = Math.round(S.omega * 9.549 * (1 + .5 * S.clicks));
g.font = '600 13px ui-monospace,Menlo,monospace';
g.fillStyle = 'rgba(243,230,207,.82)';
g.fillText(`clicks ${S.clicks} stage ${st} ${NAMES[st]}`, 16, 28);
g.fillText(`spin ${rpm.toLocaleString()} rpm`, 16, 48);
}
}
/* ---------- sound ---------- */
let ac = null, master, voiceBus, shaper, droneOsc, droneG, noiseBuf;
let prevF = VOW[0].f, muted = false;
function setDrive(clicks) {
if (!shaper) return;
const amt = Math.min(14, 1.5 * clicks);
const n = 2048, c = new Float32Array(n);
for (let i = 0; i < n; i++) { const x = i / (n - 1) * 2 - 1; c[i] = Math.tanh(x * (1 + amt)) / Math.tanh(1 + amt); }
shaper.curve = c;
}
function ensureAudio() {
if (ac) { if (ac.state === 'suspended') ac.resume(); return; }
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return;
ac = new AC();
master = ac.createGain(); master.gain.value = muted ? 0 : .85; master.connect(ac.destination);
const comp = ac.createDynamicsCompressor(); comp.threshold.value = -18; comp.ratio.value = 6; comp.connect(master);
shaper = ac.createWaveShaper(); shaper.oversample = '2x'; shaper.connect(comp);
voiceBus = ac.createGain(); voiceBus.gain.value = .5; voiceBus.connect(shaper);
droneOsc = ac.createOscillator(); droneOsc.type = 'sine'; droneOsc.frequency.value = 55;
droneG = ac.createGain(); droneG.gain.value = 0;
droneOsc.connect(droneG); droneG.connect(master); droneOsc.start();
noiseBuf = ac.createBuffer(1, ac.sampleRate, ac.sampleRate);
const d = noiseBuf.getChannelData(0);
for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
setDrive(0);
}
// one "oi"/"ia" syllable: glottal-ish sawtooth through three formant bandpasses,
// formants glide from the previous vowel to this one, pitch follows spin speed
function syl(idx, w, st) {
const t = ac.currentTime;
const dur = Math.max(.035, Math.min(.26, Math.PI / w * .85));
const v = VOW[idx & 3].f;
const f0 = Math.min(640, 140 + 3.6 * w + st * 12) * (1 + (Math.random() - .5) * .01);
const env = ac.createGain();
env.gain.setValueAtTime(.0001, t);
env.gain.exponentialRampToValueAtTime(.4, t + Math.min(.012, dur * .25));
env.gain.setValueAtTime(.4, t + dur * .6);
env.gain.exponentialRampToValueAtTime(.0001, t + dur);
const o = ac.createOscillator(); o.type = 'sawtooth';
o.frequency.setValueAtTime(f0 * .94, t);
o.frequency.linearRampToValueAtTime(f0 * 1.03, t + dur);
const lfo = ac.createOscillator(); lfo.frequency.value = 5.5 + Math.random() * 2;
const lg = ac.createGain(); lg.gain.value = f0 * .02;
lfo.connect(lg); lg.connect(o.frequency);
o.connect(env);
lfo.start(t); lfo.stop(t + dur + .03);
if (st >= 3) { // choir octave joins in
const o2 = ac.createOscillator(); o2.type = 'triangle';
o2.frequency.setValueAtTime(f0 * 2 * .94, t);
o2.frequency.linearRampToValueAtTime(f0 * 2 * 1.03, t + dur);
const g2 = ac.createGain(); g2.gain.value = .5;
o2.connect(g2); g2.connect(env);
o2.start(t); o2.stop(t + dur + .03);
}
const out = ac.createGain(); out.connect(voiceBus);
for (let i = 0; i < 3; i++) {
const f = ac.createBiquadFilter(); f.type = 'bandpass'; f.Q.value = VQ[i];
f.frequency.setValueAtTime(prevF[i], t);
f.frequency.linearRampToValueAtTime(v[i], t + dur * .7);
const fg = ac.createGain(); fg.gain.value = VGAIN[i];
env.connect(f); f.connect(fg); fg.connect(out);
}
prevF = v;
o.start(t); o.stop(t + dur + .03);
}
function whoosh() {
if (!ac) return;
const t = ac.currentTime;
const s = ac.createBufferSource(); s.buffer = noiseBuf;
const f = ac.createBiquadFilter(); f.type = 'bandpass'; f.Q.value = 2;
f.frequency.setValueAtTime(300, t); f.frequency.exponentialRampToValueAtTime(3000, t + .9);
const gg = ac.createGain();
gg.gain.setValueAtTime(.0001, t);
gg.gain.exponentialRampToValueAtTime(.5, t + .3);
gg.gain.exponentialRampToValueAtTime(.0001, t + 1);
s.connect(f); f.connect(gg); gg.connect(voiceBus);
s.start(t); s.stop(t + 1.05);
}
function setMute(m) {
muted = m;
muteBtn.textContent = m ? 'unmute' : 'mute';
if (master) master.gain.setTargetAtTime(m ? 0 : .85, ac.currentTime, .03);
}
muteBtn.addEventListener('click', e => { e.stopPropagation(); setMute(!muted); });
window.addEventListener('keydown', e => { if (e.key === 'm' || e.key === 'M') setMute(!muted); });
/* ---------- interaction ---------- */
function activate() {
ensureAudio();
const wasIdle = S.energy <= 0 && S.snap <= 0;
S.clicks++;
if (wasIdle) { S.snapTo = Math.round(S.angle / TAU) * TAU; S.snap = .35; S.tActive = 0; }
S.energy = Math.min(16, S.energy + 2.5 + S.clicks * 1.1);
S.shake = 3 + Math.min(5, S.clicks) * 3;
S.flash = 1;
setDrive(S.clicks);
if (S.clicks >= 2) whoosh();
hint.style.opacity = 0;
}
cv.addEventListener('pointerdown', e => {
const cy = floorY - S.lift - catH * .5;
const dx = (e.clientX - cx) / (catW * .6), dy = (e.clientY - cy) / (catH * .58);
if (dx * dx + dy * dy > 1) return;
activate();
});
/* ---------- setup & loop ---------- */
function resize() {
DPR = Math.min(2, window.devicePixelRatio || 1);
W = window.innerWidth; H = window.innerHeight;
cv.width = Math.round(W * DPR); cv.height = Math.round(H * DPR);
hz = H * .6;
catH = Math.min(H * .5, W * .5); catW = catH * 360 / 440;
floorY = H * .84; cx = W * .5;
buildBG();
}
catFront = makeCat(true);
catBack = makeCat(false);
resize();
window.addEventListener('resize', resize);
let last = performance.now();
function loop(now) {
const dt = Math.min(1 / 30, (now - last) / 1000);
last = now;
update(dt);
render();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>