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); }
d3c8c3fSound panel, mute, lambda vs energy scatter, hover cursor on chartsJeremy Magland 59 button.mute { width: 36px; height: 30px; padding: 0; display: inline-flex; align-items: center; justify-content: center; color: var(--muted); }
60 button.mute svg { width: 18px; height: 18px; }
61 button.mute.audible { background: var(--accent-soft); border-color: #b9cde0; color: var(--accent); }
63 #controls { margin-top: 14px; }
88f11eeStrike energy label and current energy; tidier section controlsJeremy Magland 64 #controls { gap: 8px; }
65 #energy { min-width: 40px; }
66 #energy-value { min-width: 4.8em; text-align: right; }
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 67 #ghost-row { margin-top: 10px; min-height: 28px; }
68 #ghost-controls { flex: 1; }
69 #ghost-offset { max-width: 140px; min-width: 80px; }
70 #ghost-sep { width: 100%; }
72 details { margin-top: 14px; border-top: 1px solid var(--line); padding-top: 10px; }
73 summary { cursor: pointer; color: var(--muted); font-size: 13px; list-style: none; }
d3c8c3fSound panel, mute, lambda vs energy scatter, hover cursor on chartsJeremy Magland 74 summary::-webkit-details-marker { display: none; }
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 75 summary::before { content: "▸ "; }
76 details[open] summary::before { content: "▾ "; }
5ca3fa4Poincare section panel; minimal textJeremy Magland 77 details.panel { border-top: 1px solid var(--line); padding-top: 16px; margin-top: 16px; }
78 details.panel summary { display: flex; align-items: baseline; gap: 10px; }
79 details.panel summary .label { font-size: 12px; color: var(--text); }
80 details.panel[open] summary { margin-bottom: 14px; }
81 #pc-energy { max-width: 360px; }
88f11eeStrike energy label and current energy; tidier section controlsJeremy Magland 82 #poincare .row + .row { margin-top: 10px; }
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 83 #param-form { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px 16px; margin-top: 10px; }
84 #param-form label { display: grid; grid-template-columns: 1.6em 5.5em auto; align-items: center; gap: 6px; font-size: 13px; }
85 #param-form .unit { color: var(--muted); font-size: 12px; }
86 #param-actions { margin-top: 10px; }
87 #param-status { color: #a33; font-size: 12px; }
88 #params { margin-top: 10px; font: 12px/1.6 var(--mono); color: var(--muted); white-space: pre; overflow-x: auto; }
4c27473Label the two clocks; section rings only at matching energy; Sound opens without a noteJeremy Magland 90 .card-head { color: var(--text); font-size: 12px; margin-bottom: 12px; }
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 91 .chips { display: flex; flex-wrap: wrap; gap: 6px; }
92 .chip { position: relative; cursor: pointer; user-select: none; }
93 .chip input { position: absolute; opacity: 0; pointer-events: none; }
94 .chip span {
95 display: inline-block; font-size: 12px; padding: 3px 11px; border-radius: 999px;
96 border: 1px solid var(--line); color: var(--muted); background: #fff;
97 }
98 .chip:hover span { border-color: #c9c8c0; }
99 .chip input:checked + span { background: var(--accent-soft); border-color: #b9cde0; color: var(--accent); }
100 .chip input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 1px; }
102 figure { margin: 14px 0 0; }
103 figure canvas { width: 100%; height: auto; border-radius: 4px; }
5ca3fa4Poincare section panel; minimal textJeremy Magland 104 .scatter-actions { justify-content: flex-end; margin-top: 6px; }
30a92c8Whole-note sound displays with Lyapunov exponent and energy; strike jitterJeremy Magland 105 #traces canvas, #audio-traces canvas { width: 100%; height: auto; margin-top: 12px; }
107 #audio { margin-top: 16px; }
108 #audio .row + .row { margin-top: 12px; }
109 #audio label { display: flex; align-items: center; gap: 8px; }
110 #audio .grow { flex: 1; min-width: 220px; }
111 #volume { max-width: 110px; }
112 </style>
113 </head>
114 <body>
115 <main>
116 <header>
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 118 </header>
120 <div class="grid">
121 <section class="card">
122 <canvas id="canvas"></canvas>
123 <form id="controls" class="row">
88f11eeStrike energy label and current energy; tidier section controlsJeremy Magland 124 <span id="energy-label" class="label">Strike energy</span>
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 125 <input id="energy" type="range" value="50" min="0" max="120" step="0.1" aria-label="Energy" />
126 <span id="energy-value" class="num">50.0 J</span>
127 <button type="submit" class="primary" title="Start from hanging rest with this energy">Strike</button>
128 <button type="button" id="pause" disabled>Pause</button>
129 </form>
130 <div id="ghost-row" class="row">
131 <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>
132 <span id="ghost-controls" class="row" hidden>
133 <span class="label">Offset</span>
134 <input id="ghost-offset" type="range" min="-12" max="-1" step="1" value="-10" aria-label="Ghost offset (power of ten)" />
135 <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 136 <button id="ghost-reset" type="button" title="Restart the ghost from the pendulum's current state">Restart ghost</button>
138 <span id="ghost-sep" class="num muted"></span>
139 </div>
140 <details>
141 <summary>Parameters</summary>
142 <form id="param-form">
143 <label><span>m<sub>1</sub></span> <input id="p-m1" type="number" step="any" /> <span class="unit">kg</span></label>
144 <label><span>m<sub>2</sub></span> <input id="p-m2" type="number" step="any" /> <span class="unit">kg</span></label>
145 <label><span>l<sub>1</sub></span> <input id="p-l1" type="number" step="any" /> <span class="unit">m</span></label>
146 <label><span>l<sub>2</sub></span> <input id="p-l2" type="number" step="any" /> <span class="unit">m</span></label>
147 <label><span>g</span> <input id="p-g" type="number" step="any" /> <span class="unit">m/s²</span></label>
148 <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>
149 </form>
150 <div id="param-actions" class="row">
151 <button type="submit" form="param-form">Apply</button>
152 <label class="muted">Preset <select id="preset">
153 <option value="">custom</option>
154 <option value="equal">l2 = 1 m</option>
155 <option value="tenth">l2 = 1.5667 m</option>
156 <option value="golden">l2 = 2 m</option>
157 <option value="eightThirds">l2 = 2.1637 m</option>
158 <option value="twelfth">l2 = 3.2476 m</option>
159 <option value="doubleOctave">l2 = 6.886 m</option>
160 </select></label>
161 <span id="param-status"></span>
162 </div>
163 <div id="params"></div>
164 </details>
165 </section>
167 <section class="card">
4c27473Label the two clocks; section rings only at matching energy; Sound opens without a noteJeremy Magland 168 <div class="card-head label">Real time</div>
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 169 <div class="chips" id="toggles">
170 <label class="chip"><input id="spec-toggle" type="checkbox" /><span>Spectrogram</span></label>
171 </div>
172 <figure id="spec-fig" hidden>
173 <div id="spectrogram"></div>
174 </figure>
175 <div id="traces"></div>
176 </section>
177 </div>
5ca3fa4Poincare section panel; minimal textJeremy Magland 179 <details class="card panel" id="audio">
b01b39eRename the sound panel "Sped up · listen"; Follow sped up opens it; section default 17 JJeremy Magland 180 <summary><span class="label">Sped up · listen</span> <span id="audio-clock" class="muted"></span></summary>
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 181 <div class="row">
d3c8c3fSound panel, mute, lambda vs energy scatter, hover cursor on chartsJeremy Magland 182 <button id="mute" type="button" class="mute" title="Unmute" aria-label="Unmute"></button>
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 183 <label><span class="label">Sound of</span> <select id="signal"></select></label>
184 <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 185 <span id="audio-chips" class="chips">
d3c8c3fSound panel, mute, lambda vs energy scatter, hover cursor on chartsJeremy Magland 186 <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 187 </span>
189 <div class="row">
190 <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>
191 <span id="pitch-value" class="num"></span>
192 </div>
193 <div class="row"><span id="audio-status" class="muted"></span></div>
d3c8c3fSound panel, mute, lambda vs energy scatter, hover cursor on chartsJeremy Magland 194 <figure id="audio-spec-fig">
954de9bDouble pendulum with audio, damping, ghost, and Lyapunov exponentJeremy Magland 195 <div id="audio-spectrogram"></div>
196 </figure>
30a92c8Whole-note sound displays with Lyapunov exponent and energy; strike jitterJeremy Magland 197 <div id="audio-traces"></div>
d3c8c3fSound panel, mute, lambda vs energy scatter, hover cursor on chartsJeremy Magland 198 <figure id="scatter-fig" hidden>
199 <div id="scatter"></div>
5ca3fa4Poincare section panel; minimal textJeremy Magland 200 <div class="row scatter-actions"><button id="scatter-clear" type="button">Clear</button></div>
201 </figure>
202 </details>
204 <details class="card panel" id="poincare">
205 <summary><span class="label">Poincaré section</span></summary>
206 <div class="row">
207 <span class="label">Energy</span>
b01b39eRename the sound panel "Sped up · listen"; Follow sped up opens it; section default 17 JJeremy Magland 208 <input id="pc-energy" type="range" value="17" min="0" max="120" step="0.1" aria-label="Section energy" />
209 <span id="pc-energy-value" class="num">17.0 J</span>
210 <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 211 <option value="off">off</option>
b01b39eRename the sound panel "Sped up · listen"; Follow sped up opens it; section default 17 JJeremy Magland 212 <option value="pendulum">real time</option>
213 <option value="sound">sped up</option>
88f11eeStrike energy label and current energy; tidier section controlsJeremy Magland 215 <button id="pc-atlas" type="button" title="Compute sections at 24 energies (about a minute or two), so Follow sound can show the orbits behind the decaying note's trail. Kept in this browser for 7 days." hidden>Precompute sections</button>
216 </div>
217 <div class="row">
218 <label class="chip"><input id="pc-lambda" type="checkbox" /><span>Color by λ</span></label>
219 <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>
221 <figure>
222 <div id="pc-plot"></div>
d3c8c3fSound panel, mute, lambda vs energy scatter, hover cursor on chartsJeremy Magland 223 </figure>
224 </details>
226 <script type="module" src="/src/main.ts"></script>
227 </body>
228</html>