| 1 | // web.ts — the TypeScript/Canvas renderer for the pet. |
| 2 | // |
| 3 | // The renderer owns no simulation: the caller steps `sim` and passes the |
| 4 | // PetState that produced the frame — the same contract as the ratatui widget, |
| 5 | // the SwiftUI view, and the Compose composable. One pass: |
| 6 | // |
| 7 | // * filled dot per particle, colour and alpha from sim.frame |
| 8 | // * hollow frames stroke the dots instead of filling them |
| 9 | // * the caller (or the DOM caption) carries the non-colour label |
| 10 | // |
| 11 | // Reduced motion: step the sim with motion:false — the same contract as every |
| 12 | // other port. |
| 13 | |
| 14 | import { PetSim, PetState, layout } from './PetSim.ts'; |
| 15 | |
| 16 | export function drawPet(ctx: CanvasRenderingContext2D, sim: PetSim, state: PetState, w: number, h: number): void { |
| 17 | const lay = layout(w, h, state); |
| 18 | const f = sim.frame; |
| 19 | ctx.globalAlpha = f.alpha; |
| 20 | const rgb = `rgb(${Math.round(f.r)},${Math.round(f.g)},${Math.round(f.b)})`; |
| 21 | ctx.lineWidth = 1; |
| 22 | const r = lay.dot / 2; |
| 23 | if (f.hollow) { |
| 24 | ctx.strokeStyle = rgb; |
| 25 | for (const q of sim.p) { |
| 26 | const x = lay.ox + q.x * lay.scale * lay.flipX; |
| 27 | const y = lay.oy + q.y * lay.scale; |
| 28 | ctx.beginPath(); |
| 29 | ctx.arc(x, y, r, 0, 6.283); |
| 30 | ctx.stroke(); |
| 31 | } |
| 32 | } else { |
| 33 | ctx.fillStyle = rgb; |
| 34 | for (const q of sim.p) { |
| 35 | const x = lay.ox + q.x * lay.scale * lay.flipX; |
| 36 | const y = lay.oy + q.y * lay.scale; |
| 37 | ctx.beginPath(); |
| 38 | ctx.arc(x, y, r, 0, 6.283); |
| 39 | ctx.fill(); |
| 40 | } |
| 41 | } |
| 42 | ctx.globalAlpha = 1; |
| 43 | } |
| 44 |