시나리오 내용 반영
This commit is contained in:
1 parent
d9287de729
commit
350bb92bf9
270 files changed
+198867
No files matched your search
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in new issue
Block a user