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,viewport-fit=cover">
<title>Oiia Cat — living room</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #15120e; }
body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
#scene {
position: fixed; inset: 0; width: 100%; height: 100%; display: block;
touch-action: manipulation; cursor: default;
}
#hud {
position: fixed; right: max(16px, env(safe-area-inset-right)); top: max(15px, env(safe-area-inset-top));
z-index: 4; display: flex; gap: 8px; align-items: center;
}
#mute {
appearance: none; -webkit-appearance: none; border: 1px solid rgba(255,255,255,.22);
background: rgba(22,19,15,.50); color: rgba(255,248,228,.86); border-radius: 999px;
padding: 8px 12px; font-size: 11px; letter-spacing: .14em; font-weight: 700;
display: flex; align-items: center; gap: 8px; cursor: pointer;
backdrop-filter: blur(9px); -webkit-backdrop-filter: blur(9px);
box-shadow: 0 8px 24px rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.08);
}
#mute:hover { background: rgba(42,34,26,.62); }
#mute .led {
width: 7px; height: 7px; border-radius: 50%; background: #e8ffbd;
box-shadow: 0 0 8px rgba(224,255,171,.8);
}
#mute.muted .led { background: #b9625a; box-shadow: 0 0 7px rgba(210,80,70,.65); }
#mute.muted { color: rgba(255,232,220,.68); }
#hint, #tag {
position: fixed; z-index: 3; pointer-events: none; color: rgba(255,244,220,.70);
text-shadow: 0 1px 5px rgba(0,0,0,.7); font-size: 11px; letter-spacing: .13em;
text-transform: uppercase; font-weight: 650;
}
#hint { left: max(18px, env(safe-area-inset-left)); bottom: max(17px, env(safe-area-inset-bottom)); opacity: .62; transition: opacity .8s ease; }
#hint.gone { opacity: 0; }
#tag { right: max(20px, env(safe-area-inset-right)); bottom: max(18px, env(safe-area-inset-bottom)); opacity: 0; transition: opacity .35s ease; }
#tag.on { opacity: .58; }
@media (max-width: 600px) {
#mute { font-size: 10px; padding: 7px 10px; }
#hint { font-size: 10px; }
}
</style>
</head>
<body>
<canvas id="scene" aria-label="A cream cat sitting in a living room. Click the cat."></canvas>
<div id="hud"><button id="mute" type="button" aria-pressed="false" aria-label="Mute sound"><span class="led"></span><span class="label">SOUND ON</span></button></div>
<div id="hint">click the cat · sound starts on click</div>
<div id="tag" aria-live="polite"></div>
<script>
(() => {
'use strict';
const TAU = Math.PI * 2;
const SW = 460, SH = 600;
const canvas = document.getElementById('scene');
const ctx = canvas.getContext('2d', { alpha: false });
const muteBtn = document.getElementById('mute');
const hint = document.getElementById('hint');
const tag = document.getElementById('tag');
let W = 0, H = 0, DPR = 1;
let roomCanvas = null, noiseCanvas = null, frontCat = null, backCat = null;
let stage = 0, overdrive = 0, active = false, muted = false, audioArmed = false;
let angle = -0.18, omega = 0, targetOmega = 0;
let lift = 0, targetLift = 0, chaos = 0, targetChaos = 0, targetVolume = 0;
let stageKick = 0, time = 0, last = performance.now();
let geomCache = null;
const STAGE_INFO = [
null,
{ omega: 20, lift: .055, chaos: .25, vol: .54, tag: 'I · LIFT-OFF / LENS' },
{ omega: 38, lift: .15, chaos: .55, vol: .68, tag: 'II · ROOM SPIN' },
{ omega: 63, lift: .28, chaos: .82, vol: .82, tag: 'III · GRAVITY ERROR' },
{ omega: 92, lift: .39, chaos: 1.08, vol: .96, tag: 'IV · OIIA EVENT' }
];
const audio = {
ctx: null, master: null, comp: null, voiceBus: null, panner: null,
spinLfo: null, spinAMGain: null, panLfo: null, panGain: null,
noise: null, noiseFilter: null, noiseGain: null, noiseBuffer: null,
nextWordTime: 0, patternIndex: 0,
pattern: ['oiia', 'oiia', 'oiiaiooooiai']
};
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const mix = (a, b, t) => a + (b - a) * t;
const fract = x => x - Math.floor(x);
const hash = n => fract(Math.sin(n * 12.9898 + 78.233) * 43758.5453);
const rgba = (r, g, b, a) => `rgba(${Math.round(r)},${Math.round(g)},${Math.round(b)},${clamp(a,0,1).toFixed(3)})`;
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 rrect(c, x, y, w, h, r) {
r = Math.max(0, Math.min(r, Math.abs(w) / 2, Math.abs(h) / 2));
c.beginPath();
c.moveTo(x + r, y);
c.lineTo(x + w - r, y);
c.quadraticCurveTo(x + w, y, x + w, y + r);
c.lineTo(x + w, y + h - r);
c.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
c.lineTo(x + r, y + h);
c.quadraticCurveTo(x, y + h, x, y + h - r);
c.lineTo(x, y + r);
c.quadraticCurveTo(x, y, x + r, y);
c.closePath();
}
function fillRRect(c, x, y, w, h, r, fill) {
rrect(c, x, y, w, h, r);
c.fillStyle = fill;
c.fill();
}
function poly(c, pts, fill) {
c.beginPath();
c.moveTo(pts[0][0], pts[0][1]);
for (let i = 1; i < pts.length; i++) c.lineTo(pts[i][0], pts[i][1]);
c.closePath();
if (fill) { c.fillStyle = fill; c.fill(); }
}
function softEllipse(c, x, y, rx, ry, r, g, b, a) {
c.save();
c.translate(x, y);
c.scale(Math.max(1, rx), Math.max(1, ry));
const gr = c.createRadialGradient(0, 0, 0, 0, 0, 1);
gr.addColorStop(0, rgba(r, g, b, a));
gr.addColorStop(.48, rgba(r, g, b, a * .46));
gr.addColorStop(1, rgba(r, g, b, 0));
c.fillStyle = gr;
c.beginPath();
c.arc(0, 0, 1, 0, TAU);
c.fill();
c.restore();
}
function resize() {
DPR = Math.min(2, window.devicePixelRatio || 1);
W = Math.max(320, window.innerWidth);
H = Math.max(320, window.innerHeight);
canvas.width = Math.floor(W * DPR);
canvas.height = Math.floor(H * DPR);
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
makeNoise();
makeRoom();
geomCache = getGeom();
}
function makeNoise() {
const s = 180;
noiseCanvas = document.createElement('canvas');
noiseCanvas.width = noiseCanvas.height = s;
const n = noiseCanvas.getContext('2d');
const im = n.createImageData(s, s);
const rng = mulberry32(99117);
for (let i = 0; i < im.data.length; i += 4) {
const q = 92 + rng() * 150;
im.data[i] = im.data[i + 1] = im.data[i + 2] = q;
im.data[i + 3] = 12 + rng() * 44;
}
n.putImageData(im, 0, 0);
}
function makeRoom() {
roomCanvas = document.createElement('canvas');
roomCanvas.width = Math.floor(W * DPR);
roomCanvas.height = Math.floor(H * DPR);
const r = roomCanvas.getContext('2d');
r.setTransform(DPR, 0, 0, DPR, 0, 0);
r.imageSmoothingEnabled = true;
r.imageSmoothingQuality = 'high';
drawRoom(r);
}
function drawRoom(g) {
const rng = mulberry32(22117);
const horizon = H * .43, floorTop = H * .50;
const wall = g.createLinearGradient(0, 0, W, floorTop);
wall.addColorStop(0, '#9f998c');
wall.addColorStop(.36, '#d5c9ae');
wall.addColorStop(.72, '#e3d8bf');
wall.addColorStop(1, '#a99f8b');
g.fillStyle = wall;
g.fillRect(0, 0, W, floorTop + 65);
const ceiling = g.createLinearGradient(0, 0, 0, H * .11);
ceiling.addColorStop(0, 'rgba(62,56,48,.42)');
ceiling.addColorStop(1, 'rgba(255,245,220,0)');
g.fillStyle = ceiling;
g.fillRect(0, 0, W, H * .11);
g.save();
g.globalAlpha = .055;
for (let x = -W; x < W * 2; x += 34) {
g.strokeStyle = x % 68 ? '#665c4f' : '#fff8e5';
g.lineWidth = x % 68 ? 1 : .7;
g.beginPath(); g.moveTo(x, 0); g.lineTo(x - 24, floorTop + 40); g.stroke();
}
for (let i = 0; i < 900; i++) {
const x = rng() * W, y = rng() * floorTop;
g.fillStyle = rng() > .5 ? 'rgba(255,255,255,.10)' : 'rgba(40,32,22,.07)';
g.fillRect(x, y, rng() * 2 + .3, rng() * 2 + .3);
}
g.restore();
drawWindow(g, W * .70, H * .065, W * .245, H * .42, rng);
drawPicture(g, W * .10, H * .105, W * .165, H * .185, rng, true);
drawPicture(g, W * .335, H * .13, W * .115, H * .135, rng, false);
drawFloor(g, horizon, floorTop, rng);
const base = g.createLinearGradient(0, floorTop - 12, 0, floorTop + 25);
base.addColorStop(0, '#efe6d0');
base.addColorStop(.45, '#c4b79e');
base.addColorStop(1, '#776b5d');
g.fillStyle = base;
g.fillRect(0, floorTop - 2, W, 19);
g.fillStyle = 'rgba(45,37,29,.22)';
g.fillRect(0, floorTop + 15, W, 4);
drawRug(g, W * .50, H * .805, W * .34, H * .125, rng);
drawFloorLamp(g, W * .115, H * .22, rng);
drawSofa(g, rng);
drawSideTable(g, W * .83, H * .60, rng);
drawPlant(g, W * .90, H * .55, rng);
drawRoomBeams(g);
const vignette = g.createRadialGradient(W * .47, H * .47, Math.min(W, H) * .19, W * .5, H * .54, Math.max(W, H) * .78);
vignette.addColorStop(0, 'rgba(0,0,0,0)');
vignette.addColorStop(.63, 'rgba(24,15,7,.06)');
vignette.addColorStop(1, 'rgba(10,7,4,.56)');
g.fillStyle = vignette;
g.fillRect(0, 0, W, H);
if (noiseCanvas) {
g.save();
g.globalAlpha = .038;
g.fillStyle = g.createPattern(noiseCanvas, 'repeat');
g.fillRect(0, 0, W, H);
g.restore();
}
}
function drawWindow(g, x, y, w, h, rng) {
g.save();
softEllipse(g, x + w * .5, y + h * .56, w * .95, h * .9, 255, 244, 205, .24);
fillRRect(g, x + 10, y + 12, w, h, 4, 'rgba(35,27,20,.30)');
fillRRect(g, x, y, w, h, 3, '#635548');
const sky = g.createLinearGradient(x, y, x, y + h);
sky.addColorStop(0, '#e8f5ff');
sky.addColorStop(.42, '#b9d7e9');
sky.addColorStop(1, '#eadbbc');
fillRRect(g, x + 9, y + 9, w - 18, h - 18, 2, sky);
g.save();
rrect(g, x + 9, y + 9, w - 18, h - 18, 2);
g.clip();
g.globalAlpha = .46;
for (let i = 0; i < 11; i++) {
const tx = x + 12 + rng() * (w - 24);
const ty = y + h * (.50 + rng() * .28);
g.fillStyle = i % 2 ? 'rgba(66,91,69,.48)' : 'rgba(72,82,66,.34)';
g.beginPath();
g.ellipse(tx, ty, w * (.035 + rng() * .055), h * (.12 + rng() * .10), 0, 0, TAU);
g.fill();
}
g.globalAlpha = .65;
g.strokeStyle = 'rgba(255,255,255,.88)';
g.lineWidth = 1.2;
for (let i = 0; i < 4; i++) {
g.beginPath();
g.moveTo(x + 14, y + h * (.15 + i * .10));
g.bezierCurveTo(x + w * .35, y + h * (.10 + i * .11), x + w * .65, y + h * (.20 + i * .09), x + w - 14, y + h * (.14 + i * .10));
g.stroke();
}
const glare = g.createLinearGradient(x + w * .08, y, x + w * .90, y + h);
glare.addColorStop(0, 'rgba(255,255,255,.68)');
glare.addColorStop(.28, 'rgba(255,255,255,.10)');
glare.addColorStop(.70, 'rgba(255,255,240,0)');
g.fillStyle = glare;
g.fillRect(x + 8, y + 8, w - 16, h - 16);
g.restore();
g.fillStyle = '#7a6857';
g.fillRect(x + w * .48, y + 8, 8, h - 16);
g.fillRect(x + 8, y + h * .49, w - 16, 8);
g.fillStyle = '#eee2cc';
g.fillRect(x + 3, y + 3, w - 6, 5);
g.fillRect(x + 3, y + h - 8, w - 6, 5);
g.fillStyle = 'rgba(42,31,22,.35)';
g.fillRect(x + 3, y + h - 3, w - 6, 10);
fillRRect(g, x - 7, y + h - 1, w + 14, 13, 3, '#cfc3ab');
g.save();
g.globalAlpha = .54;
const curtain = g.createLinearGradient(x - w * .16, y, x + w * .10, y + h);
curtain.addColorStop(0, 'rgba(255,251,235,.86)');
curtain.addColorStop(1, 'rgba(214,202,177,.10)');
g.fillStyle = curtain;
g.beginPath();
g.moveTo(x - w * .12, y - 5);
g.bezierCurveTo(x + w * .02, y + h * .25, x - w * .09, y + h * .65, x + w * .05, y + h + 8);
g.lineTo(x - w * .20, y + h + 8);
g.lineTo(x - w * .20, y - 5);
g.closePath();
g.fill();
g.restore();
g.restore();
}
function drawPicture(g, x, y, w, h, rng, withCat) {
g.save();
g.translate(x + w / 2, y + h / 2);
g.rotate((rng() - .5) * .035);
g.translate(-w / 2, -h / 2);
fillRRect(g, 5, 7, w, h, 3, 'rgba(34,25,17,.30)');
fillRRect(g, 0, 0, w, h, 2, '#4b3a2c');
fillRRect(g, 7, 7, w - 14, h - 14, 1, '#d9cbaa');
const inner = g.createLinearGradient(9, 9, w - 9, h - 9);
inner.addColorStop(0, '#b7c2b1');
inner.addColorStop(.55, '#d6c6a9');
inner.addColorStop(1, '#786e59');
fillRRect(g, 10, 10, w - 20, h - 20, 1, inner);
g.save();
rrect(g, 10, 10, w - 20, h - 20, 1);
g.clip();
g.fillStyle = 'rgba(54,63,57,.28)';
g.beginPath();
g.moveTo(10, h * .66);
g.quadraticCurveTo(w * .35, h * .42, w * .63, h * .70);
g.lineTo(w, h * .82);
g.lineTo(w, h);
g.lineTo(0, h);
g.closePath();
g.fill();
if (withCat) {
g.fillStyle = '#e9d8ae';
g.beginPath(); g.ellipse(w * .53, h * .67, w * .13, h * .23, 0, 0, TAU); g.fill();
g.beginPath(); g.ellipse(w * .53, h * .40, w * .12, h * .16, 0, 0, TAU); g.fill();
g.beginPath(); g.moveTo(w * .44, h * .32); g.lineTo(w * .43, h * .20); g.lineTo(w * .52, h * .29); g.fill();
g.beginPath(); g.moveTo(w * .62, h * .32); g.lineTo(w * .64, h * .20); g.lineTo(w * .54, h * .29); g.fill();
g.fillStyle = '#29251f';
g.beginPath(); g.ellipse(w * .49, h * .40, 2, 4, 0, 0, TAU); g.fill();
g.beginPath(); g.ellipse(w * .57, h * .40, 2, 4, 0, 0, TAU); g.fill();
}
g.restore();
g.strokeStyle = 'rgba(255,255,255,.18)';
g.lineWidth = 1;
g.strokeRect(3, 3, w - 6, h - 6);
g.restore();
}
function drawFloor(g, horizon, floorTop, rng) {
const floor = g.createLinearGradient(0, floorTop, 0, H);
floor.addColorStop(0, '#8e775c');
floor.addColorStop(.30, '#b9976e');
floor.addColorStop(.72, '#8b6a4a');
floor.addColorStop(1, '#4f3b2c');
g.fillStyle = floor;
g.fillRect(0, floorTop + 17, W, H - floorTop);
const vanishX = W * .43, vanishY = horizon + 12;
g.save();
g.lineWidth = 1;
for (let i = -12; i < 24; i++) {
const bx = i * W / 9;
g.strokeStyle = i % 2 ? 'rgba(44,28,18,.22)' : 'rgba(255,226,180,.08)';
g.beginPath(); g.moveTo(vanishX, vanishY); g.lineTo(bx, H + 20); g.stroke();
}
let y = floorTop + 32;
while (y < H + 20) {
g.strokeStyle = 'rgba(49,32,20,.20)';
g.lineWidth = 1 + (y - floorTop) / H;
g.beginPath(); g.moveTo(0, y); g.lineTo(W, y + Math.sin(y * .013) * 3); g.stroke();
g.strokeStyle = 'rgba(255,231,196,.055)';
g.beginPath(); g.moveTo(0, y + 2); g.lineTo(W, y + 5); g.stroke();
y += 18 + (y - floorTop) * .105;
}
for (let i = 0; i < 1400; i++) {
const px = rng() * W, py = floorTop + rng() * (H - floorTop);
g.fillStyle = rng() > .5 ? 'rgba(255,230,190,.045)' : 'rgba(30,18,10,.055)';
g.fillRect(px, py, 1 + rng() * 3, .5 + rng());
}
g.restore();
g.save();
g.globalCompositeOperation = 'screen';
const patch = g.createLinearGradient(W * .78, horizon, W * .23, H);
patch.addColorStop(0, 'rgba(255,238,190,.32)');
patch.addColorStop(.55, 'rgba(255,224,166,.13)');
patch.addColorStop(1, 'rgba(255,220,160,0)');
poly(g, [[W * .69, floorTop - 8], [W, floorTop + H * .18], [W * .70, H], [W * .18, H * .87], [W * .32, H * .68]], patch);
g.restore();
}
function drawRug(g, cx, cy, rx, ry, rng) {
g.save();
softEllipse(g, cx + 8, cy + 22, rx * .96, ry * .74, 20, 14, 9, .34);
const rg = g.createRadialGradient(cx - rx * .2, cy - ry * .4, 5, cx, cy, rx);
rg.addColorStop(0, '#c8b79a');
rg.addColorStop(.52, '#9b8064');
rg.addColorStop(.85, '#725943');
rg.addColorStop(1, '#4b382b');
g.fillStyle = rg;
g.beginPath(); g.ellipse(cx, cy, rx, ry, -.025, 0, TAU); g.fill();
g.save();
g.beginPath(); g.ellipse(cx, cy, rx, ry, -.025, 0, TAU); g.clip();
g.globalAlpha = .38;
for (let i = 0; i < 190; i++) {
const a = rng() * TAU, rr = Math.sqrt(rng());
const x = cx + Math.cos(a) * rx * rr, y = cy + Math.sin(a) * ry * rr;
g.fillStyle = rng() > .5 ? '#e5d0aa' : '#433127';
g.fillRect(x, y, 2 + rng() * 5, 1 + rng() * 2);
}
g.globalAlpha = .5;
for (let k = 1; k <= 4; k++) {
g.strokeStyle = k % 2 ? 'rgba(230,204,160,.45)' : 'rgba(63,45,31,.40)';
g.lineWidth = k === 2 ? 4 : 1.5;
g.beginPath(); g.ellipse(cx, cy, rx * (1 - k * .13), ry * (1 - k * .13), -.025, 0, TAU); g.stroke();
}
for (let i = 0; i < 44; i++) {
const x = cx - rx + i * rx * 2 / 43;
g.strokeStyle = 'rgba(226,204,167,.32)';
g.lineWidth = 1;
g.beginPath(); g.moveTo(x, cy + ry * .86); g.lineTo(x + Math.sin(i) * 3, cy + ry * 1.10); g.stroke();
g.beginPath(); g.moveTo(x, cy - ry * .86); g.lineTo(x - Math.sin(i) * 3, cy - ry * 1.10); g.stroke();
}
g.restore();
g.restore();
}
function drawSofa(g, rng) {
const x = W * .025, y = H * .395, w = W * .675, h = H * .305;
softEllipse(g, x + w * .48, y + h * .93, w * .48, h * .12, 20, 14, 10, .34);
const back = g.createLinearGradient(x, y, x + w, y + h);
back.addColorStop(0, '#6f6a61');
back.addColorStop(.32, '#a49b8b');
back.addColorStop(.72, '#827a6e');
back.addColorStop(1, '#585248');
fillRRect(g, x + 14, y, w - 28, h * .64, 34, back);
g.save();
rrect(g, x + 14, y, w - 28, h * .64, 34);
g.clip();
for (let i = 0; i < 1250; i++) {
const px = x + 14 + rng() * (w - 28), py = y + rng() * h * .64;
g.fillStyle = rng() > .5 ? 'rgba(255,250,230,.055)' : 'rgba(20,18,15,.055)';
g.fillRect(px, py, 1 + rng() * 3, .5 + rng() * 2);
}
g.restore();
const seat = g.createLinearGradient(0, y + h * .48, 0, y + h);
seat.addColorStop(0, '#b4aa98');
seat.addColorStop(.45, '#8f8778');
seat.addColorStop(1, '#625d53');
fillRRect(g, x, y + h * .40, w, h * .48, 27, seat);
g.strokeStyle = 'rgba(45,40,34,.26)';
g.lineWidth = 2;
for (let i = 1; i < 3; i++) {
const sx = x + w * i / 3;
g.beginPath(); g.moveTo(sx, y + h * .42); g.bezierCurveTo(sx - 8, y + h * .58, sx + 8, y + h * .73, sx, y + h * .86); g.stroke();
}
fillRRect(g, x - W * .012, y + h * .30, W * .095, h * .62, 26, '#777065');
fillRRect(g, x + w - W * .075, y + h * .29, W * .09, h * .62, 26, '#716b60');
g.fillStyle = '#3c3129';
g.fillRect(x + W * .035, y + h * .83, 13, h * .18);
g.fillRect(x + w - W * .05, y + h * .83, 13, h * .18);
g.save();
g.translate(x + w * .70, y + h * .31);
g.rotate(-.19);
const pillow = g.createLinearGradient(-40, -35, 55, 50);
pillow.addColorStop(0, '#c5a66e');
pillow.addColorStop(1, '#715840');
fillRRect(g, -55, -48, 110, 95, 18, pillow);
g.strokeStyle = 'rgba(255,236,190,.22)';
g.strokeRect(-47, -40, 94, 79);
g.restore();
g.save();
g.globalAlpha = .36;
g.strokeStyle = '#d8c6a3';
g.lineWidth = 3;
g.beginPath();
g.moveTo(x + w * .05, y + h * .73);
g.bezierCurveTo(x + w * .12, y + h * .54, x + w * .19, y + h * .82, x + w * .25, y + h * .63);
g.stroke();
g.restore();
}
function drawFloorLamp(g, x, y, rng) {
softEllipse(g, x, y + H * .135, W * .11, H * .18, 255, 188, 92, .18);
g.strokeStyle = '#42362a';
g.lineWidth = 5;
g.beginPath(); g.moveTo(x, y + H * .08); g.lineTo(x, y + H * .48); g.stroke();
g.fillStyle = '#3d3229';
g.beginPath(); g.ellipse(x, y + H * .49, W * .045, H * .018, 0, 0, TAU); g.fill();
const shade = g.createLinearGradient(x - W * .06, y, x + W * .06, y);
shade.addColorStop(0, '#80694d');
shade.addColorStop(.50, '#e1c994');
shade.addColorStop(1, '#796349');
poly(g, [[x - W * .038, y], [x + W * .038, y], [x + W * .075, y + H * .11], [x - W * .075, y + H * .11]], shade);
g.fillStyle = 'rgba(255,230,170,.45)';
g.beginPath(); g.ellipse(x, y + H * .11, W * .075, H * .012, 0, 0, TAU); g.fill();
g.save();
g.globalAlpha = .06;
g.strokeStyle = '#3a2b1f';
for (let i = 0; i < 30; i++) {
const yy = y + H * .015 + rng() * H * .095;
g.beginPath(); g.moveTo(x - W * .065, yy); g.lineTo(x + W * .065, yy + 2); g.stroke();
}
g.restore();
}
function drawSideTable(g, x, y, rng) {
g.save();
softEllipse(g, x, y + H * .16, W * .085, H * .035, 15, 10, 7, .24);
fillRRect(g, x - W * .075, y, W * .15, H * .035, 4, '#6f4e34');
g.fillStyle = '#3b2a20';
g.fillRect(x - W * .061, y + H * .03, 8, H * .14);
g.fillRect(x + W * .052, y + H * .03, 8, H * .14);
for (let i = 0; i < 3; i++) {
const bw = W * (.045 + rng() * .018);
fillRRect(g, x - bw / 2 + (rng() - .5) * 12, y - H * (.009 + i * .014), bw, H * .010, 2, ['#7b3f35', '#6b6c4a', '#8a6f4b'][i]);
}
g.fillStyle = '#d4c2a2';
g.beginPath(); g.ellipse(x + W * .045, y - H * .008, W * .018, H * .012, 0, 0, TAU); g.fill();
g.strokeStyle = '#b8a184'; g.lineWidth = 3;
g.beginPath(); g.arc(x + W * .063, y - H * .011, W * .010, -.3, Math.PI * 1.15); g.stroke();
g.restore();
}
function drawPlant(g, x, y, rng) {
g.save();
softEllipse(g, x, y + H * .19, W * .07, H * .035, 16, 12, 7, .30);
const pot = g.createLinearGradient(x - 35, y, x + 35, y + H * .16);
pot.addColorStop(0, '#a76f49');
pot.addColorStop(.55, '#75472f');
pot.addColorStop(1, '#4c2e22');
poly(g, [[x - W * .038, y + H * .06], [x + W * .038, y + H * .06], [x + W * .028, y + H * .17], [x - W * .028, y + H * .17]], pot);
for (let i = 0; i < 26; i++) {
const a = -Math.PI * .5 + (rng() - .5) * 2.25;
const len = H * (.12 + rng() * .16);
const px = x + Math.cos(a) * len * .54, py = y + H * .045 + Math.sin(a) * len;
const leaf = g.createLinearGradient(x, y, px, py);
leaf.addColorStop(0, '#314c32');
leaf.addColorStop(1, rng() > .5 ? '#78945a' : '#4d7043');
g.fillStyle = leaf;
g.beginPath();
g.moveTo(x, y + H * .06);
g.quadraticCurveTo((x + px) * .5 + (rng() - .5) * 30, (y + py) * .5, px, py);
g.quadraticCurveTo((x + px) * .5 - (rng() - .5) * 30, (y + py) * .5 + 14, x, y + H * .06);
g.fill();
g.strokeStyle = 'rgba(224,236,177,.14)';
g.lineWidth = 1;
g.beginPath(); g.moveTo(x, y + H * .06); g.lineTo(px, py); g.stroke();
}
g.restore();
}
function drawRoomBeams(g) {
g.save();
g.globalCompositeOperation = 'screen';
const beam = g.createLinearGradient(W * .78, H * .08, W * .18, H);
beam.addColorStop(0, 'rgba(255,249,220,.26)');
beam.addColorStop(.55, 'rgba(255,232,184,.10)');
beam.addColorStop(1, 'rgba(255,230,185,0)');
poly(g, [[W * .70, H * .08], [W * .94, H * .08], [W * .43, H], [W * .04, H * .83]], beam);
const beam2 = g.createLinearGradient(W * .72, H * .12, W * .65, H);
beam2.addColorStop(0, 'rgba(255,255,235,.18)');
beam2.addColorStop(1, 'rgba(255,244,210,0)');
poly(g, [[W * .72, H * .16], [W * .77, H * .16], [W * .62, H], [W * .38, H]], beam2);
g.restore();
}
function makeCatSprites() {
frontCat = document.createElement('canvas');
frontCat.width = SW * 2; frontCat.height = SH * 2;
const f = frontCat.getContext('2d');
f.setTransform(2, 0, 0, 2, 0, 0);
f.imageSmoothingEnabled = true;
drawCatFront(f);
backCat = document.createElement('canvas');
backCat.width = SW * 2; backCat.height = SH * 2;
const b = backCat.getContext('2d');
b.setTransform(2, 0, 0, 2, 0, 0);
b.imageSmoothingEnabled = true;
drawCatBack(b);
}
function pathBody(c) {
c.beginPath();
c.moveTo(230, 240);
c.bezierCurveTo(155, 240, 111, 322, 102, 426);
c.bezierCurveTo(93, 517, 139, 568, 230, 568);
c.bezierCurveTo(321, 568, 367, 517, 358, 426);
c.bezierCurveTo(349, 322, 305, 240, 230, 240);
c.closePath();
}
function pathHead(c) {
c.beginPath();
c.moveTo(230, 82);
c.bezierCurveTo(150, 82, 101, 135, 96, 212);
c.bezierCurveTo(90, 283, 143, 337, 230, 340);
c.bezierCurveTo(317, 337, 370, 283, 364, 212);
c.bezierCurveTo(359, 135, 310, 82, 230, 82);
c.closePath();
}
function pathLeftEar(c) {
c.beginPath();
c.moveTo(129, 170);
c.lineTo(107, 46);
c.quadraticCurveTo(110, 35, 124, 44);
c.lineTo(190, 101);
c.quadraticCurveTo(157, 121, 129, 170);
c.closePath();
}
function pathRightEar(c) {
c.beginPath();
c.moveTo(331, 170);
c.lineTo(353, 46);
c.quadraticCurveTo(350, 35, 336, 44);
c.lineTo(270, 101);
c.quadraticCurveTo(303, 121, 331, 170);
c.closePath();
}
function drawTail(c, rng, front) {
c.save();
c.lineCap = 'round'; c.lineJoin = 'round';
c.lineWidth = 66;
const tail = c.createLinearGradient(320, 490, 420, 350);
tail.addColorStop(0, front ? '#e5d0aa' : '#cdb58d');
tail.addColorStop(.55, '#f4e8ce');
tail.addColorStop(1, '#a87849');
c.strokeStyle = tail;
c.beginPath();
c.moveTo(326, 474);
c.bezierCurveTo(410, 486, 429, 402, 383, 360);
c.stroke();
c.lineWidth = 1.1;
for (let i = 0; i < 130; i++) {
const t = rng(), x = 326 + 100 * t + Math.sin(t * 4) * 32 + (rng() - .5) * 45;
const y = 474 - 105 * t + (rng() - .5) * 55;
c.strokeStyle = rng() > .48 ? 'rgba(255,250,230,.13)' : 'rgba(112,77,43,.09)';
c.beginPath(); c.moveTo(x, y); c.lineTo(x + 6 + rng() * 11, y - 5 - rng() * 8); c.stroke();
}
c.restore();
}
function furTexture(c, pathFn, box, count, rng, ang, spread) {
c.save();
pathFn(c); c.clip();
for (let i = 0; i < count / 16; i++) {
c.fillStyle = rng() > .5 ? 'rgba(255,255,240,.025)' : 'rgba(141,101,58,.025)';
c.beginPath();
c.ellipse(box[0] + rng() * box[2], box[1] + rng() * box[3], 8 + rng() * 38, 5 + rng() * 22, rng() * TAU, 0, TAU);
c.fill();
}
for (let i = 0; i < count; i++) {
const x = box[0] + rng() * box[2], y = box[1] + rng() * box[3];
const a = ang + (rng() - .5) * spread;
const len = 3 + rng() * 15;
c.strokeStyle = rng() > .48 ? `rgba(255,252,232,${.045 + rng() * .18})` : `rgba(133,95,52,${.025 + rng() * .10})`;
c.lineWidth = .45 + rng() * 1.15;
c.beginPath();
c.moveTo(x, y);
c.quadraticCurveTo(x + Math.cos(a) * len * .5 + (rng() - .5) * 3, y + Math.sin(a) * len * .5, x + Math.cos(a) * len, y + Math.sin(a) * len);
c.stroke();
}
c.restore();
}
function furOutline(c, cx, cy, rx, ry, count, rng) {
c.save();
c.lineWidth = 1;
for (let i = 0; i < count; i++) {
const a = rng() * TAU;
const x = cx + Math.cos(a) * rx * (.97 + rng() * .06);
const y = cy + Math.sin(a) * ry * (.97 + rng() * .06);
const len = 4 + rng() * 13;
c.strokeStyle = rng() > .5 ? 'rgba(255,250,231,.27)' : 'rgba(151,111,66,.12)';
c.beginPath();
c.moveTo(x, y);
c.quadraticCurveTo(x + Math.cos(a) * len * .5, y + Math.sin(a) * len * .5, x + Math.cos(a) * len, y + Math.sin(a) * len);
c.stroke();
}
c.restore();
}
function drawEar(c, side, front, rng) {
const p = side === 'left' ? pathLeftEar : pathRightEar;
c.save();
p(c);
const grad = c.createLinearGradient(0, 40, 0, 180);
grad.addColorStop(0, front ? '#c79b69' : '#b89568');
grad.addColorStop(.55, '#f1e2c2');
grad.addColorStop(1, '#b18a5f');
c.fillStyle = grad;
c.fill();
p(c); c.clip();
const inner = c.createRadialGradient(side === 'left' ? 145 : 315, 90, 5, side === 'left' ? 145 : 315, 105, 85);
inner.addColorStop(0, front ? 'rgba(232,151,139,.86)' : 'rgba(180,124,103,.62)');
inner.addColorStop(1, 'rgba(128,76,62,.12)');
c.fillStyle = inner;
c.beginPath();
if (side === 'left') {
c.moveTo(127, 150); c.lineTo(119, 68); c.lineTo(173, 109); c.closePath();
} else {
c.moveTo(333, 150); c.lineTo(341, 68); c.lineTo(287, 109); c.closePath();
}
c.fill();
c.lineWidth = 1;
for (let i = 0; i < 90; i++) {
const x = (side === 'left' ? 110 : 275) + rng() * 75, y = 48 + rng() * 118;
c.strokeStyle = rng() > .5 ? 'rgba(255,248,222,.18)' : 'rgba(116,75,43,.10)';
c.beginPath(); c.moveTo(x, y); c.lineTo(x + (side === 'left' ? -1 : 1) * (4 + rng() * 8), y - 3 - rng() * 7); c.stroke();
}
c.restore();
}
function drawPaws(c, rng, front) {
for (const x of [170, 290]) {
c.save();
rrect(c, x - 49, 468, 98, 100, 42);
const paw = c.createRadialGradient(x - 14, 488, 4, x, 520, 75);
paw.addColorStop(0, front ? '#fffbed' : '#e2c99f');
paw.addColorStop(.65, front ? '#f2e2c5' : '#d5bd94');
paw.addColorStop(1, '#b18d62');
c.fillStyle = paw;
c.fill();
rrect(c, x - 49, 468, 98, 100, 42); c.clip();
for (let i = 0; i < 340; i++) {
const px = x - 49 + rng() * 98, py = 468 + rng() * 100;
c.strokeStyle = rng() > .5 ? 'rgba(255,255,238,.18)' : 'rgba(124,87,48,.08)';
c.lineWidth = .6 + rng();
c.beginPath(); c.moveTo(px, py); c.lineTo(px + 3 + rng() * 7, py + 4 + rng() * 8); c.stroke();
}
c.strokeStyle = 'rgba(111,77,46,.25)';
c.lineWidth = 1.4;
for (let k = -1; k <= 1; k++) {
c.beginPath();
c.moveTo(x + k * 23, 526);
c.quadraticCurveTo(x + k * 23 + 3, 538, x + k * 23, 548);
c.stroke();
}
c.restore();
}
}
function drawEye(c, x, y) {
c.save();
c.translate(x, y);
softEllipse(c, 0, 5, 45, 48, 35, 24, 15, .24);
c.fillStyle = '#eee6c9';
c.beginPath(); c.ellipse(0, 0, 37, 43, 0, 0, TAU); c.fill();
const iris = c.createRadialGradient(-7, -10, 3, 0, 0, 31);
iris.addColorStop(0, '#e7d97a');
iris.addColorStop(.34, '#a8b55b');
iris.addColorStop(.72, '#5f7943');
iris.addColorStop(1, '#26351e');
c.fillStyle = iris;
c.beginPath(); c.ellipse(0, 0, 28, 38, 0, 0, TAU); c.fill();
c.strokeStyle = 'rgba(33,28,18,.75)';
c.lineWidth = 2;
c.beginPath(); c.ellipse(0, 0, 28, 38, 0, 0, TAU); c.stroke();
c.fillStyle = '#070807';
c.beginPath(); c.ellipse(0, 0, 10.5, 29, 0, 0, TAU); c.fill();
c.fillStyle = 'rgba(255,255,246,.96)';
rrect(c, -13, -18, 10, 14, 3, 'rgba(255,255,246,.96)');
c.beginPath(); c.arc(12, 13, 3, 0, TAU); c.fill();
c.fillStyle = 'rgba(255,255,255,.30)';
c.beginPath(); c.ellipse(-11, 20, 7, 3, -.25, 0, TAU); c.fill();
c.strokeStyle = 'rgba(69,49,29,.48)';
c.lineWidth = 2;
c.beginPath(); c.arc(0, 0, 38, Math.PI * 1.07, Math.PI * 1.93); c.stroke();
c.restore();
}
function drawWhiskers(c) {
c.save();
c.lineWidth = 1.4;
for (let side of [-1, 1]) {
for (let i = -1; i <= 1; i++) {
const sx = 230 + side * 48, sy = 272 + i * 8;
const ex = 230 + side * (160 + 10 * Math.abs(i)), ey = 248 + i * 27;
c.strokeStyle = 'rgba(40,30,22,.25)';
c.beginPath(); c.moveTo(sx, sy + 1); c.bezierCurveTo(230 + side * 95, sy - 8 + i * 3, 230 + side * 135, ey - 2, ex, ey + 1); c.stroke();
c.strokeStyle = 'rgba(255,252,237,.70)';
c.beginPath(); c.moveTo(sx, sy); c.bezierCurveTo(230 + side * 95, sy - 9 + i * 3, 230 + side * 135, ey - 3, ex, ey); c.stroke();
}
}
c.restore();
}
function drawCatFront(c) {
const rng = mulberry32(77001);
c.clearRect(0, 0, SW, SH);
drawTail(c, rng, true);
pathBody(c);
const body = c.createRadialGradient(186, 278, 30, 230, 430, 300);
body.addColorStop(0, '#fffdf1');
body.addColorStop(.50, '#f5e7cc');
body.addColorStop(.79, '#dcc394');
body.addColorStop(1, '#9d7448');
c.fillStyle = body; c.fill();
furTexture(c, pathBody, [96, 238, 270, 335], 2400, rng, 1.36, 1.15);
c.save();
pathBody(c); c.clip();
const lit = c.createLinearGradient(95, 260, 360, 520);
lit.addColorStop(0, 'rgba(80,55,34,.13)');
lit.addColorStop(.52, 'rgba(255,252,229,.15)');
lit.addColorStop(1, 'rgba(70,45,27,.06)');
c.fillStyle = lit; c.fillRect(80, 220, 310, 360);
c.restore();
drawEar(c, 'left', true, rng);
drawEar(c, 'right', true, rng);
pathHead(c);
const head = c.createRadialGradient(176, 130, 25, 230, 232, 240);
head.addColorStop(0, '#fffef4');
head.addColorStop(.46, '#f8ecd3');
head.addColorStop(.76, '#e2c79c');
head.addColorStop(1, '#a97c4e');
c.fillStyle = head; c.fill();
furTexture(c, pathHead, [92, 80, 276, 266], 3000, rng, 1.20, 1.30);
c.save();
pathHead(c); c.clip();
const sideLight = c.createLinearGradient(90, 100, 370, 310);
sideLight.addColorStop(0, 'rgba(58,41,27,.12)');
sideLight.addColorStop(.45, 'rgba(255,255,239,.10)');
sideLight.addColorStop(1, 'rgba(255,250,225,.22)');
c.fillStyle = sideLight; c.fillRect(80, 70, 300, 290);
c.strokeStyle = 'rgba(166,117,69,.14)';
c.lineWidth = 13;
for (let i = -1; i <= 1; i++) {
c.beginPath(); c.moveTo(230 + i * 30, 103); c.quadraticCurveTo(230 + i * 38, 137, 230 + i * 27, 163); c.stroke();
}
c.restore();
furOutline(c, 230, 213, 135, 128, 520, rng);
furOutline(c, 230, 421, 128, 145, 430, rng);
c.save();
c.fillStyle = 'rgba(255,253,240,.56)';
c.beginPath(); c.ellipse(207, 276, 48, 34, -.10, 0, TAU); c.fill();
c.beginPath(); c.ellipse(253, 276, 48, 34, .10, 0, TAU); c.fill();
c.restore();
drawEye(c, 174, 216);
drawEye(c, 286, 216);
c.fillStyle = '#d28c87';
c.beginPath(); c.moveTo(230, 266); c.lineTo(217, 276); c.quadraticCurveTo(230, 285, 243, 276); c.closePath(); c.fill();
c.strokeStyle = 'rgba(78,53,39,.68)'; c.lineWidth = 2;
c.beginPath(); c.moveTo(230, 282); c.lineTo(230, 294); c.stroke();
c.beginPath(); c.moveTo(230, 294); c.quadraticCurveTo(216, 308, 203, 293); c.stroke();
c.beginPath(); c.moveTo(230, 294); c.quadraticCurveTo(244, 308, 257, 293); c.stroke();
c.fillStyle = 'rgba(63,43,29,.55)';
for (const side of [-1, 1]) {
for (let i = 0; i < 7; i++) {
c.beginPath(); c.arc(230 + side * (46 + (i % 3) * 9), 267 + Math.floor(i / 3) * 8, 1.1, 0, TAU); c.fill();
}
}
drawPaws(c, rng, true);
drawWhiskers(c);
c.save();
pathHead(c);
c.strokeStyle = 'rgba(78,52,30,.18)';
c.lineWidth = 2;
c.stroke();
c.restore();
}
function drawCatBack(c) {
const rng = mulberry32(55019);
c.clearRect(0, 0, SW, SH);
drawTail(c, rng, false);
pathBody(c);
const body = c.createRadialGradient(230, 280, 20, 230, 440, 310);
body.addColorStop(0, '#f7edd4');
body.addColorStop(.55, '#e4cba1');
body.addColorStop(1, '#9b754d');
c.fillStyle = body; c.fill();
furTexture(c, pathBody, [96, 238, 270, 335], 2200, rng, 1.40, 1.05);
drawEar(c, 'left', false, rng);
drawEar(c, 'right', false, rng);
pathHead(c);
const head = c.createRadialGradient(190, 125, 20, 230, 230, 220);
head.addColorStop(0, '#fff8df');
head.addColorStop(.55, '#e6cda1');
head.addColorStop(1, '#a57b51');
c.fillStyle = head; c.fill();
furTexture(c, pathHead, [92, 80, 276, 266], 2600, rng, 1.30, 1.20);
c.save();
pathHead(c); c.clip();
c.strokeStyle = 'rgba(126,90,52,.20)';
c.lineWidth = 22;
c.beginPath(); c.moveTo(230, 88); c.bezierCurveTo(216, 160, 244, 238, 230, 335); c.stroke();
c.fillStyle = 'rgba(171,119,69,.12)';
c.beginPath(); c.ellipse(310, 170, 60, 100, .35, 0, TAU); c.fill();
c.beginPath(); c.ellipse(145, 190, 50, 92, -.30, 0, TAU); c.fill();
c.restore();
furOutline(c, 230, 213, 135, 128, 470, rng);
furOutline(c, 230, 421, 128, 145, 390, rng);
drawPaws(c, rng, false);
}
function getGeom() {
let h = Math.min(H * .50, W * .72, 520);
h = Math.max(220, h) * (1 + chaos * .055 + overdrive * .008);
const w = h * SW / SH;
const groundY = H * .825;
const cx = W * .50 + (stage >= 2 ? Math.sin(time * .6 + angle * .02) * W * .012 * chaos : 0);
const cy = groundY - h * .5 - lift * H;
return { cx, cy, w, h, groundY, bottom: cy + h * .5 };
}
function roomAnchor(g) { return { x: g.cx, y: g.cy + g.h * .08 }; }
function drawRoomWarped(c, g) {
c.drawImage(roomCanvas, 0, 0, W, H);
if (!active) return;
const a = roomAnchor(g);
const ch = clamp(chaos, 0, 1.38);
const roll = (stage >= 2 ? .12 * Math.sin(angle * .06 + time * .7) : .012 * Math.sin(angle * .25)) * ch
+ (stage >= 4 ? .18 * Math.sin(angle * .018) : 0);
const zoom = 1 + .022 * ch + .025 * ch * Math.sin(time * 2.2) + stageKick * .012;
c.save();
c.translate(a.x, a.y);
c.rotate(roll);
c.scale(zoom, zoom);
c.translate(-a.x, -a.y);
c.globalAlpha = .25 + .45 * ch;
c.drawImage(roomCanvas, 0, 0, W, H);
c.restore();
if (stage >= 3) {
c.save();
c.globalAlpha = .10 * ch;
c.globalCompositeOperation = 'screen';
c.translate(0, H);
c.scale(1, -1);
c.drawImage(roomCanvas, 0, 0, W, H);
c.restore();
}
if (ch > .35) drawRoomSlices(c, a, ch);
}
function drawRoomSlices(c, a, ch) {
const slices = stage >= 4 ? 38 : stage >= 3 ? 27 : 17;
const diag = Math.hypot(W, H) * 1.25;
c.save();
for (let i = 0; i < slices; i++) {
const base = i * TAU / slices;
const start = base - TAU / slices * .55;
const end = base + TAU / slices * .55 + ch * .025;
c.save();
c.translate(a.x, a.y);
c.beginPath();
c.moveTo(0, 0);
c.arc(0, 0, diag, start, end);
c.closePath();
c.clip();
c.rotate(Math.sin(i * 1.7 + time * .8) * .025 * ch + .075 * ch * Math.sin(angle * .015 + i));
c.scale(1 + .018 * ch * (1 + Math.sin(i)), 1 + .018 * ch * (1 + Math.cos(i)));
c.globalAlpha = (stage >= 4 ? .058 : .042) * ch;
c.globalCompositeOperation = 'screen';
c.drawImage(roomCanvas, -a.x, -a.y, W, H);
c.restore();
}
c.translate(a.x, a.y);
c.globalCompositeOperation = 'screen';
c.globalAlpha = .075 * ch;
c.strokeStyle = 'rgba(255,226,170,.38)';
c.lineWidth = 1;
for (let i = 0; i < slices; i++) {
const aa = i * TAU / slices + angle * .008;
c.beginPath();
c.moveTo(Math.cos(aa) * 25, Math.sin(aa) * 25);
c.lineTo(Math.cos(aa) * diag, Math.sin(aa) * diag);
c.stroke();
}
c.restore();
}
function drawDust(c, g) {
const count = stage >= 3 ? 130 : 76;
c.save();
c.globalCompositeOperation = 'screen';
for (let i = 0; i < count; i++) {
let x = hash(i * 3.1) * W;
let y = hash(i * 7.7 + 3) * H;
const motion = active ? clamp(chaos, 0, 1.4) : .12;
x += Math.sin(time * (.35 + hash(i) * .55) + i) * (8 + motion * 35);
y += Math.cos(time * (.27 + hash(i + 9) * .4) + i * .7) * (5 + motion * 24);
const a = (.018 + hash(i + 44) * .05) * (stage >= 2 ? 1.35 : 1);
c.fillStyle = rgba(255, 239, 198, a);
c.beginPath(); c.arc(x, y, .6 + hash(i + 2)