fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 1import { useCallback, useEffect, useRef, useState } from "react";
185968eRevert "Dome example: chaos that keeps its pitch..."Jeremy Magland 2import { wavBlob } from "./sim/analysis";
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 3import { BeadView } from "./ui/BeadView";
4import { Editor } from "./ui/Editor";
0330c41Damping by decay times: bp_decay helper, per-mode decay-time plot, examples convertedJeremy Magland 5import { DecayTimes, Energy, ModeEnergy, Separation, Spectrogram, Spectrum, Waveform } from "./ui/Plots";
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 6import type { FromWorker, RunPayload, RunRequest } from "./worker";
8const exampleSources = import.meta.glob("../matlab/examples/*.m", { query: "?raw", import: "default", eager: true }) as Record<string, string>;
9const helperSources = import.meta.glob("../matlab/helpers/*.m", { query: "?raw", import: "default", eager: true }) as Record<string, string>;
10const examples = Object.entries(exampleSources)
11 .map(([path, source]) => ({ name: path.split("/").pop()!.replace(/\.m$/, ""), source }))
12 .sort((a, b) => (a.name === "drum" ? -1 : b.name === "drum" ? 1 : a.name.localeCompare(b.name)));
13const helpers = Object.entries(helperSources).map(([path, source]) => ({ name: path.split("/").pop()!, source }));
15const STORE_KEY = "bead-percussion:script";
917e5cdWebGPU toggle: single-precision GPU integration (displacement-based, multi-dispatch phases, frames read back), linear modes behind a toggle (off by default), shared prepare()Jeremy Magland 16
17// a boolean remembered in this browser (falls back to the default if storage is unavailable)
18function usePersistent(name: string, initial: boolean): [boolean, (v: boolean) => void] {
19 const key = `bead-percussion:${name}`;
20 const [value, setValue] = useState(() => {
21 try {
22 const s = localStorage.getItem(key);
23 return s === null ? initial : s === "1";
24 } catch {
25 return initial;
26 }
27 });
28 const set = (v: boolean) => {
29 setValue(v);
30 try {
31 localStorage.setItem(key, v ? "1" : "0");
32 } catch {
33 /* storage unavailable */
34 }
35 };
36 return [value, set];
37}
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 38const load = () => {
39 try {
40 return localStorage.getItem(STORE_KEY);
41 } catch {
42 return null;
43 }
44};
45const save = (s: string) => {
46 try {
47 localStorage.setItem(STORE_KEY, s);
48 } catch {
49 /* storage unavailable */
50 }
51};
53export default function App() {
54 const [script, setScript] = useState(() => load() ?? examples[0].source);
55 const [running, setRunning] = useState(false);
56 const [stage, setStage] = useState("");
57 const [progress, setProgress] = useState<number | null>(null);
58 const [log, setLog] = useState("");
59 const [error, setError] = useState<string | null>(null);
60 const [payload, setPayload] = useState<RunPayload | null>(null);
61 const [fmax, setFmax] = useState(10000);
62 const [cursor, setCursor] = useState<number | null>(null);
63 const [viewTime, setViewTime] = useState<number | null>(null);
64 const [dtCheck, setDtCheck] = useState(false);
65 const [linearRef, setLinearRef] = useState(false);
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 66 const [twin, setTwin] = useState(false);
917e5cdWebGPU toggle: single-precision GPU integration (displacement-based, multi-dispatch phases, frames read back), linear modes behind a toggle (off by default), shared prepare()Jeremy Magland 67 const [modesOn, setModesOn] = usePersistent("modes", false);
68 const [gpuOn, setGpuOn] = usePersistent("gpu", false);
69 const gpuAvailable = typeof navigator !== "undefined" && "gpu" in navigator;
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 70 const [playing, setPlaying] = useState<"main" | "linear" | "twin" | null>(null);
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 71 const [runId, setRunId] = useState(0); // remounts panels with per-run state
72 const worker = useRef<Worker | null>(null);
73 const audioCtx = useRef<AudioContext | null>(null);
74 const source = useRef<AudioBufferSourceNode | null>(null);
75 const startedAt = useRef(0);
77 useEffect(() => save(script), [script]);
79 const stopAudio = useCallback(() => {
80 source.current?.stop();
81 source.current = null;
82 setCursor(null);
83 setPlaying(null);
84 }, []);
86 const run = useCallback(
87 () => {
88 worker.current?.terminate();
89 stopAudio();
90 const w = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" });
91 worker.current = w;
92 setRunning(true);
93 setError(null);
94 setLog("");
95 setProgress(null);
96 setStage("Starting");
97 w.onmessage = (ev: MessageEvent<FromWorker>) => {
98 const m = ev.data;
99 if (m.type === "log") setLog(l => l + m.text);
100 else if (m.type === "stage") {
101 setStage(m.text);
102 setProgress(null);
103 } else if (m.type === "progress") setProgress(m.t / m.duration);
104 else if (m.type === "error") {
105 setError(m.message);
106 setRunning(false);
107 w.terminate();
108 } else if (m.type === "result") {
109 setPayload(m.payload);
110 setRunId(i => i + 1);
185968eRevert "Dome example: chaos that keeps its pitch..."Jeremy Magland 111 const fsHalf = m.payload.result.sampleRate / 2;
112 const top = m.payload.modeFreqs ? m.payload.modeFreqs[m.payload.modeFreqs.length - 1] * 1.15 : 10000;
113 setFmax([1000, 2000, 5000, 10000, 20000].find(f => f >= top && f <= fsHalf) ?? Math.min(20000, fsHalf));
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 114 setRunning(false);
115 w.terminate();
116 }
117 };
118 w.onerror = e => {
119 setError(e.message || "Worker error");
120 setRunning(false);
121 };
917e5cdWebGPU toggle: single-precision GPU integration (displacement-based, multi-dispatch phases, frames read back), linear modes behind a toggle (off by default), shared prepare()Jeremy Magland 122 const req: RunRequest = { type: "run", script, dtCheck, linear: linearRef, twin, modes: modesOn, gpu: gpuOn && gpuAvailable };
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 123 w.postMessage(req);
124 },
917e5cdWebGPU toggle: single-precision GPU integration (displacement-based, multi-dispatch phases, frames read back), linear modes behind a toggle (off by default), shared prepare()Jeremy Magland 125 [script, stopAudio, dtCheck, linearRef, twin, modesOn, gpuOn, gpuAvailable]
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 126 );
128 const stop = () => {
129 worker.current?.terminate();
130 worker.current = null;
131 setRunning(false);
132 setStage("Stopped");
133 };
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 135 const play = (which: "main" | "linear" | "twin") => {
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 136 if (!payload) return;
137 stopAudio();
138 const { sampleRate } = payload.result;
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 139 const audio =
140 which === "linear" && payload.linear
141 ? payload.linear.audio
142 : which === "twin" && payload.twin
143 ? payload.twin.audio
144 : payload.result.audio;
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 145 const ctx = audioCtx.current ?? new AudioContext();
146 audioCtx.current = ctx;
147 // both signals share the main run's gain, so the A/B comparison keeps levels
148 let peak = 0;
149 for (const v of payload.result.audio) peak = Math.max(peak, Math.abs(v));
150 const buf = ctx.createBuffer(1, audio.length, sampleRate);
151 const ch = buf.getChannelData(0);
152 const g = peak > 0 ? 0.9 / peak : 1;
153 for (let i = 0; i < audio.length; i++) ch[i] = Math.max(-1, Math.min(1, g * audio[i]));
154 const src = ctx.createBufferSource();
155 src.buffer = buf;
156 src.connect(ctx.destination);
157 src.onended = () => {
158 if (source.current === src) {
159 source.current = null;
160 setCursor(null);
161 setPlaying(null);
162 }
163 };
164 src.start();
165 source.current = src;
166 setPlaying(which);
167 startedAt.current = ctx.currentTime;
168 const T = audio.length / sampleRate;
169 const tick = () => {
170 if (source.current !== src) return;
171 const t = ctx.currentTime - startedAt.current;
172 setCursor(Math.min(t, T));
173 if (t < T) requestAnimationFrame(tick);
174 };
175 requestAnimationFrame(tick);
176 };
178 const download = () => {
179 if (!payload) return;
180 const url = URL.createObjectURL(wavBlob(payload.result.audio, payload.result.sampleRate));
181 const a = document.createElement("a");
182 a.href = url;
183 a.download = "bead-percussion.wav";
184 a.click();
185 setTimeout(() => URL.revokeObjectURL(url), 1000);
186 };
188 const r = payload?.result;
189 return (
190 <div className="app">
191 <header>
192 <h1>bead-percussion</h1>
193 <span className="sub">beads and springs, set up in numbl, heard through one bead</span>
194 </header>
195 <div className="columns">
196 <section className="left">
197 <div className="controls">
198 <select
199 value=""
200 onChange={e => {
201 const ex = examples.find(x => x.name === e.target.value);
202 if (ex && (script === ex.source || confirm(`Replace the script with the "${ex.name}" example?`))) setScript(ex.source);
203 }}
204 >
205 <option value="" disabled>
206 Load example…
207 </option>
208 {examples.map(ex => (
209 <option key={ex.name} value={ex.name}>
210 {ex.name}
211 </option>
212 ))}
213 </select>
214 <span className="grow" />
215 {running ? (
216 <button onClick={stop}>Stop</button>
217 ) : (
218 <button className="primary" onClick={run} title="Ctrl/Cmd+Enter">
219 Run
220 </button>
221 )}
222 </div>
223 <div className="controls options">
917e5cdWebGPU toggle: single-precision GPU integration (displacement-based, multi-dispatch phases, frames read back), linear modes behind a toggle (off by default), shared prepare()Jeremy Magland 224 <label title="Compute the linear modes (eigenvalues) and track the energy in each; can take seconds for large or curved nets">
225 <input type="checkbox" checked={modesOn} onChange={e => setModesOn(e.target.checked)} /> linear modes
226 </label>
227 <label
228 title={
229 gpuAvailable
230 ? "Integrate on the GPU with WebGPU (single precision). Faster for large nets; the perturbed twin still runs on the CPU."
231 : "WebGPU is not available in this browser"
232 }
233 >
234 <input type="checkbox" disabled={!gpuAvailable} checked={gpuOn && gpuAvailable} onChange={e => setGpuOn(e.target.checked)} /> WebGPU
235 </label>
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 236 <label title="Run again at dt/2 and compare the listener signals">
237 <input type="checkbox" checked={dtCheck} onChange={e => setDtCheck(e.target.checked)} /> dt/2 check
238 </label>
239 <label title="Run again with every hit scaled by 1e-3 (then scaled back up): what the system would do if it were linear">
240 <input type="checkbox" checked={linearRef} onChange={e => setLinearRef(e.target.checked)} /> small-strike reference
241 </label>
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 242 <label title="Run again with every hit changed by one part in 1e9 and track how fast the two runs separate (exponential growth means chaos)">
243 <input type="checkbox" checked={twin} onChange={e => setTwin(e.target.checked)} /> perturbed twin
244 </label>
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 245 </div>
246 <Editor value={script} onChange={setScript} onRun={() => !running && run()} />
247 {running && (
248 <div className="status">
249 {stage}
250 {progress !== null && ` ${(100 * progress).toFixed(0)}%`}
251 <div className="bar">
252 <div style={{ width: `${100 * (progress ?? 0)}%` }} />
253 </div>
254 </div>
255 )}
256 {error && <pre className="error">{error}</pre>}
257 {log && <pre className="log">{log}</pre>}
258 {payload && <Report p={payload} />}
259 <details className="helpers">
260 <summary>Helper functions (bp_*.m)</summary>
261 {helpers.map(h => (
262 <div key={h.name}>
263 <h4>{h.name}</h4>
264 <pre>{h.source}</pre>
265 </div>
266 ))}
267 </details>
268 <details className="helpers">
269 <summary>sys fields</summary>
270 <pre>{SYS_DOC}</pre>
271 </details>
272 </section>
273 <section className="right">
274 {!payload && <div className="empty">Run a script to hear and see the result.</div>}
275 {payload && r && (
276 <>
277 <div className="controls">
278 <button className="primary" onClick={() => (playing === "main" ? stopAudio() : play("main"))}>
279 {playing === "main" ? "Stop" : "Play"}
280 </button>
281 {payload.linear && (
282 <button onClick={() => (playing === "linear" ? stopAudio() : play("linear"))} title="The same strike scaled down 1000x, then scaled back up">
283 {playing === "linear" ? "Stop" : "Play small-strike reference"}
284 </button>
285 )}
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 286 {payload.twin && (
287 <button onClick={() => (playing === "twin" ? stopAudio() : play("twin"))} title="The run with the strike changed by one part in 1e9">
288 {playing === "twin" ? "Stop" : "Play twin"}
289 </button>
290 )}
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 291 <button onClick={download}>Download WAV</button>
292 <span className="grow" />
293 <label>
294 freq range{" "}
295 <select value={fmax} onChange={e => setFmax(+e.target.value)}>
296 {[1000, 2000, 5000, 10000, 20000]
297 .filter(f => f <= r.sampleRate / 2)
298 .map(f => (
299 <option key={f} value={f}>
300 {f / 1000} kHz
301 </option>
302 ))}
303 </select>
304 </label>
305 </div>
306 <h3>Listener signal</h3>
307 <Waveform p={payload} cursor={cursor ?? viewTime} />
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 308 {payload.twin && (
309 <>
310 <h3>Separation from the perturbed twin (chaos test)</h3>
311 <Separation p={payload} />
312 </>
313 )}
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 314 <h3>Spectrogram</h3>
315 <Spectrogram p={payload} fmax={fmax} />
316 <h3>Spectrum, two windows (detuning shows as peaks shifting)</h3>
85eb848Fix duplicated plots during playback: give per-run panels distinct keysJeremy Magland 317 <Spectrum key={`spectrum-${runId}`} p={payload} fmax={fmax} />
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 318 <h3>Energy by linear mode (the cascade)</h3>
85eb848Fix duplicated plots during playback: give per-run panels distinct keysJeremy Magland 319 <ModeEnergy key={`modes-${runId}`} p={payload} />
0330c41Damping by decay times: bp_decay helper, per-mode decay-time plot, examples convertedJeremy Magland 320 {payload.modeFreqs && payload.modeDecay && (
321 <>
322 <h3>Decay time by mode</h3>
323 <DecayTimes p={payload} />
324 </>
325 )}
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 326 <h3>Energy</h3>
327 <Energy p={payload} />
328 <h3>Beads</h3>
85eb848Fix duplicated plots during playback: give per-run panels distinct keysJeremy Magland 329 <BeadView key={`beads-${runId}`} p={payload} onTime={setViewTime} />
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 330 </>
331 )}
332 </section>
333 </div>
334 </div>
335 );
336}
338function Report({ p }: { p: RunPayload }) {
339 const r = p.result;
340 const nPinned = p.sys.pinned.reduce((a, b) => a + b, 0);
341 const rows: [string, string][] = [
342 ["beads", `${p.sys.n} (${nPinned} pinned)`],
3bd39d8Dome: hinge elements (sys.hinges, Bridson gradient, rest angle from sys.pos), bp_hinges, full 3D linear modes for curved nets, axial dashpots so bp_decay is exact for all modes, bp_decay takes and returns sys; dome exampleJeremy Magland 343 ["springs, bends, hinges, dashpots", `${p.sys.si.length}, ${p.sys.nBends}, ${p.sys.nHinges}, ${p.sys.nDashpots}`],
917e5cdWebGPU toggle: single-precision GPU integration (displacement-based, multi-dispatch phases, frames read back), linear modes behind a toggle (off by default), shared prepare()Jeremy Magland 344 ["method", `${p.sys.method} (${p.sys.engine})`],
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 345 ["dt", `${r.dt.toExponential(3)} s (${r.substeps} per audio sample)`],
346 ["highest linear frequency", `${(r.omegaMax / (2 * Math.PI)).toFixed(0)} Hz`],
347 ["frequency error", r.freqErrors.map(e => `${e.cents.toFixed(e.cents < 0.1 ? 3 : 2)}¢ at ${e.hz / 1000} kHz`).join(", ")],
348 ["energy drift after excitation", r.energyDrift.toExponential(2)],
349 ["run time", `${(r.elapsedMs / 1000).toFixed(1)} s (${(r.elapsedMs / 1000 / p.sys.duration).toFixed(1)}x real time)`],
350 ];
351 if (p.modeFreqs) rows.push(["lowest modes", Array.from(p.modeFreqs.slice(0, 5)).map(f => f.toFixed(1)).join(", ") + " Hz"]);
352 return (
353 <div className="report">
354 {r.aborted && <div className="error">{r.aborted}</div>}
355 {[...p.notes, ...r.warnings].map((w, i) => (
356 <div key={i} className="warn">
357 {w}
358 </div>
359 ))}
360 <table>
361 <tbody>
362 {rows.map(([k, v]) => (
363 <tr key={k}>
364 <td>{k}</td>
365 <td>{v}</td>
366 </tr>
367 ))}
368 </tbody>
369 </table>
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 370 {p.twin && (
371 <div className="caption">
372 Perturbed twin (hits changed by 1 part in 10^9):{" "}
373 {p.twin.rate !== null
374 ? `separation grows exponentially, e-folding time ${(1000 / p.twin.rate).toPrecision(2)} ms (rate ${p.twin.rate.toPrecision(3)} /s). Chaotic.`
375 : `separation reached ${p.twin.sep.reduce((a, b) => Math.max(a, b), 0).toExponential(1)} of the motion's size; no exponential growth to 1e-2 within the run.`}
376 </div>
377 )}
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 378 {p.linear && (
379 <>
380 <div className="caption">
381 Listener signal vs the small-strike reference (how nonlinear the run is): relative RMS difference over [0, t]
382 </div>
383 <table>
384 <tbody>
385 {p.linear.windows.map(w => (
386 <tr key={w.t}>
387 <td>t = {w.t} s</td>
388 <td>{w.rel.toExponential(2)}</td>
389 </tr>
390 ))}
391 </tbody>
392 </table>
393 </>
394 )}
395 {p.check && (
396 <>
397 <div className="caption">
398 Listener signal, dt vs dt/2 ({p.check.dt.toExponential(2)} s): relative RMS difference over [0, t]
399 </div>
400 <table>
401 <tbody>
402 {p.check.windows.map(w => (
403 <tr key={w.t}>
404 <td>t = {w.t} s</td>
405 <td>{w.rel.toExponential(2)}</td>
406 </tr>
407 ))}
408 </tbody>
409 </table>
410 </>
411 )}
412 </div>
413 );
414}
416const SYS_DOC = `Required
417 sys.pos n-by-3 bead positions (m)
418 sys.mass scalar or n-by-1 (kg)
452405aListener quantity: acceleration (default), velocity or displacement; acceleration from forces with boundary-corrected damping; zero-mode threshold 1e-6 of top frequencyJeremy Magland 419 sys.listen struct: beads (index or indices, averaged), dir (default [0 0 1]),
420 quantity: 'acceleration' (default), 'velocity' or 'displacement'
421 (sound in air follows acceleration more closely; displacement
422 favors low frequencies by 1/f^2)
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 423Elements
424 sys.springs rows [i j k L0] or [i j k L0 k3]; force k*e + k3*e^3, e = length - L0
74b1dcdFree edges: bending triplets (sys.bends) with damping, lattice triplets from bp_hexdisk/bp_chain, bp_decay for bending, bp_anchor soft supports, zero-mode warning; plate and bar examplesJeremy Magland 425 sys.bends rows [i j k kb] or [i j k kb cb], j the middle bead: bending,
426 energy kb/2 |x_i - 2x_j + x_k|^2 (straight, evenly spaced rest);
427 cb damps (v_i - 2v_j + v_k). Triplets come from bp_hexdisk, bp_chain
3bd39d8Dome: hinge elements (sys.hinges, Bridson gradient, rest angle from sys.pos), bp_hinges, full 3D linear modes for curved nets, axial dashpots so bp_decay is exact for all modes, bp_decay takes and returns sys; dome exampleJeremy Magland 428 sys.hinges rows [i j k l kh] or [i j k l kh ch]: bending across the edge j-k
429 shared by triangles (i,j,k) and (l,k,j); energy kh/2 (theta-theta0)^2,
430 theta0 the angle at sys.pos (so any rest shape, e.g. a dome, is
431 unstressed). Rows come from bp_hinges(tri)
432 sys.dashpots rows [i j c] or [i j c ca]; force c*(v_j - v_i) on bead i, plus
433 ca*(d.(v_j - v_i))*d along the direction d between the beads
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 434 sys.friction scalar or n-by-1; force -c0*v
ac09b0fDefault damping: every mode falls 60 dB in 2.5 s; examples at 2.5 s; twin separation relative to recent displacement; stronger chaos strikeJeremy Magland 435 (if neither friction nor dashpots is given: every mode falls 60 dB in 2.5 s)
3bd39d8Dome: hinge elements (sys.hinges, Bridson gradient, rest angle from sys.pos), bp_hinges, full 3D linear modes for curved nets, axial dashpots so bp_decay is exact for all modes, bp_decay takes and returns sys; dome exampleJeremy Magland 436 sys = bp_decay(sys, [f1 T1; f2 T2]) sets friction, dashpots and the
437 damping columns of bends and hinges from decay times (T60, s) at
438 two frequencies (Hz); call it after the elements are set
74b1dcdFree edges: bending triplets (sys.bends) with damping, lattice triplets from bp_hexdisk/bp_chain, bp_decay for bending, bp_anchor soft supports, zero-mode warning; plate and bar examplesJeremy Magland 439 sys.pinned bead indices (or logical mask) held fixed; for a soft hold
440 (cord, felt) use bp_anchor
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 441Excitation
442 sys.hits struct or struct array, each with
443 beads, t (default 0), and either
444 velocity [vx vy vz]: added instantly at t, or
445 force [fx fy fz] + duration: raised-cosine pulse per bead
446Solver (optional)
447 sys.duration seconds (default 2)
448 sys.method 'verlet' (default) or 'yoshida4'
449 sys.safety fraction of the stability limit for automatic dt (default 0.5)
450 sys.dt fixed step (rounded to divide the audio sample interval)
451 sys.sampleRate audio rate (default 48000)
452 sys.frameRate energy/mode/animation records per second (default 500)
917e5cdWebGPU toggle: single-precision GPU integration (displacement-based, multi-dispatch phases, frames read back), linear modes behind a toggle (off by default), shared prepare()Jeremy Magland 453 sys.modes true/false: compute linear modes, overriding the page's
454 "linear modes" toggle (off by default)
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 455Bead indices are 1-based.`;