/* ============================================================ Wildlife Kingdom — js/util.js Math, RNG, color & canvas helpers. Exposes global WK. ============================================================ */ (function(){ 'use strict'; const WK = (window.WK = window.WK || {}); WK.TILE_W = 64; // iso diamond width WK.TILE_H = 32; // iso diamond height WK.HW = WK.TILE_W / 2; WK.HH = WK.TILE_H / 2; /* ---- math ---- */ WK.clamp = (v,a,b)=> vb?b:v); WK.lerp = (a,b,t)=> a+(b-a)*t; WK.dist2 = (ax,ay,bx,by)=>{const dx=ax-bx,dy=ay-by;return dx*dx+dy*dy;}; WK.dist = (ax,ay,bx,by)=> Math.hypot(ax-bx,ay-by); WK.TAU = Math.PI*2; WK.easeOutCubic = t=>1-Math.pow(1-t,3); WK.easeInOut = t=> t<0.5 ? 2*t*t : 1-Math.pow(-2*t+2,2)/2; /* deterministic RNG */ WK.mulberry32 = function(seed){ let a = seed>>>0; return function(){ a |= 0; a = (a + 0x6D2B79F5)|0; let t = Math.imul(a ^ (a>>>15), 1|a); t = (t + Math.imul(t ^ (t>>>7), 61|t)) ^ t; return ((t ^ (t>>>14)) >>> 0) / 4294967296; }; }; WK.hash2 = function(x,y,s){ let h = (x*374761393 + y*668265263) ^ (s|0)*1442695041; h = (h ^ (h>>>13)) >>> 0; h = Math.imul(h, 1274126177) >>> 0; return ((h ^ (h>>>16)) % 100000)/100000; // 0..1 }; /* ---- formatting ---- */ WK.fmtMoney = n=>{ const neg = n<0; n = Math.round(Math.abs(n)); let s = String(n).replace(/\B(?=(\d{3})+(?!\d))/g,','); return (neg?'-$':'$')+s; }; WK.fmtClock = min=>{ // minutes in day min = ((min%1440)+1440)%1440; const h=Math.floor(min/60), m=Math.floor(min%60); return String(h).padStart(2,'0')+':'+String(m).padStart(2,'0'); }; /* ---- colors ---- */ WK.hexToRgb = hex=>{ hex = hex.replace('#',''); if(hex.length===3) hex = hex.split('').map(c=>c+c).join(''); const n=parseInt(hex,16); return [(n>>16)&255,(n>>8)&255,n&255]; }; WK.shade = (hex,amt)=>{ // amt -1..1 (negative darker) const [r,g,b]=WK.hexToRgb(hex); const f=c=> WK.clamp(Math.round(amt>=0 ? c+(255-c)*amt : c*(1+amt)),0,255); return `rgb(${f(r)},${f(g)},${f(b)})`; }; WK.rgba = (hex,a)=>{ const [r,g,b]=WK.hexToRgb(hex); return `rgba(${r},${g},${b},${a})`; }; WK.mix = (h1,h2,t)=>{ const a=WK.hexToRgb(h1), b=WK.hexToRgb(h2); return `rgb(${Math.round(WK.lerp(a[0],b[0],t))},${Math.round(WK.lerp(a[1],b[1],t))},${Math.round(WK.lerp(a[2],b[2],t))})`; }; /* ---- canvas helpers ---- */ WK.mkCanvas = (w,h)=>{ const c=document.createElement('canvas'); c.width=Math.max(1,Math.ceil(w)); c.height=Math.max(1,Math.ceil(h)); return c; }; WK.rr = function(ctx,x,y,w,h,r){ // rounded rect path if(typeof r==='number') r={tl:r,tr:r,br:r,bl:r}; else r = Object.assign({tl:0,tr:0,br:0,bl:0},r); ctx.beginPath(); ctx.moveTo(x+r.tl,y); ctx.lineTo(x+w-r.tr,y); ctx.arcTo(x+w,y,x+w,y+r.tr,r.tr); ctx.lineTo(x+w,y+h-r.br); ctx.arcTo(x+w,y+h,x+w-r.br,y+h,r.br); ctx.lineTo(x+r.bl,y+h); ctx.arcTo(x,y+h,x,y+h-r.bl,r.bl); ctx.lineTo(x,y+r.tl); ctx.arcTo(x,y,x+r.tl,y,r.tl); ctx.closePath(); }; WK.softShadow = function(ctx, blur, color, offY){ ctx.shadowBlur=blur; ctx.shadowColor=color||'rgba(70,45,10,.35)'; ctx.shadowOffsetY = offY==null?2:offY; }; WK.clearShadow = ctx=>{ ctx.shadowBlur=0; ctx.shadowColor='transparent'; ctx.shadowOffsetY=0; }; /* ellipse blob with vertical gradient for cute volume */ WK.blob = function(ctx,x,y,rx,ry,color,lighten){ const g = ctx.createRadialGradient(x-rx*0.35,y-ry*0.55,ry*0.15,x,y,Math.max(rx,ry)); g.addColorStop(0, lighten || WK.shade(color,0.28)); g.addColorStop(1, color); ctx.fillStyle=g; ctx.beginPath(); ctx.ellipse(x,y,rx,ry,0,0,WK.TAU); ctx.fill(); }; /* simple eye: white + pupil + shine */ WK.eye = function(ctx,x,y,r,opt){ opt=opt||{}; if(opt.closed){ ctx.strokeStyle=opt.lidColor||'#3a2c20'; ctx.lineWidth=Math.max(1.5,r*0.5); ctx.beginPath(); ctx.moveTo(x-r,y*0.98); ctx.quadraticCurveTo(x,y+r*0.9,x+r,y*0.98); ctx.stroke(); return; } ctx.fillStyle='#fff'; ctx.beginPath(); ctx.ellipse(x,y,r,r*(opt.tall||1),0,0,WK.TAU); ctx.fill(); const px = x + (opt.lookX||0)*r*0.28, py = y + (opt.lookY||0.08)*r*0.4; ctx.fillStyle=opt.pupil||'#33261a'; ctx.beginPath(); ctx.ellipse(px,py,r*0.52,r*0.62*(opt.tall||1),0,0,WK.TAU); ctx.fill(); ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(px-r*0.18,py-r*0.24,r*0.2,0,WK.TAU); ctx.fill(); }; /* tiny smile / open mouth */ WK.mouth = function(ctx,x,y,w,open,color){ ctx.fillStyle = color||'#7c4a3a'; if(open>0.05){ ctx.beginPath(); ctx.ellipse(x,y,w,w*(0.5+open),0,0,Math.PI); ctx.fill(); ctx.fillStyle='#e8756f'; ctx.beginPath(); ctx.ellipse(x,y+w*(0.35+open*0.5),w*0.55,w*0.4*open,0,0,Math.PI); ctx.fill(); }else{ ctx.strokeStyle=color||'#7c4a3a'; ctx.lineWidth=Math.max(1.2,w*0.22); ctx.lineCap='round'; ctx.beginPath(); ctx.moveTo(x-w,y-w*0.25); ctx.quadraticCurveTo(x,y+w*0.65,x+w,y-w*0.25); ctx.stroke(); } }; WK.blush = function(ctx,x,y,r,color,alpha){ ctx.globalAlpha = alpha==null?0.35:alpha; ctx.fillStyle=color||'#ff9d8a'; ctx.beginPath(); ctx.ellipse(x,y,r,r*0.62,0,0,WK.TAU); ctx.fill(); ctx.globalAlpha=1; }; /* object pool-ish array clear */ WK.clearArr = a=>{ a.length=0; return a; }; /* pick weighted {id:w} map or [ [item,w],... ] */ WK.weightedPick = function(entries,rnd){ rnd = rnd||Math.random; let tot=0; for(const e of entries) tot+=e.w; let r=rnd()*tot; for(const e of entries){ r-=e.w; if(r<=0) return e; } return entries[entries.length-1]; }; })();