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.
This sounds like the “OIIA OIIA” spinning-cat meme: an ordinary cat abruptly rotates while a pitched-up vocal loops and the surrounding world loses its composure. The opening frame needs to read first as a softly lit photograph of a cat genuinely sitting on a living-room floor, with believable depth, contact shadow, fur, and restrained motionlessness.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>Quiet Cat</title>
<style>
:root {
--period: 1.12s;
--accent: #d9c3a0;
}
* { box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #17130f;
}
body {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: white;
user-select: none;
}
button { font: inherit; }
#scene {
position: fixed;
inset: 0;
overflow: hidden;
isolation: isolate;
background:
radial-gradient(circle at 48% 58%, #8b6f51 0, #403226 48%, #17130f 100%);
}
#room {
position: absolute;
inset: -3%;
width: 106%;
height: 106%;
transform-origin: 50% 54%;
will-change: transform, filter;
}
#atmosphere {
position: absolute;
inset: 0;
z-index: 2;
pointer-events: none;
background:
radial-gradient(ellipse at 29% 28%, rgba(255,245,214,.18), transparent 35%),
radial-gradient(ellipse at 50% 85%, transparent 40%, rgba(17,12,8,.38) 100%);
mix-blend-mode: screen;
}
#vignette {
position: absolute;
inset: 0;
z-index: 20;
pointer-events: none;
box-shadow: inset 0 0 14vmin rgba(12,8,5,.43);
transition: box-shadow .4s, background .4s;
}
#lightBloom {
position: absolute;
left: -12%;
top: -16%;
width: 65%;
height: 90%;
z-index: 4;
pointer-events: none;
opacity: .28;
filter: blur(28px);
transform: rotate(-16deg);
background: linear-gradient(115deg, rgba(255,248,218,.82), transparent 65%);
mix-blend-mode: screen;
}
#speedLines {
position: absolute;
z-index: 12;
inset: -40vmax;
opacity: 0;
pointer-events: none;
background: repeating-conic-gradient(
from 0deg at 50% 57%,
transparent 0deg 5deg,
rgba(255,240,196,.16) 5.15deg 5.7deg,
transparent 5.9deg 10deg
);
mask-image: radial-gradient(circle at 50% 57%, transparent 0 18%, #000 40% 100%);
transition: opacity .35s;
}
#portal {
position: absolute;
z-index: 5;
left: 50%;
top: 57%;
width: 14vmin;
height: 14vmin;
border-radius: 50%;
transform: translate(-50%,-50%) scale(.2);
opacity: 0;
pointer-events: none;
border: 2px solid rgba(255,224,150,.3);
box-shadow:
0 0 0 5vmin rgba(109,201,255,.08),
0 0 0 10vmin rgba(255,156,90,.07),
0 0 0 16vmin rgba(172,112,255,.06),
0 0 12vmin rgba(124,205,255,.4);
background:
repeating-radial-gradient(circle, transparent 0 7%, rgba(255,224,158,.14) 8% 10%, transparent 11% 18%),
conic-gradient(from 20deg, #251737, #14778a, #e48645, #5a2876, #251737);
filter: blur(.2px) saturate(1.4);
transition: opacity .4s, transform .5s;
}
#orbit {
position: absolute;
z-index: 13;
left: 50%;
top: 57%;
width: min(78vw, 900px);
aspect-ratio: 1;
transform: translate(-50%,-50%);
opacity: 0;
pointer-events: none;
}
.orb {
position: absolute;
left: 50%;
top: 50%;
width: 52px;
height: 52px;
filter: drop-shadow(0 8px 7px rgba(0,0,0,.45));
}
.orb::before, .orb::after {
content: "";
position: absolute;
}
.orb.fish {
color: #eebc73;
border-radius: 60% 45% 45% 60%;
background: linear-gradient(145deg,#ffe0a1,#b66a39);
}
.orb.fish::before {
right: -24px;
top: 8px;
border-left: 27px solid #ba713f;
border-top: 18px solid transparent;
border-bottom: 18px solid transparent;
}
.orb.fish::after {
width: 5px;
height: 5px;
border-radius: 50%;
left: 10px;
top: 14px;
background: #27170e;
box-shadow: 0 0 0 2px #f5d7a4;
}
.orb.yarn {
border-radius: 50%;
background:
repeating-radial-gradient(ellipse at 45% 50%, transparent 0 5px, rgba(255,255,255,.25) 6px 7px),
linear-gradient(145deg,#bd5663,#63293b);
}
.orb.yarn::after {
width: 72px;
height: 30px;
left: 32px;
top: 36px;
border-bottom: 4px solid #87374a;
border-radius: 50%;
transform: rotate(16deg);
}
.orb.leaf {
width: 35px;
height: 65px;
border-radius: 100% 0 100% 0;
background: linear-gradient(135deg,#86aa61,#31532e);
box-shadow: inset 8px 0 rgba(255,255,255,.09);
}
.orb.cushion {
width: 68px;
height: 58px;
border-radius: 13px 18px 12px 17px;
background: linear-gradient(135deg,#b48d68,#694d39);
box-shadow: inset 0 0 0 5px rgba(255,255,255,.08);
}
.orb:nth-child(1) { transform: rotate(0deg) translateX(310px) rotate(0deg); }
.orb:nth-child(2) { transform: rotate(90deg) translateX(285px) rotate(-90deg); }
.orb:nth-child(3) { transform: rotate(180deg) translateX(320px) rotate(-180deg); }
.orb:nth-child(4) { transform: rotate(270deg) translateX(270px) rotate(-270deg); }
#catButton {
position: absolute;
z-index: 15;
left: 50%;
bottom: -2.8%;
width: clamp(280px, 34vw, 530px);
height: min(72vh, 675px);
padding: 0;
border: 0;
outline: 0;
background: transparent;
cursor: pointer;
transform: translateX(-50%);
-webkit-tap-highlight-color: transparent;
perspective: 1100px;
}
#catButton:focus-visible {
filter: drop-shadow(0 0 12px rgba(255,239,183,.9));
}
#contactShadow {
position: absolute;
z-index: 0;
left: 16%;
right: 13%;
bottom: 1.5%;
height: 11%;
border-radius: 50%;
background: radial-gradient(ellipse, rgba(20,13,8,.7), rgba(20,13,8,.28) 48%, transparent 73%);
filter: blur(7px);
transform: rotate(-2deg);
transition: transform .4s, opacity .4s;
}
#spinner {
position: absolute;
inset: 0;
z-index: 1;
transform-style: preserve-3d;
transform-origin: 50% 61%;
transform: rotateY(-15deg);
will-change: transform, filter;
}
#catSvg {
width: 100%;
height: 100%;
overflow: visible;
filter: drop-shadow(10px 18px 12px rgba(29,18,10,.25));
}
#catButton:hover #catSvg {
filter: drop-shadow(10px 18px 12px rgba(29,18,10,.3)) brightness(1.015);
}
#hud {
position: fixed;
z-index: 50;
right: max(14px, env(safe-area-inset-right));
top: max(14px, env(safe-area-inset-top));
display: flex;
gap: 9px;
align-items: center;
}
#mute {
border: 1px solid rgba(255,255,255,.22);
border-radius: 999px;
padding: 8px 12px;
color: rgba(255,255,255,.82);
background: rgba(24,18,14,.43);
box-shadow: 0 4px 18px rgba(0,0,0,.2);
backdrop-filter: blur(10px);
cursor: pointer;
}
#mute:hover { background: rgba(45,34,26,.67); }
#mute:focus-visible { outline: 2px solid white; outline-offset: 2px; }
#meter {
display: flex;
gap: 5px;
padding: 8px 10px;
border-radius: 999px;
background: rgba(24,18,14,.32);
backdrop-filter: blur(8px);
}
#meter i {
display: block;
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(255,255,255,.22);
transition: background .25s, box-shadow .25s, transform .25s;
}
#meter i.on {
background: #ffe2a5;
box-shadow: 0 0 9px #ffb85c;
transform: scale(1.18);
}
#status {
position: fixed;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
}
/* SVG room movement */
#sofa, #lamp, #plant, #frames, #sideTable {
transform-box: fill-box;
transform-origin: center;
}
.stage1 { --period: 1.12s; }
.stage2 { --period: .69s; }
.stage3 { --period: .405s; }
.stage4 { --period: .225s; }
.active #spinner {
animation: catSpin var(--period) linear infinite;
}
@keyframes catSpin {
0% { transform: perspective(1000px) rotateY(0deg) translateY(0); }
25% { transform: perspective(1000px) rotateY(90deg) scaleX(.92) translateY(-1.2%); }
50% { transform: perspective(1000px) rotateY(180deg) translateY(0); }
75% { transform: perspective(1000px) rotateY(270deg) scaleX(.92) translateY(-1.2%); }
100% { transform: perspective(1000px) rotateY(360deg) translateY(0); }
}
.stage1 #room {
animation: roomBreathe var(--period) ease-in-out infinite;
}
@keyframes roomBreathe {
0%,100% { transform: scale(1.01) rotate(-1.2deg); }
50% { transform: scale(1.055) rotate(1.2deg); }
}
.stage1 #lightBloom { animation: bloom 1.12s ease-in-out infinite; }
@keyframes bloom { 50% { opacity: .48; transform: rotate(-12deg) scale(1.08); } }
.stage2 #room {
animation: roomSwing var(--period) ease-in-out infinite;
filter: saturate(1.16) contrast(1.03);
}
@keyframes roomSwing {
0%,100% { transform: scale(1.08) rotate(-7deg); }
50% { transform: scale(1.13) rotate(7deg); }
}
.stage2 #sofa { animation: furnitureWobble 1.38s ease-in-out infinite; }
.stage2 #lamp { animation: furnitureWobble .69s ease-in-out infinite reverse; }
.stage2 #plant { animation: furnitureWobble .92s ease-in-out infinite; }
@keyframes furnitureWobble {
0%,100% { transform: translateY(0) rotate(-1deg); }
50% { transform: translateY(-12px) rotate(2deg); }
}
.stage2 #speedLines { opacity: .35; animation: lineSpin 2.07s linear infinite; }
@keyframes lineSpin { to { transform: rotate(360deg); } }
.stage3 #room {
animation: roomRoll calc(var(--period) * 4) linear infinite;
filter: saturate(1.45) contrast(1.07) hue-rotate(-8deg);
}
@keyframes roomRoll {
to { transform: scale(1.84) rotate(-360deg); }
}
.stage3 #speedLines {
opacity: .72;
animation: lineSpin .9s linear infinite reverse;
}
.stage3 #orbit {
opacity: 1;
animation: orbitSpin 2.45s linear infinite;
}
@keyframes orbitSpin {
to { transform: translate(-50%,-50%) rotate(360deg); }
}
.stage3 #contactShadow {
opacity: .55;
transform: scale(.72);
}
.stage3 #catButton {
animation: levitate .81s ease-in-out infinite;
}
@keyframes levitate {
0%,100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(-2.6vh); }
}
.stage4 #room {
animation: finalRoom calc(var(--period) * 7) linear infinite;
filter: saturate(1.85) contrast(1.11) hue-rotate(0deg);
}
@keyframes finalRoom {
0% { transform: scale(1.84) rotate(0); filter: saturate(1.7) hue-rotate(0deg); }
50% { transform: scale(2.03) rotate(-180deg); filter: saturate(2) hue-rotate(65deg); }
100% { transform: scale(1.84) rotate(-360deg); filter: saturate(1.7) hue-rotate(0deg); }
}
.stage4 #spinner {
animation-name: finalCat;
filter:
drop-shadow(22px 0 0 rgba(57,208,255,.17))
drop-shadow(-22px 0 0 rgba(255,105,93,.17));
}
@keyframes finalCat {
0% { transform: perspective(850px) rotateY(0deg) rotateZ(-2deg) scale(1.02); }
25% { transform: perspective(850px) rotateY(90deg) rotateZ(3deg) scale(.96,1.05); }
50% { transform: perspective(850px) rotateY(180deg) rotateZ(-2deg) scale(1.04); }
75% { transform: perspective(850px) rotateY(270deg) rotateZ(3deg) scale(.96,1.05); }
100% { transform: perspective(850px) rotateY(360deg) rotateZ(-2deg) scale(1.02); }
}
.stage4 #portal {
opacity: .92;
transform: translate(-50%,-50%) scale(4.8);
animation: portalTurn 2.2s linear infinite;
}
@keyframes portalTurn {
to { rotate: 360deg; filter: hue-rotate(360deg) saturate(1.5); }
}
.stage4 #orbit {
opacity: 1;
animation: orbitSpin .72s linear infinite reverse;
}
.stage4 #speedLines {
opacity: .9;
animation: lineSpin .32s linear infinite;
}
.stage4 #vignette {
box-shadow: inset 0 0 18vmin rgba(19,5,31,.56);
background: radial-gradient(circle at 50% 56%, transparent 25%, rgba(53,16,70,.18));
}
.stage4 #catButton {
animation: finalLevitate .45s ease-in-out infinite;
}
@keyframes finalLevitate {
0%,100% { transform: translateX(-50%) translateY(-3vh) rotate(-1deg); }
50% { transform: translateX(-50%) translateY(-7vh) rotate(1deg); }
}
@media (max-width: 700px) {
#catButton {
width: min(76vw, 470px);
height: 63vh;
bottom: -1%;
}
#room {
inset: -7%;
width: 114%;
height: 114%;
}
.orb:nth-child(1) { transform: rotate(0deg) translateX(40vw); }
.orb:nth-child(2) { transform: rotate(90deg) translateX(37vw) rotate(-90deg); }
.orb:nth-child(3) { transform: rotate(180deg) translateX(41vw) rotate(-180deg); }
.orb:nth-child(4) { transform: rotate(270deg) translateX(36vw) rotate(-270deg); }
}
</style>
</head>
<body>
<main id="scene" aria-label="A cat sitting in a quiet living room">
<svg id="room" viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<defs>
<linearGradient id="wallLight" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#cbb99d"/>
<stop offset=".42" stop-color="#ae9b80"/>
<stop offset="1" stop-color="#796751"/>
</linearGradient>
<linearGradient id="sideWall" x1="0" y1="0" x2="1" y2=".7">
<stop stop-color="#94816a"/>
<stop offset="1" stop-color="#594a3c"/>
</linearGradient>
<linearGradient id="floorGrad" x1=".2" y1="0" x2=".75" y2="1">
<stop stop-color="#9d7651"/>
<stop offset=".5" stop-color="#68472f"/>
<stop offset="1" stop-color="#39261b"/>
</linearGradient>
<linearGradient id="windowGlow" x1="0" y1="0" x2="1" y2="1">
<stop stop-color="#fffbe8"/>
<stop offset=".45" stop-color="#d8e7dc"/>
<stop offset="1" stop-color="#9eb6ad"/>
</linearGradient>
<linearGradient id="sofaFabric" x1="0" y1="0" x2="1" y2="1">
<stop stop-color="#8e806f"/>
<stop offset=".55" stop-color="#695d50"/>
<stop offset="1" stop-color="#493f36"/>
</linearGradient>
<radialGradient id="rugGrad">
<stop stop-color="#9c7860"/>
<stop offset=".72" stop-color="#74523f"/>
<stop offset="1" stop-color="#4e372c"/>
</radialGradient>
<filter id="wallTexture" x="-10%" y="-10%" width="120%" height="120%">
<feTurbulence type="fractalNoise" baseFrequency=".55" numOctaves="2" seed="7" result="n"/>
<feColorMatrix in="n" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 .065 0"/>
<feBlend in="SourceGraphic" mode="multiply"/>
</filter>
<filter id="softShadow" x="-30%" y="-30%" width="160%" height="180%">
<feGaussianBlur in="SourceAlpha" stdDeviation="12"/>
<feOffset dx="8" dy="16"/>
<feComponentTransfer><feFuncA type="linear" slope=".38"/></feComponentTransfer>
<feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="blur12"><feGaussianBlur stdDeviation="12"/></filter>
</defs>
<rect width="1600" height="900" fill="#30251c"/>
<path d="M0 0H1195L1110 575H0Z" fill="url(#wallLight)" filter="url(#wallTexture)"/>
<path d="M1195 0H1600V620L1110 575Z" fill="url(#sideWall)" filter="url(#wallTexture)"/>
<path d="M0 0H1600L1195 22H0Z" fill="#d2c4ae" opacity=".32"/>
<path d="M0 560L1110 575L1600 620V900H0Z" fill="url(#floorGrad)"/>
<!-- floorboards and perspective seams -->
<g opacity=".46" stroke="#2c1d15" stroke-width="3">
<path d="M0 627L1600 682"/>
<path d="M0 704L1600 752"/>
<path d="M0 796L1600 827"/>
<path d="M75 900L505 562"/>
<path d="M310 900L650 567"/>
<path d="M570 900L789 570"/>
<path d="M865 900L920 572"/>
<path d="M1185 900L1030 574"/>
<path d="M1500 900L1100 576"/>
</g>
<g opacity=".22" stroke="#e2ba83" stroke-width="2">
<path d="M0 642L1600 696"/>
<path d="M0 718L1600 766"/>
<path d="M0 810L1600 841"/>
</g>
<!-- window -->
<g filter="url(#softShadow)">
<rect x="105" y="82" width="390" height="355" rx="5" fill="#59483a"/>
<rect x="120" y="97" width="360" height="325" fill="url(#windowGlow)"/>
<path d="M120 330Q190 260 258 312T390 280T480 302V422H120Z" fill="#789180" opacity=".72"/>
<path d="M120 363Q205 297 290 352T480 331V422H120Z" fill="#596f5c" opacity=".8"/>
<path d="M300 97V422M120 252H480" stroke="#6c6257" stroke-width="13"/>
<path d="M300 97V422M120 252H480" stroke="#d2c8b7" stroke-width="7"/>
<rect x="101" y="72" width="398" height="18" rx="7" fill="#4b4037"/>
</g>
<!-- curtains -->
<path d="M72 65Q126 96 115 188T92 450L34 488Q53 288 44 67Z" fill="#9a8067" opacity=".94"/>
<path d="M515 67Q486 158 500 253T535 476L587 443Q548 270 574 68Z" fill="#78624f"/>
<g fill="none" stroke="#c0a58a" opacity=".35" stroke-width="10">
<path d="M69 78Q95 228 65 461"/>
<path d="M543 79Q513 241 556 451"/>
</g>
<!-- daylight beam -->
<path d="M365 315L943 900H233L183 408Z" fill="#fff2c3" opacity=".115"/>
<path d="M435 292L1160 900H746L270 370Z" fill="#fff8dd" opacity=".07"/>
<!-- frames -->
<g id="frames" filter="url(#softShadow)">
<rect x="670" y="118" width="185" height="132" fill="#47382d"/>
<rect x="682" y="130" width="161" height="108" fill="#baad93"/>
<path d="M698 217L742 169L770 194L810 148L831 217Z" fill="#677763"/>
<rect x="906" y="150" width="119" height="168" fill="#493a30"/>
<rect x="917" y="162" width="97" height="144" fill="#c0ad8c"/>
<circle cx="966" cy="214" r="27" fill="#8a6a4d"/>
<path d="M929 287Q963 238 1002 287Z" fill="#63705b"/>
</g>
<!-- rug -->
<ellipse cx="790" cy="794" rx="535" ry="118" fill="#21150f" opacity=".25" filter="url(#blur12)"/>
<ellipse cx="781" cy="775" rx="510" ry="111" fill="url(#rugGrad)"/>
<ellipse cx="781" cy="775" rx="470" ry="87" fill="none" stroke="#d0a77e" stroke-width="4" opacity=".36"/>
<ellipse cx="781" cy="775" rx="405" ry="65" fill="none" stroke="#4f746c" stroke-width="14" stroke-dasharray="18 14" opacity=".45"/>
<!-- sofa -->
<g id="sofa" filter="url(#softShadow)">
<path d="M1055 397Q1060 354 1110 351H1480Q1533 357 1533 415V672H1032Z" fill="#51483f"/>
<rect x="1081" y="373" width="205" height="205" rx="43" fill="url(#sofaFabric)"/>
<rect x="1270" y="373" width="218" height="205" rx="43" fill="url(#sofaFabric)"/>
<path d="M1030 529Q1040 494 1085 502H1485Q1534 502 1543 542L1512 692H1045Z" fill="#665a4d"/>
<path d="M1074 515Q1202 493 1284 515V654Q1184 674 1060 649Z" fill="#786b5c"/>
<path d="M1288 515Q1410 491 1510 524L1496 649Q1396 673 1285 654Z" fill="#716457"/>
<path d="M1080 600H1518" stroke="#3c342d" stroke-width="5" opacity=".55"/>
<rect x="1054" y="650" width="28" height="100" rx="5" fill="#34281f"/>
<rect x="1477" y="648" width="28" height="104" rx="5" fill="#34281f"/>
<path d="M1110 428Q1172 401 1231 438L1204 551Q1144 566 1093 522Z" fill="#a17557"/>
<path d="M1324 422Q1398 405 1457 454L1435 548Q1360 557 1307 520Z" fill="#526860"/>
</g>
<!-- lamp -->
<g id="lamp" filter="url(#softShadow)">
<ellipse cx="1030" cy="744" rx="82" ry="21" fill="#3b2e25"/>
<path d="M1026 365L1029 733" stroke="#52483e" stroke-width="13"/>
<path d="M950 351Q1031 317 1112 351L1083 466H978Z" fill="#c6ab82"/>
<path d="M970 448H1091" stroke="#795f45" stroke-width="8"/>
<ellipse cx="1031" cy="447" rx="55" ry="14" fill="#ffefbe" opacity=".24"/>
</g>
<!-- side table -->
<g id="sideTable" filter="url(#softShadow)">
<ellipse cx="1510" cy="581" rx="86" ry="25" fill="#5a402d"/>
<path d="M1467 581L1448 755M1550 581L1572 755" stroke="#442e20" stroke-width="16"/>
<rect x="1467" y="526" width="71" height="55" rx="7" fill="#82654c"/>
<path d="M1475 540H1528" stroke="#c1a077" stroke-width="4"/>
</g>
<!-- plant -->
<g id="plant" filter="url(#softShadow)">
<path d="M133 595Q153 695 168 760M178 568Q178 669 168 760M215 610Q191 688 168 760"
fill="none" stroke="#37513a" stroke-width="10"/>
<g fill="#526f4d">
<ellipse cx="122" cy="584" rx="35" ry="74" transform="rotate(-35 122 584)"/>
<ellipse cx="184" cy="552" rx="35" ry="79" transform="rotate(13 184 552)"/>
<ellipse cx="232" cy="598" rx="33" ry="69" transform="rotate(38 232 598)"/>
<ellipse cx="113" cy="666" rx="31" ry="65" transform="rotate(-63 113 666)"/>
<ellipse cx="219" cy="678" rx="31" ry="65" transform="rotate(58 219 678)"/>
</g>
<path d="M106 733H232L214 842H126Z" fill="#80634c"/>
<path d="M119 760H221" stroke="#b89472" stroke-width="8" opacity=".55"/>
</g>
<!-- dust motes -->
<g fill="#fff8dc" opacity=".45">
<circle cx="330" cy="427" r="3"/>
<circle cx="510" cy="522" r="2"/>
<circle cx="616" cy="400" r="3"/>
<circle cx="393" cy="650" r="2"/>
<circle cx="726" cy="519" r="2"/>
<circle cx="270" cy="540" r="2"/>
</g>
</svg>
<div id="atmosphere"></div>
<div id="lightBloom"></div>
<div id="portal"></div>
<div id="orbit" aria-hidden="true">
<i class="orb fish"></i>
<i class="orb yarn"></i>
<i class="orb leaf"></i>
<i class="orb cushion"></i>
</div>
<div id="speedLines"></div>
<button id="catButton" type="button" aria-label="Spin the cat">
<span id="contactShadow"></span>
<span id="spinner">
<svg id="catSvg" viewBox="0 0 500 650" aria-hidden="true">
<defs>
<linearGradient id="furBody" x1=".15" y1=".05" x2=".85" y2="1">
<stop stop-color="#c4a27d"/>
<stop offset=".3" stop-color="#9a7657"/>
<stop offset=".62" stop-color="#76533b"/>
<stop offset="1" stop-color="#442f24"/>
</linearGradient>
<radialGradient id="furChest" cx=".42" cy=".28">
<stop stop-color="#e4cfb3"/>
<stop offset=".65" stop-color="#b89470"/>
<stop offset="1" stop-color="#75523c"/>
</radialGradient>
<linearGradient id="faceFur" x1=".15" y1=".1" x2=".8" y2=".9">
<stop stop-color="#d2b391"/>
<stop offset=".53" stop-color="#a47d5c"/>
<stop offset="1" stop-color="#694936"/>
</linearGradient>
<radialGradient id="eye" cx=".38" cy=".35">
<stop stop-color="#d9d37c"/>
<stop offset=".52" stop-color="#84913e"/>
<stop offset="1" stop-color="#314120"/>
</radialGradient>
<filter id="furEdge" x="-20%" y="-20%" width="140%" height="150%">
<feTurbulence type="fractalNoise" baseFrequency=".035 .24" numOctaves="2" seed="19" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="4"/>
</filter>
<filter id="catShadow" x="-30%" y="-30%" width="170%" height="180%">
<feGaussianBlur in="SourceAlpha" stdDeviation="8"/>
<feOffset dx="7" dy="12"/>
<feComponentTransfer><feFuncA type="linear" slope=".28"/></feComponentTransfer>
<feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="stripeBlur"><feGaussianBlur stdDeviation="1.4"/></filter>
</defs>
<!-- tail -->
<path d="M330 455Q444 420 448 512Q452 582 364 582Q331 580 334 553Q337 531 370 536Q403 539 402 511Q400 480 335 494Z"
fill="#674632" stroke="#3d2a21" stroke-width="6" filter="url(#catShadow)"/>
<path d="M383 474Q414 481 420 499M377 535Q405 542 416 532M359 568Q382 572 399 564"
fill="none" stroke="#32231c" stroke-width="14" opacity=".55"/>
<!-- haunch and torso -->
<path d="M145 590Q108 508 133 401Q151 323 218 294Q292 273 347 329Q383 365 378 462Q374 541 346 598Z"
fill="url(#furBody)" stroke="#4a3328" stroke-width="5" filter="url(#furEdge)"/>
<ellipse cx="317" cy="501" rx="85" ry="109" fill="#79553e" opacity=".72"/>
<path d="M214 305Q175 355 173 450Q172 521 201 590H300Q322 504 305 405Q294 335 258 302Z"
fill="url(#furChest)" opacity=".92"/>
<!-- body stripes -->
<g fill="none" stroke="#3d2a21" stroke-linecap="round" opacity=".62" filter="url(#stripeBlur)">
<path d="M155 374Q202 403 228 398" stroke-width="15"/>
<path d="M143 418Q190 443 221 438" stroke-width="13"/>
<path d="M139 463Q176 482 207 481" stroke-width="12"/>
<path d="M333 360Q301 389 284 414" stroke-width="16"/>
<path d="M358 405Q324 431 307 460" stroke-width="15"/>
<path d="M365 459Q336 479 319 510" stroke-width="14"/>
</g>
<!-- front legs and paws -->
<path d="M188 414Q179 508 181 588Q171 608 201 619H250Q266 608 252 590L256 425Z"
fill="#b18c68" stroke="#5b3d2e" stroke-width="4"/>
<path d="M258 423Q251 511 259 591Q247 610 279 620H327Q345 608 330 589L325 420Z"
fill="#967052" stroke="#51372b" stroke-width="4"/>
<path d="M181 591Q213 579 253 592M258 592Q294 579 331 591"
fill="none" stroke="#e0c3a1" stroke-width="10" opacity=".48"/>
<path d="M203 608V619M221 606V620M280 607V620M301 606V620"
stroke="#5f4131" stroke-width="3" opacity=".65"/>
<!-- head silhouette and ears -->
<path d="M145 114L164 42L224 89Q266 78 310 94L367 46L374 134Q402 178 385 239Q368 298 302 320Q231 340 174 298Q117 255 126 188Q129 145 145 114Z"
fill="url(#faceFur)" stroke="#483128" stroke-width="6" filter="url(#furEdge)"/>
<path d="M158 104L168 61L211 96Z" fill="#8e6255"/>
<path d="M325 99L361 64L363 118Z" fill="#8d6154"/>
<path d="M169 70L181 91M353 74L344 100" stroke="#ddb39b" stroke-width="8" opacity=".55"/>
<!-- forehead tabby marks -->
<g fill="none" stroke="#4b3226" stroke-linecap="round" opacity=".8">
<path d="M231 91Q237 131 253 153" stroke-width="13"/>
<path d="M260 88Q258 128 253 153" stroke-width="12"/>
<path d="M289 94Q278 130 255 154" stroke-width="11"/>
<path d="M157 148Q197 159 216 177" stroke-width="13"/>
<path d="M352 145Q315 157 292 177" stroke-width="13"/>
</g>
<!-- cheeks -->
<path d="M137 209Q161 180 217 196Q235 230 222 275Q175 302 143 266Z"
fill="#c4a07f" opacity=".75"/>
<path d="M288 194Q346 180 381 211L367 273Q327 302 282 273Q270 231 288 194Z"
fill="#b59070" opacity=".72"/>
<!-- eyes -->
<path d="M160 179Q190 151 225 178Q206 213 169 199Z"
fill="#30291e" stroke="#503c2b" stroke-width="4"/>
<path d="M283 177Q321 150 350 179Q337 206 296 201Z"
fill="#30291e" stroke="#503c2b" stroke-width="4"/>
<ellipse cx="193" cy="182" rx="21" ry="25" fill="url(#eye)"/>
<ellipse cx="316" cy="181" rx="21" ry="25" fill="url(#eye)"/>
<ellipse cx="194" cy="183" rx="6" ry="20" fill="#0b0b08"/>
<ellipse cx="316" cy="182" rx="6" ry="20" fill="#0b0b08"/>
<circle cx="186" cy="172" r="5" fill="#fff" opacity=".88"/>
<circle cx="309" cy="171" r="5" fill="#fff" opacity=".88"/>
<!-- muzzle -->
<ellipse cx="226" cy="246" rx="47" ry="39" fill="#dfc4a5" opacity=".88"/>
<ellipse cx="281" cy="246" rx="47" ry="39" fill="#d7b896" opacity=".88"/>
<path d="M236 222Q253 211 271 222Q268 243 254 248Q239 241 236 222Z"
fill="#6c4942"/>
<path d="M254 247Q253 268 237 274M254 247Q257 267 274 274"
fill="none" stroke="#553a34" stroke-width="4" stroke-linecap="round"/>
<path d="M247 281Q255 287 263 281" fill="none" stroke="#542f30" stroke-width="5"/>
<!-- whisker roots -->
<g fill="#604236" opacity=".7">
<circle cx="198" cy="242" r="3"/><circle cx="190" cy="256" r="3"/><circle cx="204" cy="266" r="3"/>
<circle cx="306" cy="241" r="3"/><circle cx="314" cy="255" r="3"/><circle cx="302" cy="267" r="3"/>
</g>
<!-- whiskers -->
<g fill="none" stroke="#f0dfc9" stroke-width="2.3" stroke-linecap="round" opacity=".87">
<path d="M212 244Q139 224 78 238"/>
<path d="M210 256Q135 252 68 275"/>
<path d="M214 268Q145 281 93 315"/>
<path d="M294 243Q365 219 432 230"/>
<path d="M297 255Q374 250 444 272"/>
<path d="M294 267Q365 281 418 316"/>
</g>
<!-- loose fur highlights -->
<g fill="none" stroke="#ead1b4" stroke-linecap="round" opacity=".3">
<path d="M180 330L163 342M190 347L169 362M326 336L345 349" stroke-width="4"/>
<path d="M170 490L150 503M338 515L359 528M227 390L217 410" stroke-width="3"/>
</g>
</svg>
</span>
</button>
<div id="vignette"></div>
</main>
<div id="hud">
<div id="meter" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
<button id="mute" type="button" aria-pressed="false">Sound: on</button>
</div>
<div id="status" aria-live="polite">The room is quiet.</div>
<script>
(() => {
const scene = document.getElementById("scene");
const cat = document.getElementById("catButton");
const muteButton = document.getElementById("mute");
const status = document.getElementById("status");
const dots = [...document.querySelectorAll("#meter i")];
const periods = [0, 1.12, 0.69, 0.405, 0.225];
const pitches = [0, 103, 126, 158, 194];
const stageNames = [
"The room is quiet.",
"Stage one: the cat is spinning.",
"Stage two: the furniture is reconsidering gravity.",
"Stage three: household objects have entered orbit.",
"Stage four: the living room has achieved escape velocity."
];
let stage = 0;
let muted = false;
let audio = null;
let master = null;
let compressor = null;
let schedulerTimer = null;
let nextLoopTime = 0;
let noiseBuffer = null;
function createAudio() {
if (audio) return;
audio = new (window.AudioContext || window.webkitAudioContext)();
master = audio.createGain();
master.gain.value = muted ? 0 : 0.44;
compressor = audio.createDynamicsCompressor();
compressor.threshold.value = -18;
compressor.knee.value = 15;
compressor.ratio.value = 5;
compressor.attack.value = 0.004;
compressor.release.value = 0.18;
master.connect(compressor);
compressor.connect(audio.destination);
noiseBuffer = audio.createBuffer(1, Math.floor(audio.sampleRate * 0.5), audio.sampleRate);
const data = noiseBuffer.getChannelData(0);
for (let i = 0; i < data.length; i++) {
const fade = 1 - i / data.length;
data[i] = (Math.random() * 2 - 1) * fade;
}
}
const formants = {
o: [[430, 8, 1.00], [860, 10, .43], [2450, 13, .16]],
i: [[285, 9, .82], [2250, 12, .62], [3100, 15, .22]],
a: [[790, 8, 1.00], [1230, 10, .50], [2750, 14, .18]],
e: [[520, 8, .92], [1850, 11, .55], [2700, 14, .20]]
};
function syllable(vowel, when, duration, basePitch, lift) {
if (!audio || when < audio.currentTime - 0.05) return;
const mix = audio.createGain();
const envelope = audio.createGain();
const source = audio.createOscillator();
const sub = audio.createOscillator();
const sourceGain = audio.createGain();
const subGain = audio.createGain();
const wave = audio.createPeriodicWave(
new Float32Array([0, 1, .42, .24, .15, .10, .07, .05, .035]),
new Float32Array(9),
{ disableNormalization: false }
);
source.setPeriodicWave(wave);
sub.setPeriodicWave(wave);
source.frequency.setValueAtTime(basePitch, when);
source.frequency.exponentialRampToValueAtTime(basePitch * (1.045 + lift), when + duration * .86);
sub.frequency.setValueAtTime(basePitch / 2, when);
sub.frequency.exponentialRampToValueAtTime(basePitch * (1.035 + lift) / 2, when + duration * .86);
sourceGain.gain.value = .76;
subGain.gain.value = .16;
source.connect(sourceGain);
sub.connect(subGain);
const lfo = audio.createOscillator();
const lfoGain = audio.createGain();
lfo.frequency.value = 5.3 + stage * .5;
lfoGain.gain.value = 2.1 + stage * .45;
lfo.connect(lfoGain);
lfoGain.connect(source.frequency);
for (const [frequency, q, gainValue] of formants[vowel]) {
const filter = audio.createBiquadFilter();
const gain = audio.createGain();
filter.type = "bandpass";
filter.frequency.setValueAtTime(frequency, when);
filter.Q.value = q;
gain.gain.value = gainValue;
sourceGain.connect(filter);
subGain.connect(filter);
filter.connect(gain);
gain.connect(mix);
}
const breath = audio.createBufferSource();
const breathFilter = audio.createBiquadFilter();
const breathGain = audio.createGain();
breath.buffer = noiseBuffer;
breathFilter.type = "bandpass";
breathFilter.frequency.value = vowel === "i" ? 4100 : 2850;
breathFilter.Q.value = 2.2;
breathGain.gain.value = .026 + stage * .004;
breath.connect(breathFilter);
breathFilter.connect(breathGain);
breathGain.connect(mix);
mix.connect(envelope);
envelope.connect(master);
envelope.gain.setValueAtTime(0.0001, when);
envelope.gain.exponentialRampToValueAtTime(.74, when + Math.min(.022, duration * .16));
envelope.gain.setValueAtTime(.74, when + duration * .56);
envelope.gain.exponentialRampToValueAtTime(.0001, when + duration * .96);
source.start(when);
sub.start(when);
lfo.start(when);
breath.start(when);
const stop = when + duration;
source.stop(stop);
sub.stop(stop);
lfo.stop(stop);
breath.stop(stop);
}
function scheduleLoop() {
if (!audio || stage === 0) return;
const lookAhead = 0.16;
const period = periods[stage];
const pattern = ["o", "i", "i", "a"];
while (nextLoopTime < audio.currentTime + lookAhead) {
const slot = period / pattern.length;
const base = pitches[stage];
pattern.forEach((vowel, i) => {
const melodic = [1, 1.07, 1.12, 1.18][i];
syllable(
vowel,
nextLoopTime + i * slot,
slot * 1.18,
base * melodic,
stage * .006
);
});
nextLoopTime += period;
}
}
function restartVoice() {
clearInterval(schedulerTimer);
nextLoopTime = audio.currentTime + 0.035;
scheduleLoop();
schedulerTimer = setInterval(scheduleLoop, 35);
}
function setStage(nextStage) {
stage = nextStage;
scene.className = "";
scene.classList.add("active", "stage" + stage);
dots.forEach((dot, index) => dot.classList.toggle("on", index < stage));
status.textContent = stageNames[stage];
createAudio();
audio.resume().then(restartVoice);
}
cat.addEventListener("click", () => {
setStage(Math.min(4, stage + 1));
});
muteButton.addEventListener("click", (event) => {
event.stopPropagation();
muted = !muted;
muteButton.textContent = muted ? "Sound: off" : "Sound: on";
muteButton.setAttribute("aria-pressed", String(muted));
if (audio && master) {
const now = audio.currentTime;
master.gain.cancelScheduledValues(now);
master.gain.setTargetAtTime(muted ? 0 : 0.44, now, 0.018);
}
});
document.addEventListener("visibilitychange", () => {
if (!audio) return;
if (document.hidden) {
audio.suspend();
} else if (stage > 0) {
audio.resume().then(restartVoice);
}
});
})();
</script>
</body>
</html>