feat(frontend): pure canvas draw functions (ship + flame + target + layout)

This commit is contained in:
meelstorm
2026-07-17 17:56:44 +00:00
committed by EugeneTes
parent ab5b22b857
commit 8018a0e678

116
Frontend/src/render.ts Normal file
View File

@@ -0,0 +1,116 @@
import type { StateFrame } from './protocol';
/** Ship as a filled triangle, drawn at (x,y) rotated by angle. Size in canvas px. */
export function drawShip(
ctx: CanvasRenderingContext2D,
worldX: number,
worldY: number,
angle: number,
engine: number,
scale: number,
offsetX: number,
offsetY: number,
) {
const px = offsetX + worldX * scale;
const py = offsetY + worldY * scale;
const size = scale * 0.04;
ctx.save();
ctx.translate(px, py);
ctx.rotate(angle);
// Body: triangle pointing up (angle=0 → up).
ctx.fillStyle = '#eee';
ctx.beginPath();
ctx.moveTo(0, -size);
ctx.lineTo(size * 0.7, size * 0.6);
ctx.lineTo(-size * 0.7, size * 0.6);
ctx.closePath();
ctx.fill();
// Flame if an engine is firing.
if (engine !== 0) {
ctx.fillStyle = '#ff9d3d';
ctx.beginPath();
if (engine === 2) {
// Main engine: flame under the ship.
const flame = size * 0.9 + Math.random() * size * 0.4;
ctx.moveTo(-size * 0.4, size * 0.6);
ctx.lineTo(size * 0.4, size * 0.6);
ctx.lineTo(0, size * 0.6 + flame);
} else if (engine === 1) {
// Left thruster: flame on right side of body.
ctx.moveTo(size * 0.7, -size * 0.2);
ctx.lineTo(size * 0.7, size * 0.2);
ctx.lineTo(size * 1.3, 0);
} else if (engine === 3) {
// Right thruster: flame on left side of body.
ctx.moveTo(-size * 0.7, -size * 0.2);
ctx.lineTo(-size * 0.7, size * 0.2);
ctx.lineTo(-size * 1.3, 0);
}
ctx.closePath();
ctx.fill();
}
ctx.restore();
}
/** Draw a crosshair at the target world position. */
export function drawTarget(
ctx: CanvasRenderingContext2D,
worldX: number,
worldY: number,
scale: number,
offsetX: number,
offsetY: number,
) {
const px = offsetX + worldX * scale;
const py = offsetY + worldY * scale;
const r = 8;
ctx.strokeStyle = '#5cf';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(px, py, r, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(px - r * 1.5, py);
ctx.lineTo(px + r * 1.5, py);
ctx.moveTo(px, py - r * 1.5);
ctx.lineTo(px, py + r * 1.5);
ctx.stroke();
}
/** Layout: fit the 1×1 world into the canvas with black letterbox bars. */
export function layout(canvas: HTMLCanvasElement) {
const scale = Math.min(canvas.width, canvas.height);
const offsetX = (canvas.width - scale) / 2;
const offsetY = (canvas.height - scale) / 2;
return { scale, offsetX, offsetY };
}
/** Render one frame: clear + world background + target + ship. */
export function renderFrame(
canvas: HTMLCanvasElement,
state: StateFrame | null,
) {
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Clear
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const { scale, offsetX, offsetY } = layout(canvas);
// World background (subtle dark band so the play area is visible).
ctx.fillStyle = '#0a0a12';
ctx.fillRect(offsetX, offsetY, scale, scale);
if (!state) return;
drawTarget(ctx, state.target[0], state.target[1], scale, offsetX, offsetY);
drawShip(ctx, state.x, state.y, state.angle, state.engine, scale, offsetX, offsetY);
}