/** * Tactical Radar HUD Canvas Component * Real-time 2D top-down aerial map showing 5 drones, trail histories, and sweep beam */ class TacticalRadar { constructor(canvasId) { this.canvas = document.getElementById(canvasId); if (!this.canvas) return; this.ctx = this.canvas.getContext('2d'); this.width = this.canvas.width = 220; this.height = this.canvas.height = 220; this.cx = this.width / 2; this.cy = this.height / 2; this.range = 75.0; // World meters represented on radar radius this.sweepAngle = 0.0; this.trails = {}; this.maxTrail = 28; } render(drones) { if (!this.ctx || !drones || !drones.length) return; const ctx = this.ctx; const w = this.width; const h = this.height; const cx = this.cx; const cy = this.cy; const r = cx - 10; // Clear with dark HUD background ctx.fillStyle = "rgba(8, 14, 22, 0.45)"; ctx.fillRect(0, 0, w, h); // Circular clip for radar scope ctx.save(); ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.clip(); // Background subtle grid rings ctx.strokeStyle = "rgba(0, 240, 255, 0.15)"; ctx.lineWidth = 1; for (let i = 1; i <= 3; i++) { ctx.beginPath(); ctx.arc(cx, cy, (r / 3) * i, 0, Math.PI * 2); ctx.stroke(); } // Crosshairs ctx.beginPath(); ctx.moveTo(cx - r, cy); ctx.lineTo(cx + r, cy); ctx.moveTo(cx, cy - r); ctx.lineTo(cx, cy + r); ctx.stroke(); // 45-degree guide dashes ctx.setLineDash([2, 4]); ctx.strokeStyle = "rgba(0, 240, 255, 0.08)"; ctx.beginPath(); ctx.moveTo(cx - r * 0.7, cy - r * 0.7); ctx.lineTo(cx + r * 0.7, cy + r * 0.7); ctx.moveTo(cx - r * 0.7, cy + r * 0.7); ctx.lineTo(cx + r * 0.7, cy - r * 0.7); ctx.stroke(); ctx.setLineDash([]); // Radar Sweep Line & Beam Gradient this.sweepAngle += 0.035; if (this.sweepAngle > Math.PI * 2) this.sweepAngle -= Math.PI * 2; const sweepGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, r); sweepGrad.addColorStop(0, "rgba(0, 240, 255, 0.0)"); sweepGrad.addColorStop(1, "rgba(0, 240, 255, 0.18)"); ctx.fillStyle = sweepGrad; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, r, this.sweepAngle - 0.45, this.sweepAngle); ctx.closePath(); ctx.fill(); ctx.strokeStyle = "rgba(0, 240, 255, 0.8)"; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + Math.cos(this.sweepAngle) * r, cy + Math.sin(this.sweepAngle) * r); ctx.stroke(); // Convert world pos (X, Z) to radar canvas coords const scale = r / this.range; const toRadar = (pos) => ({ x: cx + (pos.x * scale), y: cy + (pos.z * scale) }); const leaderPos = toRadar(drones[0].position); // Draw formation link lines from leader to all wingmen ctx.strokeStyle = "rgba(0, 240, 255, 0.25)"; ctx.lineWidth = 1; ctx.setLineDash([2, 2]); for (let i = 1; i < drones.length; i++) { const fPos = toRadar(drones[i].position); ctx.beginPath(); ctx.moveTo(leaderPos.x, leaderPos.y); ctx.lineTo(fPos.x, fPos.y); ctx.stroke(); } ctx.setLineDash([]); // Update & Render each drone drones.forEach((d, idx) => { const radPos = toRadar(d.position); if (!this.trails[d.id]) this.trails[d.id] = []; this.trails[d.id].push({ ...radPos }); if (this.trails[d.id].length > this.maxTrail) this.trails[d.id].shift(); // Draw Trail this._drawTrail(this.trails[d.id], d.color); // Draw Drone Blip const yawRad = THREE.MathUtils.degToRad(d.rotation.yaw || 0); const label = idx === 0 ? "L-01" : `W-0${idx + 1}`; this._drawDroneBlip(radPos.x, radPos.y, yawRad, d.color, label, idx === 0); }); ctx.restore(); // Outer Ring Border ctx.strokeStyle = "rgba(0, 240, 255, 0.6)"; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.stroke(); // Compass Cardinal labels ctx.fillStyle = "rgba(0, 240, 255, 0.75)"; ctx.font = "9px 'Share Tech Mono', monospace"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("N", cx, cy - r + 8); ctx.fillText("S", cx, cy + r - 8); ctx.fillText("W", cx - r + 8, cy); ctx.fillText("E", cx + r - 8, cy); } _drawTrail(trail, hexColor) { if (trail.length < 2) return; const ctx = this.ctx; for (let i = 0; i < trail.length - 1; i++) { const alpha = (i / trail.length) * 0.45; ctx.strokeStyle = hexColor; ctx.globalAlpha = alpha; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(trail[i].x, trail[i].y); ctx.lineTo(trail[i + 1].x, trail[i + 1].y); ctx.stroke(); } ctx.globalAlpha = 1.0; } _drawDroneBlip(x, y, yaw, color, label, isLeader) { const ctx = this.ctx; ctx.save(); ctx.translate(x, y); // Blip glow ctx.fillStyle = color; ctx.shadowColor = color; ctx.shadowBlur = isLeader ? 10 : 6; ctx.beginPath(); ctx.arc(0, 0, isLeader ? 4.5 : 3.0, 0, Math.PI * 2); ctx.fill(); // Heading arrow ctx.rotate(yaw); ctx.strokeStyle = color; ctx.lineWidth = isLeader ? 2 : 1.5; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, isLeader ? 10 : 7); ctx.stroke(); ctx.restore(); // Text label ctx.fillStyle = "#ffffff"; ctx.font = "8px 'Share Tech Mono', monospace"; ctx.textAlign = "left"; ctx.fillText(label, x + 5, y - 3); } } window.TacticalRadar = TacticalRadar;