fuzzyworld

bubbles / files / src / rendering / Renderer.ts

Renderer.ts

4.4 KB · history · blame

1import { GlassRenderer } from './GlassRenderer.js';
2import { WaterRenderer } from './WaterRenderer.js';
3import { IconRenderer } from './IconRenderer.js';
4import { World } from '../physics/World.js';
5
6/**
7 * Top-level rendering coordinator.
8 * Manages canvas sizing and the draw order:
9 * 1. Clear
10 * 2. Glass clip → Water fill
11 * 3. Icons (with refraction for submerged)
12 * 4. Glass outline (drawn on top to mask overflow)
13 */
14export class Renderer {
15 readonly canvas: HTMLCanvasElement;
16 readonly ctx: CanvasRenderingContext2D;
17
18 private glass: GlassRenderer;
19 private waterRend: WaterRenderer;
20 private iconRend: IconRenderer;
21
22 constructor(canvas: HTMLCanvasElement, private world: World) {
23 this.canvas = canvas;
24 const ctx = canvas.getContext('2d');
25 if (!ctx) throw new Error('Could not get 2D context');
26 this.ctx = ctx;
27
28 this.glass = this.makeGlassRenderer();
29 this.waterRend = new WaterRenderer(this.ctx, world.water, world.glassX, world.glassY);
30 this.iconRend = new IconRenderer(this.ctx, world.water, world.glassX, world.glassY);
31
32 this.resize();
33 window.addEventListener('resize', this.resize);
34 }
35
36 private readonly resize = (): void => {
37 const dpr = window.devicePixelRatio || 1;
38 const W = window.innerWidth;
39 const H = window.innerHeight;
40
41 this.canvas.width = W * dpr;
42 this.canvas.height = H * dpr;
43 this.canvas.style.width = `${W}px`;
44 this.canvas.style.height = `${H}px`;
45 this.ctx.scale(dpr, dpr);
46
47 // Recompute glass geometry
48 const { gx, gy, gw, gh } = this.computeGlassDims(W, H);
49
50 this.world.glassX = gx;
51 this.world.glassY = gy;
52 this.world.water.resize(gw, gh);
53
54 this.glass = this.makeGlassRenderer();
55 this.waterRend.glassX = gx;
56 this.waterRend.glassY = gy;
57 this.iconRend.glassX = gx;
58 this.iconRend.glassY = gy;
59 };
60
61 private computeGlassDims(W: number, H: number): { gx: number; gy: number; gw: number; gh: number } {
62 // Glass occupies most of the viewport, centred, portrait-friendly
63 const glassW = Math.min(W * 0.82, 420);
64 const glassH = H * 0.72;
65 const gx = (W - glassW) / 2;
66 const gy = (H - glassH) / 2 - H * 0.02;
67 return { gx, gy, gw: glassW, gh: glassH };
68 }
69
70 private makeGlassRenderer(): GlassRenderer {
71 const W = window.innerWidth;
72 const H = window.innerHeight;
73 const { gx, gy, gw, gh } = this.computeGlassDims(W, H);
74 return new GlassRenderer(this.ctx, gx, gy, gw, gh);
75 }
76
77 update(dt: number): void {
78 this.waterRend.update(dt);
79 }
80
81 /** Register a pre-rendered QR canvas to be drawn on top of water. */
82 setQRTexture(tex: HTMLCanvasElement): void {
83 this._qrTexture = tex;
84 }
85 private _qrTexture: HTMLCanvasElement | undefined;
86
87 draw(): void {
88 const ctx = this.ctx;
89 const W = this.canvas.width / (window.devicePixelRatio || 1);
90 const H = this.canvas.height / (window.devicePixelRatio || 1);
91
92 // Clear
93 ctx.clearRect(0, 0, W, H);
94
95 // Background
96 ctx.fillStyle = '#0f172a';
97 ctx.fillRect(0, 0, W, H);
98
99 // Clip to glass interior
100 this.glass.beginClip();
101
102 // Icons first (behind water)
103 this.iconRend.draw(this.world.bodies, (x) => this.waterRend.getRefractionDx(x));
104
105 // Water on top of icons
106 this.waterRend.draw();
107
108 // Textured bodies (QR card) drawn on top of water, inside glass clip
109 for (const body of this.world.bodies) {
110 if (!body.texture) continue;
111 const ctx = this.ctx;
112 const s = body.radius * 2;
113 ctx.save();
114 ctx.translate(body.position.x, body.position.y);
115 ctx.rotate(body.angle);
116 // White card background
117 ctx.fillStyle = '#ffffff';
118 const r = 4;
119 ctx.beginPath();
120 ctx.roundRect(-s / 2, -s / 2, s, s, r);
121 ctx.fill();
122 // QR image centred with small padding
123 const pad = s * 0.06;
124 ctx.drawImage(body.texture, -s / 2 + pad, -s / 2 + pad, s - pad * 2, s - pad * 2);
125 ctx.restore();
126 }
127
128 this.glass.endClip();
129
130 // Glass outline on top
131 this.glass.draw();
132 }
133
134 destroy(): void {
135 window.removeEventListener('resize', this.resize);
136 }
137}