2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>FPUT chain chaos</title>
7 <style>
8 :root {
9 --bg: #f6f6f3;
10 --card: #ffffff;
11 --line: #e3e2dc;
12 --text: #23262b;
13 --muted: #7a7d82;
14 --accent: #2f5d8a;
15 --accent-soft: #e6eef6;
16 --serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
17 --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
18 --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
19 }
20 * { box-sizing: border-box; }
21 body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.45 var(--sans); }
22 main { max-width: 1040px; margin: 0 auto; padding: 28px 16px 48px; }
23 header { margin-bottom: 16px; }
24 h1 { font: 400 28px/1.2 var(--serif); margin: 0 0 4px; letter-spacing: 0.01em; }
25 .grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; align-items: start; }
26 @media (max-width: 900px) { .grid { grid-template-columns: minmax(0, 1fr); } }
27 .card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 16px; }
28 .card-head { font-size: 12px; margin-bottom: 12px; display: flex; align-items: center; }
29 .row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
30 .row + .row { margin-top: 10px; }
31 .num { font: 13px var(--mono); font-variant-numeric: tabular-nums; }
32 .muted { color: var(--muted); font-size: 12px; }
33 canvas { display: block; width: 100%; }
34 [hidden] { display: none ; }
35 #chain { aspect-ratio: 1.4; }
36 #modes { aspect-ratio: 2.2; }
37 figure { margin: 14px 0 0; }
38 #toggles { display: contents; }
39 #controls { margin-top: 14px; }
40 input[type="range"] { accent-color: var(--accent); flex: 1; min-width: 120px; margin: 0; }
41 select, input[type="number"] {
42 font: inherit; font-size: 13px; color: var(--text); background: #fff;
43 border: 1px solid var(--line); border-radius: 6px; padding: 3px 6px;
44 }
45 input[type="number"] { width: 4.5em; font-family: var(--mono); }
46 button {
47 font: inherit; font-size: 13px; color: var(--text); background: #fff; cursor: pointer;
48 border: 1px solid var(--line); border-radius: 6px; padding: 5px 12px;
49 }
50 button:hover { border-color: #c9c8c0; }
51 button.primary { background: var(--accent); border-color: var(--accent); color: #fff; min-width: 76px; }
52 button.primary:hover { filter: brightness(1.08); }
53 button:disabled { color: #b5b5b0; cursor: default; }
54 #energy-value { min-width: 4.5em; text-align: right; }
55 #ghost-row { margin-top: 10px; min-height: 28px; }
56 #ghost-controls { flex: 1; }
57 #ghost-controls .row, #ghost-controls { margin-top: 0; }
58 #ghost-offset { max-width: 140px; min-width: 80px; }
59 #ghost-sep { width: 100%; }
60 #mode-stats { margin-top: 6px; }
61 .label { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
62 details.panel { margin-top: 16px; border: 1px solid var(--line); padding: 16px; }
63 details.panel summary { display: flex; align-items: center; gap: 10px; }
64 details.panel summary .label { font-size: 12px; color: var(--text); }
65 details.panel[open] summary { margin-bottom: 14px; }
66 #sound .row + .row { margin-top: 12px; }
67 #sound label { display: flex; align-items: center; gap: 8px; }
68 #sound .grow { flex: 1; min-width: 220px; }
69 #volume { max-width: 110px; }
70 #sound-mass { width: 3.5em; }
71 #sound-chips { margin-left: auto; }
72 #sound-hint { margin: 14px 0 0; }
73 #spectrogram canvas, #mode-history canvas, #live-modes canvas { width: 100%; height: auto; border-radius: 4px; }
74 #sound-traces canvas { height: auto; margin-top: 12px; }
75 #scatter canvas, #pc-plot canvas { width: 100%; height: auto; }
76 #pc-energy { max-width: 360px; }
77 #poincare .row + .row { margin-top: 10px; }
78 #poincare label { display: flex; align-items: center; gap: 8px; }
79 .scatter-actions { justify-content: flex-end; margin-top: 6px; }
80 button.mute { min-width: 36px; height: 30px; padding: 0 9px; display: inline-flex; align-items: center; justify-content: center; gap: 5px; }
81 button.mute svg { width: 18px; height: 18px; flex: none; }
82 button.mute.is-muted { background: #fbece4; border-color: #e8c2ad; color: #b0482a; font-size: 12px; }
83 button.mute.is-muted:hover { border-color: #d9a68c; }
84 button.mute.audible { background: var(--accent-soft); border-color: #b9cde0; color: var(--accent); }
85 .chips { display: flex; flex-wrap: wrap; gap: 6px; }
86 .chip { position: relative; cursor: pointer; user-select: none; }
87 .chip input { position: absolute; opacity: 0; pointer-events: none; }
88 .chip span {
89 display: inline-block; font-size: 12px; padding: 3px 11px; border-radius: 999px;
90 border: 1px solid var(--line); color: var(--muted); background: #fff;
91 }
92 .chip:hover span { border-color: #c9c8c0; }
93 .chip input:checked + span { background: var(--accent-soft); border-color: #b9cde0; color: var(--accent); }
94 .chip input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 1px; }
95 #traces canvas { height: auto; margin-top: 12px; }
96 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; 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; }
112 summary::-webkit-details-marker { display: none; }
113 summary::before { content: "▸ "; }
114 details[open] summary::before { content: "▾ "; }
115 #param-form { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px 16px; margin-top: 10px; }
116 #param-form label { display: grid; grid-template-columns: 1.6em 5.5em auto; align-items: center; gap: 6px; font-size: 13px; }
117 #param-form .unit { color: var(--muted); font-size: 12px; }
118 #param-actions { margin-top: 10px; }
119 #param-status { color: #a33; font-size: 12px; }
120 #params { margin-top: 10px; font: 12px/1.6 var(--mono); color: var(--muted); white-space: pre; overflow-x: auto; }
121 </style>
122 </head>
123 <body>
124 <main>
125 <header><h1>FPUT chain chaos</h1></header>
126 <div class="grid">
127 <section class="card">
128 <canvas id="chain"></canvas>
129 <div id="controls">
130 <div class="row">
131 <button id="strike" class="primary">Strike</button>
132 <select id="strike-kind">
133 <option value="mode">mode</option>
134 <option value="mass" selected>mass</option>
135 </select>
136 <input id="strike-index" type="number" min="1" value="1" step="1" />
137 <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>
139 </div>
140 <div class="row">
141 <span class="muted">Strike energy</span>
142 <input id="energy" type="range" min="-5" max="2" step="0.01" value="0.4" />
143 <span id="energy-value" class="num"></span>
144 </div>
145 <div class="row">
146 <span class="muted">Masses</span>
147 <input id="n" type="number" min="2" max="64" step="1" value="2" />
148 <span class="muted" style="margin-left: 12px">Speed</span>
149 <select id="speed">
150 <option value="0.1">0.1×</option>
151 <option value="0.25">0.25×</option>
152 <option value="0.5">0.5×</option>
153 <option value="1" selected>1× (real time)</option>
154 <option value="2">2×</option>
155 <option value="5">5×</option>
156 <option value="10">10×</option>
157 <option value="20">20×</option>
158 </select>
159 </div>
160 </div>
161 <div id="ghost-row" class="row">
162 <label class="chip" title="A second chain started with every mass moved by a tiny offset"><input id="ghost-toggle" type="checkbox" /><span>Ghost</span></label>
163 <span id="ghost-controls" class="row" hidden>
164 <span class="muted">Offset</span>
165 <input id="ghost-offset" type="range" min="-12" max="-1" step="1" value="-10" aria-label="Ghost offset (power of ten, m)" />
166 <span id="ghost-offset-value" class="num"></span>
167 <button id="ghost-reset" type="button" title="Restart the ghost from the chain's current state">Restart ghost</button>
168 </span>
169 <span id="ghost-sep" class="num muted"></span>
170 </div>
171 <details>
172 <summary>Parameters <button type="button" class="help" data-help="params" title="About the parameters" aria-label="About the parameters">?</button></summary>
173 <div id="param-form">
174 <label>m <input id="p-m" type="number" step="any" /> <span class="unit">kg</span></label>
175 <label>k <input id="p-k" type="number" step="any" /> <span class="unit">N/m</span></label>
176 <label>k3 <input id="p-k3" type="number" step="any" /> <span class="unit">N/m³</span></label>
177 <label>a <input id="p-a" type="number" step="any" /> <span class="unit">m (spacing)</span></label>
178 <label>τ <input id="p-tau" type="number" step="any" /> <span class="unit">s (decay, 0 = off)</span></label>
179 </div>
180 <div id="param-actions" class="row">
181 <button id="param-apply">Apply</button>
182 <span id="param-status"></span>
183 </div>
184 <div id="params"></div>
185 </details>
186 </section>
187 <section class="card">
188 <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>
189 <div class="chips">
190 <label class="chip"><input id="bars-toggle" type="checkbox" checked /><span>Mode bars</span></label>
191 <label class="chip" title="Each mode's share of the energy over the last 300 s"><input id="live-modes-toggle" type="checkbox" /><span>Mode history</span></label>
192 <span id="toggles"></span>
193 </div>
194 <figure id="bars">
195 <canvas id="modes"></canvas>
196 <div id="mode-stats" class="num muted"></div>
197 </figure>
198 <figure id="live-modes-fig" hidden><div id="live-modes"></div></figure>
199 <div id="traces"></div>
200 </section>
201 </div>
202 <details class="card panel" id="sound" open>
203 <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>
204 <div class="row">
205 <button id="mute" type="button" class="mute" title="Unmute" aria-label="Unmute"></button>
206 <label><span class="label">Signal</span> <select id="signal"></select></label>
207 <input id="sound-mass" type="number" min="1" step="1" value="1" aria-label="Mass" />
208 <label><span class="label">Volume</span> <input id="volume" type="range" min="0" max="1" step="0.01" value="0.5" /></label>
209 <button id="replay" type="button" disabled title="Play the last strike again with the current settings">Replay</button>
210 <button id="save-wav" type="button" disabled title="Save the last note as a WAV file">Save WAV</button>
211 <span id="sound-chips" class="chips">
212 <label class="chip"><input id="spec-toggle" type="checkbox" checked /><span>Spectrogram</span></label>
213 <label class="chip" title="Each mode's share of the energy over the note"><input id="modes-toggle" type="checkbox" /><span>Modes</span></label>
214 </span>
215 </div>
216 <div class="row">
217 <label class="grow"><span class="label">Pitch</span> <input id="pitch" type="range" min="0.699" max="3" step="0.001" value="2" aria-label="Pitch of the lowest mode" /></label>
218 <span id="pitch-value" class="num"></span>
219 </div>
220 <div class="row"><span id="sound-status" class="muted"></span></div>
221 <p id="sound-hint" class="muted">Strike to hear the whole decay.</p>
222 <figure id="spec-fig"><div id="spectrogram"></div></figure>
223 <figure id="modes-fig" hidden><div id="mode-history"></div></figure>
224 <div id="sound-traces"></div>
225 <figure id="scatter-fig" hidden>
226 <div id="scatter"></div>
227 <div class="row scatter-actions"><button id="scatter-clear" type="button">Clear</button></div>
228 </figure>
229 </details>
231 <details class="card panel" id="poincare" open hidden>
232 <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>
233 <div class="row">
234 <span class="label">Energy</span>
235 <input id="pc-energy" type="range" min="-5" max="2" step="0.01" value="0.4" aria-label="Section energy" />
236 <span id="pc-energy-value" class="num"></span>
237 <label title="Real time: the section's energy follows the chain's as it decays. Sped up: the sped-up run's crossings are shown as it plays, on the section at its current energy."><span class="label">Follow</span> <select id="pc-follow">
238 <option value="off">off</option>
239 <option value="realtime">real time</option>
240 <option value="sound">sped up</option>
241 </select></label>
242 <button id="pc-atlas" type="button" title="Compute sections at 32 energies (1 mJ to 100 J), so Follow sped up can show the orbits behind the decaying note's trail. Kept in this browser for 7 days." hidden>Precompute sections</button>
243 </div>
244 <div class="row">
245 <label class="chip"><input id="pc-lambda" type="checkbox" /><span>Color by λ</span></label>
246 <label class="chip" title="Clicking a point also strikes the chain from that exact state, at this energy"><input id="pc-strike" type="checkbox" checked /><span>Click to strike</span></label>
247 </div>
248 <figure><div id="pc-plot"></div></figure>
249 </details>
250 </main>
251 <dialog id="help">
252 <div id="help-body"></div>
253 <div id="help-actions"><button id="help-close" type="button">Close</button></div>
254 </dialog>
256 <template id="help-chain">
257 <h2>The chain</h2>
258 <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>
259 <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>
260 </template>
261 <template id="help-params">
262 <h2>Parameters</h2>
263 <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>
264 </template>
265 <template id="help-realtime">
266 <h2>Real time</h2>
267 <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>
268 <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>
269 </template>
270 <template id="help-sound">
271 <h2>Sped up · listen</h2>
272 <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>
273 <p>Save WAV saves the last note as heard (16-bit mono, normalized). 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>
274 </template>
275 <template id="help-section">
276 <h2>Poincaré section</h2>
277 <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>
278 <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>
279 </template>
280 <script type="module" src="/src/main.ts"></script>
281 </body>
282</html>