1035139A plucked dulcimer string and its box, as two coupled wave equations on WebGPUJeremy Magland 1/**
2 * The string on its own: u(x), drawn flat.
3 *
4 * The 3D view shows the string in context but a millimetre of displacement
5 * needs exaggeration there; this plot gives the displacement an honest axis.
6 * The vertical scale is fixed to the pluck height rather than following the
7 * field, so the decay of the note is visible as the curve settling, not as an
8 * axis chasing it.
9 */
10export class StringPlot {
11 #canvas: HTMLCanvasElement;
12 #ctx: CanvasRenderingContext2D;
14 constructor(canvas: HTMLCanvasElement) {
15 this.#canvas = canvas;
16 const ctx = canvas.getContext('2d');
17 if (!ctx) throw new Error('string plot canvas has no 2d context');
18 this.#ctx = ctx;
19 }
21 resize(): void {
22 const rect = this.#canvas.getBoundingClientRect();
23 const w = Math.max(1, Math.round(rect.width * devicePixelRatio));
24 const h = Math.max(1, Math.round(rect.height * devicePixelRatio));
25 if (this.#canvas.width !== w || this.#canvas.height !== h) {
26 this.#canvas.width = w;
27 this.#canvas.height = h;
28 }
29 }
31 /** Draw the displacement `u` (metres) against a fixed range ±`umax`. */
32 draw(u: Float32Array | null, umax: number, dark: boolean): void {
33 const ctx = this.#ctx;
34 const { width: w, height: h } = this.#canvas;
35 ctx.clearRect(0, 0, w, h);
37 const ink = dark ? 'rgba(230, 233, 236, 0.9)' : 'rgba(31, 35, 40, 0.9)';
38 const faint = dark ? 'rgba(154, 164, 175, 0.35)' : 'rgba(87, 96, 106, 0.35)';
39 const pad = 6 * devicePixelRatio;
41 // Rest line and end posts.
42 ctx.lineWidth = devicePixelRatio;
43 ctx.strokeStyle = faint;
44 ctx.beginPath();
45 ctx.moveTo(pad, h / 2);
46 ctx.lineTo(w - pad, h / 2);
47 ctx.stroke();
48 ctx.beginPath();
49 ctx.moveTo(pad, pad);
50 ctx.lineTo(pad, h - pad);
51 ctx.moveTo(w - pad, pad);
52 ctx.lineTo(w - pad, h - pad);
53 ctx.stroke();
55 if (!u || u.length < 2 || !(umax > 0)) return;
56 ctx.lineWidth = 1.6 * devicePixelRatio;
57 ctx.strokeStyle = ink;
58 ctx.beginPath();
59 for (let i = 0; i < u.length; i++) {
60 const x = pad + ((w - 2 * pad) * i) / (u.length - 1);
61 const y = h / 2 - (h / 2 - pad) * Math.max(-1, Math.min(1, u[i] / umax));
62 if (i === 0) ctx.moveTo(x, y);
63 else ctx.lineTo(x, y);
64 }
65 ctx.stroke();
66 }
67}