1026 lines
37 KiB
JavaScript
1026 lines
37 KiB
JavaScript
/**
|
||
* TankSkinRenderer.js
|
||
* Shared tank-skin drawing primitives used by BOTH the SkinScene preview
|
||
* and the in-game Tank.render() path. This is the SINGLE SOURCE OF TRUTH
|
||
* for how each skin looks.
|
||
*
|
||
* Coordinate contract for every _tankXxx(ctx, bc, tc, kc, t) function:
|
||
* • ctx is already translated to the tank center and rotated so that
|
||
* the barrel points toward negative-Y (i.e. "up" on screen).
|
||
* • All drawings use a DESIGN UNIT of s ≈ 12 pixels (matches the
|
||
* SkinScene preview). In-game callers should call ctx.scale(k, k)
|
||
* beforehand where k = this.halfSize / DESIGN_HALF_SIZE to make the
|
||
* skin match the actual collision box.
|
||
* • bc / tc / kc are body / turret / track colors.
|
||
* • t is a seconds-based animation timer (optional for static skins).
|
||
*/
|
||
|
||
const DESIGN_HALF_SIZE = 12; // matches the "s" used in most _tankXxx funcs
|
||
|
||
// ---------------------------------------------------------------
|
||
// Rounded-rect helper (local copy so we don't depend on any scene)
|
||
// ---------------------------------------------------------------
|
||
function _roundRect(ctx, x, y, w, h, r) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(x + r, y);
|
||
ctx.lineTo(x + w - r, y);
|
||
ctx.arcTo(x + w, y, x + w, y + r, r);
|
||
ctx.lineTo(x + w, y + h - r);
|
||
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
|
||
ctx.lineTo(x + r, y + h);
|
||
ctx.arcTo(x, y + h, x, y + h - r, r);
|
||
ctx.lineTo(x, y + r);
|
||
ctx.arcTo(x, y, x + r, y, r);
|
||
ctx.closePath();
|
||
}
|
||
|
||
// ======== DEFAULT — Classic rounded tank ========
|
||
function _tankDefault(ctx, bc, tc, kc /* , t */) {
|
||
const s = 12;
|
||
ctx.fillStyle = kc;
|
||
_roundRect(ctx, -s - 4, -s + 1, 4, s * 2 - 2, 2); ctx.fill();
|
||
_roundRect(ctx, s, -s + 1, 4, s * 2 - 2, 2); ctx.fill();
|
||
ctx.fillStyle = bc;
|
||
_roundRect(ctx, -s, -s, s * 2, s * 2, 3); ctx.fill();
|
||
ctx.fillStyle = 'rgba(255,255,255,0.15)';
|
||
ctx.fillRect(-s + 2, -s + 2, s - 2, 3);
|
||
ctx.fillStyle = tc;
|
||
ctx.beginPath(); ctx.arc(0, 0, s * 0.4, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = tc;
|
||
_roundRect(ctx, -2.5, -s - 8, 5, s - 2, 2); ctx.fill();
|
||
ctx.fillStyle = 'rgba(255,255,255,0.2)';
|
||
ctx.fillRect(-1.5, -s - 8, 3, 3);
|
||
}
|
||
|
||
// ======== ARCTIC ========
|
||
function _tankArctic(ctx, bc, tc, kc /* , t */) {
|
||
const s = 12;
|
||
ctx.fillStyle = kc;
|
||
ctx.fillRect(-s - 5, -s, 5, s * 2);
|
||
ctx.fillRect(s, -s, 5, s * 2);
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
|
||
ctx.lineWidth = 0.8;
|
||
for (let ty = -s + 3; ty < s; ty += 4) {
|
||
ctx.beginPath(); ctx.moveTo(-s - 5, ty); ctx.lineTo(-s, ty); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(s, ty); ctx.lineTo(s + 5, ty); ctx.stroke();
|
||
}
|
||
ctx.fillStyle = bc;
|
||
ctx.beginPath();
|
||
ctx.moveTo(0, -s - 2);
|
||
ctx.lineTo(s - 1, -s + 4);
|
||
ctx.lineTo(s - 1, s - 4);
|
||
ctx.lineTo(0, s + 2);
|
||
ctx.lineTo(-s + 1, s - 4);
|
||
ctx.lineTo(-s + 1, -s + 4);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
||
ctx.beginPath();
|
||
ctx.moveTo(-s + 3, -s + 4);
|
||
ctx.lineTo(0, -s - 1);
|
||
ctx.lineTo(2, -s + 5);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
|
||
ctx.lineWidth = 1;
|
||
ctx.beginPath(); ctx.moveTo(0, -4); ctx.lineTo(0, 4); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(-4, 0); ctx.lineTo(4, 0); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(-3, -3); ctx.lineTo(3, 3); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(3, -3); ctx.lineTo(-3, 3); ctx.stroke();
|
||
ctx.fillStyle = tc;
|
||
ctx.beginPath();
|
||
ctx.moveTo(0, -5); ctx.lineTo(5, 0); ctx.lineTo(0, 5); ctx.lineTo(-5, 0);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.fillStyle = tc;
|
||
ctx.beginPath();
|
||
ctx.moveTo(-3, -s + 3); ctx.lineTo(3, -s + 3);
|
||
ctx.lineTo(2, -s - 9); ctx.lineTo(-2, -s - 9);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.fillStyle = 'rgba(176,224,230,0.6)';
|
||
ctx.beginPath(); ctx.arc(0, -s - 9, 2, 0, Math.PI * 2); ctx.fill();
|
||
}
|
||
|
||
// ======== INFERNO ========
|
||
function _tankInferno(ctx, bc, tc, kc /* , t */) {
|
||
const s = 12;
|
||
ctx.fillStyle = kc;
|
||
ctx.fillRect(-s - 4, -s, 4, s * 2);
|
||
ctx.fillRect(s, -s, 4, s * 2);
|
||
for (let ty = -s + 2; ty < s - 2; ty += 5) {
|
||
ctx.fillStyle = kc;
|
||
ctx.beginPath();
|
||
ctx.moveTo(-s - 4, ty); ctx.lineTo(-s - 7, ty + 2.5); ctx.lineTo(-s - 4, ty + 5);
|
||
ctx.fill();
|
||
ctx.beginPath();
|
||
ctx.moveTo(s + 4, ty); ctx.lineTo(s + 7, ty + 2.5); ctx.lineTo(s + 4, ty + 5);
|
||
ctx.fill();
|
||
}
|
||
ctx.fillStyle = bc;
|
||
ctx.beginPath();
|
||
ctx.moveTo(-s + 2, -s - 1);
|
||
ctx.lineTo(s - 2, -s - 1);
|
||
ctx.lineTo(s + 1, -s + 4);
|
||
ctx.lineTo(s + 1, s - 2);
|
||
ctx.lineTo(s - 3, s + 1);
|
||
ctx.lineTo(-s + 3, s + 1);
|
||
ctx.lineTo(-s - 1, s - 2);
|
||
ctx.lineTo(-s - 1, -s + 4);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.fillStyle = 'rgba(255,165,0,0.5)';
|
||
ctx.beginPath();
|
||
ctx.moveTo(-s + 3, s); ctx.lineTo(-s + 6, 0); ctx.lineTo(-s + 3, -2);
|
||
ctx.lineTo(-s + 1, s - 2);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.beginPath();
|
||
ctx.moveTo(s - 3, s); ctx.lineTo(s - 6, 0); ctx.lineTo(s - 3, -2);
|
||
ctx.lineTo(s + 1, s - 2);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.fillStyle = 'rgba(255,69,0,0.3)';
|
||
ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = tc;
|
||
ctx.beginPath();
|
||
ctx.moveTo(0, -5); ctx.lineTo(5, -2); ctx.lineTo(4, 4);
|
||
ctx.lineTo(-4, 4); ctx.lineTo(-5, -2);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.fillStyle = tc;
|
||
_roundRect(ctx, -4, -s - 10, 3, s - 1, 1); ctx.fill();
|
||
_roundRect(ctx, 1, -s - 10, 3, s - 1, 1); ctx.fill();
|
||
ctx.fillStyle = 'rgba(255,100,0,0.7)';
|
||
ctx.beginPath(); ctx.arc(-2.5, -s - 10, 2, 0, Math.PI * 2); ctx.fill();
|
||
ctx.beginPath(); ctx.arc(2.5, -s - 10, 2, 0, Math.PI * 2); ctx.fill();
|
||
}
|
||
|
||
// ======== PHANTOM ========
|
||
function _tankPhantom(ctx, bc, tc, kc, t) {
|
||
const s = 12;
|
||
const tt = t || 0;
|
||
const flicker = 0.6 + Math.sin(tt * 5) * 0.15;
|
||
ctx.save();
|
||
ctx.globalAlpha = ctx.globalAlpha * flicker;
|
||
ctx.fillStyle = kc;
|
||
_roundRect(ctx, -s - 3, -s + 2, 3, s * 2 - 4, 1.5); ctx.fill();
|
||
_roundRect(ctx, s, -s + 2, 3, s * 2 - 4, 1.5); ctx.fill();
|
||
ctx.fillStyle = bc;
|
||
ctx.beginPath();
|
||
ctx.ellipse(0, 0, s - 1, s + 1, 0, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.strokeStyle = 'rgba(147,112,219,0.3)';
|
||
ctx.lineWidth = 1;
|
||
ctx.beginPath(); ctx.ellipse(0, 0, s + 2, s + 4, 0, 0, Math.PI * 2); ctx.stroke();
|
||
ctx.strokeStyle = 'rgba(147,112,219,0.15)';
|
||
ctx.beginPath(); ctx.ellipse(0, 0, s + 5, s + 7, 0, 0, Math.PI * 2); ctx.stroke();
|
||
ctx.fillStyle = 'rgba(200,180,255,0.2)';
|
||
ctx.beginPath(); ctx.ellipse(0, -2, s * 0.5, s * 0.6, 0, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = tc;
|
||
ctx.beginPath();
|
||
ctx.arc(0, 0, 5, Math.PI * 0.2, Math.PI * 1.8);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.fillStyle = tc;
|
||
ctx.fillRect(-1.5, -s - 10, 3, s);
|
||
ctx.fillStyle = 'rgba(147,112,219,0.8)';
|
||
ctx.beginPath(); ctx.arc(0, -s - 11, 2.5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
||
ctx.beginPath(); ctx.arc(0, -s - 11, 1, 0, Math.PI * 2); ctx.fill();
|
||
ctx.restore();
|
||
}
|
||
|
||
// ======== JUNGLE ========
|
||
function _tankJungle(ctx, bc, tc, kc /* , t */) {
|
||
const s = 13;
|
||
ctx.fillStyle = kc;
|
||
_roundRect(ctx, -s - 6, -s, 6, s * 2, 2); ctx.fill();
|
||
_roundRect(ctx, s, -s, 6, s * 2, 2); ctx.fill();
|
||
ctx.strokeStyle = 'rgba(0,100,0,0.4)';
|
||
ctx.lineWidth = 1;
|
||
for (let ty = -s + 2; ty < s - 2; ty += 4) {
|
||
ctx.beginPath(); ctx.moveTo(-s - 6, ty); ctx.lineTo(-s - 3, ty + 2); ctx.lineTo(-s - 6, ty + 4); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(s + 6, ty); ctx.lineTo(s + 3, ty + 2); ctx.lineTo(s + 6, ty + 4); ctx.stroke();
|
||
}
|
||
ctx.fillStyle = bc;
|
||
_roundRect(ctx, -s, -s, s * 2, s * 2, 2); ctx.fill();
|
||
ctx.fillStyle = 'rgba(0,80,0,0.35)';
|
||
ctx.beginPath(); ctx.arc(-4, -4, 5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = 'rgba(34,60,20,0.3)';
|
||
ctx.beginPath(); ctx.arc(5, 3, 4, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = 'rgba(0,50,0,0.25)';
|
||
ctx.beginPath(); ctx.arc(-2, 6, 3.5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.strokeStyle = 'rgba(0,100,0,0.4)';
|
||
ctx.lineWidth = 0.8;
|
||
ctx.beginPath(); ctx.moveTo(6, -8); ctx.quadraticCurveTo(10, -6, 7, -3); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(8, -7); ctx.quadraticCurveTo(11, -8, 9, -4); ctx.stroke();
|
||
ctx.fillStyle = tc;
|
||
_roundRect(ctx, -5, -5, 10, 10, 3); ctx.fill();
|
||
ctx.fillStyle = 'rgba(0,50,0,0.3)';
|
||
ctx.beginPath(); ctx.arc(0, 0, 3, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = tc;
|
||
_roundRect(ctx, -3, -s - 7, 6, s - 3, 2); ctx.fill();
|
||
ctx.fillStyle = kc;
|
||
ctx.fillRect(-4, -s - 7, 8, 2);
|
||
}
|
||
|
||
// ======== NEON ========
|
||
function _tankNeon(ctx, bc, tc, kc, t) {
|
||
const s = 12;
|
||
const tt = t || 0;
|
||
const glow = 0.6 + Math.sin(tt * 4) * 0.4;
|
||
ctx.save();
|
||
ctx.shadowColor = bc;
|
||
ctx.shadowBlur = 8 * glow;
|
||
ctx.strokeStyle = kc;
|
||
ctx.lineWidth = 2;
|
||
ctx.beginPath(); ctx.moveTo(-s - 3, -s); ctx.lineTo(-s - 3, s); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(s + 3, -s); ctx.lineTo(s + 3, s); ctx.stroke();
|
||
ctx.fillStyle = kc;
|
||
ctx.beginPath(); ctx.arc(-s - 3, -s, 2, 0, Math.PI * 2); ctx.fill();
|
||
ctx.beginPath(); ctx.arc(-s - 3, s, 2, 0, Math.PI * 2); ctx.fill();
|
||
ctx.beginPath(); ctx.arc(s + 3, -s, 2, 0, Math.PI * 2); ctx.fill();
|
||
ctx.beginPath(); ctx.arc(s + 3, s, 2, 0, Math.PI * 2); ctx.fill();
|
||
ctx.strokeStyle = bc;
|
||
ctx.lineWidth = 1.5;
|
||
ctx.strokeRect(-s, -s, s * 2, s * 2);
|
||
ctx.lineWidth = 0.8;
|
||
ctx.beginPath(); ctx.moveTo(-s, -s); ctx.lineTo(s, s); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(s, -s); ctx.lineTo(-s, s); ctx.stroke();
|
||
ctx.fillStyle = 'rgba(255,20,147,0.08)';
|
||
ctx.fillRect(-s, -s, s * 2, s * 2);
|
||
ctx.strokeStyle = bc;
|
||
ctx.lineWidth = 0.6;
|
||
ctx.beginPath(); ctx.moveTo(-s, 0); ctx.lineTo(-3, 0); ctx.lineTo(-3, -5); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(s, 0); ctx.lineTo(3, 0); ctx.lineTo(3, 5); ctx.stroke();
|
||
ctx.strokeStyle = tc;
|
||
ctx.lineWidth = 1.5;
|
||
ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI * 2); ctx.stroke();
|
||
ctx.fillStyle = bc;
|
||
ctx.beginPath(); ctx.arc(0, 0, 2, 0, Math.PI * 2); ctx.fill();
|
||
ctx.strokeStyle = tc;
|
||
ctx.lineWidth = 2;
|
||
ctx.beginPath(); ctx.moveTo(0, -5); ctx.lineTo(0, -s - 10); ctx.stroke();
|
||
ctx.fillStyle = bc;
|
||
ctx.beginPath(); ctx.arc(0, -s - 2, 1.5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.beginPath(); ctx.arc(0, -s - 6, 1.5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.beginPath(); ctx.arc(0, -s - 10, 2.5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.restore();
|
||
}
|
||
|
||
// ======== NEBULA ========
|
||
function _tankNebula(ctx, bc, tc, kc, t) {
|
||
const s = 12;
|
||
const tt = t || 0;
|
||
const pulse = 0.7 + Math.sin(tt * 3) * 0.3;
|
||
ctx.fillStyle = kc;
|
||
_roundRect(ctx, -s - 4, -s + 1, 4, s * 2 - 2, 2); ctx.fill();
|
||
_roundRect(ctx, s, -s + 1, 4, s * 2 - 2, 2); ctx.fill();
|
||
ctx.strokeStyle = 'rgba(255,0,255,0.45)';
|
||
ctx.lineWidth = 0.8;
|
||
ctx.beginPath(); ctx.moveTo(-s - 4, -s + 1); ctx.lineTo(-s - 4, s - 1); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(s + 4, -s + 1); ctx.lineTo(s + 4, s - 1); ctx.stroke();
|
||
ctx.fillStyle = bc;
|
||
_roundRect(ctx, -s, -s, s * 2, s * 2, 5); ctx.fill();
|
||
ctx.fillStyle = 'rgba(180,0,255,0.15)';
|
||
ctx.beginPath();
|
||
ctx.moveTo(-s, -s); ctx.lineTo(s, -s); ctx.lineTo(-s, s);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.save();
|
||
const stars = [
|
||
{ x: -7, y: -8, size: 1.2, phase: 0 },
|
||
{ x: 5, y: -6, size: 0.9, phase: 1.2 },
|
||
{ x: -4, y: 3, size: 1.0, phase: 2.4 },
|
||
{ x: 8, y: 5, size: 0.8, phase: 3.6 },
|
||
{ x: -2, y: -3, size: 1.1, phase: 4.8 },
|
||
{ x: 6, y: -1, size: 0.7, phase: 0.8 },
|
||
{ x: -8, y: 6, size: 0.9, phase: 2.0 },
|
||
{ x: 3, y: 7, size: 1.0, phase: 3.2 },
|
||
];
|
||
for (const star of stars) {
|
||
const twinkle = 0.4 + Math.sin(tt * 5 + star.phase) * 0.6;
|
||
ctx.globalAlpha = Math.max(0, twinkle);
|
||
ctx.fillStyle = '#FFFFFF';
|
||
ctx.beginPath();
|
||
ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
if (star.size > 0.9 && twinkle > 0.7) {
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
|
||
ctx.lineWidth = 0.4;
|
||
ctx.beginPath(); ctx.moveTo(star.x - 2, star.y); ctx.lineTo(star.x + 2, star.y); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(star.x, star.y - 2); ctx.lineTo(star.x, star.y + 2); ctx.stroke();
|
||
}
|
||
}
|
||
ctx.restore();
|
||
ctx.save();
|
||
ctx.globalAlpha = 0.2;
|
||
ctx.strokeStyle = '#FF00FF';
|
||
ctx.lineWidth = 1;
|
||
ctx.beginPath();
|
||
for (let a = 0; a < Math.PI * 4; a += 0.15) {
|
||
const r = 2 + a * 1.2;
|
||
const sx = Math.cos(a + tt * 0.5) * r;
|
||
const sy = Math.sin(a + tt * 0.5) * r;
|
||
if (a === 0) ctx.moveTo(sx, sy);
|
||
else ctx.lineTo(sx, sy);
|
||
if (r > s - 2) break;
|
||
}
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
ctx.save();
|
||
ctx.shadowColor = '#FF00FF';
|
||
ctx.shadowBlur = 6 * pulse;
|
||
ctx.fillStyle = tc;
|
||
ctx.beginPath(); ctx.arc(0, 0, 5.5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.restore();
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
|
||
ctx.lineWidth = 0.8;
|
||
ctx.beginPath(); ctx.arc(0, 0, 3.5, 0, Math.PI * 2); ctx.stroke();
|
||
ctx.fillStyle = `rgba(255,200,255,${0.5 + pulse * 0.3})`;
|
||
ctx.beginPath(); ctx.arc(0, 0, 2, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = 'rgba(255,255,255,0.7)';
|
||
ctx.beginPath(); ctx.arc(0, 0, 0.8, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = tc;
|
||
ctx.beginPath();
|
||
ctx.moveTo(-3, -5); ctx.lineTo(3, -5);
|
||
ctx.lineTo(2, -s - 9); ctx.lineTo(-2, -s - 9);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.strokeStyle = `rgba(255,0,255,${pulse * 0.6})`;
|
||
ctx.lineWidth = 1;
|
||
ctx.beginPath(); ctx.moveTo(-3.5, -s); ctx.lineTo(3.5, -s); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(-3, -s - 4); ctx.lineTo(3, -s - 4); ctx.stroke();
|
||
ctx.save();
|
||
ctx.shadowColor = '#FF00FF';
|
||
ctx.shadowBlur = 8 * pulse;
|
||
ctx.fillStyle = '#FF00FF';
|
||
ctx.beginPath(); ctx.arc(0, -s - 9, 2.8, 0, Math.PI * 2); ctx.fill();
|
||
ctx.restore();
|
||
ctx.fillStyle = 'rgba(255,255,255,0.8)';
|
||
ctx.beginPath(); ctx.arc(0, -s - 9, 1.2, 0, Math.PI * 2); ctx.fill();
|
||
}
|
||
|
||
// ======== ROYAL ========
|
||
function _tankRoyal(ctx, bc, tc, kc /* , t */) {
|
||
const s = 12;
|
||
ctx.fillStyle = kc;
|
||
_roundRect(ctx, -s - 5, -s + 1, 5, s * 2 - 2, 2); ctx.fill();
|
||
_roundRect(ctx, s, -s + 1, 5, s * 2 - 2, 2); ctx.fill();
|
||
ctx.strokeStyle = 'rgba(255,215,0,0.5)';
|
||
ctx.lineWidth = 0.8;
|
||
ctx.beginPath(); ctx.moveTo(-s - 5, -s + 1); ctx.lineTo(-s - 5, s - 1); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(s + 5, -s + 1); ctx.lineTo(s + 5, s - 1); ctx.stroke();
|
||
ctx.fillStyle = bc;
|
||
_roundRect(ctx, -s, -s, s * 2, s * 2, 5); ctx.fill();
|
||
ctx.fillStyle = tc;
|
||
ctx.beginPath();
|
||
ctx.moveTo(-4, -5);
|
||
ctx.lineTo(4, -5);
|
||
ctx.lineTo(4, 1);
|
||
ctx.quadraticCurveTo(4, 5, 0, 7);
|
||
ctx.quadraticCurveTo(-4, 5, -4, 1);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.fillStyle = 'rgba(255,215,0,0.6)';
|
||
ctx.beginPath();
|
||
ctx.moveTo(-2, -3);
|
||
ctx.lineTo(2, -3);
|
||
ctx.lineTo(2, 0);
|
||
ctx.quadraticCurveTo(2, 3, 0, 4);
|
||
ctx.quadraticCurveTo(-2, 3, -2, 0);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.fillStyle = '#FFD700';
|
||
ctx.beginPath();
|
||
ctx.moveTo(-6, -s + 2);
|
||
ctx.lineTo(-5, -s - 2);
|
||
ctx.lineTo(-3, -s + 1);
|
||
ctx.lineTo(0, -s - 4);
|
||
ctx.lineTo(3, -s + 1);
|
||
ctx.lineTo(5, -s - 2);
|
||
ctx.lineTo(6, -s + 2);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.fillStyle = '#FF0000';
|
||
ctx.beginPath(); ctx.arc(0, -s - 2, 1.2, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = '#0066FF';
|
||
ctx.beginPath(); ctx.arc(-4, -s, 0.8, 0, Math.PI * 2); ctx.fill();
|
||
ctx.beginPath(); ctx.arc(4, -s, 0.8, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = tc;
|
||
_roundRect(ctx, -2.5, -s - 10, 5, s - 4, 2); ctx.fill();
|
||
ctx.strokeStyle = '#FFD700';
|
||
ctx.lineWidth = 1;
|
||
ctx.beginPath(); ctx.moveTo(-3, -s - 3); ctx.lineTo(3, -s - 3); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(-3, -s - 7); ctx.lineTo(3, -s - 7); ctx.stroke();
|
||
ctx.fillStyle = '#FFD700';
|
||
ctx.beginPath(); ctx.arc(0, -s - 10, 2.5, 0, Math.PI * 2); ctx.fill();
|
||
}
|
||
|
||
// ======== SAKURA ========
|
||
function _tankSakura(ctx, bc, tc, kc, t) {
|
||
const s = 12;
|
||
const tt = t || 0;
|
||
ctx.fillStyle = kc;
|
||
_roundRect(ctx, -s - 4, -s + 1, 4, s * 2 - 2, 2); ctx.fill();
|
||
_roundRect(ctx, s, -s + 1, 4, s * 2 - 2, 2); ctx.fill();
|
||
ctx.strokeStyle = 'rgba(255,105,180,0.4)';
|
||
ctx.lineWidth = 0.7;
|
||
ctx.beginPath(); ctx.moveTo(-s - 4, -s + 1); ctx.lineTo(-s - 4, s - 1); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(s + 4, -s + 1); ctx.lineTo(s + 4, s - 1); ctx.stroke();
|
||
ctx.fillStyle = bc;
|
||
_roundRect(ctx, -s, -s, s * 2, s * 2, 6); ctx.fill();
|
||
ctx.fillStyle = 'rgba(255,255,255,0.18)';
|
||
ctx.beginPath();
|
||
ctx.moveTo(-s + 2, -s);
|
||
ctx.lineTo(s - 2, -s);
|
||
ctx.lineTo(-s + 2, -s + 8);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.save();
|
||
ctx.globalAlpha = 0.6;
|
||
const petals = [
|
||
{ ox: -6, oy: -7, phase: 0 },
|
||
{ ox: 7, oy: -4, phase: 1.5 },
|
||
{ ox: -3, oy: 6, phase: 3 },
|
||
{ ox: 5, oy: 5, phase: 4.5 },
|
||
];
|
||
for (const p of petals) {
|
||
const drift = Math.sin(tt * 2 + p.phase) * 1.5;
|
||
const px = p.ox + drift;
|
||
const py = p.oy;
|
||
ctx.fillStyle = '#FF69B4';
|
||
ctx.beginPath();
|
||
for (let i = 0; i < 5; i++) {
|
||
const angle = (i / 5) * Math.PI * 2 - Math.PI / 2;
|
||
const pr = 2.2;
|
||
const fx = px + Math.cos(angle) * pr;
|
||
const fy = py + Math.sin(angle) * pr;
|
||
ctx.moveTo(fx, fy);
|
||
ctx.arc(fx, fy, 1.2, 0, Math.PI * 2);
|
||
}
|
||
ctx.fill();
|
||
ctx.fillStyle = '#FFE4E1';
|
||
ctx.beginPath(); ctx.arc(px, py, 0.8, 0, Math.PI * 2); ctx.fill();
|
||
}
|
||
ctx.restore();
|
||
ctx.fillStyle = tc;
|
||
ctx.beginPath(); ctx.arc(0, 0, 5.5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.4)';
|
||
ctx.lineWidth = 0.8;
|
||
ctx.beginPath(); ctx.arc(0, 0, 3.5, 0, Math.PI * 2); ctx.stroke();
|
||
ctx.fillStyle = '#FFE4E1';
|
||
ctx.beginPath(); ctx.arc(0, 0, 1.5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = tc;
|
||
ctx.beginPath();
|
||
ctx.moveTo(-3, -5); ctx.lineTo(3, -5);
|
||
ctx.lineTo(2, -s - 9); ctx.lineTo(-2, -s - 9);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.fillStyle = 'rgba(255,183,197,0.7)';
|
||
ctx.beginPath(); ctx.arc(0, -s - 9, 2.5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
||
ctx.beginPath(); ctx.arc(0, -s - 9, 1.2, 0, Math.PI * 2); ctx.fill();
|
||
}
|
||
|
||
// ======== THUNDER ========
|
||
function _tankThunder(ctx, bc, tc, kc, t) {
|
||
const s = 12;
|
||
const tt = t || 0;
|
||
const flash = 0.7 + Math.sin(tt * 8) * 0.3;
|
||
ctx.fillStyle = kc;
|
||
_roundRect(ctx, -s - 5, -s, 5, s * 2, 2); ctx.fill();
|
||
_roundRect(ctx, s, -s, 5, s * 2, 2); ctx.fill();
|
||
ctx.save();
|
||
ctx.globalAlpha = flash * 0.6;
|
||
ctx.strokeStyle = '#00BFFF';
|
||
ctx.lineWidth = 0.8;
|
||
for (let ty = -s + 2; ty < s - 2; ty += 5) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(-s - 5, ty);
|
||
ctx.lineTo(-s - 3, ty + 1.5);
|
||
ctx.lineTo(-s - 5, ty + 3);
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.moveTo(s + 5, ty);
|
||
ctx.lineTo(s + 3, ty + 1.5);
|
||
ctx.lineTo(s + 5, ty + 3);
|
||
ctx.stroke();
|
||
}
|
||
ctx.restore();
|
||
ctx.fillStyle = bc;
|
||
ctx.beginPath();
|
||
ctx.moveTo(-s + 1, -s - 2);
|
||
ctx.lineTo(s - 1, -s - 2);
|
||
ctx.lineTo(s + 2, -s + 3);
|
||
ctx.lineTo(s + 2, s - 3);
|
||
ctx.lineTo(s - 1, s + 2);
|
||
ctx.lineTo(-s + 1, s + 2);
|
||
ctx.lineTo(-s - 2, s - 3);
|
||
ctx.lineTo(-s - 2, -s + 3);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.fillStyle = 'rgba(0,191,255,0.5)';
|
||
ctx.beginPath();
|
||
ctx.moveTo(-1, -7);
|
||
ctx.lineTo(3, -7);
|
||
ctx.lineTo(0, -1);
|
||
ctx.lineTo(4, -1);
|
||
ctx.lineTo(-2, 8);
|
||
ctx.lineTo(0, 2);
|
||
ctx.lineTo(-3, 2);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.save();
|
||
ctx.shadowColor = '#00BFFF';
|
||
ctx.shadowBlur = 6 * flash;
|
||
ctx.strokeStyle = 'rgba(0,191,255,0.35)';
|
||
ctx.lineWidth = 1.5;
|
||
ctx.beginPath();
|
||
ctx.moveTo(-s + 1, -s - 2);
|
||
ctx.lineTo(s - 1, -s - 2);
|
||
ctx.lineTo(s + 2, -s + 3);
|
||
ctx.lineTo(s + 2, s - 3);
|
||
ctx.lineTo(s - 1, s + 2);
|
||
ctx.lineTo(-s + 1, s + 2);
|
||
ctx.lineTo(-s - 2, s - 3);
|
||
ctx.lineTo(-s - 2, -s + 3);
|
||
ctx.closePath();
|
||
ctx.stroke();
|
||
ctx.fillStyle = tc;
|
||
ctx.beginPath();
|
||
for (let i = 0; i < 6; i++) {
|
||
const angle = (i / 6) * Math.PI * 2 - Math.PI / 6;
|
||
const hx = Math.cos(angle) * 5.5;
|
||
const hy = Math.sin(angle) * 5.5;
|
||
if (i === 0) ctx.moveTo(hx, hy);
|
||
else ctx.lineTo(hx, hy);
|
||
}
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.fillStyle = `rgba(0,191,255,${0.4 + flash * 0.3})`;
|
||
ctx.beginPath(); ctx.arc(0, 0, 3, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = `rgba(255,255,255,${0.5 + flash * 0.3})`;
|
||
ctx.beginPath(); ctx.arc(0, 0, 1.5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = tc;
|
||
_roundRect(ctx, -2.5, -s - 10, 5, s - 3, 2); ctx.fill();
|
||
ctx.strokeStyle = `rgba(0,191,255,${flash * 0.8})`;
|
||
ctx.lineWidth = 1;
|
||
ctx.beginPath();
|
||
ctx.moveTo(-2.5, -s); ctx.lineTo(-5, -s - 3);
|
||
ctx.lineTo(-2.5, -s - 5); ctx.lineTo(-5, -s - 8);
|
||
ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.moveTo(2.5, -s - 1); ctx.lineTo(5, -s - 4);
|
||
ctx.lineTo(2.5, -s - 6); ctx.lineTo(5, -s - 9);
|
||
ctx.stroke();
|
||
ctx.fillStyle = '#00BFFF';
|
||
ctx.beginPath(); ctx.arc(0, -s - 10, 3, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = 'rgba(255,255,255,0.8)';
|
||
ctx.beginPath(); ctx.arc(0, -s - 10, 1.5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.restore();
|
||
}
|
||
|
||
// ======== DIAMOND 💎 ========
|
||
// Full crystalline tank — all-diamond body, gem-encrusted tracks,
|
||
// hexagonal faceted turret, jewel barrel. Stays within the standard
|
||
// [−s, +s] design footprint so it matches the size of every other skin.
|
||
// Reference: clean icy-blue gem tank, faceted triangular surfaces,
|
||
// sapphire track wheels, centered diamond rivet.
|
||
function _tankDiamond(ctx, bc, tc, kc, t) {
|
||
const s = 12;
|
||
const tt = t || 0;
|
||
const breathe = 0.5 + Math.sin(tt * 0.5) * 0.5; // very slow breathing
|
||
const shimmer = 0.5 + Math.sin(tt * 0.7) * 0.5; // slow shimmer
|
||
|
||
// Ice-blue palette (reference image):
|
||
// light = crystal highlight, mid = surface, deep = shadow / outline
|
||
const light = '#EAF6FF';
|
||
const midA = '#BCDFFF';
|
||
const midB = '#7DC4FF';
|
||
const deep = '#1E5B9E';
|
||
const edge = '#7DF9FF';
|
||
|
||
// ── 0. Very faint outer aura (doesn't enlarge footprint) ──
|
||
ctx.save();
|
||
ctx.globalAlpha = 0.10 + breathe * 0.06;
|
||
ctx.fillStyle = edge;
|
||
ctx.beginPath();
|
||
ctx.arc(0, 0, s + 2, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.restore();
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// TRACKS (left & right) — sapphire-bead chains with hub wheels
|
||
// ═══════════════════════════════════════════════════════════
|
||
const tW = 4; // track half-width
|
||
const tx = -s - tW * 0.5; // left track center x
|
||
const rx = s + tW * 0.5; // right track center x
|
||
const trackTop = -s + 1;
|
||
const trackBot = s - 1;
|
||
|
||
// Track outer shell (dark steel)
|
||
ctx.fillStyle = '#0E3562';
|
||
ctx.fillRect(tx - tW, trackTop - 0.5, tW * 2, trackBot - trackTop + 1);
|
||
ctx.fillRect(rx - tW, trackTop - 0.5, tW * 2, trackBot - trackTop + 1);
|
||
|
||
// Track top/bottom rounded caps
|
||
ctx.beginPath(); ctx.arc(tx, trackTop, tW, Math.PI, Math.PI * 2); ctx.fill();
|
||
ctx.beginPath(); ctx.arc(tx, trackBot, tW, 0, Math.PI); ctx.fill();
|
||
ctx.beginPath(); ctx.arc(rx, trackTop, tW, Math.PI, Math.PI * 2); ctx.fill();
|
||
ctx.beginPath(); ctx.arc(rx, trackBot, tW, 0, Math.PI); ctx.fill();
|
||
|
||
// Gem beads along each track (small blue sapphires)
|
||
const beadCount = 5;
|
||
for (let i = 0; i < beadCount; i++) {
|
||
const by = trackTop + 3 + (trackBot - trackTop - 6) * (i / (beadCount - 1));
|
||
for (const cx of [tx, rx]) {
|
||
// Bead body (rhombus-ish)
|
||
ctx.fillStyle = midB;
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx, by - 1.6);
|
||
ctx.lineTo(cx + 2.2, by);
|
||
ctx.lineTo(cx, by + 1.6);
|
||
ctx.lineTo(cx - 2.2, by);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
// Highlight
|
||
ctx.fillStyle = light;
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx, by - 1.6);
|
||
ctx.lineTo(cx + 0.8, by - 0.4);
|
||
ctx.lineTo(cx - 0.8, by - 0.4);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
// Dark edge bottom
|
||
ctx.fillStyle = deep;
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx + 2.2, by);
|
||
ctx.lineTo(cx, by + 1.6);
|
||
ctx.lineTo(cx + 0.4, by + 0.2);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
}
|
||
}
|
||
|
||
// Hub wheels (big sapphire wheels at each end)
|
||
const hubR = 2.6;
|
||
for (const cx of [tx, rx]) {
|
||
for (const hy of [trackTop, trackBot]) {
|
||
// Outer ring
|
||
ctx.fillStyle = deep;
|
||
ctx.beginPath(); ctx.arc(cx, hy, hubR + 0.6, 0, Math.PI * 2); ctx.fill();
|
||
// Gem core
|
||
ctx.fillStyle = midB;
|
||
ctx.beginPath(); ctx.arc(cx, hy, hubR, 0, Math.PI * 2); ctx.fill();
|
||
// Star-cut facets (4-point)
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.65)';
|
||
ctx.lineWidth = 0.5;
|
||
ctx.beginPath(); ctx.moveTo(cx - hubR, hy); ctx.lineTo(cx + hubR, hy); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(cx, hy - hubR); ctx.lineTo(cx, hy + hubR); ctx.stroke();
|
||
// Inner highlight
|
||
ctx.fillStyle = light;
|
||
ctx.beginPath(); ctx.arc(cx - 0.6, hy - 0.6, 0.8, 0, Math.PI * 2); ctx.fill();
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// BODY — teardrop/bullet silhouette, faceted diamond surface
|
||
// Width stays within ±s; height uses the full ±s footprint.
|
||
// ═══════════════════════════════════════════════════════════
|
||
// Main body outline (rear wider, front slightly narrower — classic tank nose)
|
||
const body = [
|
||
[-s + 2, -s + 1], // front-left
|
||
[ s - 2, -s + 1], // front-right
|
||
[ s, -s + 4],
|
||
[ s, s - 4],
|
||
[ s - 2, s - 1],
|
||
[-s + 2, s - 1],
|
||
[-s, s - 4],
|
||
[-s, -s + 4],
|
||
];
|
||
|
||
// Base fill — soft ice-blue gradient approximation (radial-ish)
|
||
ctx.fillStyle = midA;
|
||
ctx.beginPath();
|
||
ctx.moveTo(body[0][0], body[0][1]);
|
||
for (let i = 1; i < body.length; i++) ctx.lineTo(body[i][0], body[i][1]);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
|
||
// Faceted triangles — all emanating from a single "highlight point" so the
|
||
// crystal looks like it has a single light source, mimicking the reference.
|
||
const hp = [-s + 4, -s + 3]; // highlight point (upper-left region)
|
||
const facetRim = [
|
||
[-s + 2, -s + 1], [ s - 2, -s + 1], [ s, -s + 4], [ s, s - 4],
|
||
[ s - 2, s - 1], [-s + 2, s - 1], [-s, s - 4], [-s, -s + 4],
|
||
];
|
||
// Triangles from highlight point to each rim edge
|
||
for (let i = 0; i < facetRim.length; i++) {
|
||
const a = facetRim[i];
|
||
const b = facetRim[(i + 1) % facetRim.length];
|
||
// Shade based on angle away from light point
|
||
const mx = (a[0] + b[0]) * 0.5;
|
||
const my = (a[1] + b[1]) * 0.5;
|
||
const dx = mx - hp[0];
|
||
const dy = my - hp[1];
|
||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||
// Nearer to hp → brighter, farther → deeper blue
|
||
const tBright = Math.max(0, 1 - dist / 22);
|
||
const colA = tBright > 0.6 ? light : tBright > 0.3 ? midA : midB;
|
||
ctx.fillStyle = colA;
|
||
ctx.globalAlpha = 0.55;
|
||
ctx.beginPath();
|
||
ctx.moveTo(hp[0], hp[1]);
|
||
ctx.lineTo(a[0], a[1]);
|
||
ctx.lineTo(b[0], b[1]);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
}
|
||
ctx.globalAlpha = 1;
|
||
|
||
// Secondary facet lines — subtle geometric net
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
|
||
ctx.lineWidth = 0.4;
|
||
// From highlight point to every rim vertex
|
||
for (const p of facetRim) {
|
||
ctx.beginPath(); ctx.moveTo(hp[0], hp[1]); ctx.lineTo(p[0], p[1]); ctx.stroke();
|
||
}
|
||
// Rim outline (crisp inner)
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.55)';
|
||
ctx.lineWidth = 0.6;
|
||
ctx.beginPath();
|
||
ctx.moveTo(body[0][0], body[0][1]);
|
||
for (let i = 1; i < body.length; i++) ctx.lineTo(body[i][0], body[i][1]);
|
||
ctx.closePath();
|
||
ctx.stroke();
|
||
|
||
// Deep blue body outline (crisp edge)
|
||
ctx.strokeStyle = deep;
|
||
ctx.lineWidth = 0.8;
|
||
ctx.beginPath();
|
||
ctx.moveTo(body[0][0], body[0][1]);
|
||
for (let i = 1; i < body.length; i++) ctx.lineTo(body[i][0], body[i][1]);
|
||
ctx.closePath();
|
||
ctx.stroke();
|
||
|
||
// ── Centered "diamond rivet" — the signature small gem on the hull ──
|
||
ctx.save();
|
||
ctx.fillStyle = midB;
|
||
ctx.beginPath();
|
||
ctx.moveTo(0, -2.8);
|
||
ctx.lineTo(2.4, 0);
|
||
ctx.lineTo(0, 2.8);
|
||
ctx.lineTo(-2.4, 0);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
// Rivet facet lines
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.7)';
|
||
ctx.lineWidth = 0.5;
|
||
ctx.beginPath(); ctx.moveTo(0, -2.8); ctx.lineTo(0, 2.8); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(-2.4, 0); ctx.lineTo(2.4, 0); ctx.stroke();
|
||
// Rivet highlight
|
||
ctx.fillStyle = light;
|
||
ctx.beginPath();
|
||
ctx.moveTo(0, -2.8); ctx.lineTo(0.9, -1); ctx.lineTo(-0.9, -1);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
// Rivet dark outline
|
||
ctx.strokeStyle = deep;
|
||
ctx.lineWidth = 0.5;
|
||
ctx.beginPath();
|
||
ctx.moveTo(0, -2.8); ctx.lineTo(2.4, 0); ctx.lineTo(0, 2.8);
|
||
ctx.lineTo(-2.4, 0); ctx.closePath();
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// TURRET — hexagonal gem prism (top view) sitting on the hull
|
||
// ═══════════════════════════════════════════════════════════
|
||
const turretR = 6.5;
|
||
const turretRy = 5.5; // slightly squashed vertically (perspective feel)
|
||
// Turret base ring (darker blue outline for contrast with body)
|
||
ctx.fillStyle = deep;
|
||
ctx.beginPath();
|
||
for (let i = 0; i < 6; i++) {
|
||
const ang = (i / 6) * Math.PI * 2 - Math.PI / 2;
|
||
const hx = Math.cos(ang) * (turretR + 0.6);
|
||
const hy = Math.sin(ang) * (turretRy + 0.6) - 1;
|
||
if (i === 0) ctx.moveTo(hx, hy); else ctx.lineTo(hx, hy);
|
||
}
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
|
||
// Turret body (6-sided gem)
|
||
const turretPts = [];
|
||
for (let i = 0; i < 6; i++) {
|
||
const ang = (i / 6) * Math.PI * 2 - Math.PI / 2;
|
||
turretPts.push([Math.cos(ang) * turretR, Math.sin(ang) * turretRy - 1]);
|
||
}
|
||
ctx.fillStyle = midA;
|
||
ctx.beginPath();
|
||
ctx.moveTo(turretPts[0][0], turretPts[0][1]);
|
||
for (let i = 1; i < 6; i++) ctx.lineTo(turretPts[i][0], turretPts[i][1]);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
|
||
// Turret facets from center
|
||
const tHp = [-1.8, -2.5];
|
||
for (let i = 0; i < 6; i++) {
|
||
const a = turretPts[i];
|
||
const b = turretPts[(i + 1) % 6];
|
||
const mx = (a[0] + b[0]) * 0.5;
|
||
const my = (a[1] + b[1]) * 0.5;
|
||
const d = Math.sqrt((mx - tHp[0]) ** 2 + (my - tHp[1]) ** 2);
|
||
const tBright = Math.max(0, 1 - d / 10);
|
||
ctx.fillStyle = tBright > 0.55 ? light : tBright > 0.25 ? midA : midB;
|
||
ctx.globalAlpha = 0.7;
|
||
ctx.beginPath();
|
||
ctx.moveTo(tHp[0], tHp[1]);
|
||
ctx.lineTo(a[0], a[1]);
|
||
ctx.lineTo(b[0], b[1]);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
}
|
||
ctx.globalAlpha = 1;
|
||
|
||
// Turret facet lines
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.55)';
|
||
ctx.lineWidth = 0.5;
|
||
for (const p of turretPts) {
|
||
ctx.beginPath(); ctx.moveTo(tHp[0], tHp[1]); ctx.lineTo(p[0], p[1]); ctx.stroke();
|
||
}
|
||
// Turret crisp outline
|
||
ctx.strokeStyle = deep;
|
||
ctx.lineWidth = 0.7;
|
||
ctx.beginPath();
|
||
ctx.moveTo(turretPts[0][0], turretPts[0][1]);
|
||
for (let i = 1; i < 6; i++) ctx.lineTo(turretPts[i][0], turretPts[i][1]);
|
||
ctx.closePath();
|
||
ctx.stroke();
|
||
|
||
// Turret crown highlight band
|
||
ctx.fillStyle = `rgba(255,255,255,${0.25 + breathe * 0.12})`;
|
||
ctx.beginPath();
|
||
ctx.ellipse(-1, -3.8, 3.2, 1.3, -0.3, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
|
||
// Turret center (small cut-gem apex)
|
||
ctx.fillStyle = midB;
|
||
ctx.beginPath();
|
||
ctx.arc(tHp[0], tHp[1], 1.2, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.fillStyle = '#FFFFFF';
|
||
ctx.beginPath();
|
||
ctx.arc(tHp[0] - 0.3, tHp[1] - 0.3, 0.5, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// BARREL — hex-prism gem cannon with metal band + glowing muzzle
|
||
// ═══════════════════════════════════════════════════════════
|
||
const barrelBase = -3; // at turret edge
|
||
const barrelTip = -s - 4; // doesn't exceed footprint much
|
||
const barrelHalfW = 2.2;
|
||
|
||
// Barrel body — faceted
|
||
ctx.fillStyle = midA;
|
||
ctx.beginPath();
|
||
ctx.moveTo(-barrelHalfW, barrelBase);
|
||
ctx.lineTo( barrelHalfW, barrelBase);
|
||
ctx.lineTo( barrelHalfW * 0.8, barrelTip);
|
||
ctx.lineTo(-barrelHalfW * 0.8, barrelTip);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
|
||
// Barrel left/right facet shade
|
||
ctx.fillStyle = midB;
|
||
ctx.globalAlpha = 0.7;
|
||
ctx.beginPath();
|
||
ctx.moveTo( barrelHalfW, barrelBase);
|
||
ctx.lineTo( barrelHalfW * 0.8, barrelTip);
|
||
ctx.lineTo( 0, barrelTip);
|
||
ctx.lineTo( 0, barrelBase);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.globalAlpha = 1;
|
||
|
||
// Barrel center highlight stripe
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.6)';
|
||
ctx.lineWidth = 0.5;
|
||
ctx.beginPath();
|
||
ctx.moveTo(-0.6, barrelBase);
|
||
ctx.lineTo(-0.5, barrelTip);
|
||
ctx.stroke();
|
||
|
||
// Barrel outline
|
||
ctx.strokeStyle = deep;
|
||
ctx.lineWidth = 0.7;
|
||
ctx.beginPath();
|
||
ctx.moveTo(-barrelHalfW, barrelBase);
|
||
ctx.lineTo( barrelHalfW, barrelBase);
|
||
ctx.lineTo( barrelHalfW * 0.8, barrelTip);
|
||
ctx.lineTo(-barrelHalfW * 0.8, barrelTip);
|
||
ctx.closePath();
|
||
ctx.stroke();
|
||
|
||
// Metal collar near turret (dark band)
|
||
ctx.fillStyle = deep;
|
||
ctx.fillRect(-barrelHalfW - 0.4, barrelBase - 0.4, (barrelHalfW + 0.4) * 2, 1.4);
|
||
ctx.fillStyle = '#3E88C8';
|
||
ctx.fillRect(-barrelHalfW - 0.4, barrelBase - 0.1, (barrelHalfW + 0.4) * 2, 0.4);
|
||
|
||
// Muzzle ring
|
||
ctx.fillStyle = deep;
|
||
ctx.fillRect(-barrelHalfW * 0.9, barrelTip - 0.8, barrelHalfW * 1.8, 1.2);
|
||
|
||
// Glowing gem muzzle
|
||
ctx.save();
|
||
ctx.shadowColor = edge;
|
||
ctx.shadowBlur = 5 + breathe * 3;
|
||
ctx.fillStyle = light;
|
||
ctx.beginPath();
|
||
ctx.arc(0, barrelTip - 0.4, 1.4, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
ctx.restore();
|
||
ctx.fillStyle = '#FFFFFF';
|
||
ctx.beginPath();
|
||
ctx.arc(0, barrelTip - 0.4, 0.7, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// SPARKLES — 4 sparse 4-point stars, very slow twinkle
|
||
// ═══════════════════════════════════════════════════════════
|
||
ctx.save();
|
||
const flares = [
|
||
{ x: -s + 3, y: -s + 2, phase: 0, len: 2.2 },
|
||
{ x: s - 4, y: -4, phase: 2.0, len: 1.8 },
|
||
{ x: -4, y: s - 3, phase: 4.0, len: 2.0 },
|
||
{ x: s - 2, y: s - 6, phase: 6.0, len: 1.6 },
|
||
];
|
||
for (const fl of flares) {
|
||
const tw = Math.max(0, Math.sin(tt * 0.6 + fl.phase));
|
||
if (tw > 0.65) {
|
||
const a = (tw - 0.65) * 3;
|
||
const col = `rgba(255,255,255,${Math.min(1, a)})`;
|
||
ctx.strokeStyle = col;
|
||
ctx.fillStyle = col;
|
||
ctx.lineWidth = 0.5;
|
||
ctx.beginPath(); ctx.moveTo(fl.x - fl.len, fl.y); ctx.lineTo(fl.x + fl.len, fl.y); ctx.stroke();
|
||
ctx.beginPath(); ctx.moveTo(fl.x, fl.y - fl.len); ctx.lineTo(fl.x, fl.y + fl.len); ctx.stroke();
|
||
ctx.beginPath(); ctx.arc(fl.x, fl.y, 0.6, 0, Math.PI * 2); ctx.fill();
|
||
}
|
||
}
|
||
ctx.restore();
|
||
|
||
// ── Very subtle shimmer band sliding across body (crystal gleam) ──
|
||
ctx.save();
|
||
ctx.globalAlpha = 0.12 * shimmer;
|
||
ctx.fillStyle = '#FFFFFF';
|
||
const gleamY = -s + (s * 2) * ((tt * 0.25) % 1);
|
||
ctx.beginPath();
|
||
ctx.moveTo(-s + 2, gleamY);
|
||
ctx.lineTo( s - 2, gleamY - 3);
|
||
ctx.lineTo( s - 2, gleamY - 1);
|
||
ctx.lineTo(-s + 2, gleamY + 2);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
ctx.restore();
|
||
}
|
||
|
||
// ---------------------------------------------------------------
|
||
// Dispatcher — the ONE function every caller should use.
|
||
// @param {CanvasRenderingContext2D} ctx — already translated+rotated to tank
|
||
// @param {string} skinId — 'default', 'arctic', ... 'diamond'
|
||
// @param {object|null} colors — { body, turret, track } (null = default)
|
||
// @param {number} t — animation timer in seconds
|
||
// ---------------------------------------------------------------
|
||
const FALLBACK_COLORS = { body: '#FFD700', turret: '#B8860B', track: '#8B6914' };
|
||
|
||
function drawTankSkin(ctx, skinId, colors, t) {
|
||
const c = colors || FALLBACK_COLORS;
|
||
const bc = c.body || FALLBACK_COLORS.body;
|
||
const tc = c.turret || FALLBACK_COLORS.turret;
|
||
const kc = c.track || FALLBACK_COLORS.track;
|
||
const tt = t || 0;
|
||
switch (skinId) {
|
||
case 'arctic': return _tankArctic(ctx, bc, tc, kc, tt);
|
||
case 'inferno': return _tankInferno(ctx, bc, tc, kc, tt);
|
||
case 'phantom': return _tankPhantom(ctx, bc, tc, kc, tt);
|
||
case 'jungle': return _tankJungle(ctx, bc, tc, kc, tt);
|
||
case 'neon': return _tankNeon(ctx, bc, tc, kc, tt);
|
||
case 'nebula': return _tankNebula(ctx, bc, tc, kc, tt);
|
||
case 'royal': return _tankRoyal(ctx, bc, tc, kc, tt);
|
||
case 'sakura': return _tankSakura(ctx, bc, tc, kc, tt);
|
||
case 'thunder': return _tankThunder(ctx, bc, tc, kc, tt);
|
||
case 'diamond': return _tankDiamond(ctx, bc, tc, kc, tt);
|
||
case 'default':
|
||
default: return _tankDefault(ctx, bc, tc, kc, tt);
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
drawTankSkin,
|
||
DESIGN_HALF_SIZE,
|
||
};
|