/ simulated_instruments / triangle
Sign in
simulated_instruments / triangle
triangle / webapp / src / components / plots / ModeShapes.tsx
122 lines · 4.0 KBBlameHistoryRaw
1// Small multiples of the lowest mode shapes on the triangle outline.
2// In-plane modes are drawn as the outline displaced within the plane;
3// out-of-plane modes keep the outline in place and color it by the normal
4// displacement (blue toward the viewer, red away), since that motion is
5// perpendicular to the drawing. The gap at the bottom-left corner is where
6// the free ends whip.
8import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils';
10interface Props {
11 nodeX: Float32Array;
12 nodeY: Float32Array;
13 shapeUx: Float32Array; // nn x n, column-major
14 shapeUy: Float32Array;
15 shapeUz: Float32Array;
16 modeF: Float32Array;
17 modeFamily: Float32Array;
20const SHOWN = 12;
21const CELL_H = 110;
23export default function ModeShapes({
24 nodeX,
25 nodeY,
26 shapeUx,
27 shapeUy,
28 shapeUz,
29 modeF,
30 modeFamily,
31}: Props) {
32 const nShown = Math.min(SHOWN, modeF.length);
33 const rows = Math.ceil(nShown / 4);
34 const ref = useCanvas(
35 (ctx, w, h) => {
36 drawFrame(ctx, w, h);
37 const nn = nodeX.length;
38 if (nn === 0 || nShown === 0) return;
39 const cols = 4;
40 const cw = w / cols;
41 const ch = h / rows;
43 // model bounding box, common to all cells
44 let xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity;
45 for (let i = 0; i < nn; i++) {
46 xMin = Math.min(xMin, nodeX[i]);
47 xMax = Math.max(xMax, nodeX[i]);
48 yMin = Math.min(yMin, nodeY[i]);
49 yMax = Math.max(yMax, nodeY[i]);
50 }
51 const span = Math.max(xMax - xMin, yMax - yMin);
52 const scale = (Math.min(cw, ch) - 34) / span;
53 const defl = span * 0.1; // displacement drawn as 10% of the frame size
55 for (let m = 0; m < nShown; m++) {
56 const cx = (m % cols) * cw + cw / 2;
57 const cy = Math.floor(m / cols) * ch + ch / 2 + 4;
58 const px = (x: number) => cx + (x - (xMin + xMax) / 2) * scale;
59 const py = (y: number) => cy - (y - (yMin + yMax) / 2) * scale;
60 const off = m * nn;
62 let uMax = 1e-12;
63 for (let i = 0; i < nn; i++) {
64 uMax = Math.max(
65 uMax,
66 Math.hypot(shapeUx[off + i], shapeUy[off + i], shapeUz[off + i]),
67 );
68 }
70 // undeformed outline, faint
71 ctx.strokeStyle = 'rgba(139, 147, 161, 0.25)';
72 ctx.lineWidth = 1;
73 ctx.beginPath();
74 for (let i = 0; i < nn; i++) {
75 const x = px(nodeX[i]);
76 const y = py(nodeY[i]);
77 if (i === 0) ctx.moveTo(x, y);
78 else ctx.lineTo(x, y);
79 }
80 ctx.stroke();
82 if (modeFamily[m] === 0) {
83 // in-plane: displaced outline
84 ctx.strokeStyle = PLOT_COLORS.trace;
85 ctx.lineWidth = 1.6;
86 ctx.beginPath();
87 for (let i = 0; i < nn; i++) {
88 const x = px(nodeX[i] + (shapeUx[off + i] / uMax) * defl);
89 const y = py(nodeY[i] + (shapeUy[off + i] / uMax) * defl);
90 if (i === 0) ctx.moveTo(x, y);
91 else ctx.lineTo(x, y);
92 }
93 ctx.stroke();
94 } else {
95 // out-of-plane: color the outline by normal displacement
96 ctx.lineWidth = 2.2;
97 for (let i = 0; i < nn - 1; i++) {
98 const t = (shapeUz[off + i] + shapeUz[off + i + 1]) / (2 * uMax);
99 const mag = Math.min(1, Math.abs(t));
100 ctx.strokeStyle =
101 t >= 0
102 ? `rgba(100, 181, 246, ${0.25 + 0.75 * mag})`
103 : `rgba(229, 115, 115, ${0.25 + 0.75 * mag})`;
104 ctx.beginPath();
105 ctx.moveTo(px(nodeX[i]), py(nodeY[i]));
106 ctx.lineTo(px(nodeX[i + 1]), py(nodeY[i + 1]));
107 ctx.stroke();
108 }
109 }
111 ctx.font = '10px system-ui';
112 ctx.textAlign = 'center';
113 ctx.fillStyle = modeFamily[m] === 0 ? PLOT_COLORS.trace : PLOT_COLORS.accent;
114 const fam = modeFamily[m] === 0 ? 'in' : 'out';
115 ctx.fillText(`${modeF[m].toFixed(0)} Hz · ${fam}`, cx, cy + (ch / 2) - 8);
116 }
117 },
118 [nodeX, nodeY, shapeUx, shapeUy, shapeUz, modeF, modeFamily, nShown, rows],
119 rows * CELL_H,
120 );
121 return <canvas ref={ref} />;
↑↓ move↵openescclose