/* ============================================================ 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{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;iE ; 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{ 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{ 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 markup for DOM panels */ SPR.iconImg = function(kind,size){ size=size||44; return ''; }; })();