76bad46d09
Loading screen, main menu, 3D flight sim with CRT post-processing, procedural terrain, airport with buildings, low-poly aircraft, flight physics, HUD instruments, and sound. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
275 lines
6.8 KiB
JavaScript
275 lines
6.8 KiB
JavaScript
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];
|
|
}
|
|
}
|