export class HUD { constructor() { this.canvas = document.getElementById('hud-canvas'); this.ctx = this.canvas.getContext('2d'); this.visible = false; } show() { this.visible = true; document.getElementById('hud-overlay').style.display = 'block'; } hide() { this.visible = false; document.getElementById('hud-overlay').style.display = 'none'; } clear() { this.ctx.clearRect(0, 0, 320, 200); } draw(flightModel) { if (!this.visible) return; this.clear(); const ctx = this.ctx; const speed = flightModel.getAirspeedKnots(); const altitude = flightModel.getAltitudeFeet(); const heading = flightModel.getHeadingDeg(); const vsi = flightModel.getVerticalSpeedFtMin(); const pitch = flightModel.getPitchDeg(); const roll = flightModel.getRollDeg(); const throttle = flightModel.thrust; // Artificial horizon (center-bottom) this.drawArtificialHorizon(ctx, pitch, roll); // Heading tape (top center) this.drawHeadingTape(ctx, heading); // Airspeed indicator (left side) this.drawAirspeed(ctx, speed); // Altimeter (right side) this.drawAltimeter(ctx, altitude); // Vertical speed indicator (right side, below altimeter) this.drawVSI(ctx, vsi); // Throttle indicator (bottom left) this.drawThrottle(ctx, throttle); // Heading number const cardinal = this.headingToCardinal(heading); ctx.fillStyle = '#00FF00'; ctx.font = '8px "Press Start 2P", monospace'; ctx.textAlign = 'center'; ctx.fillText(`${Math.round(heading)}° ${cardinal}`, 160, 12); } drawArtificialHorizon(ctx, pitch, roll) { const w = 100; const h = 60; const x = 110; const y = 140; ctx.save(); ctx.beginPath(); ctx.rect(x, y, w, h); ctx.clip(); const cx = x + w / 2; const cy = y + h / 2; ctx.translate(cx, cy); ctx.rotate(-roll * Math.PI / 180); const pitchOffset = pitch * 1.5; // Sky ctx.fillStyle = '#0000AA'; ctx.fillRect(-60, -40 + pitchOffset, 120, 40); // Ground ctx.fillStyle = '#8B4513'; ctx.fillRect(-60, pitchOffset, 120, 40); // Horizon line ctx.strokeStyle = '#FFFFFF'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(-60, pitchOffset); ctx.lineTo(60, pitchOffset); ctx.stroke(); // Pitch lines ctx.strokeStyle = '#FFFFFF88'; ctx.lineWidth = 1; for (let p = -20; p <= 20; p += 10) { if (p === 0) continue; const py = p * 1.5; ctx.beginPath(); ctx.moveTo(-10, py); ctx.lineTo(10, py); ctx.stroke(); } ctx.restore(); // Aircraft reference symbol ctx.strokeStyle = '#FF8800'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(cx - 18, cy); ctx.lineTo(cx - 6, cy); ctx.stroke(); ctx.beginPath(); ctx.moveTo(cx + 6, cy); ctx.lineTo(cx + 18, cy); ctx.stroke(); ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx, cy + 3); ctx.stroke(); // Border ctx.strokeStyle = '#00FF00'; ctx.lineWidth = 1; ctx.strokeRect(x, y, w, h); } drawHeadingTape(ctx, heading) { const y = 22; const ctx2 = ctx; ctx2.fillStyle = 'rgba(0, 0, 0, 0.5)'; ctx2.fillRect(80, 14, 160, 18); ctx2.strokeStyle = '#00FF00'; ctx2.lineWidth = 1; ctx2.strokeRect(80, 14, 160, 18); ctx2.font = '6px "Press Start 2P", monospace'; ctx2.textAlign = 'center'; ctx2.fillStyle = '#00FF00'; const cards = [ { deg: 0, label: 'N' }, { deg: 90, label: 'E' }, { deg: 180, label: 'S' }, { deg: 270, label: 'W' }, ]; cards.forEach(({ deg, label }) => { const diff = heading - deg; const px = (diff / 30) * 8; const lx = 160 + px; if (lx > 85 && lx < 235) { ctx2.fillText(label, lx, y); } }); } drawAirspeed(ctx, speed) { const x = 8; const y = 40; const w = 30; const h = 100; ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; ctx.fillRect(x, y, w, h); ctx.strokeStyle = '#00FF00'; ctx.lineWidth = 1; ctx.strokeRect(x, y, w, h); // Speed bar const fill = Math.min(1, speed / 200); const barH = fill * (h - 10); ctx.fillStyle = fill > 0.85 ? '#FF5555' : fill > 0.6 ? '#FFFF55' : '#00FF00'; ctx.fillRect(x + 2, y + h - 4 - barH, w - 4, barH); // Speed ticks ctx.fillStyle = '#00FF00'; ctx.font = '6px "Press Start 2P", monospace'; ctx.textAlign = 'left'; for (let s = 0; s <= 200; s += 50) { const tickY = y + h - 4 - (s / 200) * (h - 10); ctx.fillRect(x, tickY - 0.5, 4, 1); } // Current speed ctx.textAlign = 'center'; ctx.font = '8px "Press Start 2P", monospace'; ctx.fillText(Math.round(speed), x + w / 2, y - 4); ctx.font = '5px "Press Start 2P", monospace'; ctx.fillText('KT', x + w / 2, y + h + 10); } drawAltimeter(ctx, altitude) { const x = 282; const y = 40; const w = 30; const h = 100; ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; ctx.fillRect(x, y, w, h); ctx.strokeStyle = '#00FF00'; ctx.lineWidth = 1; ctx.strokeRect(x, y, w, h); // Altitude bar const fill = Math.min(1, altitude / 15000); const barH = fill * (h - 10); ctx.fillStyle = '#00FF00'; ctx.fillRect(x + 2, y + h - 4 - barH, w - 4, barH); // Current altitude ctx.fillStyle = '#00FF00'; ctx.font = '8px "Press Start 2P", monospace'; ctx.textAlign = 'center'; ctx.fillText(Math.round(altitude), x + w / 2, y - 4); ctx.font = '5px "Press Start 2P", monospace'; ctx.fillText('FT', x + w / 2, y + h + 10); } drawVSI(ctx, vsi) { const x = 282; const y = 155; const w = 30; const h = 35; ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; ctx.fillRect(x, y, w, h); ctx.strokeStyle = '#00FF00'; ctx.lineWidth = 1; ctx.strokeRect(x, y, w, h); ctx.fillStyle = '#00FF00'; ctx.font = '6px "Press Start 2P", monospace'; ctx.textAlign = 'center'; const vsiText = vsi >= 0 ? `+${Math.round(vsi)}` : Math.round(vsi); ctx.fillText(vsiText, x + w / 2, y + 14); ctx.font = '5px "Press Start 2P", monospace'; ctx.fillText('FT/M', x + w / 2, y + 26); } drawThrottle(ctx, throttle) { const x = 8; const y = 160; const w = 40; const h = 8; ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; ctx.fillRect(x, y, w, h); ctx.strokeStyle = '#00FF00'; ctx.lineWidth = 1; ctx.strokeRect(x, y, w, h); const fill = throttle * (w - 2); ctx.fillStyle = throttle > 0.8 ? '#FF5555' : '#00AAAA'; ctx.fillRect(x + 1, y + 1, fill, h - 2); ctx.fillStyle = '#00FF00'; ctx.font = '5px "Press Start 2P", monospace'; ctx.textAlign = 'left'; ctx.fillText('THR', x, y - 3); } headingToCardinal(deg) { const dirs = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']; const idx = Math.round(deg / 22.5) % 16; return dirs[idx]; } }