2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
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 {
22 margin: 0;
23 background: var(--bg);
24 color: var(--text);
25 font: 14px/1.45 var(--sans);
26 }
27 main { max-width: 1040px; margin: 0 auto; padding: 28px 16px 48px; }
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 29 h1 { font: 400 28px/1.2 var(--serif); margin: 0 0 4px; letter-spacing: 0.01em; }
30 .grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; align-items: start; }
31 @media (max-width: 900px) { .grid { grid-template-columns: minmax(0, 1fr); } }
32 .card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 16px; }
33 .label {
34 font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted);
35 }
36 .row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
37 .num { font: 13px var(--mono); font-variant-numeric: tabular-nums; color: var(--text); }
38 .muted { color: var(--muted); font-size: 12px; }
40 canvas { display: block; }
41 canvas[hidden], figure[hidden], [hidden] { display: none ; }
42 #canvas { width: 100%; aspect-ratio: 1; }
44 input[type="range"] { accent-color: var(--accent); flex: 1; min-width: 120px; margin: 0; }
45 select, input[type="number"] {
46 font: inherit; font-size: 13px; color: var(--text); background: #fff;
47 border: 1px solid var(--line); border-radius: 6px; padding: 3px 6px;
48 }
49 input[type="number"] { width: 5.5em; font-family: var(--mono); }
50 button {
51 font: inherit; font-size: 13px; color: var(--text); background: #fff; cursor: pointer;
52 border: 1px solid var(--line); border-radius: 6px; padding: 5px 12px;
53 }
54 button:hover { border-color: #c9c8c0; }
55 button.primary { background: var(--accent); border-color: var(--accent); color: #fff; min-width: 76px; }
56 button.primary:hover { filter: brightness(1.08); }
57 button:disabled { color: #b5b5b0; cursor: default; }
58 button:disabled:hover { border-color: var(--line); }
3833e70Help buttons on each panel; wider spectrogram frequency range; Signal selector; clearer muted stateJeremy Magland 59 button.mute { min-width: 36px; height: 30px; padding: 0 9px; display: inline-flex; align-items: center; justify-content: center; gap: 5px; }
60 button.mute svg { width: 18px; height: 18px; flex: none; }
61 button.mute.is-muted { background: #fbece4; border-color: #e8c2ad; color: #b0482a; font-size: 12px; }
62 button.mute.is-muted:hover { border-color: #d9a68c; }
d3c8c3fSound panel, mute, lambda vs energy scatter, hover cursor on chartsJeremy Magland 63 button.mute.audible { background: var(--accent-soft); border-color: #b9cde0; color: var(--accent); }
65 #controls { margin-top: 14px; }
88f11eeStrike energy label and current energy; tidier section controlsJeremy Magland 66 #controls { gap: 8px; }
67 #energy { min-width: 40px; }
68 #energy-value { min-width: 4.8em; text-align: right; }
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 69 #ghost-row { margin-top: 10px; min-height: 28px; }
70 #ghost-controls { flex: 1; }
71 #ghost-offset { max-width: 140px; min-width: 80px; }
72 #ghost-sep { width: 100%; }
74 details { margin-top: 14px; border-top: 1px solid var(--line); padding-top: 10px; }
3833e70Help buttons on each panel; wider spectrogram frequency range; Signal selector; clearer muted stateJeremy Magland 75 summary { cursor: pointer; color: var(--muted); font-size: 13px; list-style: none; display: flex; align-items: center; gap: 4px; }
d3c8c3fSound panel, mute, lambda vs energy scatter, hover cursor on chartsJeremy Magland 76 summary::-webkit-details-marker { display: none; }
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 77 summary::before { content: "▸ "; }
78 details[open] summary::before { content: "▾ "; }
5ca3fa4Poincare section panel; minimal textJeremy Magland 79 details.panel { border-top: 1px solid var(--line); padding-top: 16px; margin-top: 16px; }
3833e70Help buttons on each panel; wider spectrogram frequency range; Signal selector; clearer muted stateJeremy Magland 80 details.panel summary { gap: 10px; }
5ca3fa4Poincare section panel; minimal textJeremy Magland 81 details.panel summary .label { font-size: 12px; color: var(--text); }
82 details.panel[open] summary { margin-bottom: 14px; }
83 #pc-energy { max-width: 360px; }
88f11eeStrike energy label and current energy; tidier section controlsJeremy Magland 84 #poincare .row + .row { margin-top: 10px; }
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 85 #param-form { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px 16px; margin-top: 10px; }
86 #param-form label { display: grid; grid-template-columns: 1.6em 5.5em auto; align-items: center; gap: 6px; font-size: 13px; }
87 #param-form .unit { color: var(--muted); font-size: 12px; }
88 #param-actions { margin-top: 10px; }
89 #param-status { color: #a33; font-size: 12px; }
90 #params { margin-top: 10px; font: 12px/1.6 var(--mono); color: var(--muted); white-space: pre; overflow-x: auto; }
3833e70Help buttons on each panel; wider spectrogram frequency range; Signal selector; clearer muted stateJeremy Magland 92 .card-head { color: var(--text); font-size: 12px; margin-bottom: 12px; display: flex; align-items: center; }
94 button.help {
95 width: 20px; height: 20px; padding: 0; margin-left: auto; flex: none; border-radius: 50%;
96 font: 600 11px/1 var(--sans); color: #a5a7aa; background: transparent; letter-spacing: 0; text-transform: none;
97 }
98 button.help:hover { color: var(--accent); border-color: #b9cde0; }
99 #pendulum-card { position: relative; }
100 #pendulum-card > button.help { position: absolute; top: 12px; right: 12px; }
101 dialog#help {
102 max-width: min(460px, calc(100vw - 32px)); padding: 0; border: 1px solid var(--line); border-radius: 10px;
103 color: var(--text); background: var(--card); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
104 }
105 dialog#help::backdrop { background: rgba(35, 38, 43, 0.25); }
106 #help-body { padding: 18px 20px 4px; font-size: 13.5px; line-height: 1.5; }
107 #help-body h2 { font: 400 19px/1.3 var(--serif); margin: 0 0 8px; }
108 #help-body p { margin: 0 0 10px; }
109 #help-actions { display: flex; justify-content: flex-end; padding: 0 20px 16px; }
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 110 .chips { display: flex; flex-wrap: wrap; gap: 6px; }
111 .chip { position: relative; cursor: pointer; user-select: none; }
112 .chip input { position: absolute; opacity: 0; pointer-events: none; }
113 .chip span {
114 display: inline-block; font-size: 12px; padding: 3px 11px; border-radius: 999px;
115 border: 1px solid var(--line); color: var(--muted); background: #fff;
116 }
117 .chip:hover span { border-color: #c9c8c0; }
118 .chip input:checked + span { background: var(--accent-soft); border-color: #b9cde0; color: var(--accent); }
119 .chip input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 1px; }
121 figure { margin: 14px 0 0; }
122 figure canvas { width: 100%; height: auto; border-radius: 4px; }
5ca3fa4Poincare section panel; minimal textJeremy Magland 123 .scatter-actions { justify-content: flex-end; margin-top: 6px; }
30a92c8Whole-note sound displays with Lyapunov exponent and energy; strike jitterJeremy Magland 124 #traces canvas, #audio-traces canvas { width: 100%; height: auto; margin-top: 12px; }
126 #audio { margin-top: 16px; }
127 #audio .row + .row { margin-top: 12px; }
128 #audio label { display: flex; align-items: center; gap: 8px; }
129 #audio .grow { flex: 1; min-width: 220px; }
130 #volume { max-width: 110px; }
2a464d3Drop the real-time spectrogram; strike hint and unmute on first open; log energy in the sped-up charts; chart time spans; section says when the pendulum is off its energyJeremy Magland 131 #audio-hint { margin: 14px 0 0; }
133 </head>
134 <body>
135 <main>
136 <header>
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 138 </header>
140 <div class="grid">
3833e70Help buttons on each panel; wider spectrogram frequency range; Signal selector; clearer muted stateJeremy Magland 141 <section class="card" id="pendulum-card">
142 <button type="button" class="help" data-help="pendulum" title="About the pendulum" aria-label="About the pendulum">?</button>
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 143 <canvas id="canvas"></canvas>
144 <form id="controls" class="row">
88f11eeStrike energy label and current energy; tidier section controlsJeremy Magland 145 <span id="energy-label" class="label">Strike energy</span>
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 146 <input id="energy" type="range" value="50" min="0" max="120" step="0.1" aria-label="Energy" />
147 <span id="energy-value" class="num">50.0 J</span>
148 <button type="submit" class="primary" title="Start from hanging rest with this energy">Strike</button>
149 <button type="button" id="pause" disabled>Pause</button>
150 </form>
151 <div id="ghost-row" class="row">
152 <label class="chip" title="A second pendulum started with the outer arm offset by a tiny angle"><input id="ghost-toggle" type="checkbox" /><span>Ghost</span></label>
153 <span id="ghost-controls" class="row" hidden>
154 <span class="label">Offset</span>
155 <input id="ghost-offset" type="range" min="-12" max="-1" step="1" value="-10" aria-label="Ghost offset (power of ten)" />
156 <span id="ghost-offset-value" class="num"></span>
0f905b0Tidier chart numbers, Lyapunov range skips the strike transient; Restart ghost; Separation only with the ghostJeremy Magland 157 <button id="ghost-reset" type="button" title="Restart the ghost from the pendulum's current state">Restart ghost</button>
159 <span id="ghost-sep" class="num muted"></span>
160 </div>
161 <details>
3833e70Help buttons on each panel; wider spectrogram frequency range; Signal selector; clearer muted stateJeremy Magland 162 <summary>Parameters <button type="button" class="help" data-help="params" title="About the parameters" aria-label="About the parameters">?</button></summary>
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 163 <form id="param-form">
164 <label><span>m<sub>1</sub></span> <input id="p-m1" type="number" step="any" /> <span class="unit">kg</span></label>
165 <label><span>m<sub>2</sub></span> <input id="p-m2" type="number" step="any" /> <span class="unit">kg</span></label>
166 <label><span>l<sub>1</sub></span> <input id="p-l1" type="number" step="any" /> <span class="unit">m</span></label>
167 <label><span>l<sub>2</sub></span> <input id="p-l2" type="number" step="any" /> <span class="unit">m</span></label>
168 <label><span>g</span> <input id="p-g" type="number" step="any" /> <span class="unit">m/s²</span></label>
169 <label title="Amplitude decay time from drag on the bobs (all modes). 0 = no damping."><span>τ</span> <input id="p-tau" type="number" step="any" min="0" /> <span class="unit">s decay</span></label>
170 </form>
171 <div id="param-actions" class="row">
172 <button type="submit" form="param-form">Apply</button>
173 <label class="muted">Preset <select id="preset">
174 <option value="">custom</option>
175 <option value="equal">l2 = 1 m</option>
176 <option value="tenth">l2 = 1.5667 m</option>
177 <option value="golden">l2 = 2 m</option>
178 <option value="eightThirds">l2 = 2.1637 m</option>
179 <option value="twelfth">l2 = 3.2476 m</option>
180 <option value="doubleOctave">l2 = 6.886 m</option>
181 </select></label>
182 <span id="param-status"></span>
183 </div>
184 <div id="params"></div>
185 </details>
186 </section>
188 <section class="card">
3833e70Help buttons on each panel; wider spectrogram frequency range; Signal selector; clearer muted stateJeremy Magland 189 <div class="card-head label">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>
2a464d3Drop the real-time spectrogram; strike hint and unmute on first open; log energy in the sped-up charts; chart time spans; section says when the pendulum is off its energyJeremy Magland 190 <div class="chips" id="toggles"></div>
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 191 <div id="traces"></div>
192 </section>
193 </div>
5ca3fa4Poincare section panel; minimal textJeremy Magland 195 <details class="card panel" id="audio">
3833e70Help buttons on each panel; wider spectrogram frequency range; Signal selector; clearer muted stateJeremy Magland 196 <summary><span class="label">Sped up · listen</span> <span id="audio-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>
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 197 <div class="row">
d3c8c3fSound panel, mute, lambda vs energy scatter, hover cursor on chartsJeremy Magland 198 <button id="mute" type="button" class="mute" title="Unmute" aria-label="Unmute"></button>
424f395Save WAVJeremy Magland 199 <button id="save-wav" type="button" disabled title="Save the current or last note as a WAV file">Save WAV</button>
3833e70Help buttons on each panel; wider spectrogram frequency range; Signal selector; clearer muted stateJeremy Magland 200 <label><span class="label">Signal</span> <select id="signal"></select></label>
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 201 <label><span class="label">Volume</span> <input id="volume" type="range" min="0" max="1" step="0.01" value="0.5" /></label>
30a92c8Whole-note sound displays with Lyapunov exponent and energy; strike jitterJeremy Magland 202 <span id="audio-chips" class="chips">
d3c8c3fSound panel, mute, lambda vs energy scatter, hover cursor on chartsJeremy Magland 203 <label class="chip"><input id="audio-spec-toggle" type="checkbox" checked /><span>Spectrogram</span></label>
30a92c8Whole-note sound displays with Lyapunov exponent and energy; strike jitterJeremy Magland 204 </span>
206 <div class="row">
207 <label class="grow"><span class="label">Pitch</span> <input id="pitch" type="range" min="0" max="1000" step="1" aria-label="Pitch of the lower mode" /></label>
208 <span id="pitch-value" class="num"></span>
209 </div>
210 <div class="row"><span id="audio-status" class="muted"></span></div>
2a464d3Drop the real-time spectrogram; strike hint and unmute on first open; log energy in the sped-up charts; chart time spans; section says when the pendulum is off its energyJeremy Magland 211 <p id="audio-hint" class="muted">Strike, or click a point on the Poincaré section, to hear the whole decay.</p>
d3c8c3fSound panel, mute, lambda vs energy scatter, hover cursor on chartsJeremy Magland 212 <figure id="audio-spec-fig">
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 213 <div id="audio-spectrogram"></div>
214 </figure>
30a92c8Whole-note sound displays with Lyapunov exponent and energy; strike jitterJeremy Magland 215 <div id="audio-traces"></div>
d3c8c3fSound panel, mute, lambda vs energy scatter, hover cursor on chartsJeremy Magland 216 <figure id="scatter-fig" hidden>
217 <div id="scatter"></div>
5ca3fa4Poincare section panel; minimal textJeremy Magland 218 <div class="row scatter-actions"><button id="scatter-clear" type="button">Clear</button></div>
219 </figure>
220 </details>
222 <details class="card panel" id="poincare">
3833e70Help buttons on each panel; wider spectrogram frequency range; Signal selector; clearer muted stateJeremy Magland 223 <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>
225 <span class="label">Energy</span>
b01b39eRename the sound panel "Sped up · listen"; Follow sped up opens it; section default 17 JJeremy Magland 226 <input id="pc-energy" type="range" value="17" min="0" max="120" step="0.1" aria-label="Section energy" />
227 <span id="pc-energy-value" class="num">17.0 J</span>
228 <label title="Real time: the section's energy follows the pendulum'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">
97e0cbfSection follows the sound; atlas; pause everythingJeremy Magland 229 <option value="off">off</option>
b01b39eRename the sound panel "Sped up · listen"; Follow sped up opens it; section default 17 JJeremy Magland 230 <option value="pendulum">real time</option>
231 <option value="sound">sped up</option>
2a464d3Drop the real-time spectrogram; strike hint and unmute on first open; log energy in the sped-up charts; chart time spans; section says when the pendulum is off its energyJeremy Magland 233 <button id="pc-atlas" type="button" title="Compute sections at 24 energies (about a minute or two), 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>
235 <div class="row">
236 <label class="chip"><input id="pc-lambda" type="checkbox" /><span>Color by λ</span></label>
237 <label class="chip" title="Clicking a point also strikes the pendulum from that exact state, at this energy"><input id="pc-strike" type="checkbox" checked /><span>Click to strike</span></label>
239 <figure>
240 <div id="pc-plot"></div>
d3c8c3fSound panel, mute, lambda vs energy scatter, hover cursor on chartsJeremy Magland 241 </figure>
242 </details>
3833e70Help buttons on each panel; wider spectrogram frequency range; Signal selector; clearer muted stateJeremy Magland 244
245 <dialog id="help">
246 <div id="help-body"></div>
247 <div id="help-actions"><button id="help-close" type="button">Close</button></div>
248 </dialog>
250 <template id="help-pendulum">
251 <h2>The pendulum</h2>
252 <p>A double pendulum, shown in real time. Strike starts it from hanging rest with the energy on the slider. At low energy the motion is regular; at higher energies most motions are chaotic, so a small change in the start leads to a completely different motion.</p>
253 <p>With damping (τ under Parameters) the energy slowly decays, and the current energy is shown at the top left. Without damping the slider changes the energy as it runs.</p>
254 <p>Ghost adds a second pendulum started a tiny angle away. In chaotic motion the two separate exponentially; in regular motion they stay together. The space bar pauses.</p>
255 </template>
256 <template id="help-params">
257 <h2>Parameters</h2>
258 <p>Masses, arm lengths, gravity, and the decay time τ of the amplitude (drag on the bobs; 0 for none). Apply restarts the pendulum at rest. They apply to everything on the page.</p>
259 <p>The presets change the outer arm's length, which sets the ratio of the two normal-mode frequencies (listed below the form). These are the pitches heard at low energy.</p>
260 </template>
261 <template id="help-realtime">
262 <h2>Real time</h2>
263 <p>Charts of the pendulum on the left, on the same clock. Click a chip to show or hide a chart. Hover over a chart to read values; click to pin the cursor.</p>
264 <p>Error is the integrator's relative error in the energy balance (the energy plus what damping has removed); it should stay tiny. Lyapunov is the largest Lyapunov exponent: the rate (1/s) at which nearby motions separate, positive for chaos and near 0 for regular motion. Separation, shown with the ghost on, is the ghost's distance from the pendulum on a log scale.</p>
265 </template>
266 <template id="help-sound">
267 <h2>Sped up · listen</h2>
268 <p>A copy of the same strike, run fast enough that the swinging becomes sound: Pitch sets where the lower normal mode lands. The whole decay plays in a few seconds. While this panel is open, each Strike plays a note.</p>
424f395Save WAVJeremy Magland 269 <p>Save WAV saves the current or last note as heard (16-bit mono, normalized). Signal chooses which coordinate is heard. Chaotic motion sounds noisy; as the energy decays it settles into steady tones.</p>
3833e70Help buttons on each panel; wider spectrogram frequency range; Signal selector; clearer muted stateJeremy Magland 270 <p>The charts span the whole note, strike at the left: spectrogram, Lyapunov exponent, energy, and λ against energy collected over notes.</p>
271 </template>
272 <template id="help-section">
273 <h2>Poincaré section</h2>
274 <p>A map of the motions at one energy, without damping. Each time the inner arm swings down through vertical moving right, the outer arm's angle θ2 and momentum p2 are plotted as a dot. Regular motions trace closed curves; chaotic ones scatter over a region. The outline bounds what is reachable at this energy.</p>
275 <p>The pendulum's own crossings are dark rings. Follow makes the section's energy track the pendulum (real time) or the playing note (sped up). Hover to pick out an orbit; click to start a new one there, which with Click to strike also strikes the pendulum from that point.</p>
276 </template>
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 277 <script type="module" src="/src/main.ts"></script>
278 </body>
279</html>