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; }
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 { 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; }
98 summary::-webkit-details-marker { display: none; }
99 summary::before { content: "▸ "; }
100 details[open] summary::before { content: "▾ "; }
101 #param-form { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px 16px; margin-top: 10px; }
102 #param-form label { display: grid; grid-template-columns: 1.6em 5.5em auto; align-items: center; gap: 6px; font-size: 13px; }
103 #param-form .unit { color: var(--muted); font-size: 12px; }
104 #param-actions { margin-top: 10px; }
105 #param-status { color: #a33; font-size: 12px; }
106 #params { margin-top: 10px; font: 12px/1.6 var(--mono); color: var(--muted); white-space: pre; overflow-x: auto; }
107 </style>
108 </head>
109 <body>
110 <main>
111 <header><h1>FPUT chain chaos</h1></header>
112 <div class="grid">
113 <section class="card">
114 <canvas id="chain"></canvas>
115 <div id="controls">
116 <div class="row">
117 <button id="strike" class="primary">Strike</button>
118 <select id="strike-kind">
119 <option value="mode">mode</option>
120 <option value="mass" selected>mass</option>
121 </select>
122 <input id="strike-index" type="number" min="1" value="1" step="1" />
123 <button id="pause" disabled>Pause</button>
124 </div>
125 <div class="row">
126 <span class="muted">Strike energy</span>
127 <input id="energy" type="range" min="-5" max="2" step="0.01" value="0.4" />
128 <span id="energy-value" class="num"></span>
129 </div>
130 <div class="row">
131 <span class="muted">Masses</span>
132 <input id="n" type="number" min="2" max="64" step="1" value="2" />
133 <span class="muted" style="margin-left: 12px">Speed</span>
134 <select id="speed">
135 <option value="0.1">0.1×</option>
136 <option value="0.25">0.25×</option>
137 <option value="0.5">0.5×</option>
138 <option value="1" selected>1× (real time)</option>
139 <option value="2">2×</option>
140 <option value="5">5×</option>
141 </select>
142 </div>
143 </div>
144 <div id="ghost-row" class="row">
145 <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>
146 <span id="ghost-controls" class="row" hidden>
147 <span class="muted">Offset</span>
148 <input id="ghost-offset" type="range" min="-12" max="-1" step="1" value="-10" aria-label="Ghost offset (power of ten, m)" />
149 <span id="ghost-offset-value" class="num"></span>
150 <button id="ghost-reset" type="button" title="Restart the ghost from the chain's current state">Restart ghost</button>
151 </span>
152 <span id="ghost-sep" class="num muted"></span>
153 </div>
154 <details>
155 <summary>Parameters</summary>
156 <div id="param-form">
157 <label>m <input id="p-m" type="number" step="any" /> <span class="unit">kg</span></label>
158 <label>k <input id="p-k" type="number" step="any" /> <span class="unit">N/m</span></label>
159 <label>k3 <input id="p-k3" type="number" step="any" /> <span class="unit">N/m³</span></label>
160 <label>a <input id="p-a" type="number" step="any" /> <span class="unit">m (spacing)</span></label>
161 <label>τ <input id="p-tau" type="number" step="any" /> <span class="unit">s (decay, 0 = off)</span></label>
162 </div>
163 <div id="param-actions" class="row">
164 <button id="param-apply">Apply</button>
165 <span id="param-status"></span>
166 </div>
167 <div id="params"></div>
168 </details>
169 </section>
170 <section class="card">
171 <div class="card-head">Real time</div>
172 <div class="chips">
173 <label class="chip"><input id="bars-toggle" type="checkbox" checked /><span>Mode bars</span></label>
174 <span id="toggles"></span>
175 </div>
176 <figure id="bars">
177 <canvas id="modes"></canvas>
178 <div id="mode-stats" class="num muted"></div>
179 </figure>
180 <div id="traces"></div>
181 </section>
182 </div>
183 <details class="card panel" id="sound" open>
184 <summary><span class="label">Sped up · listen</span> <span id="sound-clock" class="muted"></span></summary>
185 <div class="row">
186 <button id="mute" type="button" class="mute" title="Unmute" aria-label="Unmute"></button>
187 <label><span class="label">Signal</span> <select id="signal"></select></label>
188 <input id="sound-mass" type="number" min="1" step="1" value="1" aria-label="Mass" />
189 <label><span class="label">Volume</span> <input id="volume" type="range" min="0" max="1" step="0.01" value="0.5" /></label>
190 <button id="replay" type="button" disabled title="Play the last strike again with the current settings">Replay</button>
191 <span id="sound-chips" class="chips">
192 <label class="chip"><input id="spec-toggle" type="checkbox" checked /><span>Spectrogram</span></label>
193 </span>
194 </div>
195 <div class="row">
196 <label class="grow"><span class="label">Pitch</span> <input id="pitch" type="range" min="1.477" max="3" step="0.001" value="2" aria-label="Pitch of the lowest mode" /></label>
197 <span id="pitch-value" class="num"></span>
198 </div>
199 <div class="row"><span id="sound-status" class="muted"></span></div>
200 <p id="sound-hint" class="muted">Strike to hear the whole decay.</p>
201 <figure id="spec-fig"><div id="spectrogram"></div></figure>
202 <div id="sound-traces"></div>
203 <figure id="scatter-fig" hidden>
204 <div id="scatter"></div>
205 <div class="row scatter-actions"><button id="scatter-clear" type="button">Clear</button></div>
206 </figure>
207 </details>
209 <details class="card panel" id="poincare" open hidden>
210 <summary><span class="label">Poincaré section</span></summary>
211 <div class="row">
212 <span class="label">Energy</span>
213 <input id="pc-energy" type="range" min="-5" max="2" step="0.01" value="0.4" aria-label="Section energy" />
214 <span id="pc-energy-value" class="num"></span>
215 <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">
216 <option value="off">off</option>
217 <option value="realtime">real time</option>
218 <option value="sound">sped up</option>
219 </select></label>
220 <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>
221 </div>
222 <div class="row">
223 <label class="chip"><input id="pc-lambda" type="checkbox" /><span>Color by λ</span></label>
224 <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>
225 </div>
226 <figure><div id="pc-plot"></div></figure>
227 </details>
228 </main>
229 <script type="module" src="/src/main.ts"></script>
230 </body>
231</html>