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 is the spinning-cat meme lineage — a realistically rendered cat that turns to camera and rotates on its vertical axis to a looping, rising vocal, with the room caught up in the turn; the closest single anchor is the classic "cat spins and yowls" loop rather than any one identifiable source piece, so I build the brief as written. The opening frame has to read as a snapshot: a warm, shallow-focus living room with a window throwing light across a plank floor, a real cat sitting three-quarter-away on a rug, breathing, blinking, not looking at you.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>the cat, in repose</title>
<style>
html,body{margin:0;padding:0;height:100%;background:#08080a;overflow:hidden}
#c{display:block;position:fixed;left:0;top:0;cursor:pointer;touch-action:none}
#hud{position:fixed;left:0;top:0;right:0;bottom:0;pointer-events:none;
font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
#hint{position:absolute;left:50%;bottom:28px;transform:translateX(-50%);
color:rgba(255,244,228,.46);font-size:11px;letter-spacing:.34em;text-transform:uppercase;
transition:opacity .7s ease;white-space:nowrap;text-shadow:0 1px 8px rgba(0,0,0,.8)}
#mute{position:absolute;right:16px;bottom:16px;width:40px;height:40px;border-radius:50%;
border:1px solid rgba(255,245,230,.17);background:rgba(16,14,12,.42);
-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);
pointer-events:auto;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;
transition:transform .18s ease,border-color .18s ease}
#mute:hover{transform:scale(1.06);border-color:rgba(255,245,230,.4)}
#mute svg{width:18px;height:18px;fill:none;stroke:rgba(255,245,230,.72);stroke-width:1.5;
stroke-linecap:round;stroke-linejoin:round}
#mute.off svg .on{display:none}
#mute:not(.off) svg .off{display:none}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hud">
<div id="hint">click the cat</div>
<button id="mute" class="off" aria-label="toggle sound">
<svg viewBox="0 0 24 24">
<path d="M4 9.5h3.4L12 5.6v12.8L7.4 14.5H4z"/>
<g class="on"><path d="M15.6 9.2a4 4 0 0 1 0 5.6"/><path d="M18.1 6.9a7.4 7.4 0 0 1 0 10.2"/></g>
<g class="off"><path d="M16 9.6l5 4.8"/><path d="M21 9.6l-5 4.8"/></g>
</svg>
</button>
</div>
<script>
(function(){
'use strict';
/* ============================== utils ============================== */
var TAU=Math.PI*2, D2R=Math.PI/180;
function clamp(v,a,b){return v<a?a:(v>b?b:v);}
function lerp(a,b,t){return a+(b-a)*t;}
function smooth(t){t=clamp(t,0,1);return t*t*(3-2*t);}
var _sd=987654321;
function srnd(){_sd=(_sd*1664525+1013904223)|0;return ((_sd>>>8)&0xffffff)/0xffffff;}
function rr(a,b){return a+srnd()*(b-a);}
function rgb(r,g,b){return 'rgb('+(r|0)+','+(g|0)+','+(b|0)+')';}
function norm(v){var l=Math.hypot(v[0],v[1],v[2])||1;return [v[0]/l,v[1]/l,v[2]/l];}
function rrect(q,x,y,w,h,r){
r=Math.min(r,w*0.5,h*0.5);
q.beginPath();q.moveTo(x+r,y);q.arcTo(x+w,y,x+w,y+h,r);q.arcTo(x+w,y+h,x,y+h,r);
q.arcTo(x,y+h,x,y,r);q.arcTo(x,y,x+w,y,r);q.closePath();
}
function softEll(g,x,y,rx,ry,col,a){
g.save();g.globalAlpha=a;g.fillStyle=col;
g.beginPath();g.ellipse(x,y,rx,ry,0,0,TAU);g.fill();g.restore();
}
/* ============================== scene consts ============================== */
var RROOM=5.6, WALLH=2.8, CEILH=2.45;
var CAM=[0,1.0,0], TGT=[0,0.58,-2.1], CATP=[0,0,-2.1];
var FOV=54*D2R;
var REVS=[0,2.0,4.2,7.6,11.5];
/* ============================== projection ============================== */
var F=[0,0,0],Rv=[0,0,0],Uv=[0,0,0];
function camInit(){
var f=norm([TGT[0]-CAM[0],TGT[1]-CAM[1],TGT[2]-CAM[2]]);
F[0]=f[0];F[1]=f[1];F[2]=f[2];
var r=norm([-f[2],0,f[0]]);
Rv[0]=r[0];Rv[1]=r[1];Rv[2]=r[2];
var u=[Rv[1]*f[2]-Rv[2]*f[1],Rv[2]*f[0]-Rv[0]*f[2],Rv[0]*f[1]-Rv[1]*f[0]];
Uv[0]=u[0];Uv[1]=u[1];Uv[2]=u[2];
}
var VW=0,VH=0,FS=1;
function setView(w,h){VW=w;VH=h;FS=(h*0.5)/Math.tan(FOV*0.5);}
function proj(x,y,z,out){
out=out||[0,0,0];
var dx=x-CAM[0],dy=y-CAM[1],dz=z-CAM[2];
var zc=dx*F[0]+dy*F[1]+dz*F[2];
out[2]=zc;
if(zc<0.06){out[0]=-1e6;out[1]=-1e6;return out;}
out[0]=VW*0.5+(dx*Rv[0]+dy*Rv[1]+dz*Rv[2])/zc*FS;
out[1]=VH*0.5-(dx*Uv[0]+dy*Uv[1]+dz*Uv[2])/zc*FS;
return out;
}
function ringPt(a,y,out){return proj(RROOM*Math.sin(a),y,-RROOM*Math.cos(a),out);}
function rotY(p,a,out){
var c=Math.cos(a),s=Math.sin(a);out=out||[0,0,0];
out[0]=p[0]*c+p[2]*s;out[1]=p[1];out[2]=-p[0]*s+p[2]*c;return out;
}
/* textured quad: q = [BL,BR,TR,TL] screen pts, source rect in image px */
function drawQuadTex(g,img,sx,sy,sw,sh,q,bleed){
var ax=0,ay=0,i;
for(i=0;i<4;i++){ax+=q[i][0];ay+=q[i][1];} ax*=0.25;ay*=0.25;
g.save();
g.beginPath();
for(i=0;i<4;i++){
var p=q[i],dx=p[0]-ax,dy=p[1]-ay,l=Math.hypot(dx,dy)||1;
var x=p[0]+dx/l*bleed,y=p[1]+dy/l*bleed;
if(i)g.lineTo(x,y);else g.moveTo(x,y);
}
g.closePath();g.clip();
var d1x=q[1][0]-q[0][0],d1y=q[1][1]-q[0][1];
var d2x=q[3][0]-q[0][0],d2y=q[3][1]-q[0][1];
var a=d1x/sw,b=d1y/sw,c=-d2x/sh,d=-d2y/sh;
var tx=q[0][0]-(a*sx+c*(sy+sh)),ty=q[0][1]-(b*sx+d*(sy+sh));
g.setTransform(a,b,c,d,tx,ty);
g.drawImage(img,sx,sy,sw,sh,sx,sy,sw,sh);
g.restore();
}
/* ============================== canvases ============================== */
var cv=document.getElementById('c'),gx=cv.getContext('2d');
var W=0,H=0,DPR=1,BGK=0.62;
var bgc=null,bg=null,BW=0,BH=0;
function resize(){
DPR=Math.min(2,window.devicePixelRatio||1);
W=Math.max(320,window.innerWidth);H=Math.max(240,window.innerHeight);
cv.width=Math.round(W*DPR);cv.height=Math.round(H*DPR);
cv.style.width=W+'px';cv.style.height=H+'px';
gx.setTransform(DPR,0,0,DPR,0,0);
BW=Math.round(W*DPR*BGK);BH=Math.round(H*DPR*BGK);
if(!bg){bg=document.createElement('canvas');bgc=bg.getContext('2d');}
bg.width=BW;bg.height=BH;
ST.bgDirty=true;
}
/* ============================== room panorama ============================== */
var PANO=null,PANOW=null,PW=0,PH=0,PPM=116;
function buildPanorama(){
var circ=TAU*RROOM;
PPM=116;PW=Math.round(circ*PPM);PH=Math.round(WALLH*PPM);
var c=document.createElement('canvas');c.width=PW;c.height=PH;
var q=c.getContext('2d');
function X(m){return PW*0.5+m*PPM;}
function Y(m){return PH-m*PPM;}
function px(m){return m*PPM;}
/* --- wall base --- */
q.fillStyle='#e3d9c9';q.fillRect(0,0,PW,PH);
for(var i=0;i<200;i++){
q.globalAlpha=rr(0.015,0.05);
q.fillStyle=srnd()<0.5?'#fff7e8':'#6d5c49';
q.beginPath();q.arc(srnd()*PW,srnd()*PH,rr(30,180),0,TAU);q.fill();
}
q.globalAlpha=1;
var lg=q.createLinearGradient(0,0,0,PH);
lg.addColorStop(0,'rgba(38,28,20,0.36)');
lg.addColorStop(0.2,'rgba(255,248,232,0.05)');
lg.addColorStop(0.72,'rgba(255,244,225,0.04)');
lg.addColorStop(1,'rgba(28,20,14,0.32)');
q.fillStyle=lg;q.fillRect(0,0,PW,PH);
/* paint grain */
for(i=0;i<9000;i++){
q.globalAlpha=rr(0.02,0.07);
q.fillStyle=srnd()<0.55?'#fffaf0':'#7a6752';
q.fillRect(srnd()*PW,srnd()*PH,rr(1,2.6),rr(1,2.2));
}
q.globalAlpha=1;
/* --- ceiling band + cornice --- */
q.fillStyle='#39322b';q.fillRect(0,0,PW,Y(CEILH)+1);
var cg=q.createLinearGradient(0,0,0,Y(CEILH));
cg.addColorStop(0,'rgba(0,0,0,0.5)');cg.addColorStop(1,'rgba(120,104,86,0.25)');
q.fillStyle=cg;q.fillRect(0,0,PW,Y(CEILH)+1);
q.fillStyle='#f0e9db';q.fillRect(0,Y(2.46),PW,px(0.115));
q.fillStyle='rgba(255,250,235,0.5)';q.fillRect(0,Y(2.46),PW,px(0.02));
q.fillStyle='rgba(40,30,20,0.3)';q.fillRect(0,Y(2.46)+px(0.1),PW,px(0.025));
/* --- baseboard --- */
q.fillStyle='#efe8da';q.fillRect(0,Y(0.118),PW,px(0.118));
q.fillStyle='rgba(255,252,242,0.55)';q.fillRect(0,Y(0.108),PW,px(0.016));
q.fillStyle='rgba(35,26,18,0.34)';q.fillRect(0,Y(0.135),PW,px(0.03));
function windowArt(x0,x1,y0,y1){
var a=X(x0),b=X(x1),t=Y(y1),bt=Y(y0),w=b-a,h=bt-t;
q.fillStyle='rgba(60,44,30,0.28)';q.fillRect(a-px(0.075),t-px(0.07),w+px(0.15),h+px(0.14));
q.fillStyle='#f1ebdf';q.fillRect(a-px(0.055),t-px(0.05),w+px(0.11),h+px(0.1));
var gg=q.createLinearGradient(0,t,0,bt);
gg.addColorStop(0,'#dfeef6');gg.addColorStop(0.3,'#ffffff');gg.addColorStop(0.68,'#fbfbf2');
gg.addColorStop(1,'#dfe7cd');
q.fillStyle=gg;q.fillRect(a,t,w,h);
q.save();q.beginPath();q.rect(a,t,w,h);q.clip();
for(var i=0;i<30;i++){
q.globalAlpha=rr(0.08,0.3);
q.fillStyle=srnd()<0.5?'#b9cfa6':'#f4f8ea';
q.beginPath();q.ellipse(a+srnd()*w,t+h*rr(0.4,1.05),rr(w*0.08,w*0.3),rr(h*0.06,h*0.22),0,0,TAU);q.fill();
}
q.globalAlpha=1;
q.fillStyle='rgba(255,255,255,0.55)';q.fillRect(a,t,w,h*0.18);
q.restore();
q.fillStyle='#f5f1e6';
q.fillRect(a+w/2-px(0.028),t,px(0.056),h);
q.fillRect(a,t+h/3-px(0.024),w,px(0.048));
q.fillRect(a,t+h*2/3-px(0.024),w,px(0.048));
q.fillRect(a+px(0.005),t,px(0.04),h);q.fillRect(b-px(0.045),t,px(0.04),h);
q.fillStyle='rgba(255,252,240,0.85)';q.fillRect(a-px(0.02),t-px(0.02),w+px(0.04),px(0.03));
/* sill */
q.fillStyle='#f4eee2';q.fillRect(a-px(0.11),bt,w+px(0.22),px(0.062));
q.fillStyle='rgba(255,252,242,0.6)';q.fillRect(a-px(0.11),bt,w+px(0.22),px(0.014));
q.fillStyle='rgba(35,26,18,0.3)';q.fillRect(a-px(0.11),bt+px(0.062),w+px(0.22),px(0.028));
}
function curtain(x0,x1){
var a=X(x0),b=X(x1),w=b-a;
var bg2=q.createLinearGradient(0,0,0,PH);
bg2.addColorStop(0,'rgba(40,30,20,0.35)');bg2.addColorStop(0.35,'rgba(0,0,0,0)');
bg2.addColorStop(1,'rgba(40,30,20,0.25)');
q.fillStyle='#ddd0b7';q.fillRect(a,0,w,PH);
for(var i=0;i<10;i++){
var fx=a+w*(i+0.5)/10, hw=w/20;
var g2=q.createLinearGradient(fx-hw,0,fx+hw,0);
g2.addColorStop(0,'rgba(58,44,28,0.34)');
g2.addColorStop(0.45,'rgba(255,247,228,0.2)');
g2.addColorStop(1,'rgba(58,44,28,0.32)');
q.fillStyle=g2;q.fillRect(fx-hw,0,hw*2,PH);
}
q.fillStyle=bg2;q.fillRect(a,0,w,PH);
q.fillStyle='#5a4c3a';q.fillRect(a-px(0.02),0,w+px(0.04),px(0.05));
}
function picture(x0,x1,y0,y1,kind){
var a=X(x0),b=X(x1),t=Y(y1),bt=Y(y0),w=b-a,h=bt-t;
q.fillStyle='rgba(40,30,20,0.3)';q.fillRect(a+px(0.03),t+px(0.05),w,h);
q.fillStyle='#3b2c20';q.fillRect(a,t,w,h);
q.fillStyle='#efe6d4';q.fillRect(a+px(0.04),t+px(0.04),w-px(0.08),h-px(0.08));
var ix=a+px(0.09),iy=t+px(0.09),iw=w-px(0.18),ih=h-px(0.18);
var pg=q.createLinearGradient(ix,iy,ix,iy+ih);
if(kind===0){pg.addColorStop(0,'#9fb6c4');pg.addColorStop(0.5,'#d8cfb4');pg.addColorStop(1,'#7b6a4c');}
else if(kind===1){pg.addColorStop(0,'#c8a271');pg.addColorStop(0.55,'#8f9a7a');pg.addColorStop(1,'#3f4a3c');}
else {pg.addColorStop(0,'#e0d3bb');pg.addColorStop(1,'#8a715a');}
q.fillStyle=pg;q.fillRect(ix,iy,iw,ih);
for(var i=0;i<26;i++){
q.globalAlpha=rr(0.08,0.32);
q.fillStyle=srnd()<0.5?'#fff4dd':'#2f2a22';
q.beginPath();q.ellipse(ix+srnd()*iw,iy+srnd()*ih,rr(iw*0.04,iw*0.25),rr(ih*0.03,ih*0.16),rr(-1,1),0,TAU);q.fill();
}
q.globalAlpha=1;
q.fillStyle='rgba(255,246,225,0.18)';q.fillRect(ix,iy,iw,ih*0.3);
q.fillStyle='rgba(255,240,215,0.35)';q.fillRect(a,t,w,px(0.012));
}
function door(x0,x1,top){
var a=X(x0),b=X(x1),t=Y(top),bt=Y(0),w=b-a,h=bt-t;
q.fillStyle='rgba(45,34,24,0.3)';q.fillRect(a-px(0.05),t-px(0.05),w+px(0.1),h+px(0.06));
q.fillStyle='#f2ecdf';q.fillRect(a-px(0.04),t-px(0.04),w+px(0.08),h+px(0.05));
var dg=q.createLinearGradient(0,t,0,bt);
dg.addColorStop(0,'#221a13');dg.addColorStop(0.65,'#120d09');dg.addColorStop(1,'#0a0705');
q.fillStyle=dg;q.fillRect(a,t,w,h);
q.fillStyle='rgba(190,160,110,0.16)';q.fillRect(a+w*0.62,t+px(0.3),w*0.3,h-px(0.4));
q.fillStyle='rgba(255,232,190,0.1)';q.fillRect(a+w*0.55,t+px(0.35),w*0.2,h*0.7);
}
function credenza(x0,x1){
var a=X(x0),b=X(x1),t=Y(0.86),bt=Y(0),w=b-a,h=bt-t;
q.fillStyle='rgba(35,26,18,0.32)';q.fillRect(a+px(0.03),t+px(0.05),w,h);
q.fillStyle='#6b4c31';q.fillRect(a,t,w,h-px(0.07));
var wg=q.createLinearGradient(0,t,0,bt);
wg.addColorStop(0,'#8a6540');wg.addColorStop(0.5,'#6d4d31');wg.addColorStop(1,'#4a3320');
q.fillStyle=wg;q.fillRect(a,t,w,h-px(0.07));
q.fillStyle='rgba(255,235,205,0.25)';q.fillRect(a,t,w,px(0.02));
q.fillStyle='rgba(20,12,6,0.45)';q.fillRect(a+w*0.5-px(0.01),t+px(0.06),px(0.02),h-px(0.2));
for(var i=0;i<2;i++){q.fillStyle='#2a1c11';q.fillRect(a+px(0.14)+i*(w-px(0.5)),bt-px(0.09),px(0.08),px(0.09));}
/* books */
for(i=0;i<26;i++){
var bx=a+px(0.08)+i*px(0.062), bh=rr(0.3,0.44);
if(bx>b-px(0.35))break;
q.fillStyle=hslStr(rr(8,44),rr(25,55),rr(24,52));
q.fillRect(bx,Y(bh),px(0.05),Y(0.86)-Y(bh));
}
/* small lamp on top */
q.fillStyle='#c9b48c';q.fillRect(a+w*0.72,Y(1.16),px(0.02),Y(0.86)-Y(1.16));
q.fillStyle='#e8d7ae';
q.beginPath();q.moveTo(a+w*0.72-px(0.1),Y(1.16));q.lineTo(a+w*0.72+px(0.1),Y(1.16));
q.lineTo(a+w*0.72+px(0.07),Y(1.34));q.lineTo(a+w*0.72-px(0.07),Y(1.34));q.closePath();q.fill();
}
function hslStr(h,s,l){return 'hsl('+h+','+s+'%,'+l+'%)';}
function fireplace(x0,x1){
var a=X(x0),b=X(x1),w=b-a;
q.fillStyle='#cfc4b0';q.fillRect(a,Y(2.0),w,Y(0)-Y(2.0));
var sg=q.createLinearGradient(0,Y(2.0),0,Y(0));
sg.addColorStop(0,'rgba(255,248,232,0.3)');sg.addColorStop(1,'rgba(40,30,20,0.28)');
q.fillStyle=sg;q.fillRect(a,Y(2.0),w,Y(0)-Y(2.0));
q.fillStyle='#e6ddcc';q.fillRect(a-px(0.12),Y(1.3),w+px(0.24),px(0.1));
q.fillStyle='rgba(30,22,14,0.35)';q.fillRect(a-px(0.12),Y(1.3)+px(0.1),w+px(0.24),px(0.03));
q.fillStyle='#241a12';q.fillRect(a+px(0.3),Y(1.1),w-px(0.6),Y(0.12)-Y(1.1));
q.fillStyle='rgba(120,80,50,0.25)';
q.beginPath();q.ellipse(a+w/2,Y(0.35),w*0.2,px(0.3),0,0,TAU);q.fill();
q.fillStyle='#1a120c';q.fillRect(a+px(0.3),Y(0.12),w-px(0.6),Y(0)-Y(0.12));
}
function blinds(x0,x1,y0,y1){
windowArt(x0,x1,y0,y1);
var a=X(x0),b=X(x1),t=Y(y1),bt=Y(y0);
q.fillStyle='rgba(226,214,190,0.9)';
for(var y=t;y<bt;y+=px(0.055))q.fillRect(a,y,b-a,px(0.03));
q.fillStyle='rgba(60,45,30,0.16)';q.fillRect(a,t,b-a,px(0.1));
}
/* --- layout along the wall (metres, 0 = behind the cat) --- */
windowArt(-3.6,-1.25,0.72,2.06);
curtain(-4.4,-3.5); curtain(-1.35,-0.62);
picture(-1.05,-0.35,1.45,2.0,0);
picture(0.55,1.55,1.3,2.05,1);
picture(-5.85,-5.1,1.5,1.98,2);
door(2.5,3.45,2.05);
credenza(3.9,5.6);
blinds(7.4,9.3,0.75,2.05);
curtain(6.6,7.4); curtain(9.3,9.95);
fireplace(-8.8,-6.2);
picture(6.1,6.7,1.35,1.85,2);
picture(-12.9,-12.2,1.3,1.9,0);
windowArt(-11.9,-9.7,0.72,2.06); /* the key window, off-frame at rest */
curtain(-12.7,-11.9); curtain(-9.7,-8.95);
picture(10.4,11.4,1.2,2.0,1);
/* switch + outlets */
q.fillStyle='#efe9dc';q.fillRect(X(1.9),Y(1.2),px(0.09),px(0.13));
q.fillStyle='rgba(40,30,20,0.2)';q.fillRect(X(1.9),Y(1.13),px(0.09),px(0.02));
for(var k=0;k<3;k++){
q.fillStyle='#e9e2d4';
q.fillRect(X([-0.2,2.05,-4.8][k]),Y(0.24),px(0.1),px(0.13));
q.fillStyle='rgba(40,30,20,0.35)';
q.fillRect(X([-0.2,2.05,-4.8][k])+px(0.025),Y(0.2)+px(0.01),px(0.012),px(0.03));
q.fillRect(X([-0.2,2.05,-4.8][k])+px(0.06),Y(0.2)+px(0.01),px(0.012),px(0.03));
}
/* warm key wash + window bloom */
q.globalCompositeOperation='lighter';
var keys=[[-2.4,1.4,4.2],[ -10.8,1.4,4.0]];
for(k=0;k<keys.length;k++){
var kx=X(keys[k][0]),ky=Y(keys[k][1]),kr=px(keys[k][2]);
var rg=q.createRadialGradient(kx,ky,px(0.3),kx,ky,kr);
rg.addColorStop(0,'rgba(255,238,205,0.5)');
rg.addColorStop(0.4,'rgba(255,226,180,0.16)');
rg.addColorStop(1,'rgba(255,210,150,0)');
q.fillStyle=rg;q.beginPath();q.arc(kx,ky,kr,0,TAU);q.fill();
}
q.globalCompositeOperation='source-over';
/* overall warm falloff away from the windows */
var wg2=q.createLinearGradient(0,0,PW,0);
wg2.addColorStop(0,'rgba(255,226,180,0.13)');
wg2.addColorStop(0.12,'rgba(255,232,195,0.03)');
wg2.addColorStop(0.32,'rgba(20,14,10,0.12)');
wg2.addColorStop(0.5,'rgba(255,228,185,0.05)');
wg2.addColorStop(0.7,'rgba(20,14,10,0.1)');
wg2.addColorStop(1,'rgba(255,226,180,0.1)');
q.fillStyle=wg2;q.fillRect(0,0,PW,PH);
PANO=c;
/* doubled panorama so texture slices never wrap */
var w2=document.createElement('canvas');w2.width=PW*2;w2.height=PH;
var q2=w2.getContext('2d');
q2.drawImage(c,0,0);q2.drawImage(c,PW,0);
PANOW=w2;
}
/* ============================== furniture sprites ============================== */
var ITEMS=[
{az:22, r:4.55, w:2.05, d:0.95, h:0.86, t:'sofa'},
{az:31, r:3.35, w:1.15, d:0.62, h:0.44, t:'table'},
{az:46, r:4.7, w:0.62, d:0.62, h:1.5, t:'lamp'},
{az:-58,r:4.35, w:0.82, d:0.8, h:0.84, t:'chair'},
{az:-50,r:4.55, w:0.6, d:0.6, h:1.3, t:'plant'}
];
function sprite(wm,hm,fn){
var sw=460,sh=Math.round(460*hm/wm);
var c=document.createElement('canvas');c.width=sw;c.height=sh;
fn(c.getContext('2d'),sw,sh);
return c;
}
function paintSofa(g,W0,H0,view){
var fab='#68766f',fabD='#43504b',fabL='#8b978e';
g.clearRect(0,0,W0,H0);
if(view==='side'){
g.fillStyle=fabD;rrect(g,W0*0.16,H0*0.06,W0*0.5,H0*0.62,H0*0.06);g.fill();
g.fillStyle=fab;rrect(g,W0*0.12,H0*0.34,W0*0.76,H0*0.3,H0*0.05);g.fill();
g.fillStyle=fabL;rrect(g,W0*0.12,H0*0.33,W0*0.76,H0*0.05,H0*0.03);g.fill();
g.fillStyle=fabD;rrect(g,W0*0.7,H0*0.28,W0*0.18,H0*0.4,H0*0.05);g.fill();
}else{
g.fillStyle=fabD;rrect(g,W0*0.04,H0*0.02,W0*0.92,H0*0.55,H0*0.08);g.fill();
g.fillStyle=fab;rrect(g,W0*0.07,H0*0.05,W0*0.86,H0*0.5,H0*0.07);g.fill();
g.fillStyle=fab;rrect(g,W0*0.03,H0*0.3,W0*0.94,H0*0.38,H0*0.07);g.fill();
g.fillStyle=fabL;rrect(g,W0*0.03,H0*0.29,W0*0.94,H0*0.07,H0*0.035);g.fill();
g.fillStyle=fabD;rrect(g,W0*0.0, H0*0.26,W0*0.13,H0*0.44,H0*0.05);g.fill();
g.fillStyle=fabD;rrect(g,W0*0.87,H0*0.26,W0*0.13,H0*0.44,H0*0.05);g.fill();
g.strokeStyle='rgba(20,26,24,0.35)';g.lineWidth=H0*0.012;
g.beginPath();g.moveTo(W0*0.5,H0*0.34);g.lineTo(W0*0.5,H0*0.66);g.stroke();
g.beginPath();g.moveTo(W0*0.33,H0*0.06);g.lineTo(W0*0.33,H0*0.3);g.moveTo(W0*0.67,H0*0.06);g.lineTo(W0*0.67,H0*0.3);g.stroke();
}
/* shading + weave */
var lg=g.createLinearGradient(0,0,W0*0.3,H0);
lg.addColorStop(0,'rgba(255,240,210,0.2)');lg.addColorStop(0.5,'rgba(0,0,0,0)');lg.addColorStop(1,'rgba(10,14,12,0.4)');
g.fillStyle=lg;g.fillRect(0,0,W0,H0);
for(var i=0;i<2600;i++){
g.globalAlpha=rr(0.02,0.07);g.fillStyle=srnd()<0.5?'#e8f0e6':'#101614';
g.fillRect(srnd()*W0,srnd()*H0,rr(1,2.4),rr(1,1.8));
}
g.globalAlpha=1;
g.fillStyle='#2a2118';
g.fillRect(W0*0.08,H0*0.66,W0*0.05,H0*0.06);
g.fillRect(W0*0.86,H0*0.66,W0*0.05,H0*0.06);
}
function paintTable(g,W0,H0,view){
g.clearRect(0,0,W0,H0);
var top=view==='side'?0.42:0.34;
g.fillStyle='#3a2718';
g.fillRect(W0*0.14,H0*top,W0*0.05,H0*(0.98-top));
g.fillRect(W0*0.81,H0*top,W0*0.05,H0*(0.98-top));
var wg=g.createLinearGradient(0,H0*0.1,0,H0*top);
wg.addColorStop(0,'#b08248');wg.addColorStop(0.4,'#8d6535');wg.addColorStop(1,'#5d3f21');
g.fillStyle=wg;rrect(g,W0*0.03,H0*0.06,W0*0.94,H0*top, H0*0.03);g.fill();
g.fillStyle='rgba(255,235,200,0.3)';g.fillRect(W0*0.03,H0*0.06,W0*0.94,H0*0.02);
for(var i=0;i<700;i++){
g.globalAlpha=rr(0.02,0.07);g.fillStyle=srnd()<0.5?'#ffe6bb':'#2a1a0c';
g.fillRect(srnd()*W0,H0*(0.06+srnd()*top),rr(2,10),1);
}
g.globalAlpha=1;
if(view!=='side'){
/* book + mug */
g.fillStyle='#8c3f34';g.fillRect(W0*0.16,H0*0.0,W0*0.3,H0*0.06);
g.fillStyle='#d9c9a3';g.fillRect(W0*0.16,H0*0.0,W0*0.3,H0*0.018);
g.fillStyle='#e8e2d2';
g.beginPath();g.ellipse(W0*0.68,H0*0.03,W0*0.055,H0*0.03,0,0,TAU);g.fill();
g.fillStyle='#b9b0a0';
g.beginPath();g.ellipse(W0*0.68,H0*0.03,W0*0.035,H0*0.018,0,0,TAU);g.fill();
}
}
function paintChair(g,W0,H0,view){
g.clearRect(0,0,W0,H0);
var f='#a4623c',fd='#6d3a22';
if(view==='side'){
g.fillStyle=fd;rrect(g,W0*0.14,H0*0.04,W0*0.52,H0*0.6,H0*0.08);g.fill();
g.fillStyle=f;rrect(g,W0*0.1,H0*0.32,W0*0.78,H0*0.32,H0*0.06);g.fill();
g.fillStyle=fd;rrect(g,W0*0.68,H0*0.26,W0*0.2,H0*0.42,H0*0.06);g.fill();
}else{
g.fillStyle=fd;rrect(g,W0*0.06,H0*0.02,W0*0.88,H0*0.56,H0*0.1);g.fill();
g.fillStyle=f;rrect(g,W0*0.1,H0*0.06,W0*0.8,H0*0.5,H0*0.09);g.fill();
g.fillStyle=f;rrect(g,W0*0.05,H0*0.32,W0*0.9,H0*0.36,H0*0.08);g.fill();
g.fillStyle=fd;rrect(g,W0*0.0,H0*0.26,W0*0.16,H0*0.44,H0*0.06);g.fill();
g.fillStyle=fd;rrect(g,W0*0.84,H0*0.26,W0*0.16,H0*0.44,H0*0.06);g.fill();
}
var lg=g.createLinearGradient(0,0,W0*0.35,H0);
lg.addColorStop(0,'rgba(255,232,195,0.22)');lg.addColorStop(0.55,'rgba(0,0,0,0)');lg.addColorStop(1,'rgba(20,10,4,0.42)');
g.fillStyle=lg;g.fillRect(0,0,W0,H0);
for(var i=0;i<1800;i++){
g.globalAlpha=rr(0.02,0.06);g.fillStyle=srnd()<0.5?'#ffe0bb':'#1a0e06';
g.fillRect(srnd()*W0,srnd()*H0,rr(1,2.2),rr(1,1.6));
}
g.globalAlpha=1;g.fillStyle='#241708';
g.fillRect(W0*0.1,H0*0.66,W0*0.05,H0*0.06);g.fillRect(W0*0.85,H0*0.66,W0*0.05,H0*0.06);
}
function paintLamp(g,W0,H0){
g.clearRect(0,0,W0,H0);
g.fillStyle='#3b3a36';
g.beginPath();g.ellipse(W0*0.5,H0*0.97,W0*0.19,H0*0.03,0,0,TAU);g.fill();
g.fillStyle='#8c7a54';g.fillRect(W0*0.475,H0*0.16,W0*0.05,H0*0.8);
var sg=g.createLinearGradient(W0*0.3,0,W0*0.7,0);
sg.addColorStop(0,'#c8b083');sg.addColorStop(0.45,'#f4e6c2');sg.addColorStop(1,'#b49a6d');
g.fillStyle=sg;
g.beginPath();g.moveTo(W0*0.26,H0*0.16);g.lineTo(W0*0.74,H0*0.16);
g.lineTo(W0*0.66,H0*0.02);g.lineTo(W0*0.34,H0*0.02);g.closePath();g.fill();
g.fillStyle='rgba(255,244,214,0.5)';g.fillRect(W0*0.26,H0*0.15,W0*0.48,H0*0.02);
}
function paintPlant(g,W0,H0){
g.clearRect(0,0,W0,H0);
g.fillStyle='#8a5334';
g.beginPath();g.moveTo(W0*0.32,H0*0.7);g.lineTo(W0*0.68,H0*0.7);
g.lineTo(W0*0.62,H0*0.99);g.lineTo(W0*0.38,H0*0.99);g.closePath();g.fill();
g.fillStyle='rgba(255,225,190,0.25)';g.fillRect(W0*0.32,H0*0.7,W0*0.36,H0*0.03);
for(var i=0;i<13;i++){
var a=-2.6+i*0.42, len=H0*rr(0.3,0.52);
var x0=W0*0.5,y0=H0*0.72;
var x1=x0+Math.cos(a)*W0*0.3, y1=y0-Math.sin(a+1.6)*len;
g.strokeStyle=i%2?'#3f6b3c':'#548244';g.lineWidth=W0*rr(0.05,0.09);g.lineCap='round';
g.beginPath();g.moveTo(x0,y0);
g.quadraticCurveTo((x0+x1)/2+rr(-20,20),(y0+y1)/2,x1,y1);g.stroke();
}
var lg=g.createLinearGradient(0,0,W0*0.4,H0);
lg.addColorStop(0,'rgba(255,240,205,0.25)');lg.addColorStop(1,'rgba(8,14,8,0.4)');
g.fillStyle=lg;g.fillRect(0,0,W0,H0);
}
function buildSprites(){
for(var i=0;i<ITEMS.length;i++){
var it=ITEMS[i];
if(it.t==='sofa'){it.front=sprite(it.w,it.h,function(g,a,b){paintSofa(g,a,b,'front');});
it.side =sprite(it.d,it.h,function(g,a,b){paintSofa(g,a,b,'side');});}
else if(it.t==='table'){it.front=sprite(it.w,it.h,function(g,a,b){paintTable(g,a,b,'front');});
it.side =sprite(it.d,it.h,function(g,a,b){paintTable(g,a,b,'side');});}
else if(it.t==='chair'){it.front=sprite(it.w,it.h,function(g,a,b){paintChair(g,a,b,'front');});
it.side =sprite(it.d,it.h,function(g,a,b){paintChair(g,a,b,'side');});}
else if(it.t==='lamp'){it.front=sprite(it.w,it.h,function(g,a,b){paintLamp(g,a,b);});it.side=it.front;}
else {it.front=sprite(it.w,it.h,function(g,a,b){paintPlant(g,a,b);});it.side=it.front;}
}
}
/* ============================== noise / grain ============================== */
var GRAIN=null;
function buildGrain(){
var c=document.createElement('canvas');c.width=220;c.height=220;
var q=c.getContext('2d'),d=q.createImageData(220,220),p=d.data;
for(var i=0;i<p.length;i+=4){
var v=128+(Math.random()*2-1)*46;
p[i]=p[i+1]=p[i+2]=v;p[i+3]=255;
}
q.putImageData(d,0,0);GRAIN=c;
}
/* ============================== cat ============================== */
var PARTS=[
{c:[0,0.205,-0.055],r:[0.158,0.185,0.195],n:64,t:760,alb:0,body:1},
{c:[0,0.285,0.045], r:[0.132,0.175,0.145],n:64,t:640,alb:0,body:1},
{c:[0,0.335,0.115], r:[0.10,0.125,0.095], n:56,t:380,alb:2,body:1},
{c:[0,0.425,0.075], r:[0.085,0.075,0.085],n:48,t:240,alb:0,head:1},
{c:[0,0.505,0.085], r:[0.108,0.10,0.098], n:64,t:700,alb:0,head:1},
{c:[0,0.468,0.158], r:[0.058,0.045,0.05], n:48,t:230,alb:2,head:1},
{c:[-0.064,0.462,0.082],r:[0.052,0.05,0.05],n:40,t:150,alb:0,head:1},
{c:[ 0.064,0.462,0.082],r:[0.052,0.05,0.05],n:40,t:150,alb:0,head:1},
{c:[-0.066,0.15,0.135], r:[0.046,0.14,0.05], n:44,t:190,alb:0},
{c:[ 0.066,0.15,0.135], r:[0.046,0.14,0.05], n:44,t:190,alb:0},
{c:[-0.066,0.036,0.165],r:[0.05,0.038,0.062],n:40,t:95, alb:2},
{c:[ 0.066,0.036,0.165],r:[0.05,0.038,0.062],n:40,t:95, alb:2}
];
var TAIL=[[0.02,0.075,-0.215],[0.11,0.058,-0.19],[0.175,0.042,-0.125],[0.198,0.036,-0.04],
[0.188,0.033,0.045],[0.152,0.031,0.112],[0.10,0.031,0.152],[0.05,0.036,0.166]];
var TAILR=[0.043,0.04,0.036,0.033,0.03,0.027,0.024,0.02];
var TUFTS=[];
var NBC=18,BUCK=[],BKN=new Int32Array(NBC);
(function(){for(var i=0;i<NBC;i++)BUCK.push(new Float32Array(4200*4));})();
function furAlbedo(px,py,pz,alb){
var ang=Math.atan2(px,pz+0.03);
var s=Math.sin(ang*7+Math.sin(py*8.5)*0.6+pz*5+py*2.2);
var base;
if(alb===2)base=[238,216,180];
else base=[206,132,62];
if(alb!==2&&s<-0.22)base=[126,72,34];
if(alb!==2&&s>0.72)base=[224,158,86];
if(py<0.09&&alb!==2)base=[188,152,112];
if(alb===0&&py>0.52&&pz>0.05&&Math.abs(px)<0.062)base=[132,74,34];
var j=0.88+srnd()*0.24;
return [base[0]*j,base[1]*j,base[2]*j];
}
function addTuft(part,pts,p){
var n=norm([p[0]/(part.r[0]*part.r[0]),p[1]/(part.r[1]*part.r[1]),p[2]/(part.r[2]*part.r[2])]);
var col=furAlbedo(p[0],p[1],p[2],part.alb);
var tan=[-n[2],srnd()*0.5-0.2,n[0]];
var tl=Math.hypot(tan[0],tan[1],tan[2])||1;
TUFTS.push({p:pts,x:p[0],y:p[1],z:p[2],nx:n[0],ny:n[1],nz:n[2],
tx:tan[0]/tl,ty:tan[1]/tl,tz:tan[2]/tl,
len:rr(0.014,0.032),c:col,cl:part.alb});
}
function buildTufts(){
var i,j,k;
for(i=0;i<PARTS.length;i++){
var P=PARTS[i];
/* fibonacci sphere samples */
P.samp=new Float32Array(P.n*3);
for(j=0;j<P.n;j++){
var y=1-2*(j+0.5)/P.n, rad=Math.sqrt(Math.max(0,1-y*y)), th=j*2.399963;
P.samp[j*3]=Math.cos(th)*rad;P.samp[j*3+1]=y;P.samp[j*3+2]=Math.sin(th)*rad;
}
for(j=0;j<P.t;j++){
var y2=1-2*(j+0.5)/P.t, rad2=Math.sqrt(Math.max(0,1-y2*y2)), th2=j*2.399963;
var nx=Math.cos(th2)*rad2,ny=y2,nz=Math.sin(th2)*rad2;
var pp=[nx*P.r[0],ny*P.r[1],nz*P.r[2]];
addTuft(i,P,pp);
}
}
for(i=0;i<TAIL.length;i++){
var R=TAILR[i], c=TAIL[i], cnt=95;
for(j=0;j<cnt;j++){
var y3=1-2*(j+0.5)/cnt, rad3=Math.sqrt(Math.max(0,1-y3*y3)), th3=j*2.399963;
var pp2=[c[0]+Math.cos(th3)*rad3*R,c[1]+y3*R,c[2]+Math.sin(th3)*rad3*R];
TUFTS.push({p:-1-i,x:pp2[0],y:pp2[1],z:pp2[2],
nx:Math.cos(th3)*rad3,ny:y3,nz:Math.sin(th3)*rad3,
tx:-Math.sin(th3),ty:0,tz:Math.cos(th3),
len:rr(0.012,0.028),c:furAlbedo(pp2[0],pp2[1],pp2[2],0),cl:0});
}
}
}
function hull2(pts,n){
var p=[],i;
for(i=0;i<n;i++)p.push([pts[i*2],pts[i*2+1]]);
p.sort(function(a,b){return a[0]-b[0]||a[1]-b[1];});
function cross(o,a,b){return (a[0]-o[0])*(b[1]-o[1])-(a[1]-o[1])*(b[0]-o[0]);}
var lo=[],hi=[];
for(i=0;i<p.length;i++){
while(lo.length>=2&&cross(lo[lo.length-2],lo[lo.length-1],p[i])<=0)lo.pop();
lo.push(p[i]);
}
for(i=p.length-1;i>=0;i--){
while(hi.length>=2&&cross(hi[hi.length-2],hi[hi.length-1],p[i])<=0)hi.pop();
hi.push(p[i]);
}
lo.pop();hi.pop();
return lo.concat(hi);
}
var LKEY=norm([-0.72,0.24,0.62]);
var LRIM=norm([-0.35,0.34,-0.87]);
var _w=[0,0,0],_s=[0,0,0],_hpts=[];
function xform(px,py,pz,head,o){
var br=o.breathe;
if(o.bodyS)py*=br;
var d=Math.hypot(px,pz), k=1+o.flare*(0.35+0.65*Math.min(1,d/0.19));
px*=k;pz*=k;
var a=head?o.faceYaw:o.yaw, c=Math.cos(a), s=Math.sin(a);
o.out[0]=CATP[0]+px*c+pz*s;o.out[1]=CATP[1]+py;o.out[2]=CATP[2]-px*s+pz*c;
}
function shadeOf(nx,ny,nz,rot){
var c=Math.cos(rot),s=Math.sin(rot);
var wx=nx*c+nz*s, wy=ny, wz=-nx*s+nz*c;
var d1=wx*LKEY[0]+wy*LKEY[1]+wz*LKEY[2];
var d2=wx*LRIM[0]+wy*LRIM[1]+wz*LRIM[2];
var sh=0.3+Math.max(0,d1)*0.72+Math.pow(Math.max(0,d2),3)*0.5+Math.max(0,wy)*0.06;
return sh;
}
var SHADE_MUL=[0.34,0.51,0.68,0.85,1.02,1.2];
var PART_CACHE=[];
function drawCat(g,o){
o.out=o.out||[0,0,0];
var i,j,list=[],P;
for(i=0;i<PARTS.length;i++){
P=PARTS[i];
P._rot=P.head?o.faceYaw:o.yaw;
xform(P.c[0],P.c[1],P.c[2],P.head,o);
proj(o.out[0],o.out[1],o.out[2],_s);
list.push({k:i,d:_s[2]});
P._c=[o.out[0],o.out[1],o.out[2]];
}
for(i=0;i<TAIL.length;i++){
var tp=tailPt(i,o.tailT);
var c=Math.cos(o.yaw),s=Math.sin(o.yaw);
var wx=tp[0]*c+tp[2]*s, wz=-tp[0]*s+tp[2]*c;
var w2=[CATP[0]+wx*o.sx,CATP[1]+tp[1],CATP[2]+wz];
proj(w2[0],w2[1],w2[2],_s);
list.push({k:-1-i,d:_s[2],c:w2});
}
list.sort(function(a,b){return b.d-a.d;});
g.save();g.globalAlpha=o.alpha;
for(i=0;i<list.length;i++)drawPart(g,list[i],o);
if(o.face)drawFace(g,o);
g.restore();
}
function tailPt(i,t){
var p=TAIL[i], sw=Math.sin(t*2.2-i*0.5)*0.016*Math.pow(i/7,1.6);
return [p[0]+sw,p[1]+Math.abs(Math.sin(t*1.7-i*0.4))*0.012*Math.pow(i/7,2),p[2]];
}
function drawPart(g,item,o){
var i=item.k,P,cen,rad,rot;
if(i>=0){P=PARTS[i];cen=P._c;rot=P._rot;}
else{
var idx=-1-i, tp=tailPt(idx,o.tailT);
var c=Math.cos(o.yaw),s=Math.sin(o.yaw);
var k2=1+o.flare*0.5;
var wx=(tp[0]*c+tp[2]*s)*k2*o.sx, wz=(-tp[0]*s+tp[2]*c)*k2;
cen=[CATP[0]+wx,CATP[1]+tp[1],CATP[2]+wz];rot=o.yaw;
P={r:[TAILR[idx],TAILR[idx],TAILR[idx]],samp:null,alb:0,n:0};
}
var sc=FS/Math.max(0.4,cen[2]-CAM[0]*0);
var depth=cen[2];
sc=FS/depth;
/* ---- base body: projected convex hull of sphere samples ---- */
if(i>=0){
var n=P.n;_hpts.length=0;
var minx=1e9,miny=1e9,maxx=-1e9,maxy=-1e9;
for(var j=0;j<n;j++){
xform(P.samp[j*3]*P.r[0],P.samp[j*3+1]*P.r[1],P.samp[j*3+2]*P.r[2],P.head,o);
proj(o.out[0],o.out[1],o.out[2],_s);
_hpts.push(_s[0],_s[1]);
if(_s[0]<minx)minx=_s[0];if(_s[0]>maxx)maxx=_s[0];
if(_s[1]<miny)miny=_s[1];if(_s[1]>maxy)maxy=_s[1];
}
var H=hull2(_hpts,n);
if(H.length>2){
var cx=(minx+maxx)/2,cy=(miny+maxy)/2,rad2=Math.max(maxx-minx,maxy-miny)/2*1.12;
var alb=P.alb===2?[236,214,178]:[190,120,56];
var m=o.tint;
var gd=g.createRadialGradient(cx-rad2*0.45,cy-rad2*0.5,rad2*0.08,cx,cy,rad2*1.05);
gd.addColorStop(0,rgb(alb[0]*0.95*m[0],alb[1]*0.95*m[1],alb[2]*0.95*m[2]));
gd.addColorStop(0.5,rgb(alb[0]*0.62*m[0],alb[1]*0.62*m[1],alb[2]*0.62*m[2]));
gd.addColorStop(1,rgb(alb[0]*0.24*m[0],alb[1]*0.24*m[1],alb[2]*0.24*m[2]));
g.beginPath();g.moveTo(H[0][0],H[0][1]);
for(var q=1;q<H.length;q++)g.lineTo(H[q][0],H[q][1]);
g.closePath();
g.fillStyle=gd;g.fill();
g.strokeStyle='rgba(255,238,210,0.16)';g.lineWidth=Math.max(1,3*sc/430);g.stroke();
}
}else{
/* tail segment: small soft blob */
var pr=TAILR[-1-i]*sc*1.05;
var alb2=[190,120,56],m2=o.tint;
var gd2=g.createRadialGradient(item.c?0:0,0,0,0,0,1);
g.save();
proj(cen[0],cen[1],cen[2],_s);
var gd3=g.createRadialGradient(_s[0]-pr*0.4,_s[1]-pr*0.4,pr*0.1,_s[0],_s[1],pr*1.1);
gd3.addColorStop(0,rgb(alb2[0]*0.85*m2[0],alb2[1]*0.85*m2[1],alb2[2]*0.85*m2[2]));
gd3.addColorStop(1,rgb(alb2[0]*0.22*m2[0],alb2[1]*0.22*m2[1],alb2[2]*0.22*m2[2]));
g.fillStyle=gd3;g.beginPath();g.ellipse(_s[0],_s[1],pr*1.15,pr,0,0,TAU);g.fill();
g.restore();
}
/* ---- fur tufts, bucketed, dark to light ---- */
var key=i;
for(var b=0;b<NBC;b++)BKN[b]=0;
var step=o.detail<0.5?3:1;
var li=Math.min(2,o.lightIdx||1);
for(var t=0;t<TUFTS.length;t+=step){
var u=TUFTS[t];
var isPart = (u.p===key);
if(!isPart)continue;
xform(u.x,u.y,u.z,P.head,o);
var rx=o.out[0],ry=o.out[1],rz=o.out[2];
xform(u.x+u.nx*u.len+u.tx*u.len*0.5,u.y+u.ny*u.len+u.ty*u.len*0.5,u.z+u.nz*u.len+u.tz*u.len*0.5,P.head,o);
var tx2=o.out[0],ty2=o.out[1],tz2=o.out[2];
var p1=proj(rx,ry,rz,_s);if(p1[2]<0.06)continue;
var x1=p1[0],y1=p1[1];
var p2=proj(tx2,ty2,tz2,_s);if(p2[2]<0.06)continue;
var sh=shadeOf(u.nx,u.ny,u.nz,P._rot);
if(li===0)sh*=0.78;if(li===2)sh*=1.14;
var si=clamp(Math.floor(sh*4.4),0,5);
var bk=u.cl*6+si, ctr=BKN[bk];
if(ctr>4190)continue;
var arr=BUCK[bk];
arr[ctr*4]=x1;arr[ctr*4+1]=y1;arr[ctr*4+2]=p2[0];arr[ctr*4+3]=p2[1];
BKN[bk]=ctr+1;
}
var lw=Math.max(0.7,1.5*sc/430*o.lw);
g.lineWidth=lw;g.lineCap='round';
for(var sh2=0;sh2<6;sh2++){
for(var cl=0;cl<3;cl++){
var bk2=cl*6+sh2,ct=BKN[bk2];
if(!ct)continue;
var A=BUCK[bk2],mul=SHADE_MUL[sh2],m3=o.tint;
var base=cl===2?[238,216,180]:(cl===1?[126,72,34]:[206,132,62]);
g.strokeStyle=rgb(base[0]*mul*m3[0],base[1]*mul*m3[1],base[2]*mul*m3[2]);
g.beginPath();
for(var z=0;z<ct;z++){
g.moveTo(A[z*4],A[z*4+1]);g.lineTo(A[z*4+2],A[z*4+3]);
}
g.stroke();
}
}
}
/* face */
function eyePts(cx,cy,cz,side,fy,ew,eh,shape){
var c=Math.cos(fy),s=Math.sin(fy);
var rx=[c,0,-s], pts=[];
for(var i=0;i<18;i++){
var t=i/18*TAU;
var u=Math.cos(t)*ew, v=Math.sin(t)*eh*(shape);
pts.push([cx+rx[0]*u*side+v*0,cy+v,cz+ -rx[2]*u*side]);
}
return pts;
}
function drawFace(g,o){
var fy=o.faceYaw;
if(Math.cos(fy)<0.14)return;
var wide=o.wide, lid=o.lid;
var hs=o.headScale||1;
function P3(x,y,z){xform(x,y,z,1,o);return proj(o.out[0],o.out[1],o.out[2],[0,0,0]);}
function poly(pts,col){
g.beginPath();
for(var i=0;i<pts.length;i++){var s=P3(pts[i][0],pts[i][1],pts[i][2]);if(s[2]<0.06)return;if(i)g.lineTo(s[0],s[1]);else g.moveTo(s[0],s[1]);}
g.closePath();g.fillStyle=col;g.fill();
}
var side,eyes=[-1,1];
for(var e=0;e<2;e++){
side=eyes[e];
var ex=0.046*side, ey=0.512, ez=0.152;
var outline=eyeRing(ex,ey,ez,side,fy,0.031*hs,0.026*hs,lid);
poly(outline,'#241a10');
poly(eyeRing(ex,ey,ez+0.001,side,fy,0.028*hs,0.0235*hs,lid),'#b6bd63');
poly(eyeRing(ex,ey,ez+0.002,side,fy,0.023*hs,0.02*hs,lid),'#8d9a3d');
var pw=wide?0.017:0.0075, ph=wide?0.019:0.02;
poly(eyeRing(ex,ey,ez+0.003,side,fy,pw*hs,ph*hs,lid),'#0a0806');
poly(eyeRing(ex-0.009,ey+0.008,ez+0.004,side,fy,0.0062*hs,0.0062*hs,1),'rgba(255,255,255,0.92)');
/* lid */
var li=[];
for(var i=0;i<=10;i++){
var t=Math.PI*(0.06+0.88*i/10);
li.push([ex+Math.cos(t+Math.PI)*0.032*hs*side, ey+Math.sin(t)*0.028*hs*lid+0.002, ez+0.004]);
}
g.beginPath();
for(i=0;i<li.length;i++){var s=P3(li[i][0],li[i][1],li[i][2]);if(s[2]<0.06)break;if(i)g.lineTo(s[0],s[1]);else g.moveTo(s[0],s[1]);}
g.strokeStyle='rgba(28,18,10,0.85)';g.lineWidth=Math.max(1,2.2*FS/900);g.stroke();
}
/* nose + mouth */
poly([[0,0.492,0.205],[-0.0145,0.477,0.208],[0.0145,0.477,0.208]],'#b06a63');
poly([[0,0.481,0.207],[-0.006,0.472,0.205],[0.006,0.472,0.205]],'#7d423d');
g.strokeStyle='rgba(40,24,16,0.5)';g.lineWidth=Math.max(1,1.6*FS/900);
for(var sd=-1;sd<=1;sd+=2){
g.beginPath();
for(i=0;i<=6;i++){
var tt=i/6;
var wx=sd*(0.012+tt*0.2), wy=0.472-Math.sin(tt*2.4)*0.012+tt*0.006, wz=0.198-tt*0.03;
var s2=P3(wx,wy,wz);if(s2[2]<0.06)break;
if(i)g.lineTo(s2[0],s2[1]);else g.moveTo(s2[0],s2[1]);
}
g.strokeStyle='rgba(255,250,240,0.34)';g.stroke();
}
}
function eyeRing(ex,ey,ez,side,fy,ew,eh,lid){
var out=[];
for(var i=0;i<18;i++){
var t=i/18*TAU;
var u=Math.cos(t)*ew, v=Math.sin(t)*eh*(0.55+0.45*lid);
var c=Math.cos(fy),s=Math.sin(fy);
var x=ex+(u*side)*c, z=ez-(u*side)*s;
out.push([x,ey+v,z]);
}
return out;
}
/* ears */
function drawEars(g,o){
function tri(a,b,c,col){
g.beginPath();
var p=[a,b,c];
for(var i=0;i<3;i++){xform(p[i][0],p[i][1],p[i][2],1,o);proj(o.out[0],o.out[1],o.out[2],_s);if(_s[2]<0.06)return;if(i)g.lineTo(_s[0],_s[1]);else g.moveTo(_s[0],_s[1]);}
g.closePath();g.fillStyle=col;g.fill();
}
var m=o.tint;
var c1=rgb(178*m[0],110*m[1],58*m[2]),c2=rgb(126*m[0],72*m[1],36*m[2]),c3=rgb(170*m[0],104*m[1],98*m[2]);
tri([-0.118,0.545,0.02],[-0.036,0.552,0.075],[-0.078,0.625,0.035],c1);
tri([-0.105,0.552,0.032],[-0.05,0.556,0.068],[-0.078,0.607,0.04],c3);
tri([0.118,0.545,0.02],[0.036,0.552,0.075],[0.078,0.625,0.035],c2);
tri([0.105,0.552,0.032],[0.05,0.556,0.068],[0.078,0.607,0.04],c3);
}
/* ============================== state ============================== */
var ST={
stage:0,spin:0,speed:0,tspeed:0,t:0,b:0,faceOff:0.45,turnT:0,
settling:0,settleT:0,shake:0,flash:0,blink:0,blinkAt:3.2,breathe:0,
muted:false,audio:false,hint:1,bgDirty:true,roomA:0,vort:0,tear:0,wide:0,
tail:0,sx:1,flare:0,cyc:0
};
var DUST=[];
(function(){for(var i=0;i<150;i++){
DUST.push({a:srnd()*TAU,r:rr(0.35,3.2),y:rr(0.05,1.85),s:rr(0.4,1.3),ph:rr(0,TAU)});
}})();
var SEG_RND=[];
(function(){for(var i=0;i<72;i++)SEG_RND.push({dx:rr(-1,1),dy:rr(-1,1),r:rr(-1,1),p:rr(0,TAU)});})();
var PLANKS=[];
(function(){
for(var k=-30;k<=30;k++){
var z=k*0.192;
PLANKS.push({z:z,x:Math.sqrt(Math.max(0.04,RROOM*RROOM-z*z)),j:[]});
}
for(var i=0;i<PLANKS.length;i++){
var n=1+Math.floor(srnd()*3);
for(var j=0;j<n;j++)PLANKS[i].j.push(rr(-0.9,0.9)*PLANKS[i].x);
}
})();
var SPECK=[];
(function(){for(var i=0;i<1400;i++){
var a=srnd()*TAU,r=Math.sqrt(srnd())*RROOM*0.99;
SPECK.push({x:Math.cos(a)*r,z:Math.sin(a)*r,s:srnd()<0.5?-1:1,v:rr(0.4,1)});
}})();
var DEBRIS=[];
(function(){for(var i=0;i<70;i++)DEBRIS.push({a:rr(0,TAU),r:rr(0.4,3.4),y:rr(0.1,1.6),s:rr(0.3,1.1),sp:rr(0.6,1.5),w:rr(0,TAU)});})();
/* ============================== audio ============================== */
var AC=null,A=null;
function noiseBuf(sec){
var n=Math.floor(AC.sampleRate*sec),b=AC.createBuffer(1,n,AC.sampleRate),d=b.getChannelData(0);
for(var i=0;i<n;i++)d[i]=Math.random()*2-1;return b;
}
function makeIR(sec,dec){
var n=Math.floor(AC.sampleRate*sec),b=AC.createBuffer(2,n,AC.sampleRate);
for(var c=0;c<2;c++){var d=b.getChannelData(c);
for(var i=0;i<n;i++){var t=i/n;d[i]=(Math.random()*2-1)*Math.pow(1-t,dec)*(1-Math.exp(-i/300));}}
return b;
}
function glottalWave(){
var N=52,re=new Float32Array(N),im=new Float32Array(N);
for(var i=1;i<N;i++)im[i]=Math.pow(i,-1.12)*(1+0.85*Math.exp(-Math.pow((i-5)/3,2)))*(i<36?1:0.15);
return AC.createPeriodicWave(re,im);
}
function clipCurve(k){
var n=1024,c=new Float32Array(n);
for(var i=0;i<n;i++){var x=i/(n-1)*2-1;c[i]=Math.tanh(x*k)/Math.tanh(k);}
return c;
}
function initAudio(){
if(AC)return;
var Ctor=window.AudioContext||window.webkitAudioContext;
if(!Ctor)return;
AC=new Ctor();
if(AC.state==='suspended')AC.resume();
var nb=noiseBuf(2.0);
var master=AC.createGain();master.gain.value=0.9;
var comp=AC.createDynamicsCompressor();
comp.threshold.value=-15;comp.knee.value=24;comp.ratio.value=6;
comp.attack.value=0.005;comp.release.value=0.16;
master.connect(comp);comp.connect(AC.destination);
var conv=AC.createConvolver();conv.buffer=makeIR(1.7,2.4);
var wet=AC.createGain();wet.gain.value=0.14;conv.connect(wet);wet.connect(master);
/* voice sources */
var oscs=[],pw=glottalWave(),det=[0,6.5,-8.5];
for(var i=0;i<3;i++){
var o=AC.createOscillator();o.setPeriodicWave(pw);o.frequency.value=208;o.detune.value=det[i];
oscs.push(o);
}
var jit=AC.createBufferSource();jit.buffer=nb;jit.loop=true;jit.playbackRate.value=0.028;
var jg=AC.createGain();jg.gain.value=10;jit.connect(jg);
var vib=AC.createOscillator();vib.frequency.value=5.3;
var vg=AC.createGain();vg.gain.value=6;vib.connect(vg);
for(i=0;i<3;i++){jg.connect(oscs[i].frequency);vg.connect(oscs[i].frequency);oscs[i].start();}
jit.start();vib.start();
/* formant bank */
var FN=[560,1180,2650,3450,4300],FQ=[7,9,10,10,11],FG=[1,0.5,0.26,0.14,0.08];
var fbank=[],fgain=[],sum=AC.createGain();sum.gain.value=0.46;
for(i=0;i<5;i++){
var bp=AC.createBiquadFilter();bp.type='bandpass';bp.frequency.value=FN[i];bp.Q.value=FQ[i];
var gg=AC.createGain();gg.gain.value=FG[i];
for(var j2=0;j2<3;j2++)oscs[j2].connect(bp);
bp.connect(gg);gg.connect(sum);fbank.push(bp);fgain.push(gg);
}
var lp=AC.createBiquadFilter();lp.type='lowpass';lp.frequency.value=850;lp.Q.value=0.6;
var lpg=AC.createGain();lpg.gain.value=0.3;
for(j2=0;j2<3;j2++)oscs[j2].connect(lp);
lp.connect(lpg);lpg.connect(sum);
var syll=AC.createGain();syll.gain.value=0.5;sum.connect(syll);
var shp=AC.createWaveShaper();shp.curve=clipCurve(1.7);shp.oversample='2x';syll.connect(shp);
var pan=AC.createStereoPanner?AC.createStereoPanner():null;
var out=AC.createGain();out.gain.value=0.55;
if(pan){shp.connect(pan);pan.connect(out);}else shp.connect(out);
out.connect(master);out.connect(conv);
/* breath */
var br=AC.createBufferSource();br.buffer=nb;br.loop=true;
var bf=AC.createBiquadFilter();bf.type='bandpass';bf.frequency.value=2400;bf.Q.value=0.8;
var bg2=AC.createGain();bg2.gain.value=0.05;
br.connect(bf);bf.connect(bg2);bg2.connect(syll);br.start();
/* spin-locked loop LFO */
var lfo=AC.createOscillator();lfo.type='sine';lfo.frequency.value=2;
function tap(gain,dest){var g2=AC.createGain();g2.gain.value=gain;lfo.connect(g2);g2.connect(dest);return g2;}
var t1=tap(190,fbank[0].frequency),t2=tap(-430,fbank[1].frequency),
t3=tap(0.3,syll.gain),t4=tap(20,oscs[0].frequency),
t5=tap(0.045,bg2.gain);
var t6=pan?tap(0.6,pan.pan):null;
tap(14,oscs[1].frequency);tap(-11,oscs[2].frequency);
lfo.start();
/* whoosh */
var wh=AC.createBufferSource();wh.buffer=nb;wh.loop=true;
var wf=AC.createBiquadFilter();wf.type='bandpass';wf.Q.value=1.7;wf.frequency.value=650;
var wg=AC.createGain();wg.gain.value=0.0;
wh.connect(wf);wf.connect(wg);wg.connect(master);wg.connect(conv);wh.start();
var tw=tap(320,wf.frequency);
/* purr */
var po=AC.createOscillator();po.type='sine';po.frequency.value=31;
var pg2=AC.createGain();pg2.gain.value=0.0;
var pm=AC.createOscillator();pm.type='sine';pm.frequency.value=23;
var pmg=AC.createGain();pmg.gain.value=0.0;
pm.connect(pmg);pmg.connect(pg2.gain);
po.connect(pg2);pg2.connect(master);po.start();pm.start();
var pn=AC.createBufferSource();pn.buffer=nb;pn.loop=true;pn.playbackRate.value=0.2;
var plf=AC.createBiquadFilter();plf.type='lowpass';plf.frequency.value=150;
var png=AC.createGain();png.gain.value=0.0;pn.connect(plf);plf.connect(png);png.connect(master);pn.start();
A={master:master,oscs:oscs,fbank:fbank,fgain:fgain,syll:syll,out:out,wet:wet,conv:conv,
lfo:lfo,pan:pan,t1:t1,t2:t2,t3:t3,t4:t4,t5:t5,wg:wg,wf:wf,pg:pg2,pmg:pmg,png:png,
nb:nb,jg:jg,vg:vg,lpg:lpg,bg2:bg2};
ST.audio=true;
updMute();
}
function updMute(){
if(!A)return;
A.master.gain.setTargetAtTime(ST.muted?0:0.9,AC.currentTime,0.03);
}
function updateAudio(){
if(!A)return;
var t=AC.currentTime, hz=Math.max(0.05,ST.speed/TAU), s=ST.stage;
A.lfo.frequency.setTargetAtTime(Math.min(hz,26),t,0.02);
var f0=168+s*56+Math.min(hz,14)*9+(s>=4?ST.vort*90:0);
for(var i=0;i<3;i++)A.oscs[i].frequency.setTargetAtTime(f0,t,0.05);
A.fbank[0].frequency.setTargetAtTime(560+ (s>=4? -120*ST.vort:0) + s*24,t,0.06);
A.fbank[1].frequency.setTargetAtTime(1180-s*70,t,0.06);
A.fbank[2].frequency.setTargetAtTime(2650+s*120,t,0.06);
A.t1.gain.setTargetAtTime(195*(1-0.55*ST.vort)*(0.7+0.06*s),t,0.08);
A.t2.gain.setTargetAtTime(-430*(1-0.5*ST.vort),t,0.08);
A.t3.gain.setTargetAtTime(0.24+0.05*s-0.12*ST.vort,t,0.08);
A.syll.gain.setTargetAtTime(0.42+0.05*s+0.1*ST.vort,t,0.08);
A.out.gain.setTargetAtTime(0.5+0.04*s,t,0.08);
A.wet.gain.setTargetAtTime(0.12+s*0.035+0.18*ST.vort,t,0.1);
A.wg.gain.setTargetAtTime(Math.min(0.17,hz*0.012),t,0.08);
A.t5.gain.setTargetAtTime(0.03+0.008*s,t,0.1);
A.jg.gain.setTargetAtTime(9+s*4,t,0.1);
A.vg.gain.setTargetAtTime(5+s*3.5,t,0.1);
A.lpg.gain.setTargetAtTime(0.32-0.12*ST.vort,t,0.1);
A.pg.gain.setTargetAtTime(ST.vort*0.5,t,0.2);
A.pmg.gain.setTargetAtTime(ST.vort*0.45,t,0.2);
A.png.gain.setTargetAtTime(ST.vort*0.09,t,0.2);
}
/* ============================== room render ============================== */
function drawWalls(g){
var N=72,du=PW/N,i;
var tear=ST.tear;
for(i=0;i<N;i++){
var a0=(i/N)*TAU+ST.roomA, a1=((i+1)/N)*TAU+ST.roomA;
var q=[ringPt(a0,0,[0,0,0]),ringPt(a1,0,[0,0,0]),ringPt(a1,WALLH,[0,0,0]),ringPt(a0,WALLH,[0,0,0])];
var ok=true,minx=1e9,maxx=-1e9,miny=1e9,maxy=-1e9,mind=1e9;
for(var k=0;k<4;k++){
if(q[k][2]<0.3){ok=false;break;}
mind=Math.min(mind,q[k][2]);
if(q[k][0]<minx)minx=q[k][0];if(q[k][0]>maxx)maxx=q[k][0];
if(q[k][1]<miny)miny=q[k][1];if(q[k][1]>maxy)maxy=q[k][1];
}
if(!ok)continue;
if(maxx<-8||minx>VW+8||maxy<-8||miny>VH+8)continue;
if(tear>0){
var R=SEG_RND[i], ph=R.p+ST.t*2.4;
var dx=Math.sin(ph*1.7)*tear*90*R.dx, dy=Math.cos(ph*1.3)*tear*130*R.dy;
var cx=(q[0][0]+q[2][0])/2, cy=(q[0][1]+q[2][1])/2;
var rot=R.r*tear*0.09*Math.sin(ph);
var co=Math.cos(rot),si=Math.sin(rot);
for(k=0;k<4;k++){
var ox=q[k][0]-cx,oy=q[k][1]-cy;
q[k][0]=cx+ox*co-oy*si+dx;q[k][1]=cy+ox*si+oy*co+dy;
}
}
var u0=((i/N)+0.5)*PW;
drawQuadTex(g,PANOW,u0,0,du+0.7,PH,q,0.55);
}
}
function floorPath(g){
g.beginPath();
var started=false,first=null,last=null;
for(var aw=-62;aw<=62;aw+=3){
var a=aw*D2R;
var s=ringPt(a,0,[0,0,0]);
if(s[2]<0.25)continue;
if(!started){g.moveTo(s[0],s[1]);first=[s[0],s[1]];started=true;}
else g.lineTo(s[0],s[1]);
last=[s[0],s[1]];
}
if(!started)return false;
g.lineTo(last[0],VH*3);g.lineTo(first[0],VH*3);g.closePath();
return true;
}
function drawFloorBG(g){
g.save();
if(!floorPath(g)){g.restore();return;}
g.clip();
/* base */
var lg=g.createLinearGradient(0,VH*0.3,0,VH);
lg.addColorStop(0,'#4a3826');lg.addColorStop(0.35,'#6b4f31');lg.addColorStop(1,'#3a2a1a');
g.fillStyle=lg;g.fillRect(0,0,VW,VH);
/* light pools */
g.globalCompositeOperation='lighter';
var pools=[[[-1.9,0,-0.35],2.2,1.5,0.3],[-0.6,0,-2.0,1.5,1.0,0.16]];
var defs=[{c:[-1.85,-0.3],rx:1.5,rz:1.05,a:0.34},{c:[-0.5,-2.2],rx:1.1,rz:0.8,a:0.15}];
for(var pi=0;pi<defs.length;pi++){
var D=defs[pi];
var cor=[[D.c[0]-D.rx,D.c[1]-D.rz],[D.c[0]+D.rx,D.c[1]-D.rz],[D.c[0]+D.rx,D.c[1]+D.rz],[D.c[0]-D.rx,D.c[1]+D.rz]];
var sx=0,sy=0,sw=0,sh2=0,pts=[];
for(var i=0;i<4;i++){
var wp=rotY([cor[i][0],0,cor[i][1]],ST.roomA,[0,0,0]);
var s=proj(wp[0],0.002,wp[2],[0,0,0]);
pts.push(s);sx+=s[0];sy+=s[1];
}
sx/=4;sy/=4;
for(i=0;i<4;i++){sw=Math.max(sw,Math.abs(pts[i][0]-sx));sh2=Math.max(sh2,Math.abs(pts[i][1]-sy));}
for(i=0;i<6;i++){
var k=1+i*0.1;
softEll(g,sx,sy,sw*k,sh2*k,'rgba(255,226,178,1)',D.a*0.1);
}
}
g.globalCompositeOperation='source-over';
/* planks */
var fade=1-clamp(ST.speed/12,0,0.85);
g.lineWidth=Math.max(0.7,1.0);
for(var p=0;p<PLANKS.length;p++){
var PL=PLANKS[p];
var a1=rotY([-PL.x,0,PL.z],ST.roomA,[0,0,0]),a2=rotY([PL.x,0,PL.z],ST.roomA,[0,0,0]);
var s1=proj(a1[0],0,a1[2],[0,0,0]),s2=proj(a2[0],0,a2[2],[0,0,0]);
if(s1[2]<0.25&&s2[2]<0.25)continue;
g.strokeStyle='rgba(28,18,10,'+(0.3*fade)+')';
g.beginPath();g.moveTo(s1[0],s1[1]);g.lineTo(s2[0],s2[1]);g.stroke();
g.strokeStyle='rgba(255,232,196,'+(0.07*fade)+')';
g.beginPath();g.moveTo(s1[0],s1[1]-1.2);g.lineTo(s2[0],s2[1]-1.2);g.stroke();
for(var j=0;j<PL.j.length;j++){
var b1=rotY([PL.j[j],0,PL.z],ST.roomA,[0,0,0]);
var b2=rotY([PL.j[j],0,PL.z+0.192],ST.roomA,[0,0,0]);
var t1=proj(b1[0],0,b1[2],[0,0,0]),t2=proj(b2[0],0,b2[2],[0,0,0]);
if(t1[2]<0.25||t2[2]<0.25)continue;
g.strokeStyle='rgba(28,18,10,'+(0.22*fade)+')';
g.beginPath();g.moveTo(t1[0],t1[1]);g.lineTo(t2[0],t2[1]);g.stroke();
}
}
/* grain speckle */
for(var s3=0;s3<SPECK.length;s3++){
var S=SPECK[s3];
var wp2=rotY([S.x,0,S.z],ST.roomA,[0,0,0]);
var sp=proj(wp2[0],0.001,wp2[2],[0,0,0]);
if(sp[2]<0.25)continue;
var al=(S.s<0?0.1:0.06)*S.v*fade;
g.fillStyle=S.s<0?'rgba(24,16,8,'+al+')':'rgba(255,238,208,'+al+')';
g.fillRect(sp[0],sp[1],Math.max(1,sp[2]*0.004),Math.max(1,sp[2]*0.003));
}
/* rug */
var rc=[[ -1.15,-0.6],[1.25,-0.6],[1.25,-3.5],[-1.15,-3.5]];
var rpts=[];
for(i=0;i<4;i++){
var rw=rotY([rc[i][0],0,rc[i][1]],ST.roomA,[0,0,0]);
rpts.push(proj(rw[0],0.004,rw[2],[0,0,0]));
}
g.beginPath();g.moveTo(rpts[0][0],rpts[0][1]);
for(i=1;i<4;i++)g.lineTo(rpts[i][0],rpts[i][1]);
g.closePath();
g.fillStyle='rgba(122,74,58,0.94)';g.fill();
g.strokeStyle='rgba(228,206,176,0.5)';g.lineWidth=Math.max(1.5,3);g.stroke();
/* rug pattern */
for(var r2=1;r2<=3;r2++){
var f=r2/4;
var q2=[];
for(i=0;i<4;i++){
var cx0=(rc[0][0]+rc[2][0])/2,cz0=(rc[0][1]+rc[2][1])/2;
var px0=cx0+(rc[i][0]-cx0)*(1-f*0.42), pz0=cz0+(rc[i][1]-cz0)*(1-f*0.42);
var rw2=rotY([px0,0,pz0],ST.roomA,[0,0,0]);
q2.push(proj(rw2[0],0.006,rw2[2],[0,0,0]));
}
g.beginPath();g.moveTo(q2[0][0],q2[0][1]);
for(i=1;i<4;i++)g.lineTo(q2[i][0],q2[i][1]);
g.closePath();
g.strokeStyle='rgba(232,206,168,'+(0.28-r2*0.05)+')';g.lineWidth=Math.max(1,2.2-r2*0.4);g.stroke();
}
g.restore();
}
function drawItems(g){
var list=[],i;
for(i=0;i<ITEMS.length;i++){
var it=ITEMS[i],az=it.az*D2R;
var pr=rotY([it.r*Math.sin(az),0,-it.r*Math.cos(az)],ST.roomA,[0,0,0]);
var s=proj(pr[0],0,pr[2],[0,0,0]);
if(s[2]<0.4)continue;
if(s[0]<-VW*0.6||s[0]>VW*1.6)continue;
/* facing + view angle */
var nf=rotY([-Math.sin(az),0,Math.cos(az)],ST.roomA,[0,0,0]);
var toCam=norm([CAM[0]-pr[0],CAM[1]-pr[1],CAM[2]-pr[2]]);
var cf=nf[0]*toCam[0]+nf[1]*toCam[1]+nf[2]*toCam[2];
var sf=Math.sqrt(Math.max(0,1-cf*cf));
list.push({it:it,p:pr,s:s,d:s[2],cf:cf,sf:sf});
}
list.sort(function(a,b){return b.d-a.d;});
for(i=0;i<list.length;i++)drawItem(g,list[i]);
}
function drawItem(g,L){
var it=L.it,p=L.p;
var lift=(ST.stage>=2)?(0.05+0.22*clamp(ST.stage-2,0,2))*Math.sin(ST.t*2+it.az):0;
var tumble=(ST.stage>=3)?clamp(ST.stage-2,0,2)*Math.sin(ST.t*1.4+it.az*2)*0.35:0;
var y0=Math.max(0,lift), h=it.h;
var pb=proj(p[0],y0,p[2],[0,0,0]),pt=proj(p[0],y0+h,p[2],[0,0,0]);
if(pb[2]<0.4)return;
var sc=FS/pb[2];
var wf=Math.sqrt(Math.pow(it.w*L.cf,2)+Math.pow(it.d*L.sf,2));
var img=(Math.abs(L.cf)>Math.abs(L.sf))?it.front:it.side;
var wpx=wf*sc, hpx=pb[1]-pt[1];
/* contact shadow */
if(y0<0.03){
g.save();g.globalAlpha=0.4*(1-clamp(ST.stage-1,0,2)*0.3);
softEll(g,pb[0],pb[1],wpx*0.52,hpx*0.11,'rgba(18,10,5,1)',0.5);
g.restore();
}
g.save();
g.globalAlpha=1;
g.translate(pb[0],pt[1]+hpx/2);
if(tumble)g.rotate(tumble);
if(ST.stage>=2)g.globalAlpha=1-clamp(ST.stage-3,0,1)*0.25;
/* motion smear */
var smear=clamp(ST.speed/16,0,1);
if(smear>0.05){
g.globalAlpha=(1-smear*0.55)*0.35;
g.drawImage(img,-wpx/2-smear*26,-hpx/2,wpx,hpx);
g.drawImage(img,-wpx/2+smear*26,-hpx/2,wpx,hpx);
g.globalAlpha=1-smear*0.2;
}
g.drawImage(img,-wpx/2,-hpx/2,wpx,hpx);
g.restore();
/* lamp glow */
if(it.t==='lamp'&&ST.stage<4){
g.save();g.globalCompositeOperation='lighter';
var gp=proj(p[0],y0+it.h*0.86,p[2],[0,0,0]);
var rg=g.createRadialGradient(gp[0],gp[1],1,gp[0],gp[1],hpx*1.1);
rg.addColorStop(0,'rgba(255,226,170,0.4)');
rg.addColorStop(0.4,'rgba(255,200,130,0.12)');
rg.addColorStop(1,'rgba(255,180,110,0)');
g.fillStyle=rg;g.beginPath();g.arc(gp[0],gp[1],hpx*1.1,0,TAU);g.fill();
g.restore();
}
}
function renderBG(){
var g=bgc;
g.setTransform(1,0,0,1,0,0);
setView(BW,BH);
g.clearRect(0,0,BW,BH);
var bgGrad=g.createLinearGradient(0,0,0,BH);
bgGrad.addColorStop(0,'#241f1a');bgGrad.addColorStop(0.5,'#332a22');bgGrad.addColorStop(1,'#191310');
g.fillStyle=bgGrad;g.fillRect(0,0,BW,BH);
drawWalls(g);
drawFloorBG(g);
drawItems(g);
/* haze from the window */
if(ST.stage<=1){
g.save();g.globalCompositeOperation='lighter';
var hz=g.createLinearGradient(0,BH*0.1,BW*0.75,BH*0.85);
hz.addColorStop(0,'rgba(255,232,190,0.09)');
hz.addColorStop(0.45,'rgba(255,222,175,0.03)');
hz.addColorStop(1,'rgba(255,210,150,0)');
g.fillStyle=hz;g.fillRect(0,0,BW,BH);
g.restore();
}
}
/* ============================== effects ============================== */
function compositeBG(g){
var smear=clamp(ST.speed/13,0,1);
g.save();
if(ST.stage>=3){
g.translate(W/2,H/2);g.rotate(Math.sin(ST.t*0.6)*0.05*clamp(ST.stage-2,0,2));g.translate(-W/2,-H/2);
}
if(ST.stage>=3&&'filter' in g){
var N=40,hh=H/N,amt=clamp(ST.stage-2,0,2)*0.5;
for(var i=0;i<N;i++){
var y=i*hh;
var k=1+amt*0.05*Math.sin(i*0.22+ST.t*2.2);
var dx=amt*10*Math.sin(i*0.31+ST.t*4.4);
g.drawImage(bg,0,(y/H)*bg.height,bg.width,(hh/H)*bg.height+1,dx+W*(1-k)/2,y,W*k,hh+0.7);
}
}else{
if(smear>0.03){
var ks=[-0.02,-0.01,0.008,0.019],als=[0.2,0.28,0.3,0.22];
for(var j=0;j<4;j++){
g.globalAlpha=als[j]*(0.4+smear*0.6);
g.save();g.translate(W/2,0);g.scale(1+ks[j]*smear*2,1);g.translate(-W/2,0);
g.drawImage(bg,0,0,W,H);g.restore();
}
g.globalAlpha=1;
}else{
g.drawImage(bg,0,0,W,H);
}
}
g.restore();
}
function drawDust(g){
g.save();g.globalCompositeOperation='lighter';
for(var i=0;i<DUST.length;i++){
var D=DUST[i];
var a=D.a+ST.roomA*0.85+Math.sin(ST.t*0.2+D.ph)*0.05;
var r=D.r, y=D.y+Math.sin(ST.t*0.35+D.ph)*0.05;
var p=rotY([r*Math.sin(a),y,-r*Math.cos(a)],0,[0,0,0]);
var s=proj(p[0],p[1],p[2],[0,0,0]);
if(s[2]<0.3)continue;
if(s[0]<0||s[0]>W||s[1]<0||s[1]>H)continue;
var al=(0.1+0.14*Math.sin(ST.t*1.4+D.ph))*D.s;
g.fillStyle='rgba(255,236,200,'+al+')';
var sz=Math.max(0.8,1.6*D.s*(FS/s[2])/430);
g.beginPath();g.arc(s[0],s[1],sz,0,TAU);g.fill();
}
g.restore();
}
function drawSpeedLines(g){
var amt=clamp((ST.speed/TAU-1.4)/8,0,1);
if(amt<=0.01)return;
g.save();g.globalCompositeOperation='lighter';
var cx=W*0.5,cy=H*0.45;
var n=Math.floor(20+amt*60);
for(var i=0;i<n;i++){
var a=(i*2.399963+ST.t*0.4)%TAU;
var r0=W*0.12+((i*97)%100)/100*W*0.4;
var len=W*(0.05+amt*0.45)*(0.4+((i*53)%100)/100);
g.strokeStyle='rgba(255,238,205,'+(0.05+amt*0.12)+')';
g.lineWidth=0.8+amt*1.6;
g.beginPath();
g.moveTo(cx+Math.cos(a)*r0,cy+Math.sin(a)*r0*0.55);
g.lineTo(cx+Math.cos(a)*(r0+len),cy+Math.sin(a)*(r0+len)*0.55);
g.stroke();
}
g.restore();
}
function drawVortex(g){
if(ST.vort<=0.01)return;
var v=ST.vort;
var cx=W*0.5,cy=H*0.44;
g.save();
g.globalAlpha=v;
/* deep field */
var bgv=g.createRadialGradient(cx,cy,10,cx,cy,W*0.75);
bgv.addColorStop(0,'rgba(50,34,20,0.85)');
bgv.addColorStop(0.35,'rgba(18,20,32,0.75)');
bgv.addColorStop(1,'rgba(5,6,12,0.9)');
g.fillStyle=bgv;g.fillRect(0,0,W,H);
/* stars */
g.globalCompositeOperation='lighter';
for(var i=0;i<160;i++){
var a=(i*2.399963)%TAU, r=(60+((i*137)%240)/240*W*0.6);
var an=a+ST.t*0.05;
var x=cx+Math.cos(an)*r,y=cy+Math.sin(an)*r*0.6;
g.fillStyle='rgba(255,236,200,'+(0.05+0.09*Math.sin(ST.t+i))+')';
g.fillRect(x,y,1.6,1.6);
}
/* log spiral arms */
var arms=8;
for(var k=0;k<arms;k++){
var off=k/arms*TAU+ST.spin*0.35;
g.beginPath();
var first=true;
for(var t=0;t<7.5;t+=0.12){
var th=off+t*1.15;
var r=8*Math.exp(t*0.36)*2.2;
var x=cx+Math.cos(th)*r,y=cy+Math.sin(th)*r*0.62;
if(first){g.moveTo(x,y);first=false;}else g.lineTo(x,y);
}
var ag=g.createLinearGradient(cx,cy,cx+W*0.4,cy);
ag.addColorStop(0,'rgba(255,232,180,0.5)');
ag.addColorStop(0.4,'rgba(255,190,110,0.16)');
ag.addColorStop(1,'rgba(120,140,220,0)');
g.strokeStyle=ag;g.lineWidth=2.6;g.stroke();
g.strokeStyle='rgba(255,248,225,0.1)';g.lineWidth=6;g.stroke();
}
/* orbiting debris silhouettes */
for(i=0;i<DEBRIS.length;i++){
var D=DEBRIS[i];
var th2=D.a+ST.spin*D.sp*0.5+ST.t*0.2;
var rr2=28+D.r*46;
var x2=cx+Math.cos(th2)*rr2,y2=cy+Math.sin(th2)*rr2*0.62;
var sz=3+D.s*7;
g.globalAlpha=v*0.55;
g.save();g.translate(x2,y2);g.rotate(th2+D.w);
g.fillStyle='rgba(28,18,10,0.85)';
g.fillRect(-sz,-sz*0.4,sz*2,sz*0.8);
g.fillStyle='rgba(255,214,160,0.25)';
g.fillRect(-sz,-sz*0.4,sz*2,sz*0.16);
g.restore();
g.globalAlpha=v;
}
/* core bloom */
var pulse=0.78+0.22*Math.sin(ST.t*1.5);
var core=g.createRadialGradient(cx,cy,1,cx,cy,W*0.16);
core.addColorStop(0,'rgba(255,250,235,'+(0.85*pulse)+')');
core.addColorStop(0.18,'rgba(255,224,170,'+(0.4*pulse)+')');
core.addColorStop(0.55,'rgba(255,170,90,0.1)');
core.addColorStop(1,'rgba(255,140,60,0)');
g.fillStyle=core;g.beginPath();g.arc(cx,cy,W*0.16,0,TAU);g.fill();
/* slow rays */
g.globalCompositeOperation='lighter';
for(i=0;i<14;i++){
var a3=i/14*TAU+ST.t*0.08;
g.save();g.translate(cx,cy);g.rotate(a3);
var rg=g.createLinearGradient(0,0,W*0.4,0);
rg.addColorStop(0,'rgba(255,236,200,0.13)');
rg.addColorStop(1,'rgba(255,200,140,0)');
g.fillStyle=rg;
g.beginPath();g.moveTo(0,0);g.lineTo(W*0.42,-W*0.02);g.lineTo(W*0.42,W*0.02);g.closePath();g.fill();
g.restore();
}
g.globalCompositeOperation='source-over';
g.restore();
}
function postFX(g){
/* grade */
g.save();
g.globalCompositeOperation='soft-light';
g.fillStyle='rgba(255,196,124,0.22)';g.fillRect(0,0,W,H);
g.globalCompositeOperation='overlay';
g.fillStyle='rgba(20,26,52,0.1)';g.fillRect(0,0,W,H);
g.globalCompositeOperation='source-over';
/* chromatic edge tint */
var ca=clamp(ST.speed/20,0,0.5)+ST.vort*0.15;
if(ca>0.01){
g.globalCompositeOperation='lighter';
var e1=g.createLinearGradient(0,0,W,0);
e1.addColorStop(0,'rgba(255,60,40,'+(ca*0.16)+')');
e1.addColorStop(0.5,'rgba(0,0,0,0)');
e1.addColorStop(1,'rgba(50,90,255,'+(ca*0.16)+')');
g.fillStyle=e1;g.fillRect(0,0,W,H);
g.globalCompositeOperation='source-over';
}
/* grain */
if(GRAIN){
g.globalAlpha=0.055+0.03*clamp(ST.speed/14,0,1);
g.globalCompositeOperation='overlay';
var ox=(Math.random()*220)|0,oy=(Math.random()*220)|0;
for(var y=-oy;y<H;y+=220)for(var x=-ox;x<W;x+=220)g.drawImage(GRAIN,x,y);
g.globalCompositeOperation='source-over';g.globalAlpha=1;
}
/* vignette */
var vg=g.createRadialGradient(W*0.5,H*0.46,Math.min(W,H)*0.28,W*0.5,H*0.5,Math.max(W,H)*0.78);
vg.addColorStop(0,'rgba(0,0,0,0)');
vg.addColorStop(0.62,'rgba(10,6,4,0.16)');
vg.addColorStop(1,'rgba(6,4,3,0.62)');
g.fillStyle=vg;g.fillRect(0,0,W,H);
g.restore();
}
/* ============================== loop ============================== */
var last=0;
function update(dt){
ST.t+=dt;
var tgt=TAU*REVS[ST.stage];
ST.tspeed=tgt;
if(!ST.settling){
ST.speed+=(ST.tspeed-ST.speed)*(1-Math.exp(-dt*3.4));
ST.spin+=ST.speed*dt;
}else{
ST.settleT+=dt;
ST.speed+=(0-ST.speed)*(1-Math.exp(-dt*9));
ST.spin=lerp(ST.spin,0,1-Math.exp(-dt*6.5));
if(ST.settleT>1.5){ST.settling=0;ST.stage=0;ST.spin=0;ST.speed=0;ST.tspeed=0;}
}
if(ST.turnT<1)ST.turnT=Math.min(1,ST.turnT+dt/0.42);
ST.b=smooth(ST.turnT);
ST.roomA=ST.spin;
ST.vort+=( (ST.stage>=4?1:0) - ST.vort)*(1-Math.exp(-dt*2.0));
ST.tear+=( (ST.stage>=3?1:0)*(0.7+0.3*Math.sin(ST.t*1.7)) - ST.tear)*(1-Math.exp(-dt*2.2));
if(ST.settling)ST.tear*=Math.exp(-dt*3);
ST.flare+=( clamp((ST.stage-1)/3,0,1)*(0.35+0.1*Math.sin(ST.t*3)) - ST.flare)*(1-Math.exp(-dt*3));
ST.wide+=( clamp((ST.stage-1)/2,0,1) - ST.wide)*(1-Math.exp(-dt*3));
ST.sx=1+ST.flare*0.28;
ST.breathe=1+Math.sin(ST.t*1.65)*0.018*(1-clamp(ST.speed/10,0,0.8));
ST.tail+=dt;
ST.shake*=Math.exp(-dt*4.5);
ST.flash*=Math.exp(-dt*7);
/* blink */
ST.blinkAt-=dt;
if(ST.blinkAt<0){ST.blink=1;ST.blinkAt=rr(3.4,7.5);}
if(ST.blink>0)ST.blink=Math.max(0,ST.blink-dt*5.2);
updateAudio();
}
function render(){
gx.setTransform(DPR,0,0,DPR,0,0);
/* background layer */
if(ST.bgDirty||ST.stage>0||ST.tear>0.01){
renderBG();ST.bgDirty=false;
}
gx.fillStyle='#0b0908';gx.fillRect(0,0,W,H);
gx.save();
if(ST.shake>0.01){
gx.translate((Math.random()-0.5)*ST.shake*9,(Math.random()-0.5)*ST.shake*7);
}
if(ST.vort<0.98)compositeBG(gx);
drawVortex(gx);
drawDust(gx);
drawSpeedLines(gx);
/* ---- cat ---- */
setView(W,H);
var idleYaw=-1.15;
var bodyYaw=lerp(idleYaw,ST.spin,ST.b);
var faceYaw;
if(ST.stage>=4)faceYaw=0.55*Math.sin(ST.t*0.32);
else faceYaw=bodyYaw+lerp(0.45,0,ST.b)-clamp(ST.speed*0.014,-0.3,0)
+Math.sin(ST.t*0.55)*0.05*(1-ST.b);
var br=ST.breathe;
var lid=ST.blink>0?(1-ST.blink*2.2<0?0.05:1-ST.blink*1.9):(ST.stage>=4?0.55:1);
lid=clamp(lid,0.04,1);
var tint=[1+ST.vort*0.1,1-ST.vort*0.03,1-ST.vort*0.12];
if(ST.stage>=3){tint=[1.04,0.98,0.92];}
var common={
yaw:bodyYaw,faceYaw:faceYaw,breathe:br,bodyS:1,flare:ST.flare,
tailT:ST.tail,sx:ST.sx,detail:1,alpha:1,face:1,lw:1,lightIdx:1,tint:tint,
wide:ST.wide,lid:lid,headScale:1+ST.wide*0.06
};
/* motion-blur ghosts */
var revs=ST.speed/TAU;
var blur=clamp(revs*1.35,0,1);
if(blur>0.06){
for(var g2=1;g2<=3;g2++){
var off=(g2/3)*(TAU*revs/60)*2.1;
var o2=Object.assign({},common);
o2.yaw=bodyYaw-off*(g2%2?1:-1)*1.0;
o2.alpha=0.16*(1-g2*0.2)*blur;
o2.detail=0.3;o2.face=(g2===1&&ST.stage<3)?1:0;o2.lw=1.4;
drawCat(gx,o2);
}
}
common.alpha=1;
drawCat(gx,common);
/* ears drawn over the head */
var eo=Object.assign({},common);eo.out=[0,0,0];
gx.save();gx.globalAlpha=1;drawEars(gx,eo);gx.restore();
gx.restore();
postFX(gx);
if(ST.flash>0.01){
gx.save();gx.globalAlpha=ST.flash*0.5;gx.globalCompositeOperation='lighter';
gx.fillStyle='rgba(255,240,215,1)';gx.fillRect(0,0,W,H);gx.restore();
}
}
function frame(now){
var dt=Math.min(0.05,(now-last)/1000)||0.016;last=now;
update(dt);
render();
requestAnimationFrame(frame);
}
/* ============================== input ============================== */
var hintEl=document.getElementById('hint'),muteEl=document.getElementById('mute');
function doClick(){
initAudio();
if(AC&&AC.state==='suspended')AC.resume();
if(ST.settling)return;
if(ST.stage===0){
ST.stage=1;ST.turnT=0;ST.blink=1;
}else if(ST.stage<4){
ST.stage++;ST.flash=1;ST.shake=1;
if(ST.stage===4){ST.spin=ST.spin%TAU;}
}else{
/* settle: the room rains back down */
ST.settling=1;ST.settleT=0;ST.spin=ST.spin%TAU;
if(A){
var t=AC.currentTime;
A.oscs[0].frequency.setTargetAtTime(120,t,0.14);
A.oscs[1].frequency.setTargetAtTime(120,t,0.14);
A.oscs[2].frequency.setTargetAtTime(120,t,0.14);
A.master.gain.setTargetAtTime(ST.muted?0:0.2,t,0.3);
setTimeout(function(){if(A)A.master.gain.setTargetAtTime(ST.muted?0:0.9,AC.currentTime,0.2);},1500);
}
}
ST.hint=0;hintEl.style.opacity='0';
}
window.addEventListener('pointerdown',function(e){
if(e.target===muteEl||muteEl.contains(e.target))return;
doClick();
});
window.addEventListener('keydown',function(e){
if(e.code==='Space'||e.code==='Enter'){e.preventDefault();doClick();}
if(e.key==='m'||e.key==='M'){toggleMute();}
});
function toggleMute(){
ST.muted=!ST.muted;
muteEl.className=ST.muted?'off':'';
if(!ST.audio){muteEl.className=ST.muted?'off':'on';}
if(ST.muted){muteEl.classList.remove('off');muteEl.classList.add('on');}
else{muteEl.classList.remove('on');muteEl.classList.add('off');}
/* class 'off' hides the sound waves: invert */
muteEl.className=(ST.muted?'off ':'')+(ST.muted?'':'');
muteEl.classList.toggle('off',!ST.muted===false?ST.muted:!ST.muted);
muteEl.classList.toggle('off',ST.muted?false:true);
updMute();
}
muteEl.addEventListener('pointerdown',function(e){e.stopPropagation();});
muteEl.addEventListener('click',function(e){e.stopPropagation();toggleMute();});
window.addEventListener('resize',resize);
/* ============================== boot ============================== */
camInit();
buildPanorama();
buildSprites();
buildGrain();
buildTufts();
resize();
setView(W,H);
renderBG();
ST.bgDirty=false;
requestAnimationFrame(frame);
})();
</script>
</body>
</html>