feat(frontend): pure canvas draw functions (ship + flame + target + layout)
This commit is contained in:
116
Frontend/src/render.ts
Normal file
116
Frontend/src/render.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user