2004981Plate dynamics lab 2: a hammer on a hung plate by Rayleigh-Ritz and by Mindlin-Reissner finite elementsJeremy Magland 1import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
2import {
3 colormapTable,
4 getColormap,
5 holePosition,
6 type FieldFrames,
7 type PlateResult,
8 type ProblemSpec,
9 type Range,
10} from "@plate-dyn/core";
12export interface Plate3DViewProps {
13 result: PlateResult;
14 spec: ProblemSpec;
15 frameIndex: number;
16 /** Frames that displace the surface along the normal (the flexural field, or a mode shape) */
17 frames?: FieldFrames;
18 /** Frames that colour the surface (any per-node field) and their colour range */
19 colorFrames?: FieldFrames;
20 colorRange?: Range;
21 colormap?: string;
22 /** Exaggeration of the displacement */
23 scale: number;
24 width?: number;
25 height?: number;
26}
28type Vec3 = [number, number, number];
30const VS = `
31attribute vec3 aPos;
32attribute vec3 aNormal;
33attribute vec3 aColor;
34uniform mat4 uProj;
35uniform mat4 uView;
36varying vec3 vColor;
37varying vec3 vNormal;
38void main() {
39 gl_Position = uProj * uView * vec4(aPos, 1.0);
40 vColor = aColor;
41 vNormal = (uView * vec4(aNormal, 0.0)).xyz;
42}
43`;
44const FS = `
45precision mediump float;
46varying vec3 vColor;
47varying vec3 vNormal;
48uniform float uFlat;
49void main() {
50 vec3 n = normalize(vNormal);
51 float diff = abs(dot(n, normalize(vec3(0.3, 0.6, 1.0))));
52 float shade = mix(0.55 + 0.45 * diff, 1.0, uFlat);
53 gl_FragColor = vec4(vColor * shade, 1.0);
54}
55`;
57function perspective(fovy: number, aspect: number, near: number, far: number): Float32Array {
58 const f = 1 / Math.tan(fovy / 2);
59 const out = new Float32Array(16);
60 out[0] = f / aspect;
61 out[5] = f;
62 out[10] = (far + near) / (near - far);
63 out[11] = -1;
64 out[14] = (2 * far * near) / (near - far);
65 return out;
66}
68function lookAt(eye: Vec3, center: Vec3, up: Vec3): Float32Array {
69 const z = norm(sub(eye, center));
70 const x = norm(cross(up, z));
71 const y = cross(z, x);
72 const out = new Float32Array(16);
73 out[0] = x[0];
74 out[4] = x[1];
75 out[8] = x[2];
76 out[1] = y[0];
77 out[5] = y[1];
78 out[9] = y[2];
79 out[2] = z[0];
80 out[6] = z[1];
81 out[10] = z[2];
82 out[12] = -dot(x, eye);
83 out[13] = -dot(y, eye);
84 out[14] = -dot(z, eye);
85 out[15] = 1;
86 return out;
87}
88const sub = (a: Vec3, b: Vec3): Vec3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
89const add = (a: Vec3, b: Vec3): Vec3 => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
90const dot = (a: Vec3, b: Vec3) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
91const cross = (a: Vec3, b: Vec3): Vec3 => [
92 a[1] * b[2] - a[2] * b[1],
93 a[2] * b[0] - a[0] * b[2],
94 a[0] * b[1] - a[1] * b[0],
95];
96const norm = (a: Vec3): Vec3 => {
97 const l = Math.hypot(a[0], a[1], a[2]) || 1;
98 return [a[0] / l, a[1] / l, a[2] / l];
99};
101/** What the movie recorder needs of the scene: its canvas, and a frame drawn on demand. */
102export interface Plate3DHandle {
103 canvas: () => HTMLCanvasElement | null;
104 /** Draw frame k; `size` overrides the backing store, for recording at a larger size */
105 drawFrame: (k: number, size?: { width: number; height: number }) => void;
106}
108/**
109 * The plate as it hangs: the thread rising from the hole near the top edge,
110 * the hammer's tip at the strike point on the near face, the surface
111 * displaced along the normal by the chosen frames (exaggerated by `scale`)
112 * and coloured by the chosen quantity. The plate's x runs across the screen
113 * and its y up it. Drag to orbit, wheel to zoom.
114 */
115export const Plate3DView = forwardRef<Plate3DHandle, Plate3DViewProps>(
116 function Plate3DView(props, ref) {
117 const {
118 result,
119 spec,
120 frameIndex,
121 frames,
122 colorFrames,
123 colorRange,
124 colormap = "bwr",
125 scale,
126 width = 460,
127 height = 340,
128 } = props;
129 const canvasRef = useRef<HTMLCanvasElement>(null);
130 const glRef = useRef<{
131 gl: WebGLRenderingContext;
132 prog: WebGLProgram;
133 buffers: { pos: WebGLBuffer; nrm: WebGLBuffer; col: WebGLBuffer; idx: WebGLBuffer };
134 lines: { pos: WebGLBuffer; col: WebGLBuffer; nrm: WebGLBuffer };
135 loc: Record<string, number | WebGLUniformLocation | null>;
136 } | null>(null);
137 const [cam, setCam] = useState({ az: 0.5, el: 0.3, zoom: 1 });
138 const drag = useRef<{ x: number; y: number } | null>(null);
140 const nx = result.grid.x.length;
141 const ny = result.grid.y.length;
142 const W = spec.plate.width;
143 const H = spec.plate.height;
144 const size = Math.max(W, H);
146 const mesh = useMemo(() => {
147 const idx: number[] = [];
148 for (let j = 0; j < nx - 1; j++)
149 for (let i = 0; i < ny - 1; i++) {
150 const a = i + j * ny;
151 const b = i + 1 + j * ny;
152 const c = i + (j + 1) * ny;
153 const d = i + 1 + (j + 1) * ny;
154 idx.push(a, b, c, b, d, c);
155 }
156 return { index: new Uint16Array(idx) };
157 }, [nx, ny]);
159 useEffect(() => {
160 const canvas = canvasRef.current;
161 if (!canvas) return;
162 const gl = canvas.getContext("webgl", { antialias: true });
163 if (!gl) return;
164 const compile = (type: number, src: string) => {
165 const sh = gl.createShader(type)!;
166 gl.shaderSource(sh, src);
167 gl.compileShader(sh);
168 return sh;
169 };
170 const prog = gl.createProgram()!;
171 gl.attachShader(prog, compile(gl.VERTEX_SHADER, VS));
172 gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FS));
173 gl.linkProgram(prog);
174 const mk = () => gl.createBuffer()!;
175 glRef.current = {
176 gl,
177 prog,
178 buffers: { pos: mk(), nrm: mk(), col: mk(), idx: mk() },
179 lines: { pos: mk(), col: mk(), nrm: mk() },
180 loc: {
181 aPos: gl.getAttribLocation(prog, "aPos"),
182 aNormal: gl.getAttribLocation(prog, "aNormal"),
183 aColor: gl.getAttribLocation(prog, "aColor"),
184 uProj: gl.getUniformLocation(prog, "uProj"),
185 uView: gl.getUniformLocation(prog, "uView"),
186 uFlat: gl.getUniformLocation(prog, "uFlat"),
187 },
188 };
189 return () => {
190 glRef.current = null;
191 };
192 }, []);
194 const draw = (kIndex: number, out?: { width: number; height: number }) => {
195 const r = glRef.current;
196 const canvas = canvasRef.current;
197 if (!r || !canvas) return;
198 const { gl, prog, buffers, lines, loc } = r;
199 const vw = out?.width ?? width;
200 const vh = out?.height ?? height;
201 const dpr = out ? 1 : Math.min(2, window.devicePixelRatio || 1);
202 canvas.width = Math.round(vw * dpr);
203 canvas.height = Math.round(vh * dpr);
204 gl.viewport(0, 0, canvas.width, canvas.height);
206 // ---- node positions: plate x across, y up, the normal towards the viewer
207 const N = nx * ny;
208 const kf = frames ? Math.min(kIndex, frames.nf - 1) : -1;
209 const pos = new Float32Array(3 * N);
210 const labPos: Vec3[] = new Array(N);
211 for (let j = 0; j < nx; j++)
212 for (let i = 0; i < ny; i++) {
213 const n = i + j * ny;
214 const f = frames && kf >= 0 ? frames.data[n + kf * N] : 0;
215 const lp: Vec3 = [
216 result.grid.x[j],
217 result.grid.y[i],
218 scale * (Number.isFinite(f) ? f : 0),
219 ];
220 labPos[n] = lp;
221 pos[3 * n] = lp[0];
222 pos[3 * n + 1] = lp[1];
223 pos[3 * n + 2] = lp[2];
224 }
225 const nrm = new Float32Array(3 * N);
226 for (let j = 0; j < nx; j++)
227 for (let i = 0; i < ny; i++) {
228 const n = i + j * ny;
229 const jl = Math.max(0, j - 1);
230 const jr = Math.min(nx - 1, j + 1);
231 const il = Math.max(0, i - 1);
232 const ir = Math.min(ny - 1, i + 1);
233 const tx = sub(labPos[i + jr * ny], labPos[i + jl * ny]);
234 const ty = sub(labPos[ir + j * ny], labPos[il + j * ny]);
235 const nn = norm(cross(tx, ty));
236 nrm[3 * n] = nn[0];
237 nrm[3 * n + 1] = nn[1];
238 nrm[3 * n + 2] = nn[2];
239 }
240 const col = new Float32Array(3 * N);
241 const table = colormapTable(getColormap(colormap));
242 const range = colorRange ?? { min: -1, max: 1 };
243 const span = range.max - range.min || 1;
244 const kc = colorFrames ? Math.min(kIndex, colorFrames.nf - 1) : -1;
245 for (let n = 0; n < N; n++) {
246 let v = colorFrames && kc >= 0 ? colorFrames.data[n + kc * N] : NaN;
247 if (!Number.isFinite(v)) v = 0.5 * (range.min + range.max);
248 let t = (v - range.min) / span;
249 t = t < 0 ? 0 : t > 1 ? 1 : t;
250 const k = Math.round(t * 255) * 3;
251 col[3 * n] = table[k] / 255;
252 col[3 * n + 1] = table[k + 1] / 255;
253 col[3 * n + 2] = table[k + 2] / 255;
254 }
256 // ---- the thread, the hammer, the outline
257 const L: number[] = [];
258 const LC: number[] = [];
259 const line = (a: Vec3, b: Vec3, rgb: Vec3) => {
260 L.push(...a, ...b);
261 LC.push(...rgb, ...rgb);
262 };
263 const dark: Vec3 = [0.15, 0.15, 0.18];
264 const red: Vec3 = [0.82, 0.29, 0.36];
265 const pale: Vec3 = [0.75, 0.75, 0.78];
266 const grey: Vec3 = [0.4, 0.4, 0.45];
267 const circle = (centre: Vec3, r: number, rgb: Vec3, axis: "z" | "y" = "z") => {
268 const n = 24;
269 for (let k = 0; k < n; k++) {
270 const a0 = (2 * Math.PI * k) / n;
271 const a1 = (2 * Math.PI * (k + 1)) / n;
272 const p = (a: number): Vec3 =>
273 axis === "z"
274 ? [centre[0] + r * Math.cos(a), centre[1] + r * Math.sin(a), centre[2]]
275 : [centre[0] + r * Math.cos(a), centre[1], centre[2] + r * Math.sin(a)];
276 line(p(a0), p(a1), rgb);
277 }
278 };
279 const hole = holePosition(spec);
280 const hookUp = Math.min(spec.thread.length, 0.7 * size);
281 line([hole.x, hole.y, 0], [hole.x, hole.y + hookUp, 0], dark);
282 line(
283 [hole.x - 0.03 * size, hole.y + hookUp, 0],
284 [hole.x + 0.03 * size, hole.y + hookUp, 0],
285 dark,
286 );
287 circle([hole.x, hole.y, 0], spec.hole.diameter / 2, dark);
288 // the hammer: its tip resting on the near face, its shank along the normal
289 const R = spec.hammer.tip_radius;
290 const s: Vec3 = [spec.strike.x, spec.strike.y, 0];
291 circle([s[0], s[1], R], R, red);
292 circle([s[0], s[1], R], R, red, "y");
293 line([s[0], s[1], 2 * R], [s[0], s[1], 2 * R + 0.35 * size], red);
294 // the probe
295 line(
296 [spec.probe.x - 0.02 * size, spec.probe.y, 0],
297 [spec.probe.x + 0.02 * size, spec.probe.y, 0],
298 grey,
299 );
300 line(
301 [spec.probe.x, spec.probe.y - 0.02 * size, 0],
302 [spec.probe.x, spec.probe.y + 0.02 * size, 0],
303 grey,
304 );
305 // outline of the undeformed plate, faint
306 line([-W / 2, -H / 2, 0], [W / 2, -H / 2, 0], pale);
307 line([W / 2, -H / 2, 0], [W / 2, H / 2, 0], pale);
308 line([W / 2, H / 2, 0], [-W / 2, H / 2, 0], pale);
309 line([-W / 2, H / 2, 0], [-W / 2, -H / 2, 0], pale);
311 // ---- camera
312 const centre: Vec3 = [0, 0.08 * size, 0];
313 const dist = (2.4 * size) / cam.zoom;
314 const eye: Vec3 = add(centre, [
315 dist * Math.cos(cam.el) * Math.sin(cam.az),
316 dist * Math.sin(cam.el),
317 dist * Math.cos(cam.el) * Math.cos(cam.az),
318 ]);
319 const view = lookAt(eye, centre, [0, 1, 0]);
320 const proj = perspective(0.6, vw / vh, 0.01 * size, 20 * size);
322 gl.clearColor(0.97, 0.97, 0.975, 1);
323 gl.enable(gl.DEPTH_TEST);
324 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
325 gl.useProgram(prog);
326 gl.uniformMatrix4fv(loc.uProj as WebGLUniformLocation, false, proj);
327 gl.uniformMatrix4fv(loc.uView as WebGLUniformLocation, false, view);
328 const attr = (buf: WebGLBuffer, data: Float32Array, a: number) => {
329 gl.bindBuffer(gl.ARRAY_BUFFER, buf);
330 gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
331 gl.enableVertexAttribArray(a);
332 gl.vertexAttribPointer(a, 3, gl.FLOAT, false, 0, 0);
333 };
334 gl.uniform1f(loc.uFlat as WebGLUniformLocation, 0);
335 attr(buffers.pos, pos, loc.aPos as number);
336 attr(buffers.nrm, nrm, loc.aNormal as number);
337 attr(buffers.col, col, loc.aColor as number);
338 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.idx);
339 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, mesh.index, gl.STATIC_DRAW);
340 gl.drawElements(gl.TRIANGLES, mesh.index.length, gl.UNSIGNED_SHORT, 0);
341 gl.uniform1f(loc.uFlat as WebGLUniformLocation, 1);
342 const lp = new Float32Array(L);
343 attr(lines.pos, lp, loc.aPos as number);
344 attr(lines.nrm, new Float32Array(L.length), loc.aNormal as number);
345 attr(lines.col, new Float32Array(LC), loc.aColor as number);
346 gl.drawArrays(gl.LINES, 0, lp.length / 3);
347 };
349 const drawRef = useRef(draw);
350 drawRef.current = draw;
351 useImperativeHandle(
352 ref,
353 () => ({
354 canvas: () => canvasRef.current,
355 drawFrame: (k: number, out?: { width: number; height: number }) => drawRef.current(k, out),
356 }),
357 [],
358 );
360 useEffect(() => {
361 drawRef.current(frameIndex);
362 }, [
363 result,
364 spec,
365 frameIndex,
366 frames,
367 colorFrames,
368 colorRange,
369 colormap,
370 scale,
371 width,
372 height,
373 cam,
374 mesh,
375 nx,
376 ny,
377 W,
378 H,
379 size,
380 ]);
382 return (
383 <canvas
384 ref={canvasRef}
385 style={{ width, height, display: "block", cursor: "grab", borderRadius: 4 }}
386 onPointerDown={(e) => {
387 drag.current = { x: e.clientX, y: e.clientY };
388 (e.target as HTMLElement).setPointerCapture(e.pointerId);
389 }}
390 onPointerMove={(e) => {
391 if (!drag.current) return;
392 const dx = e.clientX - drag.current.x;
393 const dy = e.clientY - drag.current.y;
394 drag.current = { x: e.clientX, y: e.clientY };
395 setCam((c) => ({
396 ...c,
397 az: c.az - dx * 0.01,
398 el: Math.max(-1.4, Math.min(1.4, c.el + dy * 0.01)),
399 }));
400 }}
401 onPointerUp={() => {
402 drag.current = null;
403 }}
404 onWheel={(e) => {
405 e.preventDefault();
406 setCam((c) => ({
407 ...c,
408 zoom: Math.max(0.3, Math.min(5, c.zoom * Math.exp(-e.deltaY * 0.001))),
409 }));
410 }}
411 aria-label="the plate on its thread"
412 />
413 );
414 },
415);