626879ed0c
- Add enemy freshness tracking (novelty bonus for repeated deploys) - Add freshness bar to sidepanel enemy cards with penalty indicator - Major overhaul of renderer-overlays.js (790+ lines for UI polish) - Enhanced combat log, shop overlays, and inventory UI - Improved weapon/upgrade display with partial ownership colors - Added element icons and weakness/resistance indicators to cards - Enhanced radial menu and tooltip system - Add "stale/%" penalty text when freshness depleted - Update play link to ffazeshift.net in index.html
447 lines
17 KiB
JavaScript
447 lines
17 KiB
JavaScript
// ═══ renderer-combat.js ═══
|
|
// ============================================================
|
|
// RENDERER COMBAT — enemies, weapons, tower, particles
|
|
// ============================================================
|
|
|
|
// ── AOE ZONES ─────────────────────────────────────────────────
|
|
function drawAoeZones() {
|
|
for (const z of G.aoeZones) {
|
|
ctx.save();
|
|
ctx.globalAlpha = z.life * 0.25;
|
|
ctx.fillStyle = z.color;
|
|
ctx.beginPath();
|
|
ctx.arc(z.x, z.y, z.radius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.globalAlpha = z.life * 0.5;
|
|
ctx.strokeStyle = z.color;
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
// ── ENEMY TRAILS ──────────────────────────────────────────────
|
|
function drawEnemyTrails() {
|
|
// Batch all trails in one pass — no save/restore per trail
|
|
ctx.lineCap = 'round';
|
|
for (const e of G.enemies) {
|
|
if (!e.alive || e.trail.length < 2) continue;
|
|
ctx.strokeStyle = e.color + '22';
|
|
ctx.lineWidth = e.radius * 0.7;
|
|
ctx.beginPath();
|
|
ctx.moveTo(e.trail[0].x, e.trail[0].y);
|
|
for (let i = 1; i < e.trail.length; i++) ctx.lineTo(e.trail[i].x, e.trail[i].y);
|
|
ctx.stroke();
|
|
}
|
|
ctx.lineCap = 'butt';
|
|
}
|
|
|
|
// ── ENEMY SHAPES ──────────────────────────────────────────────
|
|
function drawEnemyShape(e, bodyColor) {
|
|
const x = e.x, y = e.y, r = e.radius;
|
|
const t = G.frame * 0.04;
|
|
|
|
ctx.fillStyle = bodyColor;
|
|
ctx.strokeStyle = bodyColor;
|
|
|
|
switch (e.defId) {
|
|
case 'grunt':
|
|
ctx.beginPath(); ctx.rect(x - r, y - r, r * 2, r * 2); ctx.fill();
|
|
ctx.strokeStyle = '#ffffff33'; ctx.lineWidth = 1; ctx.stroke();
|
|
break;
|
|
|
|
case 'runner': {
|
|
const ang = Math.atan2(canvas.height/2 - y, canvas.width/2 - x);
|
|
ctx.save(); ctx.translate(x, y); ctx.rotate(ang);
|
|
ctx.beginPath(); ctx.moveTo(r*1.6,0); ctx.lineTo(0,r*0.65); ctx.lineTo(-r*0.8,0); ctx.lineTo(0,-r*0.65); ctx.closePath(); ctx.fill();
|
|
ctx.restore(); break;
|
|
}
|
|
|
|
case 'brute':
|
|
ctx.beginPath();
|
|
for (let i = 0; i < 6; i++) { const a=(i/6)*Math.PI*2-Math.PI/6; i===0?ctx.moveTo(x+Math.cos(a)*r,y+Math.sin(a)*r):ctx.lineTo(x+Math.cos(a)*r,y+Math.sin(a)*r); }
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.strokeStyle='#ffffff33'; ctx.lineWidth=2; ctx.stroke();
|
|
break;
|
|
|
|
case 'swarm': {
|
|
const ang = Math.atan2(canvas.height/2 - y, canvas.width/2 - x);
|
|
ctx.save(); ctx.translate(x,y); ctx.rotate(ang);
|
|
ctx.beginPath(); ctx.moveTo(r*1.2,0); ctx.lineTo(-r*0.8,r*0.8); ctx.lineTo(-r*0.8,-r*0.8); ctx.closePath(); ctx.fill();
|
|
ctx.restore(); break;
|
|
}
|
|
|
|
case 'phantom':
|
|
ctx.globalAlpha *= 0.85 + Math.sin(t*3.1 + e.x*0.1)*0.15;
|
|
ctx.beginPath();
|
|
for (let i=0;i<8;i++){const a=(i/8)*Math.PI*2,rad=i%2===0?r:r*0.45;i===0?ctx.moveTo(x+Math.cos(a)*rad,y+Math.sin(a)*rad):ctx.lineTo(x+Math.cos(a)*rad,y+Math.sin(a)*rad);}
|
|
ctx.closePath(); ctx.fill(); break;
|
|
|
|
case 'iceling':
|
|
ctx.beginPath();
|
|
for (let i=0;i<8;i++){const a=(i/8)*Math.PI*2+Math.PI/8;i===0?ctx.moveTo(x+Math.cos(a)*r,y+Math.sin(a)*r):ctx.lineTo(x+Math.cos(a)*r,y+Math.sin(a)*r);}
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.strokeStyle='#ffffff55'; ctx.lineWidth=1;
|
|
ctx.beginPath(); ctx.moveTo(x-r*.5,y-r*.5); ctx.lineTo(x+r*.5,y+r*.5); ctx.stroke();
|
|
ctx.beginPath(); ctx.moveTo(x+r*.5,y-r*.5); ctx.lineTo(x-r*.5,y+r*.5); ctx.stroke();
|
|
break;
|
|
|
|
case 'sparkling':
|
|
ctx.beginPath();
|
|
for (let i=0;i<5;i++){const a=(i/5)*Math.PI*2-Math.PI/2,rad=(i%2===0?r:r*0.5)+r*0.12*Math.sin(t*5+i);i===0?ctx.moveTo(x+Math.cos(a)*rad,y+Math.sin(a)*rad):ctx.lineTo(x+Math.cos(a)*rad,y+Math.sin(a)*rad);}
|
|
ctx.closePath(); ctx.fill(); break;
|
|
|
|
case 'venom':
|
|
ctx.beginPath();
|
|
for (let i=0;i<=7;i++){const a=(i/7)*Math.PI*2,rad=r*(1+0.22*Math.sin(t*2+i*1.3+e.x*0.05));i===0?ctx.moveTo(x+Math.cos(a)*rad,y+Math.sin(a)*rad):ctx.lineTo(x+Math.cos(a)*rad,y+Math.sin(a)*rad);}
|
|
ctx.closePath(); ctx.fill(); break;
|
|
|
|
case 'titan':
|
|
ctx.beginPath();
|
|
for (let i=0;i<5;i++){const a=(i/5)*Math.PI*2-Math.PI/2;i===0?ctx.moveTo(x+Math.cos(a)*r,y+Math.sin(a)*r):ctx.lineTo(x+Math.cos(a)*r,y+Math.sin(a)*r);}
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.beginPath();
|
|
for (let i=0;i<5;i++){const a=(i/5)*Math.PI*2-Math.PI/2;i===0?ctx.moveTo(x+Math.cos(a)*r*.6,y+Math.sin(a)*r*.6):ctx.lineTo(x+Math.cos(a)*r*.6,y+Math.sin(a)*r*.6);}
|
|
ctx.closePath(); ctx.strokeStyle='#ff000099'; ctx.lineWidth=2.5; ctx.stroke(); break;
|
|
|
|
case 'wraith': {
|
|
const pulseR = r + Math.sin(t*2 + e.x*0.05)*2;
|
|
ctx.strokeStyle = bodyColor; ctx.lineWidth = 3;
|
|
ctx.beginPath(); ctx.arc(x, y, pulseR, 0, Math.PI*2); ctx.stroke();
|
|
ctx.lineWidth = 1.5;
|
|
ctx.beginPath(); ctx.arc(x, y, pulseR*0.5, 0, Math.PI*2); ctx.stroke();
|
|
break;
|
|
}
|
|
|
|
case 'commander': {
|
|
// Pentagon body with rank stripes
|
|
ctx.beginPath();
|
|
for (let i=0;i<5;i++){const a=(i/5)*Math.PI*2-Math.PI/2;i===0?ctx.moveTo(x+Math.cos(a)*r,y+Math.sin(a)*r):ctx.lineTo(x+Math.cos(a)*r,y+Math.sin(a)*r);}
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.strokeStyle='#ffffff44'; ctx.lineWidth=1.5; ctx.stroke();
|
|
// Rank stripes
|
|
ctx.strokeStyle='#000000aa'; ctx.lineWidth=1.5;
|
|
ctx.beginPath(); ctx.moveTo(x-r*0.5,y-r*0.2); ctx.lineTo(x+r*0.5,y-r*0.2); ctx.stroke();
|
|
ctx.beginPath(); ctx.moveTo(x-r*0.35,y+r*0.1); ctx.lineTo(x+r*0.35,y+r*0.1); ctx.stroke();
|
|
break;
|
|
}
|
|
|
|
case 'juggernaut': {
|
|
// Heavy octagon
|
|
ctx.beginPath();
|
|
for (let i=0;i<8;i++){const a=(i/8)*Math.PI*2;i===0?ctx.moveTo(x+Math.cos(a)*r,y+Math.sin(a)*r):ctx.lineTo(x+Math.cos(a)*r,y+Math.sin(a)*r);}
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.strokeStyle='#ff000066'; ctx.lineWidth=3; ctx.stroke();
|
|
ctx.strokeStyle='#ffffff22'; ctx.lineWidth=1;
|
|
ctx.beginPath();
|
|
for (let i=0;i<8;i++){const a=(i/8)*Math.PI*2;i===0?ctx.moveTo(x+Math.cos(a)*r*0.55,y+Math.sin(a)*r*0.55):ctx.lineTo(x+Math.cos(a)*r*0.55,y+Math.sin(a)*r*0.55);}
|
|
ctx.closePath(); ctx.stroke();
|
|
break;
|
|
}
|
|
|
|
case 'siegebreaker': {
|
|
// Cross / plus shape (large, slow tank)
|
|
const arm = r * 0.55;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x-arm,y-r); ctx.lineTo(x+arm,y-r);
|
|
ctx.lineTo(x+arm,y-arm); ctx.lineTo(x+r,y-arm);
|
|
ctx.lineTo(x+r,y+arm); ctx.lineTo(x+arm,y+arm);
|
|
ctx.lineTo(x+arm,y+r); ctx.lineTo(x-arm,y+r);
|
|
ctx.lineTo(x-arm,y+arm); ctx.lineTo(x-r,y+arm);
|
|
ctx.lineTo(x-r,y-arm); ctx.lineTo(x-arm,y-arm);
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.strokeStyle='#ffffff33'; ctx.lineWidth=2; ctx.stroke();
|
|
// Regen pulse ring when regen is active
|
|
if (!e.regenPausedUntil || G.frame >= e.regenPausedUntil) {
|
|
const regenAlpha = 0.15 + 0.15 * Math.sin(t * 3);
|
|
ctx.strokeStyle = `rgba(136,14,79,${regenAlpha})`;
|
|
ctx.lineWidth = 3;
|
|
ctx.beginPath(); ctx.arc(x, y, r * 1.4, 0, Math.PI*2); ctx.stroke();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'echo': {
|
|
// Twin overlapping rings — split visual
|
|
const off = r * 0.3;
|
|
ctx.beginPath(); ctx.arc(x-off, y, r*0.8, 0, Math.PI*2); ctx.fill();
|
|
ctx.beginPath(); ctx.arc(x+off, y, r*0.8, 0, Math.PI*2); ctx.fill();
|
|
ctx.strokeStyle='#ffffff44'; ctx.lineWidth=1;
|
|
ctx.beginPath(); ctx.arc(x-off, y, r*0.8, 0, Math.PI*2); ctx.stroke();
|
|
ctx.beginPath(); ctx.arc(x+off, y, r*0.8, 0, Math.PI*2); ctx.stroke();
|
|
break;
|
|
}
|
|
|
|
case 'voidherald': {
|
|
// Diamond with void inner ring
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, y-r); ctx.lineTo(x+r*0.8, y); ctx.lineTo(x, y+r); ctx.lineTo(x-r*0.8, y);
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.strokeStyle=bodyColor+'88'; ctx.lineWidth=1.5;
|
|
ctx.beginPath(); ctx.arc(x, y, r*0.45, 0, Math.PI*2); ctx.stroke();
|
|
break;
|
|
}
|
|
|
|
default:
|
|
ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI*2); ctx.fill();
|
|
}
|
|
}
|
|
|
|
// ── ENEMIES ───────────────────────────────────────────────────
|
|
function drawEnemies() {
|
|
const enemies = G.enemies;
|
|
|
|
// Pass 0: Commander plasma links + self-glow (drawn under all bodies)
|
|
for (const e of enemies) {
|
|
if (!e.alive || e.defId !== 'commander') continue;
|
|
const auraRadSq = 80 * 80;
|
|
const linked = enemies.filter(t => t.alive && t !== e && distSq(e.x, e.y, t.x, t.y) <= auraRadSq);
|
|
const cap = Math.min(linked.length, 8);
|
|
|
|
// Plasma links to each buffed enemy
|
|
if (cap > 0) {
|
|
ctx.save();
|
|
ctx.shadowColor = 'rgba(255,200,50,0.8)';
|
|
ctx.shadowBlur = 8;
|
|
ctx.strokeStyle = 'rgba(255,200,50,0.55)';
|
|
ctx.lineWidth = 1;
|
|
for (let i = 0; i < cap; i++) {
|
|
const t = linked[i];
|
|
const mx = (e.x + t.x) / 2;
|
|
const my = (e.y + t.y) / 2;
|
|
const dx = t.x - e.x, dy = t.y - e.y;
|
|
const len = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
const perp = Math.sin(G.frame * 0.04 + i * 1.3) * 18;
|
|
const cpx = mx - (dy / len) * perp;
|
|
const cpy = my + (dx / len) * perp;
|
|
ctx.beginPath();
|
|
ctx.moveTo(e.x, e.y);
|
|
ctx.quadraticCurveTo(cpx, cpy, t.x, t.y);
|
|
ctx.stroke();
|
|
}
|
|
ctx.shadowBlur = 0;
|
|
ctx.restore();
|
|
}
|
|
|
|
// Self-glow: pulses large and bright when alone, dims as links form
|
|
const glowR = linked.length === 0 ? 40 : Math.max(14, 28 - linked.length * 2);
|
|
const selfAlpha = linked.length === 0
|
|
? 0.35 + 0.15 * Math.sin(G.frame * 0.05)
|
|
: Math.max(0.05, 0.18 - linked.length * 0.02);
|
|
const grd = ctx.createRadialGradient(e.x, e.y, 0, e.x, e.y, glowR);
|
|
grd.addColorStop(0, `rgba(255,200,50,${selfAlpha.toFixed(2)})`);
|
|
grd.addColorStop(1, 'rgba(255,200,50,0)');
|
|
ctx.fillStyle = grd;
|
|
ctx.beginPath();
|
|
ctx.arc(e.x, e.y, glowR, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// Pass 1: elemental glows (no shadow, just gradient blobs)
|
|
for (const e of enemies) {
|
|
if (!e.alive || !e.element) continue;
|
|
const el = ELEMENTS[e.element];
|
|
if (!el) continue;
|
|
ctx.save();
|
|
const grd = getElemGrad(el, e.x, e.y, e.radius * 2.2);
|
|
ctx.fillStyle = grd;
|
|
ctx.beginPath();
|
|
ctx.arc(e.x, e.y, e.radius * 2.2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
|
|
// Pass 2: shadows ON — draw all glowing shapes together
|
|
ctx.shadowBlur = 8;
|
|
for (const e of enemies) {
|
|
if (!e.alive) continue;
|
|
ctx.save();
|
|
ctx.shadowColor = e.glowColor || e.color;
|
|
let bodyColor = e.color;
|
|
if (e.frozen > 0) bodyColor = '#7ecfff';
|
|
else if (e.hitFlash > 0) bodyColor = '#ffffff';
|
|
drawEnemyShape(e, bodyColor);
|
|
ctx.restore();
|
|
}
|
|
ctx.shadowBlur = 0; // turn off once for all
|
|
|
|
// Pass 3: overlays (armor ring, freeze tint, DoT dots) — no shadow needed
|
|
for (const e of enemies) {
|
|
if (!e.alive) continue;
|
|
|
|
if (e.armor > 0) {
|
|
ctx.beginPath();
|
|
ctx.arc(e.x, e.y, e.radius + 1.5, 0, Math.PI * 2);
|
|
ctx.strokeStyle = '#aaaaaa88';
|
|
ctx.lineWidth = Math.min(e.armor * 0.8, 3.5);
|
|
ctx.stroke();
|
|
}
|
|
|
|
if (e.frozen > 0) {
|
|
ctx.globalAlpha = 0.35;
|
|
ctx.fillStyle = '#7ecfff';
|
|
ctx.beginPath();
|
|
ctx.arc(e.x, e.y, e.radius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
if (e.slow && e.frozen <= 0) {
|
|
ctx.strokeStyle = '#7ecfffaa';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.arc(e.x, e.y, e.radius + 4, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
|
|
if (e.amplified > 0) {
|
|
ctx.strokeStyle = '#ff77e9aa';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.arc(e.x, e.y, e.radius + 7, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
|
|
if (e.dots && e.dots.length > 0) {
|
|
for (let i = 0; i < e.dots.length; i++) {
|
|
const angle = G.frame * 0.08 + i * (Math.PI * 2 / e.dots.length);
|
|
ctx.fillStyle = e.dots[i].color;
|
|
ctx.beginPath();
|
|
ctx.arc(e.x + Math.cos(angle)*(e.radius+5), e.y + Math.sin(angle)*(e.radius+5), 2, 0, Math.PI*2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// Void Herald shield bubble
|
|
if (e.defId === 'voidherald') {
|
|
const shieldActive = (e._voidHitsThisFrame || 0) >= 3;
|
|
if (shieldActive) {
|
|
ctx.save();
|
|
ctx.strokeStyle = '#c77dff';
|
|
ctx.lineWidth = 3;
|
|
ctx.shadowColor = '#c77dff';
|
|
ctx.shadowBlur = 18;
|
|
ctx.beginPath(); ctx.arc(e.x, e.y, e.radius + 5, 0, Math.PI*2); ctx.stroke();
|
|
ctx.globalAlpha = 0.15;
|
|
ctx.fillStyle = '#c77dff';
|
|
ctx.beginPath(); ctx.arc(e.x, e.y, e.radius + 5, 0, Math.PI*2); ctx.fill();
|
|
ctx.restore();
|
|
} else {
|
|
ctx.strokeStyle = 'rgba(124,77,255,0.22)';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath(); ctx.arc(e.x, e.y, e.radius + 4, 0, Math.PI*2); ctx.stroke();
|
|
}
|
|
}
|
|
|
|
// HP bar — only when damaged
|
|
if (e.hp < e.maxHp) {
|
|
const bw = e.radius * 2.4, bh = 3;
|
|
const bx = e.x - bw/2, by = e.y - e.radius - 8;
|
|
ctx.fillStyle = '#111'; ctx.fillRect(bx-1, by-1, bw+2, bh+2);
|
|
ctx.fillStyle = '#333'; ctx.fillRect(bx, by, bw, bh);
|
|
const pct = Math.max(0, e.hp / e.maxHp);
|
|
ctx.fillStyle = pct > 0.5 ? '#00ff88' : pct > 0.25 ? '#ffd700' : '#ff3355';
|
|
ctx.fillRect(bx, by, bw * pct, bh);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── PROJECTILES ───────────────────────────────────────────────
|
|
function drawProjectiles() {
|
|
// Batch all projectiles under one shadowBlur pass
|
|
ctx.shadowBlur = 10;
|
|
for (const p of G.projectiles) {
|
|
if (p.life <= 0) continue;
|
|
ctx.shadowColor = p.glow;
|
|
ctx.fillStyle = p.color;
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y, p.radius ?? 4, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
// ── BEAMS ─────────────────────────────────────────────────────
|
|
function drawChainArcs() {
|
|
if (!G.chainArcs || G.chainArcs.length === 0) return;
|
|
for (const arc of G.chainArcs) {
|
|
if (arc.life <= 0 || arc.pts.length < 2) continue;
|
|
ctx.save();
|
|
ctx.lineJoin = 'round';
|
|
ctx.lineCap = 'round';
|
|
// Outer glow
|
|
ctx.globalAlpha = arc.life * 0.7;
|
|
ctx.strokeStyle = arc.color;
|
|
ctx.lineWidth = 5;
|
|
ctx.shadowColor = arc.color;
|
|
ctx.shadowBlur = 22;
|
|
ctx.beginPath();
|
|
ctx.moveTo(arc.pts[0].x, arc.pts[0].y);
|
|
for (let i = 1; i < arc.pts.length; i++) ctx.lineTo(arc.pts[i].x, arc.pts[i].y);
|
|
ctx.stroke();
|
|
// Bright white core
|
|
ctx.globalAlpha = arc.life;
|
|
ctx.strokeStyle = '#ffffff';
|
|
ctx.lineWidth = 1.5;
|
|
ctx.shadowColor = '#ffffff';
|
|
ctx.shadowBlur = 8;
|
|
ctx.beginPath();
|
|
ctx.moveTo(arc.pts[0].x, arc.pts[0].y);
|
|
for (let i = 1; i < arc.pts.length; i++) ctx.lineTo(arc.pts[i].x, arc.pts[i].y);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
function drawBeams() {
|
|
const cx = ARENA_CX, cy = ARENA_CY;
|
|
ctx.shadowBlur = 15;
|
|
for (const b of G.beams) {
|
|
ctx.save();
|
|
ctx.globalAlpha = b.life;
|
|
ctx.shadowColor = b.glow;
|
|
// Glow stroke
|
|
ctx.strokeStyle = b.color;
|
|
ctx.lineWidth = 3 * b.life;
|
|
ctx.beginPath();
|
|
ctx.moveTo(b.x1, b.y1);
|
|
ctx.lineTo(b.x1 + Math.cos(b.angle)*b.length, b.y1 + Math.sin(b.angle)*b.length);
|
|
ctx.stroke();
|
|
// Bright core
|
|
ctx.strokeStyle = '#ffffff';
|
|
ctx.lineWidth = 1;
|
|
ctx.globalAlpha = b.life * 0.5;
|
|
ctx.beginPath();
|
|
ctx.moveTo(b.x1, b.y1);
|
|
ctx.lineTo(b.x1 + Math.cos(b.angle)*b.length, b.y1 + Math.sin(b.angle)*b.length);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
// ── PARTICLES ─────────────────────────────────────────────────
|
|
function drawParticles() {
|
|
// All particles in one pass — no save/restore
|
|
for (const p of G.particles) {
|
|
const alpha = (p.life * 255 | 0).toString(16).padStart(2, '0');
|
|
ctx.fillStyle = p.color + alpha;
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y, p.radius * p.life, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// ── FLOATERS ──────────────────────────────────────────────────
|
|
function drawFloaters() {
|
|
ctx.textAlign = 'center';
|
|
for (const f of G.floaters) {
|
|
const alpha = (f.life * 255 | 0).toString(16).padStart(2, '0');
|
|
ctx.font = `bold ${Math.round(11 * f.scale)}px "Share Tech Mono", monospace`;
|
|
ctx.fillStyle = f.color + alpha;
|
|
ctx.fillText(f.text, f.x, f.y);
|
|
}
|
|
ctx.textAlign = 'left';
|
|
}
|