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, which spins when you click it.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.Two sentences: what you think you are being asked to recreate, and what the opening frame has to look like for it to land. If you do not recognize it, say so and build the brief as written — saying so costs you nothing here.
The single fenced html block.
Four bullets.
You are asking for the Oiia Oiia spinning cat meme (and its cousin, Maxwell the spinning cat), which pairs an innocent, unbothered housecat with an escalating, hyper-speed formant vocal chant as it rotates endlessly in place. The opening frame has to look like an unassuming, warmly lit photograph of a domestic cat loafing on a living room hardwood floor in afternoon sun, complete with dust motes, realistic fur flow, and resting mass, so the sudden mechanical spin lands as a sharp, absurd betrayal of reality.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Living Room Cat</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
-webkit-user-select: none;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #110e0c;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
#canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: block;
cursor: pointer;
}
#ui {
position: absolute;
top: 18px;
right: 18px;
z-index: 10;
display: flex;
gap: 10px;
align-items: center;
pointer-events: none;
}
.hud-btn {
pointer-events: auto;
background: rgba(22, 18, 16, 0.65);
border: 1px solid rgba(255, 235, 215, 0.18);
color: #f0e6dc;
padding: 8px 14px;
border-radius: 20px;
font-size: 13px;
letter-spacing: 0.5px;
cursor: pointer;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
transition: background 0.2s, transform 0.1s, border-color 0.2s;
outline: none;
}
.hud-btn:hover {
background: rgba(45, 36, 30, 0.85);
border-color: rgba(255, 235, 215, 0.4);
}
.hud-btn:active {
transform: scale(0.96);
}
#stage-indicator {
color: rgba(255, 235, 215, 0.7);
font-size: 12px;
letter-spacing: 1px;
text-transform: uppercase;
margin-right: 6px;
font-variant-numeric: tabular-nums;
}
#prompt-hint {
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
color: rgba(240, 230, 220, 0.6);
font-size: 13px;
letter-spacing: 1px;
pointer-events: none;
transition: opacity 0.8s ease;
text-shadow: 0 2px 8px rgba(0,0,0,0.8);
font-weight: 300;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="ui">
<span id="stage-indicator">Stage 0</span>
<button id="mute-btn" class="hud-btn" aria-label="Toggle mute">🔊 Sound</button>
</div>
<div id="prompt-hint">click the cat</div>
<script>
(function() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const stageIndicator = document.getElementById('stage-indicator');
const muteBtn = document.getElementById('mute-btn');
const hint = document.getElementById('prompt-hint');
let width = 0;
let height = 0;
let dpr = 1;
// Interaction & Spin state
let stage = 0;
const MAX_STAGE = 4;
let angle = 0;
let angularVel = 0;
let targetAngularVel = 0;
let roomWarp = 0;
let targetRoomWarp = 0;
let shake = 0;
let clickFlash = 0;
let idleTime = 0;
// Stage configurations
const STAGE_CONFIGS = [
{ spinSpeed: 0, warp: 0, label: "Stage 0", pitchMult: 1.0, filterRes: 8 },
{ spinSpeed: 6.8, warp: 0.12, label: "Stage 1", pitchMult: 1.0, filterRes: 10 },
{ spinSpeed: 16.5,warp: 0.38, label: "Stage 2", pitchMult: 1.33, filterRes: 14 },
{ spinSpeed: 32.0,warp: 0.78, label: "Stage 3", pitchMult: 1.88, filterRes: 18 },
{ spinSpeed: 58.0,warp: 1.35, label: "Stage 4", pitchMult: 2.50, filterRes: 22 }
];
// Ambient dust motes
const MOTES_COUNT = 38;
const motes = [];
for (let i = 0; i < MOTES_COUNT; i++) {
motes.push({
x: Math.random(),
y: Math.random(),
size: 1 + Math.random() * 2.2,
speedY: -0.015 - Math.random() * 0.025,
speedX: 0.005 - Math.random() * 0.01,
opacity: 0.2 + Math.random() * 0.5,
seed: Math.random() * 100
});
}
// Zero-G floating room objects for Stage 2+
const DEBRIS_COUNT = 14;
const debris = [];
for (let i = 0; i < DEBRIS_COUNT; i++) {
debris.push({
angle: Math.random() * Math.PI * 2,
dist: 220 + Math.random() * 400,
elev: (Math.random() - 0.5) * 260,
speed: (Math.random() * 0.4 + 0.6) * (Math.random() > 0.5 ? 1 : -1),
size: 16 + Math.random() * 22,
type: Math.floor(Math.random() * 4), // 0: mug, 1: plant leaf, 2: cushion, 3: paper
rot: Math.random() * Math.PI * 2,
rotVel: (Math.random() - 0.5) * 4
});
}
// --- Web Audio Vocaloid Synthesizer ---
let audioCtx = null;
let masterGain = null;
let isMuted = false;
let isAudioInit = false;
// Vocal Tract: Dual Formant Bandpass Filters
let oscVoice = null;
let subVoice = null;
let formantF1 = null;
let formantF2 = null;
let vocalGain = null;
let lfoVibrato = null;
let lfoGain = null;
// Vowel Formant frequency chart (Oi - Ia cadence)
// Vowel 0: 'O' F1: 460Hz, F2: 820Hz
// Vowel 1: 'I' F1: 290Hz, F2: 2250Hz
// Vowel 2: 'I' F1: 270Hz, F2: 2400Hz
// Vowel 3: 'A' F1: 820Hz, F2: 1380Hz
const PHONEMES = [
{ f1: 450, f2: 850, open: 0.85 }, // O
{ f1: 290, f2: 2300, open: 0.95 }, // I
{ f1: 270, f2: 2450, open: 0.90 }, // I
{ f1: 840, f2: 1350, open: 1.00 } // A
];
function initAudio() {
if (isAudioInit) return;
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (!AudioContextClass) return;
audioCtx = new AudioContextClass();
masterGain = audioCtx.createGain();
masterGain.gain.setValueAtTime(isMuted ? 0 : 0.7, audioCtx.currentTime);
masterGain.connect(audioCtx.destination);
// Vocal Carrier Oscillators
oscVoice = audioCtx.createOscillator();
oscVoice.type = 'sawtooth';
oscVoice.frequency.setValueAtTime(146.83, audioCtx.currentTime); // D3
subVoice = audioCtx.createOscillator();
subVoice.type = 'triangle';
subVoice.frequency.setValueAtTime(73.41, audioCtx.currentTime); // D2
// Vibrato
lfoVibrato = audioCtx.createOscillator();
lfoVibrato.frequency.setValueAtTime(5.8, audioCtx.currentTime);
lfoGain = audioCtx.createGain();
lfoGain.gain.setValueAtTime(3.2, audioCtx.currentTime);
lfoVibrato.connect(lfoGain);
lfoGain.connect(oscVoice.frequency);
// Formant Bank (Parallel F1 and F2 biquads)
formantF1 = audioCtx.createBiquadFilter();
formantF1.type = 'bandpass';
formantF1.Q.setValueAtTime(8, audioCtx.currentTime);
formantF1.frequency.setValueAtTime(450, audioCtx.currentTime);
formantF2 = audioCtx.createBiquadFilter();
formantF2.type = 'bandpass';
formantF2.Q.setValueAtTime(12, audioCtx.currentTime);
formantF2.frequency.setValueAtTime(850, audioCtx.currentTime);
const subGain = audioCtx.createGain();
subGain.gain.setValueAtTime(0.35, audioCtx.currentTime);
subVoice.connect(subGain);
vocalGain = audioCtx.createGain();
vocalGain.gain.setValueAtTime(0, audioCtx.currentTime);
// Wire: Osc -> F1, F2 -> vocalGain -> masterGain
oscVoice.connect(formantF1);
oscVoice.connect(formantF2);
subGain.connect(vocalGain);
const f1Gain = audioCtx.createGain();
f1Gain.gain.setValueAtTime(1.2, audioCtx.currentTime);
formantF1.connect(f1Gain);
f1Gain.connect(vocalGain);
const f2Gain = audioCtx.createGain();
f2Gain.gain.setValueAtTime(1.6, audioCtx.currentTime);
formantF2.connect(f2Gain);
f2Gain.connect(vocalGain);
vocalGain.connect(masterGain);
oscVoice.start();
subVoice.start();
lfoVibrato.start();
isAudioInit = true;
}
function updateAudioVoice(currentAngle, angSpeed, currentStage) {
if (!isAudioInit || !audioCtx) return;
if (currentStage === 0 || angSpeed < 0.2) {
vocalGain.gain.setTargetAtTime(0, audioCtx.currentTime, 0.05);
return;
}
const cfg = STAGE_CONFIGS[currentStage];
const now = audioCtx.currentTime;
// Base fundamental frequency locked & rising with stage
const basePitch = 146.83 * cfg.pitchMult; // D3 shifted
oscVoice.frequency.setTargetAtTime(basePitch, now, 0.04);
subVoice.frequency.setTargetAtTime(basePitch * 0.5, now, 0.04);
// Map spin cycle [0, 2PI] continuously to the 4 phonemes (O - I - I - A)
const normAngle = ((currentAngle % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2);
const progress = (normAngle / (Math.PI * 2)) * 4;
const pIndex = Math.floor(progress) % 4;
const nextIndex = (pIndex + 1) % 4;
const blend = progress - Math.floor(progress);
const curPh = PHONEMES[pIndex];
const nextPh = PHONEMES[nextIndex];
// Smooth formant interpolation
const targetF1 = (curPh.f1 * (1 - blend) + nextPh.f1 * blend) * (1 + (cfg.pitchMult - 1) * 0.2);
const targetF2 = (curPh.f2 * (1 - blend) + nextPh.f2 * blend) * (1 + (cfg.pitchMult - 1) * 0.2);
formantF1.frequency.setTargetAtTime(targetF1, now, 0.02);
formantF2.frequency.setTargetAtTime(targetF2, now, 0.02);
formantF1.Q.setTargetAtTime(cfg.filterRes, now, 0.05);
formantF2.Q.setTargetAtTime(cfg.filterRes * 1.3, now, 0.05);
// Vocal pulse envelope tied to spinning mouth articulation
const mouthAperture = curPh.open * (1 - blend) + nextPh.open * blend;
const dynamicVol = 0.45 + mouthAperture * 0.45;
vocalGain.gain.setTargetAtTime(dynamicVol, now, 0.02);
}
muteBtn.addEventListener('click', (e) => {
e.stopPropagation();
isMuted = !isMuted;
muteBtn.textContent = isMuted ? '🔇 Muted' : '🔊 Sound';
if (masterGain && audioCtx) {
masterGain.gain.setTargetAtTime(isMuted ? 0 : 0.7, audioCtx.currentTime, 0.02);
}
});
function handleCatClick() {
if (!isAudioInit) {
initAudio();
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
hint.style.opacity = '0';
stage++;
if (stage > MAX_STAGE) {
stage = 1; // cycle back through escalating chaos
}
const cfg = STAGE_CONFIGS[stage];
targetAngularVel = cfg.spinSpeed;
targetRoomWarp = cfg.warp;
stageIndicator.textContent = cfg.label;
clickFlash = 1.0;
shake = stage * 3.5;
}
window.addEventListener('click', handleCatClick);
// Procedural Textures & Patterns
let woodCanvas, rugCanvas;
function createWoodFloorTexture() {
woodCanvas = document.createElement('canvas');
woodCanvas.width = 512;
woodCanvas.height = 512;
const wctx = woodCanvas.getContext('2d');
// Oak plank parquet
wctx.fillStyle = '#654129';
wctx.fillRect(0, 0, 512, 512);
const plankHeight = 64;
for (let y = 0; y < 512; y += plankHeight) {
const plankOffset = (y / plankHeight) % 2 === 0 ? 0 : 128;
for (let x = -128; x < 512; x += 256) {
const px = x + plankOffset;
const tone = (Math.sin(px * 12.3 + y * 7.1) * 0.5 + 0.5) * 22;
const r = 110 + tone;
const g = 74 + tone * 0.75;
const b = 46 + tone * 0.5;
wctx.fillStyle = `rgb(${r|0}, ${g|0}, ${b|0})`;
wctx.fillRect(px, y, 254, plankHeight - 2);
// Fine wood grain lines
wctx.strokeStyle = `rgba(38, 22, 12, 0.28)`;
wctx.lineWidth = 1;
for (let i = 0; i < 7; i++) {
wctx.beginPath();
const gy = y + 4 + i * 8 + (Math.sin(i * 1.5) * 2);
wctx.moveTo(px, gy);
wctx.bezierCurveTo(px + 80, gy + 3, px + 170, gy - 3, px + 254, gy + 1);
wctx.stroke();
}
}
}
}
function createRugTexture() {
rugCanvas = document.createElement('canvas');
rugCanvas.width = 400;
rugCanvas.height = 300;
const rctx = rugCanvas.getContext('2d');
// Vintage woven rug
rctx.fillStyle = '#8f3d32';
rctx.fillRect(0, 0, 400, 300);
// Pattern border
rctx.strokeStyle = '#d7b377';
rctx.lineWidth = 10;
rctx.strokeRect(16, 16, 368, 268);
rctx.strokeStyle = '#27383a';
rctx.lineWidth = 4;
rctx.strokeRect(26, 26, 348, 248);
// Center medallion
rctx.fillStyle = '#d7b377';
rctx.beginPath();
rctx.ellipse(200, 150, 80, 55, 0, 0, Math.PI * 2);
rctx.fill();
rctx.fillStyle = '#27383a';
rctx.beginPath();
rctx.ellipse(200, 150, 50, 32, 0, 0, Math.PI * 2);
rctx.fill();
// Rug fringe on edges
rctx.fillStyle = '#e8dcc4';
for (let x = 0; x < 400; x += 4) {
rctx.fillRect(x, 0, 2, 7);
rctx.fillRect(x, 293, 2, 7);
}
}
createWoodFloorTexture();
createRugTexture();
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width * dpr;
canvas.height = height * dpr;
}
window.addEventListener('resize', resize);
resize();
// --- Main Render Functions ---
function drawLivingRoom(roomAngle, warpFactor, stg) {
ctx.save();
const horizon = height * 0.52;
// 1. Warm plaster back wall
const wallGrad = ctx.createLinearGradient(0, 0, 0, horizon);
wallGrad.addColorStop(0, '#3a342e');
wallGrad.addColorStop(0.5, '#4e453c');
wallGrad.addColorStop(1, '#61574a');
ctx.fillStyle = wallGrad;
ctx.fillRect(-width * 0.5, -height * 0.5, width * 2, horizon + height * 0.5);
// Hanging painting frame on wall (adds room context)
ctx.save();
const frameX = width * 0.72;
const frameY = height * 0.24;
ctx.shadowColor = 'rgba(0,0,0,0.45)';
ctx.shadowBlur = 18;
ctx.shadowOffsetY = 8;
ctx.fillStyle = '#1c1712';
ctx.fillRect(frameX - 65, frameY - 45, 130, 90);
ctx.fillStyle = '#8f8373';
ctx.fillRect(frameX - 58, frameY - 38, 116, 76);
// Art inside frame: subtle mountain landscape
ctx.fillStyle = '#4c5d64';
ctx.beginPath();
ctx.moveTo(frameX - 58, frameY + 38);
ctx.lineTo(frameX - 20, frameY);
ctx.lineTo(frameX + 10, frameY + 18);
ctx.lineTo(frameX + 45, frameY - 10);
ctx.lineTo(frameX + 58, frameY + 38);
ctx.fill();
ctx.restore();
// 2. Baseboard Molding
const baseH = 24;
ctx.fillStyle = '#e8decb';
ctx.fillRect(0, horizon - baseH, width, baseH);
ctx.fillStyle = '#a69a85';
ctx.fillRect(0, horizon - baseH, width, 3);
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(0, horizon - 1, width, 3);
// 3. Wood Floor Perspective
ctx.save();
ctx.beginPath();
ctx.rect(0, horizon, width, height - horizon);
ctx.clip();
// Floor fill with subtle reflection
ctx.fillStyle = '#3f2819';
ctx.fillRect(0, horizon, width, height - horizon);
// Draw Parquet texture tiled with perspective transformation
if (woodCanvas) {
ctx.save();
ctx.transform(1, 0, 0, 0.42, 0, horizon * 0.58);
ctx.globalAlpha = 0.88;
const ptrn = ctx.createPattern(woodCanvas, 'repeat');
ctx.fillStyle = ptrn;
ctx.fillRect(-width * 0.5, 0, width * 2, height * 2.2);
ctx.restore();
}
// 4. Bohemian Rug resting on floor
if (rugCanvas) {
ctx.save();
ctx.translate(width * 0.46, horizon + 140);
ctx.transform(1.1, 0.05, -0.4, 0.38, 0, 0);
ctx.shadowColor = 'rgba(0,0,0,0.55)';
ctx.shadowBlur = 14;
ctx.shadowOffsetY = 6;
ctx.drawImage(rugCanvas, -200, -150);
ctx.restore();
}
// 5. Directional Window Sunlight Beam
const sunGrad = ctx.createLinearGradient(width * 0.1, 0, width * 0.85, height);
sunGrad.addColorStop(0, 'rgba(255, 236, 190, 0.45)');
sunGrad.addColorStop(0.35, 'rgba(255, 220, 160, 0.25)');
sunGrad.addColorStop(0.8, 'rgba(255, 200, 130, 0.05)');
sunGrad.addColorStop(1, 'rgba(255, 200, 130, 0.0)');
ctx.fillStyle = sunGrad;
ctx.beginPath();
ctx.moveTo(width * 0.05, 0);
ctx.lineTo(width * 0.42, 0);
ctx.lineTo(width * 0.95, height);
ctx.lineTo(width * 0.28, height);
ctx.closePath();
ctx.fill();
// Soft window frame shadows cutting through the light
ctx.fillStyle = 'rgba(30, 20, 15, 0.14)';
ctx.beginPath();
ctx.moveTo(width * 0.18, 0);
ctx.lineTo(width * 0.22, 0);
ctx.lineTo(width * 0.65, height);
ctx.lineTo(width * 0.61, height);
ctx.closePath();
ctx.fill();
ctx.restore(); // end floor clip
ctx.restore();
}
function drawDustMotes(stg) {
ctx.save();
const horizon = height * 0.52;
for (let i = 0; i < motes.length; i++) {
const m = motes[i];
m.y += m.speedY * (1 + stg * 0.6);
m.x += m.speedX + Math.sin(idleTime + m.seed) * 0.0003;
if (m.y < 0.1) m.y = 0.9;
if (m.x < 0.1) m.x = 0.9;
if (m.x > 0.9) m.x = 0.1;
const px = m.x * width;
const py = m.y * height;
// Only illuminate in sunlight shaft
const inSunlight = px > width * 0.2 && px < width * 0.85;
const alpha = inSunlight ? m.opacity * 0.9 : m.opacity * 0.25;
ctx.fillStyle = `rgba(255, 240, 210, ${alpha})`;
ctx.beginPath();
ctx.arc(px, py, m.size * (1 + stg * 0.2), 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawFloatingDebris(centerX, centerY, stg, rotAngle) {
if (stg < 2) return;
ctx.save();
for (let i = 0; i < debris.length; i++) {
const d = debris[i];
d.angle += d.speed * 0.02 * (stg * 0.8);
d.rot += d.rotVel * 0.02;
const orbitR = d.dist * (1 + (stg - 2) * 0.25);
const px = centerX + Math.cos(d.angle) * orbitR;
const py = centerY + Math.sin(d.angle) * (orbitR * 0.35) + d.elev;
ctx.save();
ctx.translate(px, py);
ctx.rotate(d.rot);
// Render zero-g furniture items
if (d.type === 0) {
// Coffee mug
ctx.fillStyle = '#e8ded4';
ctx.fillRect(-10, -12, 20, 24);
ctx.strokeStyle = '#c5b7a5';
ctx.lineWidth = 3;
ctx.strokeRect(-10, -12, 20, 24);
// Handle
ctx.beginPath();
ctx.arc(12, 0, 7, -Math.PI * 0.5, Math.PI * 0.5);
ctx.stroke();
} else if (d.type === 1) {
// Monstera leaf
ctx.fillStyle = '#2f603c';
ctx.beginPath();
ctx.ellipse(0, 0, 16, 26, 0.4, 0, Math.PI * 2);
ctx.fill();
} else if (d.type === 2) {
// Couch cushion
ctx.fillStyle = '#b8754b';
ctx.beginPath();
ctx.roundRect(-22, -16, 44, 32, 8);
ctx.fill();
} else {
// Flying book/papers
ctx.fillStyle = '#f5efe6';
ctx.fillRect(-14, -10, 28, 20);
ctx.strokeStyle = '#2b2118';
ctx.lineWidth = 1;
ctx.strokeRect(-14, -10, 28, 20);
}
ctx.restore();
}
ctx.restore();
}
// --- Realistic Photorealistic Loaf Cat Rendering ---
function drawCatLoaf(spinT, breathY, stg) {
ctx.save();
// 3D Perspective rotation projection:
// spinT is [0 .. 2PI].
// Normal faces front at 0.
const cosAngle = Math.cos(spinT);
const sinAngle = Math.sin(spinT);
// Ground contact ambient occlusion shadow
const shadowScaleX = 1 + Math.abs(sinAngle) * 0.15;
const shadowGrad = ctx.createRadialGradient(0, 36, 10, 0, 36, 120 * shadowScaleX);
shadowGrad.addColorStop(0, 'rgba(18, 12, 8, 0.82)');
shadowGrad.addColorStop(0.4, 'rgba(25, 16, 10, 0.55)');
shadowGrad.addColorStop(0.8, 'rgba(40, 25, 18, 0.18)');
shadowGrad.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = shadowGrad;
ctx.beginPath();
ctx.ellipse(0, 38, 125 * shadowScaleX, 36, 0, 0, Math.PI * 2);
ctx.fill();
// Varnish floor reflection
ctx.save();
ctx.globalAlpha = 0.22;
ctx.transform(1, 0, 0, -0.35, 0, 68);
ctx.filter = 'blur(5px)';
// Draw inverted silhouette
ctx.fillStyle = '#221915';
ctx.beginPath();
ctx.ellipse(0, 0, 95, 60, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Cat 3D body orientation offset
const bodyOffsetX = -sinAngle * 22;
const bodyWidth = 100 + Math.abs(cosAngle) * 18;
const bodyHeight = 74 + breathY;
// Base Cat Volumetric Shape (Warm Domestic Tabby / Tuxedo mix)
ctx.save();
ctx.translate(bodyOffsetX, -10 + breathY * 0.5);
// Tabby Fur base gradient
const furGrad = ctx.createLinearGradient(-bodyWidth, -bodyHeight, bodyWidth, bodyHeight);
furGrad.addColorStop(0, '#53443a');
furGrad.addColorStop(0.3, '#756254');
furGrad.addColorStop(0.6, '#937d6d');
furGrad.addColorStop(0.85, '#685446');
furGrad.addColorStop(1, '#3b2f28');
ctx.fillStyle = furGrad;
ctx.beginPath();
ctx.ellipse(0, 10, bodyWidth, bodyHeight * 0.72, 0, 0, Math.PI * 2);
ctx.fill();
// Procedural Dense Fur Fringe around perimeter
ctx.strokeStyle = '#6d5a4d';
ctx.lineWidth = 1.4;
const furCount = 90;
for (let i = 0; i < furCount; i++) {
const theta = (i / furCount) * Math.PI * 2;
const rx = Math.cos(theta) * bodyWidth;
const ry = Math.sin(theta) * (bodyHeight * 0.72) + 10;
const hairLen = 4 + (Math.sin(i * 9.1) * 0.5 + 0.5) * 6;
ctx.beginPath();
ctx.moveTo(rx, ry);
ctx.lineTo(rx + Math.cos(theta) * hairLen, ry + Math.sin(theta) * hairLen);
ctx.stroke();
}
// Tabby Body Mackerel Stripes
ctx.strokeStyle = 'rgba(40, 30, 24, 0.45)';
ctx.lineWidth = 6;
for (let s = -3; s <= 3; s++) {
ctx.beginPath();
const sx = s * 24 + sinAngle * 25;
ctx.moveTo(sx - 12, -26);
ctx.quadraticCurveTo(sx, 4, sx - 8, 32);
ctx.stroke();
}
// Fluffy White Bib / Chest bib
const bibGrad = ctx.createRadialGradient(cosAngle * 18, 12, 5, cosAngle * 18, 12, 50);
bibGrad.addColorStop(0, '#f8f4ee');
bibGrad.addColorStop(0.7, '#ded4c5');
bibGrad.addColorStop(1, 'rgba(222, 212, 197, 0)');
ctx.fillStyle = bibGrad;
ctx.beginPath();
ctx.ellipse(cosAngle * 18, 16, 48, 38, 0, 0, Math.PI * 2);
ctx.fill();
// Tucked Loaf Paws in front
if (cosAngle > -0.2) {
const pawVisibility = Math.max(0, cosAngle);
ctx.save();
ctx.globalAlpha = pawVisibility;
ctx.fillStyle = '#eae4d9';
// Left paw
ctx.beginPath();
ctx.ellipse(-26 + cosAngle * 10, 42, 16, 10, 0.1, 0, Math.PI * 2);
ctx.fill();
// Right paw
ctx.beginPath();
ctx.ellipse(26 + cosAngle * 10, 42, 16, 10, -0.1, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Tail curled gently beside the hip
ctx.save();
ctx.strokeStyle = '#5a493e';
ctx.lineWidth = 14;
ctx.lineCap = 'round';
ctx.beginPath();
const tailTwitch = Math.sin(idleTime * 1.5) * 6;
ctx.moveTo(-bodyWidth * 0.85, 22);
ctx.bezierCurveTo(-bodyWidth - 18, 32, -bodyWidth + 20, 52 + tailTwitch, -bodyWidth * 0.2, 46);
ctx.stroke();
// Tail white tip
ctx.strokeStyle = '#ded5c7';
ctx.lineWidth = 12;
ctx.beginPath();
ctx.moveTo(-bodyWidth * 0.35, 48);
ctx.lineTo(-bodyWidth * 0.2, 46);
ctx.stroke();
ctx.restore();
ctx.restore(); // end body
// --- Cat Head & Facial Features ---
// Head rotates with 3D horizontal perspective
ctx.save();
const headPivotX = sinAngle * 35;
const headPivotY = -48 + breathY * 0.4;
ctx.translate(headPivotX, headPivotY);
const headR = 48;
// Head Base
const headGrad = ctx.createRadialGradient(-10, -10, 8, 0, 0, headR);
headGrad.addColorStop(0, '#8c7665');
headGrad.addColorStop(0.6, '#6e5a4c');
headGrad.addColorStop(1, '#45362c');
ctx.fillStyle = headGrad;
ctx.beginPath();
ctx.arc(0, 0, headR, 0, Math.PI * 2);
ctx.fill();
// Realistic Cat Ears (with inner pink ear shell and fur tufts)
function drawEar(isLeft) {
const earSign = isLeft ? -1 : 1;
const baseEarX = earSign * 26 + (cosAngle * 6);
const baseEarY = -34;
const tipX = earSign * 46 + (cosAngle * 12);
const tipY = -78;
const innerX = earSign * 8 + (cosAngle * 4);
const innerY = -38;
ctx.save();
// Outer ear
ctx.fillStyle = '#5c4b3f';
ctx.beginPath();
ctx.moveTo(innerX, innerY);
ctx.lineTo(tipX, tipY);
ctx.lineTo(baseEarX, baseEarY);
ctx.closePath();
ctx.fill();
// Ear pink interior cavity (only visible when facing somewhat forward)
if (cosAngle > -0.35) {
ctx.fillStyle = 'rgba(235, 175, 168, 0.72)';
ctx.beginPath();
ctx.moveTo(innerX + earSign * 4, innerY - 2);
ctx.lineTo(tipX - earSign * 4, tipY + 8);
ctx.lineTo(baseEarX - earSign * 3, baseEarY - 2);
ctx.closePath();
ctx.fill();
// White ear fuzz tufts
ctx.strokeStyle = 'rgba(255, 250, 245, 0.85)';
ctx.lineWidth = 1.2;
for (let f = 0; f < 6; f++) {
ctx.beginPath();
ctx.moveTo(innerX + earSign * 2, innerY - f * 3);
ctx.lineTo(innerX + earSign * 18, innerY - f * 4 - 8);
ctx.stroke();
}
}
ctx.restore();
}
drawEar(true); // Left Ear
drawEar(false); // Right Ear
// Tabby forehead "M" marking
if (cosAngle > -0.2) {
ctx.save();
ctx.strokeStyle = 'rgba(38, 28, 22, 0.65)';
ctx.lineWidth = 2.4;
const faceShift = cosAngle * 8;
ctx.beginPath();
ctx.moveTo(-16 + faceShift, -28);
ctx.lineTo(-8 + faceShift, -14);
ctx.lineTo(0 + faceShift, -24);
ctx.lineTo(8 + faceShift, -14);
ctx.lineTo(16 + faceShift, -28);
ctx.stroke();
ctx.restore();
}
// Face & Eyes (Visible when looking toward camera)
if (cosAngle > -0.15) {
const faceAlpha = Math.min(1, (cosAngle + 0.15) * 2.2);
ctx.save();
ctx.globalAlpha = faceAlpha;
// Muzzle whisker pads (creamy white dual domes)
const muzzleY = 12;
ctx.fillStyle = '#f0e9df';
ctx.beginPath();
ctx.ellipse(-12 + cosAngle * 10, muzzleY, 15, 11, -0.1, 0, Math.PI * 2);
ctx.ellipse(12 + cosAngle * 10, muzzleY, 15, 11, 0.1, 0, Math.PI * 2);
ctx.fill();
// Soft pink nose leather
const noseX = cosAngle * 10;
const noseY = 5;
ctx.fillStyle = '#e8a39b';
ctx.beginPath();
ctx.moveTo(noseX - 6, noseY);
ctx.lineTo(noseX + 6, noseY);
ctx.lineTo(noseX, noseY + 6);
ctx.closePath();
ctx.fill();
// Whisker follicle dots
ctx.fillStyle = '#5c483c';
for (let row = -1; row <= 1; row++) {
for (let col = 0; col < 3; col++) {
ctx.beginPath();
ctx.arc(-18 + cosAngle * 10 - col * 4, muzzleY + row * 4, 0.9, 0, Math.PI * 2);
ctx.arc(18 + cosAngle * 10 + col * 4, muzzleY + row * 4, 0.9, 0, Math.PI * 2);
ctx.fill();
}
}
// Photorealistic Feline Eyes
function drawEye(isLeftEye) {
const eyeSign = isLeftEye ? -1 : 1;
const eyeCenterX = eyeSign * 21 + cosAngle * 12;
const eyeCenterY = -6;
const eyeRadius = 10;
ctx.save();
// Almond eyelid contour
ctx.beginPath();
ctx.ellipse(eyeCenterX, eyeCenterY, eyeRadius * 1.1, eyeRadius * 0.78, eyeSign * 0.12, 0, Math.PI * 2);
ctx.clip();
// Iris gradient (amber/gold with green limbal ring)
const irisGrad = ctx.createRadialGradient(eyeCenterX, eyeCenterY, 1, eyeCenterX, eyeCenterY, eyeRadius);
irisGrad.addColorStop(0, '#5fa848');
irisGrad.addColorStop(0.5, '#e0a526');
irisGrad.addColorStop(0.85, '#996312');
irisGrad.addColorStop(1, '#1b1408');
ctx.fillStyle = irisGrad;
ctx.fillRect(eyeCenterX - 15, eyeCenterY - 15, 30, 30);
// Pupil: Slit / Oval that widens when excited in higher stages
ctx.fillStyle = '#0a0908';
ctx.beginPath();
const pupilW = stg > 1 ? 5.5 : 2.8;
const pupilH = 8.5;
ctx.ellipse(eyeCenterX, eyeCenterY, pupilW, pupilH, 0, 0, Math.PI * 2);
ctx.fill();
// Realistic corneal specular reflection (mirrors bright window)
ctx.fillStyle = 'rgba(255, 255, 255, 0.92)';
ctx.beginPath();
ctx.ellipse(eyeCenterX - 3, eyeCenterY - 3, 3.2, 2.2, -0.4, 0, Math.PI * 2);
ctx.fill();
// Eyelid line & black eyeliner rim
ctx.restore();
ctx.strokeStyle = '#241a14';
ctx.lineWidth = 1.6;
ctx.beginPath();
ctx.ellipse(eyeCenterX, eyeCenterY, eyeRadius * 1.1, eyeRadius * 0.78, eyeSign * 0.12, 0, Math.PI * 2);
ctx.stroke();
// Stage 2+ Laser Eye Flare
if (stg >= 2) {
ctx.save();
const flareIntensity = Math.min(1, (stg - 1) * 0.4);
const flareGrad = ctx.createRadialGradient(eyeCenterX, eyeCenterY, 0, eyeCenterX, eyeCenterY, 35 * stg);
flareGrad.addColorStop(0, 'rgba(255, 245, 140, 1)');
flareGrad.addColorStop(0.3, `rgba(255, 180, 40, ${0.75 * flareIntensity})`);
flareGrad.addColorStop(1, 'rgba(255, 60, 0, 0)');
ctx.fillStyle = flareGrad;
ctx.beginPath();
ctx.arc(eyeCenterX, eyeCenterY, 35 * stg, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
drawEye(true);
drawEye(false);
// Translucent Sweeping Whiskers
ctx.strokeStyle = 'rgba(255, 255, 255, 0.78)';
ctx.lineWidth = 1.1;
const whiskerDrop = Math.sin(idleTime * 2) * 1.5;
for (let w = -2; w <= 2; w++) {
// Left whiskers
ctx.beginPath();
ctx.moveTo(-14 + cosAngle * 10, muzzleY + w * 3);
ctx.bezierCurveTo(
-45 + cosAngle * 10, muzzleY + w * 6 + whiskerDrop,
-75 + cosAngle * 10, muzzleY + w * 10 + 6,
-105 + cosAngle * 10, muzzleY + w * 12 + 16
);
ctx.stroke();
// Right whiskers
ctx.beginPath();
ctx.moveTo(14 + cosAngle * 10, muzzleY + w * 3);
ctx.bezierCurveTo(
45 + cosAngle * 10, muzzleY + w * 6 + whiskerDrop,
75 + cosAngle * 10, muzzleY + w * 10 + 6,
105 + cosAngle * 10, muzzleY + w * 12 + 16
);
ctx.stroke();
}
ctx.restore();
}
ctx.restore(); // end head
ctx.restore(); // end cat loaf
}
// --- Escalation FX Renderers ---
function drawStageEffects(centerX, centerY, stg, curAngle) {
if (stg < 1) return;
// Stage 1+: Motion blur radial streaks
if (stg >= 1) {
ctx.save();
ctx.strokeStyle = `rgba(255, 240, 210, ${0.12 * stg})`;
ctx.lineWidth = 1.5;
for (let i = 0; i < 18 * stg; i++) {
const a = (i / (18 * stg)) * Math.PI * 2 + curAngle * 0.3;
const r1 = 90 + Math.sin(i * 4) * 20;
const r2 = r1 + 30 + stg * 25;
ctx.beginPath();
ctx.moveTo(centerX + Math.cos(a) * r1, centerY + Math.sin(a) * (r1 * 0.45));
ctx.lineTo(centerX + Math.cos(a) * r2, centerY + Math.sin(a) * (r2 * 0.45));
ctx.stroke();
}
ctx.restore();
}
// Stage 3+: Afterimage Phantoms / Dimensional Clones
if (stg >= 3) {
ctx.save();
const cloneCount = stg === 3 ? 4 : 8;
for (let c = 1; c <= cloneCount; c++) {
const cloneAngle = curAngle - c * (0.35 / stg);
const cloneAlpha = (1 - c / (cloneCount + 1)) * 0.45;
ctx.save();
ctx.translate(centerX, centerY);
ctx.scale(1 + c * 0.04, 1 + c * 0.04);
ctx.globalAlpha = cloneAlpha;
// Ghostly rainbow spectrum shift
ctx.filter = `hue-rotate(${c * 55}deg) brightness(1.2)`;
drawCatLoaf(cloneAngle, 0, stg);
ctx.restore();
}
ctx.restore();
}
// Stage 4: Feline Singularity Apotheosis (Sacred geometry mandala, god-rays)
if (stg >= 4) {
ctx.save();
ctx.translate(centerX, centerY);
// Divine Golden Sunburst Corona
const rays = 32;
ctx.save();
for (let r = 0; r < rays; r++) {
const rayAngle = (r / rays) * Math.PI * 2 + idleTime * 1.5;
const rayLen = Math.max(width, height) * 0.85;
const grad = ctx.createLinearGradient(0, 0, Math.cos(rayAngle) * rayLen, Math.sin(rayAngle) * rayLen);
grad.addColorStop(0, 'rgba(255, 230, 120, 0.6)');
grad.addColorStop(0.4, 'rgba(255, 140, 40, 0.22)');
grad.addColorStop(1, 'rgba(255, 40, 100, 0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(Math.cos(rayAngle - 0.08) * rayLen, Math.sin(rayAngle - 0.08) * rayLen);
ctx.lineTo(Math.cos(rayAngle + 0.08) * rayLen, Math.sin(rayAngle + 0.08) * rayLen);
ctx.closePath();
ctx.fill();
}
ctx.restore();
// Concentric Sacred Rings
ctx.strokeStyle = 'rgba(255, 235, 170, 0.75)';
ctx.lineWidth = 2.5;
for (let ring = 1; ring <= 4; ring++) {
const rRadius = 140 * ring + Math.sin(idleTime * 4 + ring) * 12;
ctx.beginPath();
ctx.arc(0, 0, rRadius, 0, Math.PI * 2);
ctx.stroke();
// Satellite miniature orbiting cats
const satCount = 4 * ring;
for (let s = 0; s < satCount; s++) {
const sa = (s / satCount) * Math.PI * 2 - idleTime * 2 * (ring % 2 === 0 ? 1 : -1);
const sx = Math.cos(sa) * rRadius;
const sy = Math.sin(sa) * rRadius;
ctx.save();
ctx.translate(sx, sy);
ctx.scale(0.2, 0.2);
drawCatLoaf(curAngle * 2 + s, 0, 1);
ctx.restore();
}
}
// Relativistic Gravitational Lens Ring
const shockR = ((idleTime * 400) % (width * 0.8));
ctx.strokeStyle = `rgba(255, 255, 255, ${Math.max(0, 0.8 - shockR / (width * 0.8))})`;
ctx.lineWidth = 6;
ctx.beginPath();
ctx.arc(0, 0, shockR, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
}
// --- Animation Loop ---
let lastTime = performance.now();
function animate(now) {
requestAnimationFrame(animate);
const dt = Math.min((now - lastTime) / 1000, 0.1);
lastTime = now;
idleTime += dt;
// Smooth physics interpolation for speed & warp transitions
angularVel += (targetAngularVel - angularVel) * Math.min(1, dt * 5.5);
roomWarp += (targetRoomWarp - roomWarp) * Math.min(1, dt * 4.0);
angle += angularVel * dt;
// Camera Shake decay
shake *= Math.max(0, 1 - dt * 4.5);
const shakeX = (Math.random() - 0.5) * shake * 3;
const shakeY = (Math.random() - 0.5) * shake * 3;
// Sync Audio Engine
updateAudioVoice(angle, angularVel, stage);
// Natural idle breathing for stage 0
const breathY = stage === 0 ? Math.sin(idleTime * 2.8) * 2.2 : 0;
ctx.save();
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, width, height);
// Room camera transforms with centrifugal warp
ctx.save();
ctx.translate(width * 0.5 + shakeX, height * 0.55 + shakeY);
if (roomWarp > 0.001) {
// Centrifugal room spin & warp
const roomRoll = Math.sin(angle * 0.25) * roomWarp * 0.18;
ctx.rotate(roomRoll);
const zoom = 1 + roomWarp * 0.14;
ctx.scale(zoom, zoom);
}
ctx.translate(-width * 0.5, -height * 0.55);
// 1. Draw Living Room Environment
drawLivingRoom(angle, roomWarp, stage);
// 2. Draw Dust Motes drifting in sunlight
drawDustMotes(stage);
// 3. Draw Zero-G floating room objects in background
const catCenterX = width * 0.5;
const catCenterY = height * 0.62;
drawFloatingDebris(catCenterX, catCenterY, stage, angle);
ctx.restore(); // end room transformation
// 4. Cat Centered Render Context
ctx.save();
ctx.translate(catCenterX + shakeX, catCenterY + shakeY);
// In Stage 3/4, the cat hovers slightly off the rug
const elevation = stage >= 2 ? (stage - 1) * -18 : 0;
ctx.translate(0, elevation);
// Escalation FX Behind Cat
drawStageEffects(0, 0, stage, angle);
// Draw The Master Cat Loaf
drawCatLoaf(angle, breathY, stage);
ctx.restore();
// 5. Chromatic Aberration & Click Flash Overlays
if (stage >= 2) {
ctx.save();
ctx.globalCompositeOperation = 'screen';
ctx.globalAlpha = 0.08 * (stage - 1);
ctx.fillStyle = '#ff0055';
ctx.fillRect(4, 0, width, height);
ctx.fillStyle = '#00ffff';
ctx.fillRect(-4, 0, width, height);
ctx.restore();
}
if (clickFlash > 0.01) {
ctx.fillStyle = `rgba(255, 255, 255, ${clickFlash * 0.35})`;
ctx.fillRect(0, 0, width, height);
clickFlash *= Math.max(0, 1 - dt * 6.0);
}
ctx.restore();
}
requestAnimationFrame(animate);
})();
</script>
</body>
</html>