/ listening-for-chaos / double-pendulum-chaos
Sign in
listening-for-chaos / double-pendulum-chaos
double-pendulum-chaos / index.html
279 lines · 17.4 KBBlameHistoryRaw
1<!doctype html>
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>Double pendulum 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 {
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; }
28 header { margin-bottom: 16px; }
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 !important; }
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); }
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; }
63 button.mute.audible { background: var(--accent-soft); border-color: #b9cde0; color: var(--accent); }
65 #controls { margin-top: 14px; }
66 #controls { gap: 8px; }
67 #energy { min-width: 40px; }
68 #energy-value { min-width: 4.8em; text-align: right; }
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; }
75 summary { cursor: pointer; color: var(--muted); font-size: 13px; list-style: none; display: flex; align-items: center; gap: 4px; }
76 summary::-webkit-details-marker { display: none; }
77 summary::before { content: "▸ "; }
78 details[open] summary::before { content: "▾ "; }
79 details.panel { border-top: 1px solid var(--line); padding-top: 16px; margin-top: 16px; }
80 details.panel summary { gap: 10px; }
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; }
84 #poincare .row + .row { margin-top: 10px; }
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; }
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; }
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; }
123 .scatter-actions { justify-content: flex-end; margin-top: 6px; }
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; }
131 #audio-hint { margin: 14px 0 0; }
132 </style>
133 </head>
134 <body>
135 <main>
136 <header>
137 <h1>Double pendulum chaos</h1>
138 </header>
140 <div class="grid">
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>
143 <canvas id="canvas"></canvas>
144 <form id="controls" class="row">
145 <span id="energy-label" class="label">Strike energy</span>
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>
157 <button id="ghost-reset" type="button" title="Restart the ghost from the pendulum's current state">Restart ghost</button>
158 </span>
159 <span id="ghost-sep" class="num muted"></span>
160 </div>
161 <details>
162 <summary>Parameters <button type="button" class="help" data-help="params" title="About the parameters" aria-label="About the parameters">?</button></summary>
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">
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>
190 <div class="chips" id="toggles"></div>
191 <div id="traces"></div>
192 </section>
193 </div>
195 <details class="card panel" id="audio">
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>
197 <div class="row">
198 <button id="mute" type="button" class="mute" title="Unmute" aria-label="Unmute"></button>
199 <button id="save-wav" type="button" disabled title="Save the current or last note as a WAV file">Save WAV</button>
200 <label><span class="label">Signal</span> <select id="signal"></select></label>
201 <label><span class="label">Volume</span> <input id="volume" type="range" min="0" max="1" step="0.01" value="0.5" /></label>
202 <span id="audio-chips" class="chips">
203 <label class="chip"><input id="audio-spec-toggle" type="checkbox" checked /><span>Spectrogram</span></label>
204 </span>
205 </div>
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>
211 <p id="audio-hint" class="muted">Strike, or click a point on the Poincaré section, to hear the whole decay.</p>
212 <figure id="audio-spec-fig">
213 <div id="audio-spectrogram"></div>
214 </figure>
215 <div id="audio-traces"></div>
216 <figure id="scatter-fig" hidden>
217 <div id="scatter"></div>
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">
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>
224 <div class="row">
225 <span class="label">Energy</span>
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">
229 <option value="off">off</option>
230 <option value="pendulum">real time</option>
231 <option value="sound">sped up</option>
232 </select></label>
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>
234 </div>
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>
238 </div>
239 <figure>
240 <div id="pc-plot"></div>
241 </figure>
242 </details>
243 </main>
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>
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>
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>
277 <script type="module" src="/src/main.ts"></script>
278 </body>
279</html>
↑↓ move↵openescclose