/** * The string on its own: u(x), drawn flat. * * The 3D view shows the string in context but a millimetre of displacement * needs exaggeration there; this plot gives the displacement an honest axis. * The vertical scale is fixed to the pluck height rather than following the * field, so the decay of the note is visible as the curve settling, not as an * axis chasing it. */ export class StringPlot { #canvas: HTMLCanvasElement; #ctx: CanvasRenderingContext2D; constructor(canvas: HTMLCanvasElement) { this.#canvas = canvas; const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('string plot canvas has no 2d context'); this.#ctx = ctx; } resize(): void { const rect = this.#canvas.getBoundingClientRect(); const w = Math.max(1, Math.round(rect.width * devicePixelRatio)); const h = Math.max(1, Math.round(rect.height * devicePixelRatio)); if (this.#canvas.width !== w || this.#canvas.height !== h) { this.#canvas.width = w; this.#canvas.height = h; } } /** Draw the displacement `u` (metres) against a fixed range ±`umax`. */ draw(u: Float32Array | null, umax: number, dark: boolean): void { const ctx = this.#ctx; const { width: w, height: h } = this.#canvas; ctx.clearRect(0, 0, w, h); const ink = dark ? 'rgba(230, 233, 236, 0.9)' : 'rgba(31, 35, 40, 0.9)'; const faint = dark ? 'rgba(154, 164, 175, 0.35)' : 'rgba(87, 96, 106, 0.35)'; const pad = 6 * devicePixelRatio; // Rest line and end posts. ctx.lineWidth = devicePixelRatio; ctx.strokeStyle = faint; ctx.beginPath(); ctx.moveTo(pad, h / 2); ctx.lineTo(w - pad, h / 2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(pad, pad); ctx.lineTo(pad, h - pad); ctx.moveTo(w - pad, pad); ctx.lineTo(w - pad, h - pad); ctx.stroke(); if (!u || u.length < 2 || !(umax > 0)) return; ctx.lineWidth = 1.6 * devicePixelRatio; ctx.strokeStyle = ink; ctx.beginPath(); for (let i = 0; i < u.length; i++) { const x = pad + ((w - 2 * pad) * i) / (u.length - 1); const y = h / 2 - (h / 2 - pad) * Math.max(-1, Math.min(1, u[i] / umax)); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } }