From 8018a0e678c56190f18157c3a4007c769bf6a6fa Mon Sep 17 00:00:00 2001 From: meelstorm Date: Fri, 17 Jul 2026 17:56:44 +0000 Subject: [PATCH] feat(frontend): pure canvas draw functions (ship + flame + target + layout) --- Frontend/src/render.ts | 116 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 Frontend/src/render.ts diff --git a/Frontend/src/render.ts b/Frontend/src/render.ts new file mode 100644 index 0000000..b382d1f --- /dev/null +++ b/Frontend/src/render.ts @@ -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); +}