Files
MeatSlicer/js/utils.js
T

138 lines
5.6 KiB
JavaScript

// ---- math / helper utilities ----
const TAU = Math.PI * 2;
function rand(a, b) { return a + Math.random() * (b - a); }
function irand(a, b) { return Math.floor(rand(a, b + 1)); }
function choice(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
function chance(p) { return Math.random() < p; }
function clamp(v, a, b) { return v < a ? a : (v > b ? b : v); }
function lerp(a, b, t) { return a + (b - a) * t; }
function dist(x1, y1, x2, y2) { return Math.hypot(x2 - x1, y2 - y1); }
function dist2(x1, y1, x2, y2) { const dx = x2 - x1, dy = y2 - y1; return dx * dx + dy * dy; }
function angleTo(x1, y1, x2, y2) { return Math.atan2(y2 - y1, x2 - x1); }
function angleLerp(a, b, t) {
let d = (b - a) % TAU;
if (d > Math.PI) d -= TAU;
if (d < -Math.PI) d += TAU;
return a + d * t;
}
function angleDelta(a, b) { let d = (b - a) % TAU; if (d > Math.PI) d -= TAU; if (d < -Math.PI) d += TAU; return d; }
// distance from point to segment (for beam weapons)
function distToSegment(px, py, x1, y1, x2, y2) {
const dx = x2 - x1, dy = y2 - y1;
const l2 = dx * dx + dy * dy;
if (l2 === 0) return dist(px, py, x1, y1);
let t = ((px - x1) * dx + (py - y1) * dy) / l2;
t = clamp(t, 0, 1);
return dist(px, py, x1 + t * dx, y1 + t * dy);
}
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
// Deterministic blood trim for UI panels. It is deliberately static so richer
// presentation never adds per-frame allocations or visual noise during combat.
function drawBloodTrim(ctx, x, y, w, seed, alpha) {
const oldAlpha = typeof ctx.globalAlpha === 'number' ? ctx.globalAlpha : 1;
const usable = Math.max(1, Math.floor(w - 18));
ctx.save();
ctx.globalAlpha = oldAlpha * (alpha === undefined ? 0.72 : alpha);
ctx.fillStyle = '#861020';
ctx.fillRect(x + 2, y, w - 4, 3);
for (let i = 0; i < 6; i++) {
const px = x + 9 + ((i * 47 + (seed || 0) * 29) % usable);
const r = 2 + ((i + (seed || 0)) % 3);
ctx.beginPath(); ctx.arc(px, y + 2, r, 0, TAU); ctx.fill();
if ((i + (seed || 0)) % 2 === 0) {
const drip = 4 + ((i * 7 + (seed || 0) * 3) % 13);
ctx.fillRect(px - 1, y + 2, 2, drip);
ctx.beginPath(); ctx.arc(px, y + 2 + drip, 1.6, 0, TAU); ctx.fill();
}
}
ctx.fillStyle = 'rgba(235,54,70,0.45)';
ctx.fillRect(x + 4, y, Math.max(10, w * 0.22), 1);
ctx.restore();
}
function pixelPanelPath(ctx, x, y, w, h, cut) {
const c = Math.max(0, Math.min(cut === undefined ? 7 : cut, w / 3, h / 3));
ctx.beginPath();
ctx.moveTo(x + c, y); ctx.lineTo(x + w - c, y);
ctx.lineTo(x + w, y + c); ctx.lineTo(x + w, y + h - c);
ctx.lineTo(x + w - c, y + h); ctx.lineTo(x + c, y + h);
ctx.lineTo(x, y + h - c); ctx.lineTo(x, y + c); ctx.closePath();
}
// Shared hard-edged panel language for HUD and modal screens. Chamfered corners,
// one-pixel highlights, and square corner pins keep it modern without losing the
// authored 2D pixel aesthetic.
function drawPixelPanel(ctx, x, y, w, h, opts) {
const o = opts || {};
const cut = o.cut === undefined ? 7 : o.cut;
ctx.save();
if (o.shadow !== false) {
ctx.fillStyle = o.shadowColor || 'rgba(0,0,0,0.5)';
pixelPanelPath(ctx, x + 4, y + 5, w, h, cut); ctx.fill();
}
ctx.fillStyle = o.fill || 'rgba(10,5,8,0.88)';
pixelPanelPath(ctx, x, y, w, h, cut); ctx.fill();
ctx.strokeStyle = o.border || 'rgba(111,45,57,0.9)';
ctx.lineWidth = o.lineWidth || 1;
pixelPanelPath(ctx, x + 0.5, y + 0.5, w - 1, h - 1, Math.max(1, cut - 0.5)); ctx.stroke();
ctx.strokeStyle = o.highlight || 'rgba(255,217,186,0.08)';
ctx.beginPath(); ctx.moveTo(x + cut + 2, y + 2); ctx.lineTo(x + w - cut - 2, y + 2); ctx.stroke();
const accent = o.accent || '#8f1f2f';
ctx.fillStyle = accent;
ctx.fillRect(x + cut, y, Math.max(16, Math.min(w * 0.34, 92)), 2);
ctx.fillRect(x + 2, y + cut, 2, Math.max(12, Math.min(h * 0.32, 34)));
ctx.fillStyle = 'rgba(210,171,115,0.28)';
ctx.fillRect(x + cut, y + 5, 3, 3);
ctx.fillRect(x + w - cut - 3, y + 5, 3, 3);
ctx.fillRect(x + cut, y + h - 8, 3, 3);
ctx.fillRect(x + w - cut - 3, y + h - 8, 3, 3);
if (o.blood) drawBloodTrim(ctx, x + 2, y + 2, w - 4, o.seed || 0, o.bloodAlpha === undefined ? 0.38 : o.bloodAlpha);
ctx.restore();
}
function drawPixelBar(ctx, x, y, w, h, value, opts) {
const o = opts || {};
const k = clamp(value, 0, 1);
ctx.save();
ctx.fillStyle = o.track || '#11080c'; ctx.fillRect(x, y, w, h);
ctx.fillStyle = o.fill || '#b51d35'; ctx.fillRect(x + 2, y + 2, Math.max(0, (w - 4) * k), Math.max(1, h - 4));
if (k > 0) {
ctx.fillStyle = o.glint || 'rgba(255,210,180,0.3)';
ctx.fillRect(x + 2, y + 2, Math.max(0, (w - 4) * k), 1);
}
const segments = o.segments || 0;
if (segments > 1) {
ctx.fillStyle = o.divider || 'rgba(5,2,4,0.6)';
for (let i = 1; i < segments; i++) {
const sx = x + Math.round((w * i) / segments);
ctx.fillRect(sx, y + 1, 1, h - 2);
}
}
ctx.strokeStyle = o.border || '#55303a'; ctx.lineWidth = 1; ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
ctx.restore();
}
function drawPixelTag(ctx, text, x, y, opts) {
const o = opts || {};
ctx.save();
ctx.font = o.font || 'bold 10px monospace';
const w = o.width || Math.ceil(ctx.measureText(text).width) + 16;
const h = o.height || 22;
drawPixelPanel(ctx, x, y, w, h, {
cut: 4, shadow: false, fill: o.fill || 'rgba(15,7,10,0.86)',
border: o.border || 'rgba(101,48,58,0.8)', accent: o.accent || '#7d1a29',
});
ctx.fillStyle = o.color || '#cdbdb2'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(text, x + w / 2, y + h / 2 + 1);
ctx.restore();
return w;
}