Help buttons on each card and panel
4 changed files+76−6
README.mdmodified+2−0View file
@@ -8,6 +8,8 @@ Current state: N masses (2 to 64, default 2) between two fixed walls, joined by
88
99 Poincaré section (shown only for N = 2 and N = 3, below the sped-up panel, open; adapted from the pendulum app): its own energy slider (log, 10 µJ to 100 J, default 2.51 J), same parameters, no damping. N = 2: each time mass 1 passes its rest position moving right, mass 2's (u2, p2) is plotted. N = 3: only the symmetric family (u1 = u3, p1 = p3; only modes 1 and 3 move, since the cubic force couples odd modes only to odd modes), which is two-dimensional: each time mass 2 passes its rest position moving right, mass 1's (u1, p1) is plotted. On either section the springs are stretched by 0 or +-x, so V = vFactor (k x^2 / 2 + k3 x^4 / 4) (vFactor 2 or 4), the plotted momentum counts once (N = 2) or twice (N = 3) in the kinetic energy, the outline is where the crossing mass's momentum vanishes, and that momentum follows from E. 42 orbits seeded on a grid, 300 crossings each, in a worker (`src/poincare-core.ts`, `src/poincare-worker.ts`, `src/poincare.ts`; integrator step stepFor(E), energy error < 5e-5; ~1 s per section offline, a few s in the pane), drawn as they arrive; per-orbit colors or Color by λ (each orbit's Lyapunov exponent over its run, log scale); hover to pick out an orbit with its λ; click inside the outline to add an orbit there, and with Click to strike (on) also strike the chain from that exact state (and play it, if the sound panel is open). Follow (as in the pendulum app): "real time" makes the section's energy track the chain's as it decays (recomputed after a 3% change, at most every 2 s, and at each strike); "sped up" (which opens the sped-up panel) shows the playing note's crossings (found in the render worker, for N = 3 only while exactly symmetric, and drawn as the audio clock reaches them) as a fading trail of the last 150, with the axes fitted to the note's current energy (refit every 2%) and each dot fitted from its own energy; nothing is computed live. "Precompute sections" (shown only with Follow "sped up" until complete) builds an atlas (`src/atlas.ts`): sections at 32 energies log-spaced from 1 mJ to 100 J (a factor ~1.45 apart), 42 orbits x 150 crossings, highest first, in its own worker (~22 s in the pane for N = 3), stored in IndexedDB per N, m, k, k3 and deleted after 7 days; the sound view shows the atlas section nearest the current energy, fitted to its allowed region (exact in the linear regime, below ~25 mJ, where sections are scaled copies; an approximation across the gaps above). Without an atlas only the outline is drawn behind the trail. Setting the section's energy by hand pauses everything and clears the rings and the trail; Follow takes over again on Resume. The chain's own crossings show as dark rings when within 5% of the section's energy and, for N = 3, only while the chain is exactly symmetric (else the legend says "chain not symmetric, not shown"; strikes on mass 2 or on mode 1 or 3 are symmetric, mode shapes are made exactly mirror-symmetric for this). Findings: the two-mass chain appears regular at every energy tried (10 mJ to 100 J): nested closed curves and island chains, no chaotic sea; the grid orbits' λ falls as the run lengthens (at 10 J, median 0.045 /s over 300 crossings, 0.007 /s over 3000), as for regular orbits. A likely reason: in mode coordinates (u = S Q) the potential is (k/2) rho^2 + (k3/8) rho^4 with rho^2 = Q1^2 + 3 Q2^2, a function of one combination of the two modes. The N = 3 symmetric family is chaotic: grid orbits with λ > 0.1 /s: 0 of 42 at 25 mJ, 10 at 0.25 J (islands in a chaotic sea), 37 at 2.51 J. It is unstable to breaking the symmetry: an exactly symmetric start stays symmetric (the arithmetic is exactly mirror-symmetric), but an asymmetry of 1e-12 grows to full size within ~100 s at 0.25 to 25 J. The ghost and the Lyapunov shadow offset all masses equally, so after a symmetric strike they too stay symmetric and measure the exponent within the family.
1010
11+Each card and panel (chain, Parameters, Real time, Sped up · listen, Poincaré section) has a small "?" button that opens a short help dialog (`src/help.ts` from the pendulum app; the texts are `<template>`s in `index.html`).
12+
1113 Live site: https://fput-chain-chaos--listening-for-chaos.vault1-sites.magland.org/ (built and deployed by `.github/workflows/pages.yml` on every push to main).
1214
1315 ```bash
index.htmlmodified+50−6View file
@@ -25,7 +25,7 @@
2525 .grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; align-items: start; }
2626 @media (max-width: 900px) { .grid { grid-template-columns: minmax(0, 1fr); } }
2727 .card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 16px; }
28- .card-head { font-size: 12px; margin-bottom: 12px; }
28+ .card-head { font-size: 12px; margin-bottom: 12px; display: flex; align-items: center; }
2929 .row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
3030 .row + .row { margin-top: 10px; }
3131 .num { font: 13px var(--mono); font-variant-numeric: tabular-nums; }
@@ -94,7 +94,21 @@
9494 .chip input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 1px; }
9595 #traces canvas { height: auto; margin-top: 12px; }
9696 details { margin-top: 14px; border-top: 1px solid var(--line); padding-top: 10px; }
97- summary { cursor: pointer; color: var(--muted); font-size: 13px; list-style: none; }
97+ summary { cursor: pointer; color: var(--muted); font-size: 13px; list-style: none; display: flex; align-items: center; gap: 4px; }
98+ button.help {
99+ width: 20px; height: 20px; padding: 0; margin-left: auto; flex: none; border-radius: 50%;
100+ font: 600 11px/1 var(--sans); color: #a5a7aa; background: transparent; letter-spacing: 0; text-transform: none;
101+ }
102+ button.help:hover { color: var(--accent); border-color: #b9cde0; }
103+ dialog#help {
104+ max-width: min(460px, calc(100vw - 32px)); padding: 0; border: 1px solid var(--line); border-radius: 10px;
105+ color: var(--text); background: var(--card); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
106+ }
107+ dialog#help::backdrop { background: rgba(35, 38, 43, 0.25); }
108+ #help-body { padding: 18px 20px 4px; font-size: 13.5px; line-height: 1.5; }
109+ #help-body h2 { font: 400 19px/1.3 var(--serif); margin: 0 0 8px; }
110+ #help-body p { margin: 0 0 10px; }
111+ #help-actions { display: flex; justify-content: flex-end; padding: 0 20px 16px; }
98112 summary::-webkit-details-marker { display: none; }
99113 summary::before { content: "▸ "; }
100114 details[open] summary::before { content: "▾ "; }
@@ -121,6 +135,7 @@
121135 </select>
122136 <input id="strike-index" type="number" min="1" value="1" step="1" />
123137 <button id="pause" disabled>Pause</button>
138+ <button type="button" class="help" data-help="chain" title="About the chain" aria-label="About the chain">?</button>
124139 </div>
125140 <div class="row">
126141 <span class="muted">Strike energy</span>
@@ -152,7 +167,7 @@
152167 <span id="ghost-sep" class="num muted"></span>
153168 </div>
154169 <details>
155- <summary>Parameters</summary>
170+ <summary>Parameters <button type="button" class="help" data-help="params" title="About the parameters" aria-label="About the parameters">?</button></summary>
156171 <div id="param-form">
157172 <label>m <input id="p-m" type="number" step="any" /> <span class="unit">kg</span></label>
158173 <label>k <input id="p-k" type="number" step="any" /> <span class="unit">N/m</span></label>
@@ -168,7 +183,7 @@
168183 </details>
169184 </section>
170185 <section class="card">
171- <div class="card-head">Real time</div>
186+ <div class="card-head">Real time <button type="button" class="help" data-help="realtime" title="About the real-time charts" aria-label="About the real-time charts">?</button></div>
172187 <div class="chips">
173188 <label class="chip"><input id="bars-toggle" type="checkbox" checked /><span>Mode bars</span></label>
174189 <span id="toggles"></span>
@@ -181,7 +196,7 @@
181196 </section>
182197 </div>
183198 <details class="card panel" id="sound" open>
184- <summary><span class="label">Sped up · listen</span> <span id="sound-clock" class="muted"></span></summary>
199+ <summary><span class="label">Sped up · listen</span> <span id="sound-clock" class="muted"></span> <button type="button" class="help" data-help="sound" title="About the sped-up run" aria-label="About the sped-up run">?</button></summary>
185200 <div class="row">
186201 <button id="mute" type="button" class="mute" title="Unmute" aria-label="Unmute"></button>
187202 <label><span class="label">Signal</span> <select id="signal"></select></label>
@@ -207,7 +222,7 @@
207222 </details>
208223
209224 <details class="card panel" id="poincare" open hidden>
210- <summary><span class="label">Poincaré section</span></summary>
225+ <summary><span class="label">Poincaré section</span> <button type="button" class="help" data-help="section" title="About the Poincaré section" aria-label="About the Poincaré section">?</button></summary>
211226 <div class="row">
212227 <span class="label">Energy</span>
213228 <input id="pc-energy" type="range" min="-5" max="2" step="0.01" value="0.4" aria-label="Section energy" />
@@ -226,6 +241,35 @@
226241 <figure><div id="pc-plot"></div></figure>
227242 </details>
228243 </main>
244+ <dialog id="help">
245+ <div id="help-body"></div>
246+ <div id="help-actions"><button id="help-close" type="button">Close</button></div>
247+ </dialog>
248+
249+ <template id="help-chain">
250+ <h2>The chain</h2>
251+ <p>Equal masses between two walls, joined by springs that stiffen as they stretch (tension k d + k3 d³), shown in real time: to scale on top, and below the same displacements drawn sideways. Strike gives one mass, or one normal mode, the energy on the slider.</p>
252+ <p>Weak strikes ring as independent normal modes. Strong ones trade energy between the modes, chaotically for three or more masses. Ghost adds a second chain started a tiny distance away: in chaotic motion the two separate exponentially. The space bar pauses.</p>
253+ </template>
254+ <template id="help-params">
255+ <h2>Parameters</h2>
256+ <p>Mass, spring constants k and k3, the rest spacing a (drawing only), and the decay time τ of the amplitude (drag on the masses; 0 for none). Apply restarts the chain at rest. The cubic term matches the linear one at the stretch d0 listed below; the motion turns nonlinear at energies around k d0².</p>
257+ </template>
258+ <template id="help-realtime">
259+ <h2>Real time</h2>
260+ <p>Charts of the chain on the left, on the same clock. Click a chip to show or hide one; hover to read values, click to pin the cursor.</p>
261+ <p>Mode bars show the energy in each normal mode now, Mode energies the same over time. Energy error should stay tiny. Lyapunov is the rate (1/s) at which nearby motions separate: positive for chaos, near 0 for regular motion. Separation (with the ghost on) is the ghost's distance from the chain.</p>
262+ </template>
263+ <template id="help-sound">
264+ <h2>Sped up · listen</h2>
265+ <p>A copy of the same strike, run fast enough to be heard: Pitch sets where the lowest normal mode lands, and the whole decay plays in a few seconds. While this panel is open each Strike plays a note; Replay plays the last one again with the current settings.</p>
266+ <p>Signal chooses what is heard. Chaotic motion sounds noisy and settles into steady tones as the energy decays. The charts span the whole note: spectrogram, Lyapunov exponent, energy, and λ against energy collected over notes.</p>
267+ </template>
268+ <template id="help-section">
269+ <h2>Poincaré section</h2>
270+ <p>A map of the motions at one energy, without damping. Two masses: each time mass 1 passes its rest position moving right, mass 2's displacement and momentum are plotted. Three masses: only motions with masses 1 and 3 moving alike, plotted each time mass 2 passes rest. Regular motions trace closed curves; chaotic ones scatter. The outline bounds what is reachable.</p>
271+ <p>The chain's own crossings are dark rings. Follow tracks the chain (real time) or the playing note (sped up). Hover to pick out an orbit; click to start one there, which with Click to strike also strikes the chain.</p>
272+ </template>
229273 <script type="module" src="/src/main.ts"></script>
230274 </body>
231275 </html>
src/help.tsadded+21−0View file
@@ -0,0 +1,21 @@
1+// Help: each "?" button (data-help="name") opens one dialog showing the
2+// contents of <template id="help-name">. Esc, Close, or a click outside the
3+// dialog closes it. A "?" inside a <summary> does not fold its panel.
4+
5+export function setupHelp(): void {
6+ const dialog = document.getElementById("help") as HTMLDialogElement;
7+ const body = document.getElementById("help-body")!;
8+ for (const button of document.querySelectorAll<HTMLButtonElement>("button.help")) {
9+ button.addEventListener("click", (e) => {
10+ e.preventDefault();
11+ e.stopPropagation();
12+ const t = document.getElementById(`help-${button.dataset.help}`) as HTMLTemplateElement;
13+ body.replaceChildren(t.content.cloneNode(true));
14+ dialog.showModal();
15+ });
16+ }
17+ document.getElementById("help-close")!.addEventListener("click", () => dialog.close());
18+ dialog.addEventListener("click", (e) => {
19+ if (e.target === dialog) dialog.close(); // the backdrop
20+ });
21+}
src/main.tsmodified+3−0View file
@@ -2,6 +2,7 @@ import {
22 Modes, Verlet, copyState, d0, energy, kinetic, newState, stepFor, potential, strikeMass, strikeMode, type Params, type State,
33 } from "./chain";
44 import { newCursor } from "./cursor";
5+import { setupHelp } from "./help";
56 import { Lyapunov } from "./lyapunov";
67 import { PoincarePanel } from "./poincare";
78 import { sectionDef } from "./poincare-core";
@@ -12,6 +13,8 @@ import { Player } from "./sound";
1213 import { NoteSpectrogram } from "./spectrogram";
1314 import { TracePanel, type Group } from "./traces";
1415
16+setupHelp();
17+
1518 const chainCanvas = document.getElementById("chain") as HTMLCanvasElement;
1619 const modesCanvas = document.getElementById("modes") as HTMLCanvasElement;
1720 const energyInput = document.getElementById("energy") as HTMLInputElement;