Files
wildlife-kingdom/js/sprites.js
T
deepseek c06786518c Wildlife Kingdom — original browser zoo-management sim
- Procedural canvas art: 15 species × 4 poses, 15 buildings, modular terrain, UI icons
- Habitat enclosure detection, biome suitability, staff/guest AI pathfinding
- Economy: tickets, shops, wages, star rating, research tiers, 15 missions
- Day/night cycle, particles, WebAudio soundtrack & synth SFX
- Mouse + touch controls, responsive UI, localStorage autosave
- Verified: 31/31 logic tests (test/smoke.js) · 12/12 render audits (test/qa.html)
2026-08-23 07:01:21 +00:00

1162 lines
51 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
Wildlife Kingdom — js/sprites.js
All original artwork, procedurally drawn on canvas.
Consistent 45° iso look · soft warm shading · cute shapes.
Global: WK.Sprites
============================================================ */
(function(){
'use strict';
const WK = window.WK;
const HW = WK.HW, HH = WK.HH, TAU = WK.TAU;
const SPR = (WK.Sprites = {});
const cache = new Map();
/* generic cached-sprite factory: drawFn draws with origin at anchor */
function get(key, w, h, ax, ay, drawFn){
let s = cache.get(key);
if(!s){
const cv = WK.mkCanvas(w,h);
const ctx = cv.getContext('2d');
ctx.translate(ax,ay);
drawFn(ctx);
s = { cv, ax, ay };
cache.set(key,s);
}
return s;
}
SPR.blit = function(ctx, s, sx, sy){
ctx.drawImage(s.cv, Math.round(sx - s.ax), Math.round(sy - s.ay));
};
/* ============================================================
GROUND TILES (painted directly into cached terrain layer)
============================================================ */
function diamondPath(ctx,cx,cy,inset){
const hw=HW-(inset||0), hh=HH-(inset||0)*0.5;
ctx.beginPath();
ctx.moveTo(cx,cy-hh); ctx.lineTo(cx+hw,cy); ctx.lineTo(cx,cy+hh); ctx.lineTo(cx-hw,cy);
ctx.closePath();
}
SPR.diamondPath = diamondPath;
SPR.paintGround = function(ctx,cx,cy,gid,h){
const g = WK.Data.GROUND[gid];
const c = h<0.5 ? g.colors[0] : g.colors[1];
diamondPath(ctx,cx,cy,0);
ctx.fillStyle=c; ctx.fill();
// speckle texture
ctx.save(); ctx.clip();
ctx.globalAlpha=0.35;
ctx.fillStyle = WK.shade(c, h>0.5?-0.09:0.14);
const n=5;
for(let i=0;i<n;i++){
const hx=WK.hash2(i*7+Math.round(cx),i*13+Math.round(cy),i+3);
const hy=WK.hash2(i*29,i*17+Math.round(cx),i+9);
const px=cx+(hx-0.5)*HW*1.2, py=cy+(hy-0.5)*HH*1.2;
ctx.beginPath();
if(g.water){ ctx.ellipse(px,py,5,1.6,0,0,TAU); }
else if(gid==='stone'){ ctx.fillRect(px-3,py-1.5,6,3); }
else { ctx.ellipse(px,py,2.6,1.2,0,0,TAU); }
ctx.fill();
}
ctx.restore();
};
/* path overlay inset on top of ground */
SPR.paintPath = function(ctx,cx,cy,pid,h){
const p = WK.Data.PATH[pid];
ctx.save();
diamondPath(ctx,cx,cy,4);
ctx.fillStyle=p.base; ctx.fill();
ctx.strokeStyle='rgba(80,55,20,.18)'; ctx.lineWidth=1.5; ctx.stroke();
ctx.clip();
ctx.fillStyle=p.dark; ctx.globalAlpha=.75;
for(let i=0;i<4;i++){
const hx=WK.hash2(Math.round(cx)+i*31,i*11,5), hy=WK.hash2(i*7,Math.round(cy)+i*13,8);
const px=cx+(hx-0.5)*(HW-14), py=cy+(hy-0.5)*(HH-8);
if(pid==='wood'){ ctx.fillRect(px-7,py-1.4,14,2.4); }
else if(pid==='stonep'){ ctx.beginPath(); ctx.ellipse(px,py,5,2.4,0,0,TAU); ctx.fill(); }
else { ctx.beginPath(); ctx.arc(px,py,1.6,0,TAU); ctx.fill(); }
}
ctx.restore();
};
/* ============================================================
NATURE OBJECTS
============================================================ */
const objDraw = {
tree(ctx){
WK.softShadow(ctx,10,'rgba(50,90,20,.3)',3);
ctx.fillStyle='#8a5a34';
WK.rr(ctx,-5,-38,10,38,4); ctx.fill(); WK.clearShadow(ctx);
const leaf=(dx,dy,r,c)=>{WK.blob(ctx,dx,dy,r,r*.85,c);};
leaf(-16,-46,22,'#57a83e'); leaf(16,-46,22,'#57a83e');
leaf(0,-66,26,'#63b34c'); leaf(0,-44,24,'#6fc457');
ctx.fillStyle='rgba(255,255,255,.25)';
ctx.beginPath(); ctx.ellipse(-8,-72,9,5,-.5,0,TAU); ctx.fill();
},
bush(ctx){
WK.blob(ctx,-8,-9,12,9,'#5aa73c'); WK.blob(ctx,9,-8,11,8,'#63b34c'); WK.blob(ctx,0,-14,13,10,'#6fc457');
ctx.fillStyle='#e86a8a'; ctx.beginPath(); ctx.arc(-4,-14,2.2,0,TAU); ctx.arc(7,-10,2.2,0,TAU); ctx.fill();
},
flower(ctx){
const cols=['#ff8aa8','#ffd23c','#ff9d47','#b78ae8'];
for(let i=0;i<5;i++){
const a=i/5*TAU, r=i%2?9:15;
const x=Math.cos(a)*r, y=-4+Math.sin(a)*r*.45;
ctx.strokeStyle='#4d9a37'; ctx.lineWidth=1.6;
ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x,y-7); ctx.stroke();
ctx.fillStyle=cols[i%4];
for(let p=0;p<5;p++){ const pa=p/5*TAU; ctx.beginPath(); ctx.arc(x+Math.cos(pa)*3,y-7+Math.sin(pa)*3,2.4,0,TAU); ctx.fill(); }
ctx.fillStyle='#fff3c2'; ctx.beginPath(); ctx.arc(x,y-7,1.8,0,TAU); ctx.fill();
}
},
rock(ctx){
WK.softShadow(ctx,8,'rgba(50,40,20,.3)',3);
WK.blob(ctx,-7,-8,14,10,'#9a958c');
WK.clearShadow(ctx);
WK.blob(ctx,7,-10,11,9,'#aaa49a');
ctx.fillStyle='rgba(255,255,255,.3)';
ctx.beginPath(); ctx.ellipse(-9,-14,5,2.5,-.4,0,TAU); ctx.fill();
},
palm(ctx){
WK.softShadow(ctx,9,'rgba(50,90,20,.28)',3);
ctx.strokeStyle='#a9743f'; ctx.lineWidth=7; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(0,0); ctx.quadraticCurveTo(6,-24,2,-44); ctx.stroke();
WK.clearShadow(ctx);
const frond=(a,len)=>{
ctx.strokeStyle='#4da339'; ctx.lineWidth=6; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(2,-44);
ctx.quadraticCurveTo(2+Math.cos(a)*len*.6,-44+Math.sin(a)*len*.6-4, 2+Math.cos(a)*len,-42+Math.sin(a)*len*.8);
ctx.stroke();
};
frond(-Math.PI*.9,26); frond(-Math.PI*.5,28); frond(-Math.PI*.1,26);
frond(-Math.PI*.7,22); frond(-Math.PI*.3,24);
ctx.fillStyle='#8a5a34'; ctx.beginPath(); ctx.arc(6,-40,3,0,TAU); ctx.fill();
},
bamboo(ctx){
for(let i=-1;i<=1;i++){
const x=i*8, h=44-Math.abs(i)*8;
ctx.strokeStyle=i===0?'#6db84a':'#5aa73c'; ctx.lineWidth=5; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x+i*2,-h); ctx.stroke();
ctx.strokeStyle='rgba(60,110,40,.5)'; ctx.lineWidth=1.4;
for(let k=1;k<4;k++){ const yy=-h*k/4; ctx.beginPath(); ctx.moveTo(x-2.6,yy); ctx.lineTo(x+2.6,yy); ctx.stroke(); }
ctx.strokeStyle='#4d9a37'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(x,-h*.7); ctx.lineTo(x+9,-h*.82); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x,-h*.55); ctx.lineTo(x-8,-h*.68); ctx.stroke();
}
},
bench(ctx){
WK.softShadow(ctx,7,'rgba(50,40,20,.28)',3);
ctx.fillStyle='#a9743f'; WK.rr(ctx,-14,-12,28,5,2); ctx.fill();
WK.rr(ctx,-14,-19,28,4,2); ctx.fill();
ctx.fillStyle='#8a5a34';
ctx.fillRect(-13,-8,3,8); ctx.fillRect(10,-8,3,8);
ctx.fillRect(-13,-19,3,7); ctx.fillRect(10,-19,3,7);
WK.clearShadow(ctx);
ctx.strokeStyle='#c98d52'; ctx.lineWidth=1;
ctx.beginPath(); ctx.moveTo(-10,-17.5); ctx.lineTo(10,-17.5); ctx.stroke();
},
bin(ctx){
WK.softShadow(ctx,6,'rgba(50,40,20,.28)',3);
WK.blob(ctx,0,-9,8,9,'#63b34c');
WK.clearShadow(ctx);
ctx.fillStyle='#3e8a33'; WK.rr(ctx,-9,-20,18,5,2); ctx.fill();
ctx.fillStyle='#fff'; ctx.font='bold 9px sans-serif'; ctx.textAlign='center';
ctx.fillText('♻',0,-12);
},
lamp(ctx){
WK.softShadow(ctx,6,'rgba(50,40,20,.28)',3);
ctx.fillStyle='#5f6b72'; ctx.fillRect(-2,-46,4,46);
WK.clearShadow(ctx);
WK.blob(ctx,0,-50,7,8,'#ffe066');
ctx.fillStyle='#5f6b72'; ctx.beginPath(); ctx.arc(0,-56,4.5,Math.PI,0); ctx.fill();
},
};
SPR.getObject = function(id, variant){
variant = variant||0;
return get('obj:'+id+':'+variant, 120,130, 60,122, ctx=>{
ctx.save();
if(variant===1) ctx.scale(.88,.88);
if(variant===2){ ctx.scale(1.08,1.08); }
if(variant===1) ctx.rotate(-.03);
objDraw[id](ctx);
ctx.restore();
});
};
SPR.objectRealSize = { tree:1.15, palm:1.1, bamboo:.95 }; // visual scale multipliers
/* ============================================================
FENCE EDGES (drawn dynamically)
============================================================ */
SPR.drawFenceEdge = function(ctx, x1,y1, x2,y2, kind, t){
// posts at ends, rails between; hedge = green blobs
if(kind===2){ // hedge
ctx.strokeStyle='#4d9a37'; ctx.lineCap='round'; ctx.lineWidth=9;
ctx.beginPath(); ctx.moveTo(x1,y1-6); ctx.lineTo(x2,y2-6); ctx.stroke();
ctx.strokeStyle='#63b34c'; ctx.lineWidth=6;
ctx.beginPath(); ctx.moveTo(x1,y1-9); ctx.lineTo(x2,y2-9); ctx.stroke();
return;
}
const wood = kind===1;
const postC = wood?'#a9743f':'#8f9aa2', railC = wood?'#c98d52':'#aab4bc';
const nx=(y2-y1), ny=-(x2-x1); const L=Math.hypot(nx,ny)||1;
const px=nx/L*2.6, py=ny/L*2.6; // perpendicular offset so rails show thickness
ctx.strokeStyle=railC; ctx.lineWidth=3; ctx.lineCap='round';
for(const dy of [-5,-11]){
ctx.beginPath(); ctx.moveTo(x1,y1+dy); ctx.lineTo(x2,y2+dy); ctx.stroke();
}
if(kind===3){ // gate: crossbar + sign
ctx.strokeStyle=postC; ctx.lineWidth=2.4;
ctx.beginPath(); ctx.moveTo(x1,y1-3); ctx.lineTo(x2,y2-13); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x1,y1-13); ctx.lineTo(x2,y2-3); ctx.stroke();
ctx.fillStyle='#ffd23c';
ctx.save(); ctx.translate((x1+x2)/2,(y1+y2)/2-9);
WK.rr(ctx,-6,-5,12,10,3); ctx.fill();
ctx.fillStyle='#7a5210'; ctx.fillRect(-1.2,-3,2.4,6); ctx.fillRect(-3,-1.2,6,2.4);
ctx.restore();
} else {
ctx.strokeStyle=railC; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(x1+px,y1-2+py); ctx.lineTo(x2+px,y2-2+py); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x1-px,y1-2-py); ctx.lineTo(x2-px,y2-2-py); ctx.stroke();
}
for(const [ex,ey] of [[x1,y1],[x2,y2]]){
ctx.fillStyle=postC;
WK.rr(ctx,ex-2.4,ey-16,4.8,16,2); ctx.fill();
ctx.fillStyle=WK.shade(postC,-.2);
ctx.beginPath(); ctx.arc(ex,ey-16,2.6,Math.PI,0); ctx.fill();
}
};
/* ============================================================
BUILDINGS
============================================================ */
/* Footprint corner math: tile (i,j) center = ((i-j)*HW,(i+j)*HH).
Returns the 4 base-diamond corners relative to footprint center. */
function fcorners(w,h){
const P=(i,j)=>[(i-j)*HW,(i+j)*HH];
const c=P(w/2,h/2);
const rel=(p)=>[p[0]-c[0],p[1]-c[1]];
return { N:rel(P(0,0)), E:rel(P(w,0)), S:rel(P(w,h)), W:rel(P(0,h)) };
}
function poly(ctx,pts,fill){
ctx.beginPath(); ctx.moveTo(pts[0][0],pts[0][1]);
for(let i=1;i<pts.length;i++) ctx.lineTo(pts[i][0],pts[i][1]);
ctx.closePath(); ctx.fillStyle=fill; ctx.fill();
}
function isoPrism(ctx,C,w,h,z,colT,colL,colR){
const k=fcorners(w,h);
const N=[k.N[0]+C[0],k.N[1]+C[1]-z], E=[k.E[0]+C[0],k.E[1]+C[1]-z],
S=[k.S[0]+C[0],k.S[1]+C[1]-z], W=[k.W[0]+C[0],k.W[1]+C[1]-z];
// walls
poly(ctx,[W,S,[k.S[0]+C[0],k.S[1]+C[1]],[k.W[0]+C[0],k.W[1]+C[1]]],colL);
poly(ctx,[S,E,[k.E[0]+C[0],k.E[1]+C[1]],[k.S[0]+C[0],k.S[1]+C[1]]],colR);
// subtle wall shading line at corner
ctx.strokeStyle='rgba(60,40,15,.18)'; ctx.lineWidth=1.4;
ctx.beginPath(); ctx.moveTo(S[0],S[1]); ctx.lineTo(k.S[0]+C[0],k.S[1]+C[1]); ctx.stroke();
// roof/top
poly(ctx,[N,E,S,W],colT);
ctx.strokeStyle='rgba(60,40,15,.14)';
ctx.beginPath(); ctx.moveTo(N[0],N[1]); ctx.lineTo(E[0],E[1]); ctx.lineTo(S[0],S[1]); ctx.lineTo(W[0],W[1]); ctx.closePath(); ctx.stroke();
return {N,E,S,W};
}
function doorOnRightFace(ctx,C,w,h,z,colD){
const k=fcorners(w,h);
// right face runs S->E ; door near middle of that face
const mx=C[0]+(k.S[0]+k.E[0])/2, my=C[1]-z+(k.S[1]+k.E[1])/2;
const dw=Math.min(16,6+w*2), dh=Math.min(22,8+h*3);
ctx.fillStyle=colD||'#6b4a2e';
ctx.beginPath();
ctx.moveTo(mx-dw/2,my); ctx.lineTo(mx-dw/2,my-dh+dw/2);
ctx.arc(mx,my-dh+dw/2,dw/2,Math.PI,0);
ctx.lineTo(mx+dw/2,my); ctx.closePath(); ctx.fill();
ctx.fillStyle='rgba(255,236,180,.85)';
ctx.beginPath(); ctx.arc(mx,my-dh+dw/2,dw/4,Math.PI,0); ctx.fill();
return [mx,my];
}
function windowsOnLeftFace(ctx,C,w,h,z,n){
const k=fcorners(w,h);
for(let i=1;i<=n;i++){
const tt=i/(n+1);
const x=C[0]+k.W[0]+(k.S[0]-k.W[0])*tt, y=C[1]-z+(k.W[1]+(k.S[1]-k.W[1])*tt);
ctx.fillStyle='rgba(255,255,255,.5)';
WK.rr(ctx,x-5,y-14,10,11,3); ctx.fill();
ctx.strokeStyle='rgba(90,60,30,.35)'; ctx.lineWidth=1.2;
WK.rr(ctx,x-5,y-14,10,11,3); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x-5,y-8.5); ctx.lineTo(x+5,y-8.5); ctx.stroke();
}
}
function signPlaque(ctx,x,y,color){
ctx.fillStyle=color; ctx.beginPath(); ctx.arc(x,y,7,0,TAU); ctx.fill();
ctx.strokeStyle='rgba(255,255,255,.8)'; ctx.lineWidth=1.6;
ctx.beginPath(); ctx.arc(x,y,4.4,0,TAU); ctx.stroke();
}
const roofDraw = {
flat(ctx,C,w,h,z,col){
const k=fcorners(w,h);
const pts=[k.N,k.E,k.S,k.W].map(p=>[p[0]+C[0],p[1]+C[1]-z]);
poly(ctx,pts.map(p=>[p[0]*0.92+C[0]*0.08, p[1]*0.92+(C[1]-z)*0.08-3]),WK.shade(col,-.12));
ctx.strokeStyle=WK.shade(col,-.3); ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(pts[0][0],pts[0][1]);
pts.slice(1).forEach(p=>ctx.lineTo(p[0],p[1])); ctx.closePath(); ctx.stroke();
ctx.fillStyle=WK.shade(col,.15);
ctx.fillRect(C[0]+k.E[0]*.4-6, C[1]-z+k.E[1]*.4-14, 12,7); // vent
},
gable(ctx,C,w,h,z,col){
const k=fcorners(w,h);
const ridgeY=z+16;
const Rw=[k.W[0]+C[0], k.W[1]+C[1]-ridgeY], Re=[k.E[0]+C[0], k.E[1]+C[1]-ridgeY];
// left slope
poly(ctx,[[k.W[0]+C[0],k.W[1]+C[1]-z],[k.N[0]+C[0],k.N[1]+C[1]-z],Re,Rw],WK.shade(col,.18));
// right slope
poly(ctx,[[k.N[0]+C[0],k.N[1]+C[1]-z],[k.E[0]+C[0],k.E[1]+C[1]-z],Re],WK.shade(col,-.05));
poly(ctx,[[k.E[0]+C[0],k.E[1]+C[1]-z],[k.S[0]+C[0],k.S[1]+C[1]-z],Rw,Re],col);
ctx.strokeStyle=WK.shade(col,-.32); ctx.lineWidth=1.6;
ctx.beginPath(); ctx.moveTo(Rw[0],Rw[1]); ctx.lineTo(Re[0],Re[1]); ctx.stroke();
},
slant(ctx,C,w,h,z,col){
const k=fcorners(w,h);
const high=z+20;
poly(ctx,[[k.W[0]+C[0],k.W[1]+C[1]-high],[k.N[0]+C[0],k.N[1]+C[1]-high*.4],[k.E[0]+C[0],k.E[1]+C[1]-high*.4],[k.S[0]+C[0],k.S[1]+C[1]-z]],col);
poly(ctx,[[k.N[0]+C[0],k.N[1]+C[1]-high*.4],[k.E[0]+C[0],k.E[1]+C[1]-high*.4],[k.S[0]+C[0],k.S[1]+C[1]-z],[k.S[0]+C[0],k.S[1]+C[1]-z]],WK.shade(col,-.15));
ctx.strokeStyle=WK.shade(col,-.28); ctx.lineWidth=1.5;
for(let i=1;i<w;i++){
const tt=i/w;
ctx.beginPath();
ctx.moveTo(C[0]+k.W[0]*(1-tt), C[1]-high*(1-tt)+k.W[1]*0 );
ctx.stroke();
}
},
coneSwirl(ctx,C,w,h,z,col){ // ice cream stand: giant soft-serve
const x=C[0], y=C[1]-z;
ctx.fillStyle='#f7e2b8';
ctx.beginPath(); ctx.moveTo(x-11,y+2); ctx.lineTo(x+11,y+2); ctx.lineTo(x,y-20); ctx.closePath(); ctx.fill();
ctx.strokeStyle='#d9b97e'; ctx.lineWidth=1.2;
for(let i=0;i<3;i++){ ctx.beginPath(); ctx.moveTo(x-9+i*3,y-2-i*4); ctx.lineTo(x+9-i*3,y-2-i*4); ctx.stroke(); }
WK.blob(ctx,x,y-24,10,7,col);
WK.blob(ctx,x,y-31,7.5,5.5,WK.shade(col,.12));
WK.blob(ctx,x,y-37,5,4,WK.shade(col,.22));
ctx.fillStyle='#e8624e'; ctx.beginPath(); ctx.arc(x+2,y-42,2.4,0,TAU); ctx.fill(); // cherry
},
dome(ctx,C,w,h,z,col){
const x=C[0], y=C[1]-z;
ctx.fillStyle=col;
ctx.beginPath(); ctx.ellipse(x,y,(w+h)*HH*0.62,(w+h)*HH*0.52,0,Math.PI,0); ctx.closePath(); ctx.fill();
ctx.fillStyle='rgba(255,255,255,.35)';
ctx.beginPath(); ctx.ellipse(x-((w+h)*HH)*.18,y-((w+h)*HH)*.2,(w+h)*HH*.16,(w+h)*HH*.2,-.5,0,TAU); ctx.fill();
ctx.strokeStyle='#eef6fb'; ctx.lineWidth=1.4;
for(let i=1;i<3;i++){
ctx.beginPath(); ctx.ellipse(x,y,(w+h)*HH*0.62*i/3,(w+h)*HH*0.52*i/3,0,Math.PI,0); ctx.stroke();
}
},
mesh(ctx,C,w,h,z,col){ // aviary dome with struts + birds
roofDraw.dome(ctx,C,w,h,z,col);
const x=C[0], y=C[1]-z, R=(w+h)*HH*0.62;
ctx.strokeStyle='rgba(70,90,100,.55)'; ctx.lineWidth=1;
for(let i=-2;i<=2;i++){
ctx.beginPath(); ctx.ellipse(x+i*R/2.6,y,R*.18,R*.92,0,Math.PI,0); ctx.stroke();
}
for(const dy of [.35,.65]){
ctx.beginPath(); ctx.ellipse(x,y-R*dy,R*(1-dy*.5),R*.16,0,Math.PI,0); ctx.stroke();
}
ctx.fillStyle='#e86a8a';
ctx.beginPath(); ctx.arc(x-R*.4,y-R*.5,2.6,0,TAU); ctx.fill();
ctx.fillStyle='#69c3ef';
ctx.beginPath(); ctx.arc(x+R*.3,y-R*.65,2.6,0,TAU); ctx.fill();
},
rock(ctx,C,w,h,z,col){ // reptile house: boulder roof
const x=C[0], y=C[1]-z;
WK.blob(ctx,x-((w)*HW)*.3,y-4,(w+h)*HH*.42,(w+h)*HH*.3,col);
WK.blob(ctx,x+((w)*HW)*.25,y-8,(w+h)*HH*.36,(w+h)*HH*.27,WK.shade(col,.1));
ctx.fillStyle='rgba(255,255,255,.22)';
ctx.beginPath(); ctx.ellipse(x-((w)*HW)*.34,y-((w+h)*HH)*.16,(w+h)*HH*.14,(w+h)*HH*.07,-.4,0,TAU); ctx.fill();
},
wave(ctx,C,w,h,z,col){ // aquarium
roofDraw.dome(ctx,C,w,h,z,col);
const x=C[0], y=C[1]-z, R=(w+h)*HH*0.62;
ctx.strokeStyle='rgba(255,255,255,.85)'; ctx.lineWidth=2.2; ctx.lineCap='round';
for(let i=0;i<3;i++){
ctx.beginPath();
ctx.moveTo(x-R*.7+i*R*.3, y-R*.28-i*3);
ctx.quadraticCurveTo(x-R*.45+i*R*.3, y-R*.5-i*3, x-R*.2+i*R*.3, y-R*.28-i*3);
ctx.stroke();
}
ctx.fillStyle='#ffd23c';
ctx.beginPath(); ctx.ellipse(x+R*.35,y-R*.4,4,2.6,-.4,0,TAU); ctx.fill();
ctx.beginPath(); ctx.moveTo(x+R*.35+4,y-R*.4); ctx.lineTo(x+R*.35+7,y-R*.4-2.6); ctx.lineTo(x+R*.35+7,y-R*.4+2.6); ctx.closePath(); ctx.fill();
},
awning(ctx,C,w,h,z,col){
roofDraw.flat(ctx,C,w,h,z,WK.shade(col,-.25));
const k=fcorners(w,h);
const fx=k.S[0]+C[0], fy=k.S[1]+C[1];
const lx=k.W[0]+C[0], ly=k.W[1]+C[1];
const rx=k.E[0]+C[0], ry=k.E[1]+C[1];
// scalloped awning across the front corner
const steps=6;
for(let i=0;i<steps;i++){
const t0=i/steps,t1=(i+1)/steps;
const ax=lx+(rx-lx)*t0, ay=ly-z*.35+(ry-ly)*t0-14;
const bx=lx+(rx-lx)*t1, by=ly-z*.35+(ry-ly)*t1-14;
poly(ctx,[[ax,ay],[bx,by],[bx,by+6],[ax,ay+6]], i%2? col : '#fffdf4');
}
},
archway(ctx,C,w,h,z,col,big){
const k=fcorners(w,h);
// two pillar prisms at front corners + banner between
const pw=Math.max(1,w*.16), ph=h*.55;
const pz=z+26;
const p1=fcorners(pw,ph), p2=fcorners(pw,ph);
const P1=[k.W[0]+C[0],k.W[1]+C[1]], P2=[k.E[0]+C[0],k.E[1]+C[1]];
const drawPillar=(cx,cy)=>{
const q=fcorners(pw,ph);
const N=[q.N[0]+cx,q.N[1]+cy-pz],E=[q.E[0]+cx,q.E[1]+cy-pz],S=[q.S[0]+cx,q.S[1]+cy-pz],W2=[q.W[0]+cx,q.W[1]+cy-pz];
poly(ctx,[W2,S,[q.S[0]+cx,q.S[1]+cy],[q.W[0]+cx,q.W[1]+cy]],'#caa763');
poly(ctx,[S,E,[q.E[0]+cx,q.E[1]+cy],[q.S[0]+cx,q.S[1]+cy]],'#b8924e');
poly(ctx,[N,E,S,W2],'#e8cf96');
};
drawPillar(P1[0],P1[1]); drawPillar(P2[0],P2[1]);
// banner
const bx1=P1[0], by1=P1[1]-pz+4, bx2=P2[0], by2=P2[1]-pz+4;
poly(ctx,[[bx1,by1-14],[bx2,by2-14],[bx2,by2+8],[bx1,by1+8]],col);
ctx.fillStyle='rgba(255,255,255,.9)';
ctx.font='bold '+Math.min(13,6+w)+'px sans-serif'; ctx.textAlign='center';
ctx.fillText('ZOO',(bx1+bx2)/2,(by1+by2)/2+5);
// scallop flags under banner
const n=big?7:5;
for(let i=0;i<n;i++){
const t=(i+.5)/n;
const fx=bx1+(bx2-bx1)*t, fy=by1+(by2-by1)*t+8;
ctx.fillStyle=['#e8624e','#ffd23c','#63b34c','#69c3ef'][i%4];
ctx.beginPath(); ctx.moveTo(fx-4,fy); ctx.lineTo(fx+4,fy); ctx.lineTo(fx,fy+7); ctx.closePath(); ctx.fill();
}
if(big){
// pennant pole
ctx.strokeStyle='#8a5a34'; ctx.lineWidth=2.4;
ctx.beginPath(); ctx.moveTo(C[0],C[1]-z-2); ctx.lineTo(C[0],C[1]-z-34); ctx.stroke();
ctx.fillStyle='#ffd23c';
ctx.beginPath(); ctx.moveTo(C[0],C[1]-z-34); ctx.lineTo(C[0]+14,C[1]-z-29); ctx.lineTo(C[0],C[1]-z-24); ctx.closePath(); ctx.fill();
}
},
};
const bldExtras = {
hut(ctx,C,w,h,z){ windowsOnLeftFace(ctx,C,w,h,z,1); },
restroom(ctx,C,w,h,z){ doorOnRightFace(ctx,C,w,h,z,'#4d7f96'); signPlaque(ctx,C[0]+fcorners(w,h).E[0]*.55,C[1]-z-6,'#4f9fd0'); },
burger(ctx,C,w,h,z){
const x=C[0],y=C[1]-z;
// burger sign on roof
WK.blob(ctx,x,y-10,11,7,'#e8a24e');
ctx.fillStyle='#63b34c'; WK.rr(ctx,x-10,y-6,20,3,1.5); ctx.fill();
ctx.fillStyle='#e8624e'; ctx.beginPath(); ctx.ellipse(x,y-13,10,3,0,Math.PI,true); ctx.fill();
ctx.fillStyle='#fff3c2'; ctx.beginPath(); ctx.ellipse(x,y-15.5,9,3.4,0,Math.PI,true); ctx.fill();
doorOnRightFace(ctx,C,w,h,z,'#8a5a34');
},
icecream(){}, // cone roof covers it
gift(ctx,C,w,h,z){
const x=C[0],y=C[1]-z-8;
ctx.fillStyle='#e8624e'; WK.rr(ctx,x-8,y-8,16,14,2); ctx.fill();
ctx.fillStyle='#ffd23c'; ctx.fillRect(x-2,y-8,4,14); ctx.fillRect(x-8,y-3,16,4);
ctx.fillStyle='#ffb63c';
ctx.beginPath(); ctx.arc(x-3,y-10,3,0,TAU); ctx.arc(x+3,y-10,3,0,TAU); ctx.fill();
doorOnRightFace(ctx,C,w,h,z,'#8a5a34');
},
visitor(ctx,C,w,h,z){ windowsOnLeftFace(ctx,C,w,h,z,2); doorOnRightFace(ctx,C,w,h,z,'#8a5a34');
// clock
const x=C[0]+fcorners(w,h).N[0]*0.2, y=C[1]-z-20;
ctx.fillStyle='#fffdf4'; ctx.beginPath(); ctx.arc(x,y,6,0,TAU); ctx.fill();
ctx.strokeStyle='#43362a'; ctx.lineWidth=1.2;
ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x,y-4); ctx.moveTo(x,y); ctx.lineTo(x+3,y); ctx.stroke();
},
staff(ctx,C,w,h,z){ doorOnRightFace(ctx,C,w,h,z,'#6b6257'); windowsOnLeftFace(ctx,C,w,h,z,1); },
research(ctx,C,w,h,z){
windowsOnLeftFace(ctx,C,w,h,z,2);
const x=C[0]+fcorners(w,h).E[0]*.3, y=C[1]-z;
ctx.strokeStyle='#7a8a94'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(x,y-6); ctx.lineTo(x,y-26); ctx.stroke();
ctx.fillStyle='#e8624e'; ctx.beginPath(); ctx.arc(x,y-28,2.6,0,TAU); ctx.fill();
signPlaque(ctx,C[0]+fcorners(w,h).E[0]*.6,C[1]-z+2,'#69c3ef');
},
vet(ctx,C,w,h,z){
doorOnRightFace(ctx,C,w,h,z,'#b85454');
const k=fcorners(w,h);
const x=C[0]+(k.S[0]+k.E[0])/2, y=C[1]-z+(k.S[1]+k.E[1])/2-16;
ctx.fillStyle='#e8624e';
WK.rr(ctx,x-2.5,y-8,5,16,2); ctx.fill(); WK.rr(ctx,x-8,y-2.5,16,5,2); ctx.fill();
},
restaurant(ctx,C,w,h,z){
windowsOnLeftFace(ctx,C,w,h,z,2); doorOnRightFace(ctx,C,w,h,z,'#7a4a8a');
// umbrella table beside door
const ux=C[0]+fcorners(w,h).E[0]*.72, uy=C[1]+fcorners(w,h).E[1]*.72;
ctx.strokeStyle='#8a5a34'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(ux,uy-4); ctx.lineTo(ux,uy-20); ctx.stroke();
ctx.fillStyle='#f2b3d0';
ctx.beginPath(); ctx.arc(ux,uy-20,9,Math.PI,0); ctx.fill();
ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(ux,uy-20,9,Math.PI,Math.PI*1.2); ctx.lineTo(ux,uy-20); ctx.closePath(); ctx.fill();
},
aviary(ctx,C,w,h,z){ doorOnRightFace(ctx,C,w,h,z,'#5f7d8a'); },
reptile(ctx,C,w,h,z){ doorOnRightFace(ctx,C,w,h,z,'#5a6e46'); signPlaque(ctx,C[0]+fcorners(w,h).E[0]*.5,C[1]-z+4,'#7f9166'); },
aquarium(ctx,C,w,h,z){
doorOnRightFace(ctx,C,w,h,z,'#3d7fa8');
windowsOnLeftFace(ctx,C,w,h,z,3);
},
entrance_s(ctx,C,w,h,z){},
entrance_g(ctx,C,w,h,z){},
};
SPR.getBuilding = function(id){
const b = WK.Data.BUILDING[id];
const w=b.w, h=b.h;
const zBase = 18 + Math.max(w,h)*4;
const styleH = { archway: b.big?64:52, gable:26, slant:30, dome:34, mesh:40, rock:24, wave:40, awning:16, coneSwirl:48, flat:10 }[b.roof.style] || 16;
const H = (w+h)*HH + zBase + styleH + 46;
const Wd = (w+h)*HW + 60;
return get('bld:'+id, Wd, H, Wd/2, H-26, ctx=>{
const C=[0,-24]; // lift so shadow fits
// ground shadow
const k=fcorners(w,h);
ctx.fillStyle='rgba(60,45,15,.22)';
ctx.beginPath();
ctx.moveTo(k.N[0]+C[0],k.N[1]+C[1]+3); ctx.lineTo(k.E[0]+C[0],k.E[1]+C[1]+3);
ctx.lineTo(k.S[0]+C[0],k.S[1]+C[1]+3); ctx.lineTo(k.W[0]+C[0],k.W[1]+C[1]+3);
ctx.closePath(); ctx.fill();
// doormat
ctx.fillStyle='rgba(120,90,50,.35)';
ctx.beginPath(); ctx.ellipse(k.S[0]+C[0],k.S[1]+C[1]+2,14,5,0,0,TAU); ctx.fill();
const z=zBase;
const tops=isoPrism(ctx,C,w,h,z,b.wall[0],b.wall[1],b.wall[2]);
const st=b.roof.style;
if(st==='cone') roofDraw.coneSwirl(ctx,C,w,h,z,b.roof.color);
else if(st==='archway') roofDraw.archway(ctx,C,w,h,z,b.roof.color,b.big);
else if(st==='awning'){ roofDraw.awning(ctx,C,w,h,z,b.roof.color); }
else roofDraw[st] && roofDraw[st](ctx,C,w,h,z,b.roof.color);
bldExtras[id] && bldExtras[id](ctx,C,w,h,z);
});
};
/* ============================================================
PEOPLE (guests / staff) — drawn live
============================================================ */
SPR.drawPerson = function(ctx, o){
// o: {phase, state:'walk'|'stand'|'sit'|'photo'|'feed', shirt, pants, skin, kid, balloon, tool}
const s=o.kid?0.68:1;
ctx.save();
ctx.scale(o.face<0?-s:s, s);
// shadow
ctx.fillStyle='rgba(60,45,15,.25)';
ctx.beginPath(); ctx.ellipse(0,1,7,3,0,0,TAU); ctx.fill();
const ph=o.phase||0;
const moving=o.state==='walk';
const l1=moving?Math.sin(ph)*3.4:0, l2=moving?-Math.sin(ph)*3.4:0;
const bob=moving?Math.abs(Math.sin(ph))*1.6:Math.sin(ph*0.4)*0.6;
if(o.state==='sit'){
// sitting on ground level offset handled by caller
ctx.fillStyle=o.pants;
WK.rr(ctx,-5,-8,10,7,3); ctx.fill();
}else{
ctx.strokeStyle=o.pants; ctx.lineWidth=3.4; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(-1.5,-8); ctx.lineTo(-1.5+l1*.6,-1); ctx.stroke();
ctx.beginPath(); ctx.moveTo(1.5,-8); ctx.lineTo(1.5+l2*.6,-1); ctx.stroke();
}
// body
WK.blob(ctx,0,-13-bob,6,7,o.shirt);
// arms
ctx.strokeStyle=o.shirt; ctx.lineWidth=2.8;
if(o.state==='photo'){
ctx.beginPath(); ctx.moveTo(-4,-16-bob); ctx.lineTo(-7,-24-bob); ctx.stroke();
ctx.beginPath(); ctx.moveTo(4,-16-bob); ctx.lineTo(7,-24-bob); ctx.stroke();
ctx.fillStyle='#43362a'; WK.rr(ctx,-4,-28-bob,8,5,1.5); ctx.fill();
ctx.fillStyle='#9fd8f5'; ctx.fillRect(-3,-27.2-bob,6,3.4);
}else if(o.state==='feed'||o.state==='point'){
ctx.beginPath(); ctx.moveTo(-4,-15-bob); ctx.lineTo(-7,-11-bob); ctx.stroke();
ctx.beginPath(); ctx.moveTo(4,-15-bob); ctx.lineTo(9,-19-bob); ctx.stroke();
}else{
const a1=moving?Math.sin(ph)*3:1, a2=moving?-Math.sin(ph)*3:-1;
ctx.beginPath(); ctx.moveTo(-4.6,-15-bob); ctx.lineTo(-4.6+a1*.8,-9-bob); ctx.stroke();
ctx.beginPath(); ctx.moveTo(4.6,-15-bob); ctx.lineTo(4.6+a2*.8,-9-bob); ctx.stroke();
}
// head
WK.blob(ctx,0,-23-bob,5.4,5.2,o.skin);
// hair cap
ctx.fillStyle=o.hair||'#5a3c22';
ctx.beginPath(); ctx.arc(0,-24.5-bob,5.2,Math.PI*1.02,Math.PI*1.98); ctx.fill();
ctx.beginPath(); ctx.ellipse(0,-25.5-bob,5.2,2.6,0,Math.PI,0); ctx.fill();
// face
ctx.fillStyle='#33261a';
ctx.beginPath(); ctx.arc(1.8,-22.6-bob,0.9,0,TAU); ctx.fill();
if(o.smile){ WK.mouth(ctx,2.2,-20.8-bob,1.6,o.state==='photo'?0.4:0.15); }
else { ctx.strokeStyle='#7c4a3a'; ctx.lineWidth=.9; ctx.beginPath(); ctx.moveTo(1.2,-20.6-bob); ctx.lineTo(3.2,-21.2-bob); ctx.stroke(); }
// balloon
if(o.balloon){
ctx.strokeStyle='rgba(70,50,30,.6)'; ctx.lineWidth=1;
ctx.beginPath(); ctx.moveTo(5,-18-bob);
ctx.quadraticCurveTo(8,-26-bob, 6+Math.sin(ph*.7)*1.5,-33-bob); ctx.stroke();
WK.blob(ctx,6+Math.sin(ph*.7)*1.5,-36-bob,4.6,5.4,o.balloon);
}
ctx.restore();
};
/* ============================================================
ANIMALS — 15 species × poses, drawn live
pose: {state:'idle'|'walk'|'eat'|'happy', ph, t}
Facing handled by caller flipping ctx. Feet at (0,0).
============================================================ */
function legs4(ctx,color,ph,len,spread,moving){
ctx.strokeStyle=color; ctx.lineCap='round';
const lw=len*0.34;
ctx.lineWidth=lw;
const o=moving?Math.sin(ph)*len*0.28:0;
// back pair then front pair
for(const [x,off] of [[-spread,o],[-spread+lw*0.8,-o*0.7],[spread-lw*0.8,o*0.7],[spread,-o]]){
ctx.beginPath(); ctx.moveTo(x,-len*0.4); ctx.lineTo(x+off*0.5,0); ctx.stroke();
}
}
function happyFx(ctx,t,r){
// little bounce sparkles
for(let i=0;i<3;i++){
const a=t*3+i*2.1;
ctx.fillStyle=i%2?'rgba(255,210,60,.9)':'rgba(255,140,170,.9)';
ctx.beginPath(); ctx.arc(Math.cos(a)*r*0.8,-r*1.15+Math.sin(a)*4,1.8,0,TAU); ctx.fill();
}
}
const ANIM = {
lion(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const bob=mv?Math.abs(Math.sin(p.ph))*2:(hp?-Math.abs(Math.sin(p.t*7))*5:Math.sin(p.t*1.8)*0.8);
legs4(ctx,'#d99c46',p.ph,10,9,mv);
ctx.save(); ctx.translate(0,bob);
WK.blob(ctx,0,-14,17,11,'#e8ac54');
WK.blob(ctx,0,-10,12,6,'#f5cd8a'); // belly
// mane
WK.blob(ctx,13,-19,10.5,10,'#b3701e');
WK.blob(ctx,13,-19,7.6,7.4,'#c98a2e');
// tail
ctx.strokeStyle='#b3701e'; ctx.lineWidth=3; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(-16,-16);
ctx.quadraticCurveTo(-24,-18,-24,-24+Math.sin(p.t*2.4)*2); ctx.stroke();
ctx.fillStyle='#7a4a10'; ctx.beginPath(); ctx.arc(-24,-25+Math.sin(p.t*2.4)*2,2.6,0,TAU); ctx.fill();
// face
WK.eye(ctx,11,-21,2.6,{lookY:p.state==='eat'?0.5:0});
WK.eye(ctx,16.5,-21,2.6,{lookY:p.state==='eat'?0.5:0});
WK.mouth(ctx,14,-16.5,3,p.state==='eat'?(0.3+0.3*Math.sin(p.t*9)):hp?0.6:0.12);
WK.blush(ctx,9,-17.5,2.4,'#ff9d8a',.3);
ctx.restore();
if(hp) happyFx(ctx,p.t,14);
},
tiger(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const bob=mv?Math.abs(Math.sin(p.ph))*2:(hp?-Math.abs(Math.sin(p.t*7))*5:Math.sin(p.t*1.8)*0.8);
legs4(ctx,'#e8823c',p.ph,10,9,mv);
ctx.save(); ctx.translate(0,bob);
WK.blob(ctx,0,-14,17,11,'#f08c3e');
WK.blob(ctx,0,-9.5,12,5.5,'#fbe3c2');
// stripes
ctx.strokeStyle='#43362a'; ctx.lineWidth=2.6; ctx.lineCap='round';
for(let i=-2;i<=2;i++){
ctx.beginPath(); ctx.moveTo(i*5.4,-23.5); ctx.quadraticCurveTo(i*5.4+2,-18,i*5.4,-13.5); ctx.stroke();
}
// ears
ctx.fillStyle='#f08c3e';
ctx.beginPath(); ctx.arc(8,-24,3.4,0,TAU); ctx.arc(18,-24,3.4,0,TAU); ctx.fill();
ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(8,-24,1.6,0,TAU); ctx.arc(18,-24,1.6,0,TAU); ctx.fill();
// tail
ctx.strokeStyle='#f08c3e'; ctx.lineWidth=3.4;
ctx.beginPath(); ctx.moveTo(-16,-15); ctx.quadraticCurveTo(-24,-16,-25,-23+Math.sin(p.t*2.6)*2); ctx.stroke();
ctx.strokeStyle='#43362a'; ctx.lineWidth=3.4; ctx.setLineDash([3,4]);
ctx.beginPath(); ctx.moveTo(-25,-23+Math.sin(p.t*2.6)*2); ctx.lineTo(-26,-27); ctx.stroke();
ctx.setLineDash([]);
WK.eye(ctx,10.5,-20,2.6,{}); WK.eye(ctx,16,-20,2.6,{});
WK.mouth(ctx,13.5,-15.5,3,p.state==='eat'?(0.3+0.3*Math.sin(p.t*9)):hp?0.6:0.12);
WK.blush(ctx,8,-16.8,2.4,'#ff9d8a',.28);
ctx.restore();
if(hp) happyFx(ctx,p.t,14);
},
elephant(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const bob=mv?Math.abs(Math.sin(p.ph))*2:(hp?-Math.abs(Math.sin(p.t*6))*4:Math.sin(p.t*1.6)*0.8);
legs4(ctx,'#9aa7b5',p.ph,11,10,mv);
ctx.save(); ctx.translate(0,bob);
WK.blob(ctx,0,-16,19,13,'#aab8c6');
// ear
const flap=Math.sin(p.t*2.2)*2;
WK.blob(ctx,4+flap*.3,-18,8.5,10,'#93a2b2');
// trunk
ctx.strokeStyle='#aab8c6'; ctx.lineWidth=6.5; ctx.lineCap='round';
const sw=p.state==='eat'?Math.sin(p.t*8)*3:Math.sin(p.t*1.9)*2.5;
ctx.beginPath(); ctx.moveTo(17,-14);
ctx.quadraticCurveTo(25+sw*.4,-12,25+sw,-4+(p.state==='eat'?4:0)); ctx.stroke();
// tusk
ctx.strokeStyle='#fdf6e7'; ctx.lineWidth=2.6;
ctx.beginPath(); ctx.arc(20,-9,4,-.4,1.2); ctx.stroke();
// tail
ctx.strokeStyle='#93a2b2'; ctx.lineWidth=3;
ctx.beginPath(); ctx.moveTo(-18,-16); ctx.quadraticCurveTo(-23,-13,-22,-8); ctx.stroke();
// eye
WK.eye(ctx,14,-19,2.6,{});
WK.blush(ctx,11,-13,2.6,'#ff9d8a',.25);
ctx.restore();
if(hp){ happyFx(ctx,p.t,16); }
},
giraffe(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const bob=mv?Math.abs(Math.sin(p.ph))*2:(hp?-Math.abs(Math.sin(p.t*6))*4:Math.sin(p.t*1.7)*0.8);
legs4(ctx,'#e8b64e',p.ph,14,7,mv);
ctx.save(); ctx.translate(0,bob);
WK.blob(ctx,0,-19,14,9.5,'#f0c05c');
// neck+head (dips when eating)
const dip=p.state==='eat'?14:0;
ctx.strokeStyle='#f0c05c'; ctx.lineWidth=8; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(8,-24);
ctx.quadraticCurveTo(17,-34-dip*.4, 20+dip*.6,-42-dip); ctx.stroke();
WK.blob(ctx,21+dip*.7,-44-dip,7,5.4,'#f0c05c');
// ossicones
ctx.strokeStyle='#c98a2e'; ctx.lineWidth=2.2;
ctx.beginPath(); ctx.moveTo(19+dip*.7,-49-dip); ctx.lineTo(18+dip*.7,-53-dip); ctx.stroke();
ctx.beginPath(); ctx.moveTo(24+dip*.7,-49-dip); ctx.lineTo(24.5+dip*.7,-53-dip); ctx.stroke();
ctx.fillStyle='#8a5a34';
ctx.beginPath(); ctx.arc(18+dip*.7,-53.5-dip,1.7,0,TAU); ctx.arc(24.5+dip*.7,-53.5-dip,1.7,0,TAU); ctx.fill();
// patches
ctx.fillStyle='#b3701e';
const patches=[[ -6,-22],[2,-16],[-2,-25],[6,-19],[-9,-15]];
for(const [px,py] of patches){ ctx.beginPath(); ctx.ellipse(px,py,2.8,2.2,.4,0,TAU); ctx.fill(); }
ctx.fillStyle='#b3701e';
ctx.beginPath(); ctx.ellipse(22+dip*.7,-43-dip,2.2,1.8,.3,0,TAU); ctx.fill();
WK.eye(ctx,24+dip*.7,-45.5-dip,2.2,{});
// tail
ctx.strokeStyle='#e8b64e'; ctx.lineWidth=2.4;
ctx.beginPath(); ctx.moveTo(-13,-20); ctx.quadraticCurveTo(-18,-18,-19,-13); ctx.stroke();
ctx.restore();
if(hp) happyFx(ctx,p.t,16);
},
zebra(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const bob=mv?Math.abs(Math.sin(p.ph))*2:(hp?-Math.abs(Math.sin(p.t*7))*4:Math.sin(p.t*1.8)*0.8);
legs4(ctx,'#eef2f5',p.ph,10,9,mv);
ctx.save(); ctx.translate(0,bob);
WK.blob(ctx,0,-14,16.5,10.5,'#f2f5f8');
// bold stripes
ctx.strokeStyle='#2e2a26'; ctx.lineWidth=2.8; ctx.lineCap='round';
for(let i=-2;i<=2;i++){
ctx.beginPath(); ctx.moveTo(i*5.2,-23); ctx.quadraticCurveTo(i*5.2+2.5,-17.5,i*5.2+1,-11.5); ctx.stroke();
}
// mane
ctx.fillStyle='#2e2a26';
ctx.beginPath(); ctx.ellipse(9,-24,4,3,-.4,0,TAU); ctx.fill();
// muzzle
WK.blob(ctx,16,-16.5,4.4,3.6,'#cfd6dc');
ctx.fillStyle='#2e2a26';
ctx.beginPath(); ctx.arc(17.5,-16,1.2,0,TAU); ctx.fill();
WK.eye(ctx,11,-20,2.5,{});
// tail swish
ctx.strokeStyle='#2e2a26'; ctx.lineWidth=2.4;
ctx.beginPath(); ctx.moveTo(-15.5,-16); ctx.quadraticCurveTo(-21,-14+Math.sin(p.t*3)*2,-22,-9); ctx.stroke();
WK.mouth(ctx,16,-13.8,2.4,hp?0.5:(p.state==='eat'?0.35:0.1));
ctx.restore();
if(hp) happyFx(ctx,p.t,13);
},
gorilla(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const bob=mv?Math.abs(Math.sin(p.ph))*2.4:(hp?-Math.abs(Math.sin(p.t*6))*5:Math.sin(p.t*1.5)*0.9);
ctx.save(); ctx.translate(0,bob);
// legs
ctx.strokeStyle='#4a4448'; ctx.lineWidth=6; ctx.lineCap='round';
const o=mv?Math.sin(p.ph)*3:0;
ctx.beginPath(); ctx.moveTo(-5,-9); ctx.lineTo(-6+o*.5,0); ctx.stroke();
ctx.beginPath(); ctx.moveTo(6,-9); ctx.lineTo(7-o*.5,0); ctx.stroke();
// body
WK.blob(ctx,0,-17,15,13,'#5a5358');
WK.blob(ctx,2,-15,9.5,8.5,'#7d7378'); // chest
// arms (long)
ctx.strokeStyle='#4a4448'; ctx.lineWidth=6.5;
if(hp){
const beat=Math.sin(p.t*14)>0?1:0;
ctx.beginPath(); ctx.moveTo(-10,-22); ctx.lineTo(-14-beat*4,-34-beat*6); ctx.stroke();
ctx.beginPath(); ctx.moveTo(11,-22); ctx.lineTo(15+beat*4,-34-beat*6); ctx.stroke();
}else{
const a=mv?Math.sin(p.ph)*3:0;
ctx.beginPath(); ctx.moveTo(-11,-20); ctx.lineTo(-13+a*.5,-2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(12,-20); ctx.lineTo(14-a*.5,-2); ctx.stroke();
}
// head
WK.blob(ctx,4,-32,7.5,6.8,'#5a5358');
WK.blob(ctx,4,-29.5,4.6,3.6,'#8a7470'); // face plate
WK.eye(ctx,2,-31,1.9,{}); WK.eye(ctx,7,-31,1.9,{});
WK.mouth(ctx,4.5,-27,2.6,hp?0.6:0.1);
ctx.restore();
if(hp) happyFx(ctx,p.t,16);
},
panda(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const eating=p.state==='eat';
const bob=mv?Math.abs(Math.sin(p.ph))*2:(hp?-Math.abs(Math.sin(p.t*6))*4:Math.sin(p.t*1.4)*1);
ctx.save(); ctx.translate(0,bob);
if(eating){
// sitting panda munching bamboo
WK.blob(ctx,0,-12,14,11,'#f5f8fa');
// bamboo stalk
ctx.strokeStyle='#6db84a'; ctx.lineWidth=3.4; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(12,-2); ctx.lineTo(15,-26); ctx.stroke();
ctx.strokeStyle='#4d9a37'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(14,-14); ctx.lineTo(22,-18); ctx.stroke();
// paws holding
WK.blob(ctx,10,-10,4,3.6,'#3a3438');
// head
WK.blob(ctx,4,-22,9.5,8.5,'#f5f8fa');
ctx.fillStyle='#3a3438';
ctx.beginPath(); ctx.arc(-1,-23,3,0,TAU); ctx.fill(); // ear
ctx.beginPath(); ctx.arc(9,-23,3,0,TAU); ctx.fill();
ctx.beginPath(); ctx.ellipse(1.4,-21.5,2.8,3.6,.3,0,TAU); ctx.fill();
ctx.beginPath(); ctx.ellipse(7.4,-21.5,2.8,3.6,-.3,0,TAU); ctx.fill();
WK.eye(ctx,1.4,-21.5,1.5,{}); WK.eye(ctx,7.4,-21.5,1.5,{});
WK.mouth(ctx,4.5,-17.5,2.6,0.25+0.25*Math.sin(p.t*9));
}else{
legs4(ctx,'#3a3438',p.ph,9,8,mv);
WK.blob(ctx,0,-13,15,11,'#f5f8fa');
ctx.strokeStyle='#3a3438'; ctx.lineWidth=5.5; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(10,-18); ctx.lineTo(15,-8); ctx.stroke();
// head
WK.blob(ctx,8,-24,9,8.2,'#f5f8fa');
ctx.fillStyle='#3a3438';
ctx.beginPath(); ctx.arc(3,-26,3,0,TAU); ctx.fill();
ctx.beginPath(); ctx.arc(13,-26,3,0,TAU); ctx.fill();
ctx.beginPath(); ctx.ellipse(6,-24,2.7,3.4,.3,0,TAU); ctx.fill();
ctx.beginPath(); ctx.ellipse(11.4,-24,2.7,3.4,-.3,0,TAU); ctx.fill();
WK.eye(ctx,6,-24,1.5,{}); WK.eye(ctx,11.4,-24,1.5,{});
WK.mouth(ctx,8.6,-20,2.4,hp?0.5:0.1);
if(hp) happyFx(ctx,p.t,14);
}
ctx.restore();
},
penguin(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const waddle=mv?Math.sin(p.ph)*0.16:0;
ctx.save(); ctx.rotate(waddle);
const bob=mv?Math.abs(Math.cos(p.ph))*1.6:(hp?-Math.abs(Math.sin(p.t*7))*5:Math.sin(p.t*1.8)*0.7);
ctx.translate(0,bob);
// feet
ctx.fillStyle='#f0a24a';
ctx.beginPath(); ctx.ellipse(-3,0,3.4,1.8,0,0,TAU); ctx.ellipse(3,0,3.4,1.8,0,0,TAU); ctx.fill();
// body egg
WK.blob(ctx,0,-13,9.5,13,'#3a4650');
WK.blob(ctx,0,-10.5,6.4,9,'#f5f8fa'); // belly
// flippers
const fl=hp?-14:(mv?Math.sin(p.ph)*3:Math.sin(p.t*2)*1.5);
ctx.strokeStyle='#3a4650'; ctx.lineWidth=4.4; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(-8,-15); ctx.quadraticCurveTo(-13,-10+fl*.4,-11,-4+fl*.6); ctx.stroke();
ctx.beginPath(); ctx.moveTo(8,-15); ctx.quadraticCurveTo(13,-10-fl*.4,11,-4-fl*.6); ctx.stroke();
// face
WK.eye(ctx,-3,-20,2.2,{}); WK.eye(ctx,4,-20,2.2,{});
WK.blush(ctx,-6,-16.5,2,'#ffb0c0',.4); WK.blush(ctx,7,-16.5,2,'#ffb0c0',.4);
// beak
ctx.fillStyle='#f0a24a';
ctx.beginPath(); ctx.moveTo(-2,-17); ctx.lineTo(3.6,-17); ctx.lineTo(0.8,-13.6); ctx.closePath(); ctx.fill();
ctx.restore();
if(hp) happyFx(ctx,p.t,11);
},
polarbear(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const bob=mv?Math.abs(Math.sin(p.ph))*2.2:(hp?-Math.abs(Math.sin(p.t*5.5))*4:Math.sin(p.t*1.5)*0.9);
legs4(ctx,'#f2f6f9',p.ph,11,10,mv);
ctx.save(); ctx.translate(0,bob);
WK.blob(ctx,0,-16,19,12.5,'#f7fafc');
// head
WK.blob(ctx,14,-20,8.5,7.4,'#f7fafc');
ctx.fillStyle='#f7fafc';
ctx.beginPath(); ctx.arc(9,-26,3,0,TAU); ctx.arc(18,-26,3,0,TAU); ctx.fill(); // ears
ctx.fillStyle='#c8d4dc';
ctx.beginPath(); ctx.arc(9,-26,1.4,0,TAU); ctx.arc(18,-26,1.4,0,TAU); ctx.fill();
ctx.fillStyle='#3a4650';
ctx.beginPath(); ctx.ellipse(20,-19.5,2,1.5,0,0,TAU); ctx.fill(); // nose
WK.eye(ctx,12.5,-21,2.2,{}); WK.eye(ctx,17,-21,2.2,{});
WK.mouth(ctx,18,-16.5,2.6,hp?0.5:(p.state==='eat'?0.35:0.1));
ctx.restore();
if(hp) happyFx(ctx,p.t,15);
},
monkey(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const bob=mv?Math.abs(Math.sin(p.ph))*2:(hp?-Math.abs(Math.sin(p.t*8))*6:Math.sin(p.t*2.2)*1);
ctx.save(); ctx.translate(0,bob);
// curly tail
ctx.strokeStyle='#8a5a34'; ctx.lineWidth=2.6; ctx.lineCap='round';
const tx=-10, ty=-14;
ctx.beginPath(); ctx.moveTo(tx,ty);
ctx.quadraticCurveTo(tx-9,ty-2,tx-9,ty-8+Math.sin(p.t*3)*2);
ctx.quadraticCurveTo(tx-9,ty-13,tx-5,ty-12); ctx.stroke();
// legs
ctx.strokeStyle='#8a5a34'; ctx.lineWidth=3.6;
const o=mv?Math.sin(p.ph)*3:0;
ctx.beginPath(); ctx.moveTo(-4,-8); ctx.lineTo(-5+o*.5,0); ctx.stroke();
ctx.beginPath(); ctx.moveTo(4,-8); ctx.lineTo(5-o*.5,0); ctx.stroke();
// body
WK.blob(ctx,0,-13,8.5,8,'#a9743f');
WK.blob(ctx,1,-12,4.6,4.6,'#e8c49a');
// arms
ctx.strokeStyle='#8a5a34'; ctx.lineWidth=3.4;
if(hp){
const wav=Math.sin(p.t*12)*3;
ctx.beginPath(); ctx.moveTo(-6,-17); ctx.lineTo(-11,-25-wav); ctx.stroke();
ctx.beginPath(); ctx.moveTo(7,-17); ctx.lineTo(12,-25+wav); ctx.stroke();
}else{
ctx.beginPath(); ctx.moveTo(-7,-15); ctx.lineTo(-9,-6); ctx.stroke();
ctx.beginPath(); ctx.moveTo(7,-15); ctx.lineTo(9,-6); ctx.stroke();
}
// head
WK.blob(ctx,2,-25,7.4,6.8,'#a9743f');
WK.blob(ctx,2.6,-24,4.6,4.2,'#e8c49a');
// ears
ctx.fillStyle='#a9743f';
ctx.beginPath(); ctx.arc(-5,-25,2.6,0,TAU); ctx.arc(9,-25,2.6,0,TAU); ctx.fill();
ctx.fillStyle='#e8c49a';
ctx.beginPath(); ctx.arc(-5,-25,1.2,0,TAU); ctx.arc(9,-25,1.2,0,TAU); ctx.fill();
WK.eye(ctx,0.6,-25,1.8,{}); WK.eye(ctx,5.2,-25,1.8,{});
WK.mouth(ctx,3,-21.5,2.2,hp?0.6:(p.state==='eat'?0.4:0.15));
ctx.restore();
if(hp) happyFx(ctx,p.t,12);
},
deer(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const bob=mv?Math.abs(Math.sin(p.ph))*2:(hp?-Math.abs(Math.sin(p.t*7))*5:Math.sin(p.t*1.8)*0.8);
legs4(ctx,'#c99552',p.ph,11,7,mv);
ctx.save(); ctx.translate(0,bob);
WK.blob(ctx,0,-15,12.5,8.5,'#d4a260');
ctx.fillStyle='#faf3e4';
ctx.beginPath(); ctx.ellipse(-8,-13,4.5,5,0,0,TAU); ctx.fill(); // rump patch
// neck & head
const dip=p.state==='eat'?12:0;
ctx.strokeStyle='#d4a260'; ctx.lineWidth=6; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(7,-20); ctx.quadraticCurveTo(12,-26-dip*.5,13+dip*.5,-31-dip); ctx.stroke();
WK.blob(ctx,14+dip*.5,-32-dip,6,4.6,'#d4a260');
// antlers
ctx.strokeStyle='#8a5a34'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(12+dip*.5,-36-dip); ctx.lineTo(10+dip*.5,-42-dip); ctx.moveTo(10.6+dip*.5,-39.5-dip); ctx.lineTo(8+dip*.5,-41-dip); ctx.stroke();
ctx.beginPath(); ctx.moveTo(16+dip*.5,-36-dip); ctx.lineTo(17.5+dip*.5,-42-dip); ctx.moveTo(16.9+dip*.5,-39.5-dip); ctx.lineTo(19+dip*.5,-41-dip); ctx.stroke();
// ear
ctx.fillStyle='#d4a260';
ctx.beginPath(); ctx.ellipse(9+dip*.5,-34-dip,3,1.6,-.5,0,TAU); ctx.fill();
ctx.fillStyle='#3a2c20';
ctx.beginPath(); ctx.arc(17+dip*.5,-32.5-dip,1.2,0,TAU); ctx.fill();
WK.eye(ctx,14.6+dip*.5,-32.5-dip,1.9,{});
ctx.fillStyle='#faf3e4';
ctx.beginPath(); ctx.arc(-4,-19,1.6,0,TAU); ctx.arc(1,-18,1.6,0,TAU); ctx.arc(6,-19.5,1.6,0,TAU); ctx.fill();
ctx.restore();
if(hp) happyFx(ctx,p.t,14);
},
rhino(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const bob=mv?Math.abs(Math.sin(p.ph))*2:(hp?-Math.abs(Math.sin(p.t*5))*3.5:Math.sin(p.t*1.5)*0.8);
legs4(ctx,'#a8b2ba',p.ph,10,10,mv);
ctx.save(); ctx.translate(0,bob);
WK.blob(ctx,0,-15,18,12,'#b4bec6');
// armor plates hint
ctx.strokeStyle='rgba(90,105,115,.4)'; ctx.lineWidth=1.6;
for(let i=-1;i<=1;i++){ ctx.beginPath(); ctx.arc(i*7-2,-15,9,-.7,.7); ctx.stroke(); }
// horn
ctx.fillStyle='#e8e2d4';
ctx.beginPath(); ctx.moveTo(16,-22); ctx.quadraticCurveTo(21,-30,22,-31); ctx.quadraticCurveTo(21,-26,19,-21); ctx.closePath(); ctx.fill();
// head mass
WK.blob(ctx,14,-17,8,7,'#b4bec6');
// ears
ctx.fillStyle='#b4bec6';
ctx.beginPath(); ctx.ellipse(9,-24,2.6,3.4,-.3,0,TAU); ctx.ellipse(17,-24,2.6,3.4,.3,0,TAU); ctx.fill();
WK.eye(ctx,12,-19,2.1,{});
WK.mouth(ctx,17,-12.5,3,hp?0.5:0.1);
ctx.restore();
if(hp) happyFx(ctx,p.t,15);
},
hippo(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const bob=mv?Math.abs(Math.sin(p.ph))*2:(hp?-Math.abs(Math.sin(p.t*5.5))*3.5:Math.sin(p.t*1.6)*0.9);
legs4(ctx,'#9a8bab',p.ph,9,11,mv);
ctx.save(); ctx.translate(0,bob);
WK.blob(ctx,0,-13,19,11.5,'#a89ab8');
// huge mouth
const open=p.state==='eat'?0.9:(hp?0.7:0.08);
WK.blob(ctx,14,-13,9,8,'#a89ab8');
WK.mouth(ctx,15,-9.5,5.5,open,'#7c5a74');
// nostrils
ctx.fillStyle='#7c5a74';
ctx.beginPath(); ctx.arc(17,-19,1.3,0,TAU); ctx.arc(20.5,-18,1.3,0,TAU); ctx.fill();
// tiny ears
ctx.fillStyle='#a89ab8';
ctx.beginPath(); ctx.arc(9,-22,2,0,TAU); ctx.arc(15,-23,2,0,TAU); ctx.fill();
WK.eye(ctx,11,-18,2,{}); WK.eye(ctx,16,-18,2,{});
WK.blush(ctx,8,-12,2.4,'#e8a0b8',.35);
ctx.restore();
if(hp) happyFx(ctx,p.t,15);
},
flamingo(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const bob=mv?Math.abs(Math.sin(p.ph))*1.8:(hp?-Math.abs(Math.sin(p.t*7))*5:Math.sin(p.t*2)*1);
ctx.save(); ctx.translate(0,bob);
// standing leg (one lifted when idle!)
ctx.strokeStyle='#f0a24a'; ctx.lineWidth=2.2; ctx.lineCap='round';
const lifted=!mv&&!hp;
ctx.beginPath(); ctx.moveTo(0,-12);
if(lifted) ctx.quadraticCurveTo(-3,-6,-5,-3); else ctx.lineTo(mv?Math.sin(p.ph)*2:0,0);
ctx.stroke();
if(!lifted){ ctx.fillStyle='#f0a24a'; ctx.beginPath(); ctx.ellipse(mv?Math.sin(p.ph)*2:0,0,3,1.4,0,0,TAU); ctx.fill(); }
if(mv){ ctx.beginPath(); ctx.moveTo(0,-12); ctx.lineTo(-Math.sin(p.ph)*3,-3); ctx.stroke(); }
// body
WK.blob(ctx,0,-17,10.5,7.5,'#f48fb1');
// wing
WK.blob(ctx,-2,-17,6.5,4.4,'#ec7ba2');
// tail feathers
ctx.strokeStyle='#ec7ba2'; ctx.lineWidth=3;
ctx.beginPath(); ctx.moveTo(-9,-18); ctx.quadraticCurveTo(-14,-19,-15,-15+Math.sin(p.t*3)); ctx.stroke();
// S-neck
const dip=p.state==='eat'?10:0;
ctx.strokeStyle='#f48fb1'; ctx.lineWidth=3.4; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(6,-20);
ctx.bezierCurveTo(12,-26,12,-30-dip,8,-36-dip);
ctx.stroke();
// head
WK.blob(ctx,7.4,-38-dip,4,3.4,'#f48fb1');
// beak
ctx.fillStyle='#3a3438';
ctx.beginPath(); ctx.moveTo(10.4,-38-dip); ctx.quadraticCurveTo(15,-37.4-dip,14,-34.6-dip); ctx.quadraticCurveTo(12,-36-dip,9.6,-36.4-dip); ctx.closePath(); ctx.fill();
ctx.fillStyle='#f0a24a'; ctx.fillRect(9.6,-37.4-dip,2.4,1.6);
WK.eye(ctx,8,-39-dip,1.7,{});
ctx.restore();
if(hp) happyFx(ctx,p.t,11);
},
crocodile(ctx,p){
const mv=p.state==='walk', hp=p.state==='happy';
const bob=Math.sin(p.t*1.6)*(mv?1.2:0.5);
ctx.save(); ctx.translate(0,bob);
// tail
ctx.strokeStyle='#5a8a46'; ctx.lineWidth=5; ctx.lineCap='round';
const wag=mv?Math.sin(p.ph)*4:Math.sin(p.t*2)*2;
ctx.beginPath(); ctx.moveTo(-14,-7); ctx.quadraticCurveTo(-24,-7+wag*.4,-30,-3+wag); ctx.stroke();
// body low & long
WK.blob(ctx,0,-7.5,16,6,'#63a04c');
// scutes
ctx.fillStyle='#4d8a3a';
for(let i=-2;i<=1;i++){ ctx.beginPath(); ctx.moveTo(i*6-2,-12); ctx.lineTo(i*6+2,-12); ctx.lineTo(i*6,-15.5); ctx.closePath(); ctx.fill(); }
// stubby legs
ctx.strokeStyle='#4d8a3a'; ctx.lineWidth=3.4;
const o=mv?Math.sin(p.ph)*2.5:0;
ctx.beginPath(); ctx.moveTo(-9,-5); ctx.lineTo(-9+o,0); ctx.stroke();
ctx.beginPath(); ctx.moveTo(8,-5); ctx.lineTo(8-o,0); ctx.stroke();
// snout
const open=p.state==='eat'?0.7:(hp?0.45:0);
WK.blob(ctx,17,-8,9,4.4,'#63a04c');
if(open>0.1){
ctx.save(); ctx.translate(20,-8.5); ctx.rotate(-open*.5);
WK.blob(ctx,4,0,7,2.6,'#63a04c'); ctx.restore();
ctx.save(); ctx.translate(20,-7); ctx.rotate(open*.4);
WK.blob(ctx,4,1,7,2,'#578f42'); ctx.restore();
}else{
ctx.strokeStyle='#3a5c2c'; ctx.lineWidth=1;
for(let i=0;i<3;i++){ ctx.beginPath(); ctx.moveTo(13+i*4,-5.4); ctx.lineTo(15+i*4,-5.4); ctx.stroke(); }
}
ctx.fillStyle='#ffd23c';
ctx.beginPath(); ctx.arc(14,-10.5,1.5,0,TAU); ctx.fill();
ctx.fillStyle='#3a3438'; ctx.beginPath(); ctx.arc(14,-10.5,0.7,0,TAU); ctx.fill();
ctx.restore();
if(hp) happyFx(ctx,p.t,14);
},
};
/* swimmer clipping: caller may clip to waterline before calling */
SPR.drawAnimal = function(ctx, speciesId, pose){
const fn = ANIM[speciesId==='hippo'?'hippo':speciesId] || ANIM.deer;
fn(ctx,pose);
};
SPR.hasSpecies = id=> !!ANIM[id];
/* ============================================================
UI ICONS
============================================================ */
const iconBg = { ground:'#dcedc2', path:'#efe0bd', nature:'#dff0d0', animal:'#ffe3d0', facility:'#dbe9f5', res:'#fff3c2' };
SPR.getIcon = function(kind, size){
size=size||52;
return get('icon:'+kind+':'+size, size, size, size/2, size/2, ctx=>{
const s=size/52;
ctx.scale(s,s);
const [cat,id]=kind.split(':');
if(cat==='res'){
ctx.fillStyle='#fff3c2'; ctx.beginPath(); ctx.arc(0,0,24,0,TAU); ctx.fill();
if(id==='coin'){
WK.blob(ctx,0,0,13,13,'#ffc93c');
ctx.strokeStyle='#d99a1e'; ctx.lineWidth=2.4; ctx.beginPath(); ctx.arc(0,0,9.4,0,TAU); ctx.stroke();
// paw print
ctx.fillStyle='#d99a1e';
ctx.beginPath(); ctx.ellipse(0,3,3.4,2.8,0,0,TAU); ctx.fill();
for(const [px,py] of [[-4,-1],[-1.6,-3.6],[1.6,-3.6],[4,-1]]){ ctx.beginPath(); ctx.arc(px,py,1.7,0,TAU); ctx.fill(); }
}else if(id==='guest'){
WK.blob(ctx,0,-4,8,8,'#f0c8a0');
ctx.fillStyle='#7a4a2a'; ctx.beginPath(); ctx.arc(0,-6.5,8,Math.PI,0); ctx.fill();
WK.blob(ctx,0,10,11,7,'#63b34c');
}else if(id==='heart'){
ctx.fillStyle='#ff6b81';
ctx.beginPath();
ctx.moveTo(0,9); ctx.bezierCurveTo(-16,-2,-9,-15,0,-6); ctx.bezierCurveTo(9,-15,16,-2,0,9);
ctx.fill();
}else if(id==='flask'){
ctx.strokeStyle='#7a8a94'; ctx.lineWidth=3;
ctx.beginPath(); ctx.moveTo(-3,-13); ctx.lineTo(-3,-4); ctx.lineTo(-10,10); ctx.quadraticCurveTo(-12,14,-7,14); ctx.lineTo(7,14); ctx.quadraticCurveTo(12,14,10,10); ctx.lineTo(3,-4); ctx.lineTo(3,-13); ctx.stroke();
ctx.fillStyle='#69c3ef'; ctx.beginPath(); ctx.moveTo(-8.4,6); ctx.lineTo(8.4,6); ctx.lineTo(6.6,12.4); ctx.lineTo(-6.6,12.4); ctx.closePath(); ctx.fill();
ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(2,9,1.6,0,TAU); ctx.fill();
}else if(id==='star'){
star(ctx,0,0,13,5.6,'#ffc93c');
}else if(id==='dozer'){
ctx.fillStyle='#e8624e'; WK.rr(ctx,-12,-12,24,24,6); ctx.fill();
ctx.strokeStyle='#fff'; ctx.lineWidth=4; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(-6,-6); ctx.lineTo(6,6); ctx.moveTo(6,-6); ctx.lineTo(-6,6); ctx.stroke();
}
return;
}
// category background
ctx.fillStyle=iconBg[cat]||'#eee';
ctx.beginPath(); ctx.arc(0,0,24,0,TAU); ctx.fill();
ctx.strokeStyle='rgba(122,94,50,.25)'; ctx.lineWidth=2; ctx.stroke();
try{
if(cat==='ground'){
SPR.diamondPath(ctx,0,2,0); ctx.scale(1.15,1.15);
SPR.paintGround(ctx,0,1.7,id,0.4);
}else if(cat==='path'){
ctx.scale(1.15,1.15);
SPR.paintGround(ctx,0,1.7,'grass',0.4);
SPR.paintPath(ctx,0,1.7,id,0.4);
}else if(cat==='nature'){
const s=SPR.getObject(id,0);
ctx.save(); ctx.scale(0.42,0.42); ctx.translate(0,52); ctx.imageSmoothingEnabled=true;
ctx.drawImage(s.cv,-s.ax,-s.ay); ctx.restore();
}else if(cat==='animal'){
const sp=WK.Data.SPECIES[id];
ctx.save(); ctx.translate(0,16); ctx.scale(0.62,0.62);
SPR.drawAnimal(ctx,id,{state:'idle',ph:0,t:1.2});
ctx.restore();
}else if(cat==='facility'){
const b=SPR.getBuilding(id);
const sc=Math.min(40/b.cv.width,40/b.cv.height)*1.6;
ctx.save(); ctx.scale(sc,sc); ctx.drawImage(b.cv,-b.cv.width/2,-b.cv.height/2+b.cv.height*0.36); ctx.restore();
}else if(cat==='fence'){
SPR.drawFenceEdge(ctx,-16,6,14,-4,id==='gate'?3:(id==='hedge'?2:1),0);
}
}catch(e){}
});
};
function star(ctx,x,y,R,r,color){
ctx.fillStyle=color; ctx.beginPath();
for(let i=0;i<10;i++){
const rad=i%2?r:R, a=-Math.PI/2 + i*Math.PI/5;
const px=x+Math.cos(a)*rad, py=y+Math.sin(a)*rad;
i?ctx.lineTo(px,py):ctx.moveTo(px,py);
}
ctx.closePath(); ctx.fill();
ctx.strokeStyle='rgba(160,100,10,.4)'; ctx.lineWidth=1.4; ctx.stroke();
}
SPR.star = star;
/* data-URL <img> markup for DOM panels */
SPR.iconImg = function(kind,size){
size=size||44;
return '<img src="'+SPR.getIcon(kind,size).cv.toDataURL()+'" width="'+size+'" height="'+size+'" alt="">';
};
})();