pjt222 commited on
Commit
e7670f1
Β·
verified Β·
1 Parent(s): 5773652

Upload index.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +1396 -18
index.html CHANGED
@@ -1,19 +1,1397 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
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>Triple Pendulum 3D Chaos Viewer</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --bg-primary: #0a0a0f;
11
+ --bg-panel: rgba(12, 12, 20, 0.88);
12
+ --bg-panel-border: rgba(255, 255, 255, 0.08);
13
+ --text-primary: #e0e0e8;
14
+ --text-muted: #888899;
15
+ --text-accent: #00f0ff;
16
+ --glow-cyan: 0 0 12px rgba(0, 240, 255, 0.4);
17
+ --font-mono: 'Share Tech Mono', monospace;
18
+ --font-display: 'Orbitron', sans-serif;
19
+ --transition-speed: 0.3s;
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ margin: 0;
25
+ padding: 0;
26
+ }
27
+
28
+ body {
29
+ background: var(--bg-primary);
30
+ color: var(--text-primary);
31
+ font-family: var(--font-mono);
32
+ overflow: hidden;
33
+ width: 100vw;
34
+ height: 100vh;
35
+ }
36
+
37
+ /* CRT Scanline Overlay */
38
+ body::after {
39
+ content: '';
40
+ position: fixed;
41
+ inset: 0;
42
+ pointer-events: none;
43
+ z-index: 9999;
44
+ background: repeating-linear-gradient(
45
+ 0deg,
46
+ transparent,
47
+ transparent 2px,
48
+ rgba(0, 0, 0, 0.04) 2px,
49
+ rgba(0, 0, 0, 0.04) 4px
50
+ );
51
+ }
52
+
53
+ #canvas-container {
54
+ position: absolute;
55
+ inset: 0;
56
+ }
57
+
58
+ canvas {
59
+ display: block;
60
+ width: 100% !important;
61
+ height: 100% !important;
62
+ }
63
+
64
+ /* Controls Panel */
65
+ #controls {
66
+ position: absolute;
67
+ top: 12px;
68
+ left: 12px;
69
+ width: 260px;
70
+ max-height: calc(100vh - 24px);
71
+ background: var(--bg-panel);
72
+ border: 1px solid var(--bg-panel-border);
73
+ border-radius: 8px;
74
+ overflow: hidden;
75
+ display: flex;
76
+ flex-direction: column;
77
+ font-size: 12px;
78
+ backdrop-filter: blur(12px);
79
+ z-index: 50;
80
+ }
81
+
82
+ #controls-header {
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: space-between;
86
+ padding: 10px 12px;
87
+ background: rgba(255, 255, 255, 0.06);
88
+ border-bottom: 1px solid var(--bg-panel-border);
89
+ cursor: pointer;
90
+ user-select: none;
91
+ flex-shrink: 0;
92
+ }
93
+
94
+ #controls-header:hover {
95
+ background: rgba(255, 255, 255, 0.1);
96
+ }
97
+
98
+ #controls-title {
99
+ font-family: var(--font-display);
100
+ font-weight: 700;
101
+ font-size: 11px;
102
+ letter-spacing: 1.5px;
103
+ text-transform: uppercase;
104
+ color: var(--text-accent);
105
+ text-shadow: var(--glow-cyan);
106
+ }
107
+
108
+ #collapse-icon {
109
+ font-size: 10px;
110
+ color: var(--text-muted);
111
+ transition: transform 0.2s;
112
+ }
113
+
114
+ #controls.collapsed #collapse-icon {
115
+ transform: rotate(-90deg);
116
+ }
117
+
118
+ #controls-body {
119
+ overflow-y: auto;
120
+ padding: 10px 12px;
121
+ display: flex;
122
+ flex-direction: column;
123
+ gap: 14px;
124
+ }
125
+
126
+ #controls.collapsed #controls-body {
127
+ display: none;
128
+ }
129
+
130
+ /* Scrollbar */
131
+ #controls-body::-webkit-scrollbar {
132
+ width: 4px;
133
+ }
134
+ #controls-body::-webkit-scrollbar-track {
135
+ background: transparent;
136
+ }
137
+ #controls-body::-webkit-scrollbar-thumb {
138
+ background: rgba(0, 240, 255, 0.2);
139
+ border-radius: 2px;
140
+ }
141
+
142
+ .section-label {
143
+ font-size: 10px;
144
+ font-weight: 700;
145
+ text-transform: uppercase;
146
+ letter-spacing: 0.08em;
147
+ color: var(--text-muted);
148
+ margin-bottom: 6px;
149
+ }
150
+
151
+ .control-row {
152
+ display: flex;
153
+ align-items: center;
154
+ justify-content: space-between;
155
+ gap: 6px;
156
+ }
157
+
158
+ .control-row label {
159
+ color: var(--text-primary);
160
+ white-space: nowrap;
161
+ flex-shrink: 0;
162
+ }
163
+
164
+ .control-row .value-label {
165
+ color: var(--text-muted);
166
+ min-width: 40px;
167
+ text-align: right;
168
+ font-variant-numeric: tabular-nums;
169
+ }
170
+
171
+ input[type="range"] {
172
+ flex: 1;
173
+ height: 3px;
174
+ appearance: none;
175
+ background: rgba(255, 255, 255, 0.15);
176
+ border-radius: 2px;
177
+ outline: none;
178
+ cursor: pointer;
179
+ }
180
+
181
+ input[type="range"]::-webkit-slider-thumb {
182
+ appearance: none;
183
+ width: 12px;
184
+ height: 12px;
185
+ border-radius: 50%;
186
+ background: var(--text-accent);
187
+ cursor: pointer;
188
+ box-shadow: 0 0 6px rgba(0, 240, 255, 0.5), 0 0 12px rgba(0, 240, 255, 0.2);
189
+ }
190
+
191
+ input[type="range"]::-moz-range-thumb {
192
+ width: 12px;
193
+ height: 12px;
194
+ border-radius: 50%;
195
+ background: var(--text-accent);
196
+ cursor: pointer;
197
+ border: none;
198
+ box-shadow: 0 0 6px rgba(0, 240, 255, 0.5), 0 0 12px rgba(0, 240, 255, 0.2);
199
+ }
200
+
201
+ input[type="checkbox"] {
202
+ appearance: none;
203
+ width: 14px;
204
+ height: 14px;
205
+ border: 1px solid rgba(255,255,255,0.3);
206
+ border-radius: 3px;
207
+ background: transparent;
208
+ cursor: pointer;
209
+ position: relative;
210
+ flex-shrink: 0;
211
+ }
212
+
213
+ input[type="checkbox"]:checked {
214
+ background: var(--text-accent);
215
+ border-color: var(--text-accent);
216
+ box-shadow: 0 0 4px rgba(0, 240, 255, 0.4);
217
+ }
218
+
219
+ input[type="checkbox"]:checked::after {
220
+ content: '';
221
+ position: absolute;
222
+ left: 3px;
223
+ top: 1px;
224
+ width: 5px;
225
+ height: 8px;
226
+ border: 2px solid var(--bg-primary);
227
+ border-top: none;
228
+ border-left: none;
229
+ transform: rotate(45deg);
230
+ }
231
+
232
+ select {
233
+ background: rgba(255, 255, 255, 0.06);
234
+ color: var(--text-primary);
235
+ border: 1px solid var(--bg-panel-border);
236
+ border-radius: 4px;
237
+ padding: 4px 8px;
238
+ font-family: var(--font-mono);
239
+ font-size: 11px;
240
+ cursor: pointer;
241
+ width: 100%;
242
+ transition: border-color var(--transition-speed);
243
+ }
244
+ select:hover { border-color: rgba(0, 240, 255, 0.3); }
245
+ select:focus { border-color: var(--text-accent); outline: none; box-shadow: 0 0 4px rgba(0, 240, 255, 0.2); }
246
+ select option { background: #0a0a1a; color: var(--text-primary); }
247
+
248
+ .divider {
249
+ border: none;
250
+ border-top: 1px solid var(--bg-panel-border);
251
+ }
252
+
253
+ /* Colormap legend */
254
+ #colormap-legend {
255
+ margin-top: 6px;
256
+ }
257
+ #legend-canvas {
258
+ width: 100%;
259
+ height: 12px;
260
+ border-radius: 2px;
261
+ display: block;
262
+ }
263
+ .legend-labels {
264
+ display: flex;
265
+ justify-content: space-between;
266
+ font-size: 9px;
267
+ color: var(--text-muted);
268
+ margin-top: 2px;
269
+ }
270
+
271
+ /* Slice axis block */
272
+ .slice-axis {
273
+ display: flex;
274
+ flex-direction: column;
275
+ gap: 5px;
276
+ }
277
+
278
+ .slice-axis-header {
279
+ display: flex;
280
+ align-items: center;
281
+ gap: 6px;
282
+ }
283
+
284
+ .slice-axis-header label {
285
+ color: var(--text-primary);
286
+ flex: 1;
287
+ }
288
+
289
+ .slice-axis-header .value-label {
290
+ color: var(--text-muted);
291
+ min-width: 44px;
292
+ text-align: right;
293
+ font-variant-numeric: tabular-nums;
294
+ }
295
+
296
+ .slice-axis input[type="range"] {
297
+ margin-left: 20px;
298
+ }
299
+
300
+ /* Stats */
301
+ #stats {
302
+ background: rgba(255, 255, 255, 0.04);
303
+ border-radius: 4px;
304
+ padding: 7px 10px;
305
+ line-height: 1.6;
306
+ color: var(--text-muted);
307
+ }
308
+
309
+ #stats span {
310
+ color: var(--text-accent);
311
+ font-weight: 600;
312
+ text-shadow: 0 0 6px rgba(0, 240, 255, 0.3);
313
+ }
314
+
315
+ /* Loading overlay */
316
+ #loading {
317
+ position: absolute;
318
+ inset: 0;
319
+ display: flex;
320
+ flex-direction: column;
321
+ align-items: center;
322
+ justify-content: center;
323
+ background: var(--bg-primary);
324
+ z-index: 100;
325
+ gap: 16px;
326
+ transition: opacity 0.4s;
327
+ }
328
+
329
+ #loading.hidden {
330
+ opacity: 0;
331
+ pointer-events: none;
332
+ }
333
+
334
+ .spinner {
335
+ width: 40px;
336
+ height: 40px;
337
+ border: 3px solid rgba(0, 240, 255, 0.15);
338
+ border-top-color: var(--text-accent);
339
+ border-radius: 50%;
340
+ animation: spin 0.8s linear infinite;
341
+ box-shadow: 0 0 12px rgba(0, 240, 255, 0.2);
342
+ }
343
+
344
+ @keyframes spin {
345
+ to { transform: rotate(360deg); }
346
+ }
347
+
348
+ #loading-text {
349
+ color: var(--text-muted);
350
+ font-size: 13px;
351
+ }
352
+
353
+ /* Info badge (demo mode) */
354
+ #demo-badge {
355
+ position: absolute;
356
+ bottom: 12px;
357
+ right: 12px;
358
+ background: rgba(0, 240, 255, 0.1);
359
+ border: 1px solid rgba(0, 240, 255, 0.3);
360
+ border-radius: 4px;
361
+ padding: 5px 10px;
362
+ font-size: 11px;
363
+ color: var(--text-accent);
364
+ text-shadow: 0 0 6px rgba(0, 240, 255, 0.3);
365
+ display: none;
366
+ z-index: 50;
367
+ }
368
+
369
+ /* Octant buttons */
370
+ .octant-btn {
371
+ flex: 1;
372
+ background: rgba(255, 255, 255, 0.06);
373
+ border: 1px solid var(--bg-panel-border);
374
+ border-radius: 4px;
375
+ color: var(--text-primary);
376
+ font-family: var(--font-mono);
377
+ font-size: 11px;
378
+ padding: 4px 0;
379
+ cursor: pointer;
380
+ transition: all var(--transition-speed);
381
+ }
382
+ .octant-btn:hover {
383
+ border-color: rgba(0, 240, 255, 0.3);
384
+ color: var(--text-accent);
385
+ }
386
+ </style>
387
+ </head>
388
+ <body>
389
+
390
+ <div id="canvas-container"></div>
391
+
392
+ <div id="loading">
393
+ <div class="spinner"></div>
394
+ <div id="loading-text">Loading simulation data...</div>
395
+ </div>
396
+
397
+ <div id="controls">
398
+ <div id="controls-header" onclick="togglePanel()">
399
+ <span id="controls-title">Triple Pendulum</span>
400
+ <span id="collapse-icon">&#9660;</span>
401
+ </div>
402
+ <div id="controls-body">
403
+
404
+ <!-- Stats -->
405
+ <div id="stats">Loading...</div>
406
+
407
+ <div class="control-row" style="margin-top:8px">
408
+ <button id="reset-view" onclick="resetView()" style="flex:1;padding:4px 0;font-family:var(--font-mono);font-size:11px;background:rgba(255,255,255,0.06);color:var(--text-muted);border:1px solid var(--bg-panel-border);border-radius:4px;cursor:pointer;">Reset view</button>
409
+ </div>
410
+
411
+ <hr class="divider" />
412
+
413
+ <!-- Dataset -->
414
+ <div>
415
+ <div class="section-label">Dataset</div>
416
+ <div class="control-row" style="margin-bottom:7px">
417
+ <label>Backend</label>
418
+ <select id="backend-select" style="flex:1" onchange="changeBackend()">
419
+ <option value="cpu">CPU</option>
420
+ <option value="gpu" selected>GPU</option>
421
+ </select>
422
+ </div>
423
+ <div class="control-row">
424
+ <label>Resolution</label>
425
+ <select id="resolution-select" style="flex:1" onchange="changeResolution()">
426
+ </select>
427
+ </div>
428
+ </div>
429
+
430
+ <hr class="divider" />
431
+
432
+ <!-- Colormap -->
433
+ <div>
434
+ <div class="section-label">Colormap</div>
435
+ <div class="control-row" style="gap:4px">
436
+ <select id="colormap-select" style="flex:1" onchange="changeColormap()">
437
+ <option value="cyberpunk" selected>Cyberpunk</option>
438
+ <option value="magma">Magma</option>
439
+ <option value="viridis">Viridis</option>
440
+ <option value="inferno">Inferno</option>
441
+ <option value="plasma">Plasma</option>
442
+ <option value="cividis">Cividis</option>
443
+ </select>
444
+ <button class="octant-btn" id="invert-btn" style="flex:0 0 auto;padding:4px 8px" onclick="toggleInvertColormap()">Inv</button>
445
+ </div>
446
+ <div id="colormap-legend">
447
+ <canvas id="legend-canvas" width="236" height="12"></canvas>
448
+ <div class="legend-labels">
449
+ <span>Fast flip</span>
450
+ <span>Stable</span>
451
+ </div>
452
+ </div>
453
+ </div>
454
+
455
+ <hr class="divider" />
456
+
457
+ <!-- Point appearance -->
458
+ <div>
459
+ <div class="section-label">Appearance</div>
460
+
461
+ <div class="control-row" style="margin-bottom:7px">
462
+ <label>Point size</label>
463
+ <input type="range" id="point-size" min="1" max="10" step="0.5" value="1" oninput="updatePointSize()" />
464
+ <span class="value-label" id="point-size-val">1</span>
465
+ </div>
466
+
467
+ <div class="control-row">
468
+ <label>Opacity</label>
469
+ <input type="range" id="opacity" min="0.05" max="1" step="0.05" value="0.05" oninput="updateOpacity()" />
470
+ <span class="value-label" id="opacity-val">0.05</span>
471
+ </div>
472
+ </div>
473
+
474
+ <hr class="divider" />
475
+
476
+ <!-- Time filter -->
477
+ <div>
478
+ <div class="section-label">Flip time filter</div>
479
+
480
+ <div class="control-row" style="margin-bottom:7px">
481
+ <label>Min</label>
482
+ <input type="range" id="time-min" min="0" max="1" step="0.01" value="0" oninput="updateTimeFilter()" />
483
+ <span class="value-label" id="time-min-val">0.0 s</span>
484
+ </div>
485
+
486
+ <div class="control-row">
487
+ <label>Max</label>
488
+ <input type="range" id="time-max" min="0" max="1" step="0.01" value="1" oninput="updateTimeFilter()" />
489
+ <span class="value-label" id="time-max-val">-- s</span>
490
+ </div>
491
+
492
+ <div class="control-row" style="margin-top:7px">
493
+ <label>Show stable</label>
494
+ <input type="checkbox" id="show-stable" onchange="applyFilters()" />
495
+ </div>
496
+ <div class="control-row" style="margin-top:4px">
497
+ <label>Show axes</label>
498
+ <input type="checkbox" id="show-axes" checked onchange="toggleAxes()" />
499
+ </div>
500
+ <div class="control-row" style="margin-top:4px">
501
+ <label>Auto-rotate</label>
502
+ <input type="checkbox" id="auto-rotate" onchange="toggleAutoRotate()" />
503
+ </div>
504
+ <div class="control-row" style="margin-top:4px">
505
+ <label>Rot. speed</label>
506
+ <input type="range" id="rotate-speed" min="0.1" max="5" step="0.1" value="1" oninput="updateRotateSpeed()" />
507
+ <span class="value-label" id="rotate-speed-val">1.0</span>
508
+ </div>
509
+ </div>
510
+
511
+ <hr class="divider" />
512
+
513
+ <!-- Slice controls -->
514
+ <div>
515
+ <div class="section-label">Slice controls</div>
516
+
517
+ <div class="control-row" style="margin-bottom:4px">
518
+ <label>Thickness</label>
519
+ <input type="range" id="slice-thickness" min="1" max="20" step="1" value="5" oninput="updateSliceThickness()" />
520
+ <span class="value-label" id="slice-thickness-val">5Β°</span>
521
+ </div>
522
+ </div>
523
+
524
+ <!-- Axis slices -->
525
+ <div class="slice-axis">
526
+ <div class="slice-axis-header">
527
+ <input type="checkbox" id="slice-x-en" onchange="applyFilters()" />
528
+ <label>&#952;&#8321; slice</label>
529
+ <span class="value-label" id="slice-x-val">0Β°</span>
530
+ </div>
531
+ <input type="range" id="slice-x" min="-170" max="170" step="1" value="0" oninput="updateSliceLabel('x')" />
532
+ </div>
533
+
534
+ <div class="slice-axis">
535
+ <div class="slice-axis-header">
536
+ <input type="checkbox" id="slice-y-en" onchange="applyFilters()" />
537
+ <label>&#952;&#8322; slice</label>
538
+ <span class="value-label" id="slice-y-val">0Β°</span>
539
+ </div>
540
+ <input type="range" id="slice-y" min="-170" max="170" step="1" value="0" oninput="updateSliceLabel('y')" />
541
+ </div>
542
+
543
+ <div class="slice-axis">
544
+ <div class="slice-axis-header">
545
+ <input type="checkbox" id="slice-z-en" onchange="applyFilters()" />
546
+ <label>&#952;&#8323; slice</label>
547
+ <span class="value-label" id="slice-z-val">0Β°</span>
548
+ </div>
549
+ <input type="range" id="slice-z" min="-170" max="170" step="1" value="0" oninput="updateSliceLabel('z')" />
550
+ </div>
551
+
552
+ <hr class="divider" />
553
+
554
+ <!-- Octant controls -->
555
+ <div>
556
+ <div class="section-label">Octant controls</div>
557
+
558
+ <div class="control-row" style="margin-bottom:8px;gap:4px">
559
+ <button class="octant-btn" onclick="showAllOctants()">Show All</button>
560
+ <button class="octant-btn" onclick="hideAllOctants()">Hide All</button>
561
+ </div>
562
+
563
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px 8px">
564
+ <div class="control-row">
565
+ <input type="checkbox" id="octant-1" checked onchange="applyFilters()" />
566
+ <label style="font-size:11px">1 +++</label>
567
+ </div>
568
+ <div class="control-row">
569
+ <input type="checkbox" id="octant-2" checked onchange="applyFilters()" />
570
+ <label style="font-size:11px">2 ++-</label>
571
+ </div>
572
+ <div class="control-row">
573
+ <input type="checkbox" id="octant-3" checked onchange="applyFilters()" />
574
+ <label style="font-size:11px">3 +-+</label>
575
+ </div>
576
+ <div class="control-row">
577
+ <input type="checkbox" id="octant-4" checked onchange="applyFilters()" />
578
+ <label style="font-size:11px">4 +--</label>
579
+ </div>
580
+ <div class="control-row">
581
+ <input type="checkbox" id="octant-5" checked onchange="applyFilters()" />
582
+ <label style="font-size:11px">5 -++</label>
583
+ </div>
584
+ <div class="control-row">
585
+ <input type="checkbox" id="octant-6" checked onchange="applyFilters()" />
586
+ <label style="font-size:11px">6 -+-</label>
587
+ </div>
588
+ <div class="control-row">
589
+ <input type="checkbox" id="octant-7" checked onchange="applyFilters()" />
590
+ <label style="font-size:11px">7 --+</label>
591
+ </div>
592
+ <div class="control-row">
593
+ <input type="checkbox" id="octant-8" checked onchange="applyFilters()" />
594
+ <label style="font-size:11px">8 ---</label>
595
+ </div>
596
+ </div>
597
+ </div>
598
+
599
+ </div><!-- end controls-body -->
600
+ </div><!-- end controls -->
601
+
602
+ <div id="demo-badge">DEMO MODE β€” random data</div>
603
+
604
+ <script type="importmap">
605
+ {
606
+ "imports": {
607
+ "three": "https://cdn.jsdelivr.net/npm/three@0.160.1/build/three.module.js",
608
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.1/examples/jsm/"
609
+ }
610
+ }
611
+ </script>
612
+
613
+ <script type="module">
614
+ import * as THREE from 'three';
615
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
616
+
617
+ // ─── State ────────────────────────────────────────────────────────────────────
618
+
619
+ const state = {
620
+ gridSize: 0,
621
+ thetaRange: [-170, 170],
622
+ flipTimes: null, // Float32Array, length N^3, NaN = never flipped
623
+ tMax: 0,
624
+ pointCount: 0,
625
+ currentColormap: 'cyberpunk',
626
+ invertColormap: false,
627
+ // Three.js objects
628
+ scene: null,
629
+ camera: null,
630
+ renderer: null,
631
+ controls: null,
632
+ points: null,
633
+ geometry: null,
634
+ // Per-point cache (computed once from raw data)
635
+ positions: null, // Float32Array N^3 * 3
636
+ rawColors: null, // Float32Array N^3 * 3 (base colormap, no opacity baking)
637
+ isStable: null, // Uint8Array N^3, 1 = never flipped
638
+ // Visible flags computed by filter pass
639
+ visibleFlags: null, // Uint8Array N^3
640
+ };
641
+
642
+ // ─── Colormaps ───────────────────────────────────────────────────────────────
643
+
644
+ const COLORMAPS = {
645
+ cyberpunk: [
646
+ [0.000, [0.040, 0.000, 0.080]],
647
+ [0.150, [0.000, 0.120, 0.300]],
648
+ [0.350, [0.000, 0.940, 1.000]],
649
+ [0.550, [1.000, 0.330, 0.870]],
650
+ [0.750, [1.000, 0.867, 0.000]],
651
+ [1.000, [1.000, 1.000, 1.000]],
652
+ ],
653
+ magma: [
654
+ [0.000, [0.001, 0.000, 0.014]],
655
+ [0.130, [0.172, 0.005, 0.200]],
656
+ [0.280, [0.460, 0.040, 0.310]],
657
+ [0.450, [0.750, 0.100, 0.140]],
658
+ [0.620, [0.950, 0.380, 0.060]],
659
+ [0.780, [0.990, 0.660, 0.200]],
660
+ [0.900, [1.000, 0.880, 0.500]],
661
+ [1.000, [1.000, 1.000, 1.000]],
662
+ ],
663
+ viridis: [
664
+ [0.000, [0.267, 0.004, 0.329]],
665
+ [0.125, [0.282, 0.141, 0.458]],
666
+ [0.250, [0.254, 0.265, 0.530]],
667
+ [0.375, [0.206, 0.372, 0.553]],
668
+ [0.500, [0.128, 0.566, 0.551]],
669
+ [0.625, [0.120, 0.620, 0.536]],
670
+ [0.750, [0.210, 0.718, 0.472]],
671
+ [0.875, [0.565, 0.811, 0.314]],
672
+ [1.000, [0.993, 0.906, 0.144]],
673
+ ],
674
+ inferno: [
675
+ [0.000, [0.001, 0.000, 0.014]],
676
+ [0.140, [0.190, 0.015, 0.260]],
677
+ [0.300, [0.500, 0.030, 0.340]],
678
+ [0.460, [0.760, 0.150, 0.170]],
679
+ [0.620, [0.940, 0.370, 0.020]],
680
+ [0.780, [0.990, 0.650, 0.040]],
681
+ [0.900, [0.990, 0.890, 0.300]],
682
+ [1.000, [0.988, 0.998, 0.645]],
683
+ ],
684
+ plasma: [
685
+ [0.000, [0.050, 0.030, 0.530]],
686
+ [0.140, [0.290, 0.010, 0.610]],
687
+ [0.300, [0.520, 0.030, 0.560]],
688
+ [0.460, [0.720, 0.120, 0.430]],
689
+ [0.620, [0.900, 0.270, 0.250]],
690
+ [0.780, [0.990, 0.490, 0.080]],
691
+ [0.900, [0.990, 0.720, 0.030]],
692
+ [1.000, [0.940, 0.975, 0.131]],
693
+ ],
694
+ cividis: [
695
+ [0.000, [0.000, 0.133, 0.302]],
696
+ [0.167, [0.153, 0.208, 0.380]],
697
+ [0.333, [0.337, 0.318, 0.380]],
698
+ [0.500, [0.478, 0.443, 0.380]],
699
+ [0.667, [0.616, 0.576, 0.349]],
700
+ [0.833, [0.769, 0.722, 0.290]],
701
+ [1.000, [0.996, 0.878, 0.216]],
702
+ ],
703
+ };
704
+
705
+ function sampleColormap(t, mapName) {
706
+ t = Math.max(0, Math.min(1, t));
707
+ if (state.invertColormap) t = 1 - t;
708
+ const stops = COLORMAPS[mapName] || COLORMAPS.cyberpunk;
709
+ let lo = stops[0], hi = stops[stops.length - 1];
710
+ for (let i = 1; i < stops.length; i++) {
711
+ if (t <= stops[i][0]) {
712
+ hi = stops[i];
713
+ lo = stops[i - 1];
714
+ break;
715
+ }
716
+ }
717
+ const span = hi[0] - lo[0];
718
+ const f = span > 0 ? (t - lo[0]) / span : 0;
719
+ return [
720
+ lo[1][0] + f * (hi[1][0] - lo[1][0]),
721
+ lo[1][1] + f * (hi[1][1] - lo[1][1]),
722
+ lo[1][2] + f * (hi[1][2] - lo[1][2]),
723
+ ];
724
+ }
725
+
726
+ function drawLegend() {
727
+ const canvas = document.getElementById('legend-canvas');
728
+ const ctx = canvas.getContext('2d');
729
+ const width = canvas.width;
730
+ const height = canvas.height;
731
+ ctx.clearRect(0, 0, width, height);
732
+ for (let x = 0; x < width; x++) {
733
+ const t = x / (width - 1);
734
+ const rgb = sampleColormap(t, state.currentColormap);
735
+ ctx.fillStyle = `rgb(${Math.round(rgb[0]*255)},${Math.round(rgb[1]*255)},${Math.round(rgb[2]*255)})`;
736
+ ctx.fillRect(x, 0, 1, height);
737
+ }
738
+ }
739
+
740
+ // ─── Data loading ─────────────────────────────────────────────────────────────
741
+
742
+ // Dataset manifest: backend -> [{resolution, file, label}]
743
+ const DATASETS = {
744
+ cpu: [
745
+ { resolution: 80, file: 'data/simulation_80_cpu.json', label: '80\u00B3 (512K pts)' },
746
+ { resolution: 40, file: 'data/simulation_40_cpu.json', label: '40\u00B3 (64K pts)' },
747
+ ],
748
+ gpu: [
749
+ // Downsampled entries β€” uncomment as simulations complete:
750
+ // { resolution: 200, file: 'data/simulation_1000_ds200_gpu.json', label: '1000\u00B3 \u2192 200\u00B3 (1.0B src)' },
751
+ // { resolution: 200, file: 'data/simulation_900_ds200_gpu.json', label: '900\u00B3 \u2192 200\u00B3 (729M src)' },
752
+ // { resolution: 200, file: 'data/simulation_800_ds200_gpu.json', label: '800\u00B3 \u2192 200\u00B3 (512M src)' },
753
+ // { resolution: 200, file: 'data/simulation_700_ds200_gpu.json', label: '700\u00B3 \u2192 200\u00B3 (343M src)' },
754
+ { resolution: 600, file: 'data/simulation_600_gpu.json', label: '600\u00B3 (216M pts)' },
755
+ { resolution: 500, file: 'data/simulation_500_gpu.json', label: '500\u00B3 (125M pts)' },
756
+ { resolution: 400, file: 'data/simulation_400_gpu.json', label: '400\u00B3 (64M pts)' },
757
+ { resolution: 300, file: 'data/simulation_300_gpu.json', label: '300\u00B3 (27M pts)' },
758
+ { resolution: 200, file: 'data/simulation_200_gpu.json', label: '200\u00B3 (8.0M pts)' },
759
+ { resolution: 190, file: 'data/simulation_190_gpu.json', label: '190\u00B3 (6.9M pts)' },
760
+ { resolution: 180, file: 'data/simulation_180_gpu.json', label: '180\u00B3 (5.8M pts)' },
761
+ { resolution: 170, file: 'data/simulation_170_gpu.json', label: '170\u00B3 (4.9M pts)' },
762
+ { resolution: 160, file: 'data/simulation_160_gpu.json', label: '160\u00B3 (4.1M pts)' },
763
+ { resolution: 150, file: 'data/simulation_150_gpu.json', label: '150\u00B3 (3.4M pts)' },
764
+ { resolution: 140, file: 'data/simulation_140_gpu.json', label: '140\u00B3 (2.7M pts)' },
765
+ { resolution: 130, file: 'data/simulation_130_gpu.json', label: '130\u00B3 (2.2M pts)' },
766
+ { resolution: 120, file: 'data/simulation_120_gpu.json', label: '120\u00B3 (1.7M pts)' },
767
+ { resolution: 110, file: 'data/simulation_110_gpu.json', label: '110\u00B3 (1.3M pts)' },
768
+ { resolution: 100, file: 'data/simulation_100_gpu.json', label: '100\u00B3 (1.0M pts)' },
769
+ { resolution: 90, file: 'data/simulation_90_gpu.json', label: '90\u00B3 (729K pts)' },
770
+ { resolution: 80, file: 'data/simulation_80_gpu.json', label: '80\u00B3 (512K pts)' },
771
+ { resolution: 70, file: 'data/simulation_70_gpu.json', label: '70\u00B3 (343K pts)' },
772
+ { resolution: 60, file: 'data/simulation_60_gpu.json', label: '60\u00B3 (216K pts)' },
773
+ { resolution: 50, file: 'data/simulation_50_gpu.json', label: '50\u00B3 (125K pts)' },
774
+ { resolution: 40, file: 'data/simulation_40_gpu.json', label: '40\u00B3 (64K pts)' },
775
+ { resolution: 30, file: 'data/simulation_30_gpu.json', label: '30\u00B3 (27K pts)' },
776
+ { resolution: 20, file: 'data/simulation_20_gpu.json', label: '20\u00B3 (8K pts)' },
777
+ ],
778
+ };
779
+
780
+ function populateResolutionSelect(backend, defaultResolution) {
781
+ const select = document.getElementById('resolution-select');
782
+ const entries = DATASETS[backend] || [];
783
+ select.innerHTML = '';
784
+ let defaultIdx = 0;
785
+ for (let i = 0; i < entries.length; i++) {
786
+ const option = document.createElement('option');
787
+ option.value = i;
788
+ option.textContent = entries[i].label;
789
+ select.appendChild(option);
790
+ if (defaultResolution && entries[i].resolution === defaultResolution) defaultIdx = i;
791
+ }
792
+ select.value = defaultIdx;
793
+ }
794
+
795
+ function getSelectedFile() {
796
+ const backend = document.getElementById('backend-select').value;
797
+ const idx = parseInt(document.getElementById('resolution-select').value);
798
+ const entries = DATASETS[backend] || [];
799
+ return entries[idx]?.file;
800
+ }
801
+
802
+ async function loadData() {
803
+ const dataPath = getSelectedFile();
804
+ if (!dataPath) {
805
+ console.warn('No dataset file available, using demo mode');
806
+ document.getElementById('demo-badge').style.display = 'block';
807
+ return generateDemoData(20);
808
+ }
809
+ try {
810
+ const res = await fetch(dataPath);
811
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
812
+ const json = await res.json();
813
+ return parseSimData(json);
814
+ } catch (err) {
815
+ console.warn('Could not load simulation data, using demo mode:', err.message);
816
+ document.getElementById('demo-badge').style.display = 'block';
817
+ document.getElementById('loading-text').textContent = 'Generating demo data...';
818
+ return generateDemoData(20);
819
+ }
820
+ }
821
+
822
+ function parseSimData(json) {
823
+ const gridSize = json.grid_size;
824
+ const thetaMin = json.theta_min !== undefined ? json.theta_min : (json.theta_range || [-170, 170])[0];
825
+ const thetaMax = json.theta_max !== undefined ? json.theta_max : (json.theta_range || [-170, 170])[1];
826
+ const thetaRange = [thetaMin, thetaMax];
827
+ const n3 = gridSize ** 3;
828
+ const flipTimes = new Float32Array(n3);
829
+ flipTimes.fill(NaN);
830
+ let tMax = 0;
831
+
832
+ if (json.flip_times) {
833
+ // Format A: flat array of grid_size^3 values (null = no flip)
834
+ const raw = json.flip_times;
835
+ for (let i = 0; i < n3; i++) {
836
+ const v = raw[i];
837
+ flipTimes[i] = (v === null || v === undefined || !isFinite(v)) ? NaN : v;
838
+ if (isFinite(flipTimes[i]) && flipTimes[i] > tMax) tMax = flipTimes[i];
839
+ }
840
+ } else if (json.points) {
841
+ // Format B: sparse array of {theta: [t1,t2,t3], flip_time: f}
842
+ const step = (thetaMax - thetaMin) / (gridSize - 1);
843
+ for (const pt of json.points) {
844
+ const i0 = Math.round((pt.theta[0] - thetaMin) / step);
845
+ const i1 = Math.round((pt.theta[1] - thetaMin) / step);
846
+ const i2 = Math.round((pt.theta[2] - thetaMin) / step);
847
+ const idx = i0 * gridSize * gridSize + i1 * gridSize + i2;
848
+ if (idx >= 0 && idx < n3) {
849
+ flipTimes[idx] = pt.flip_time;
850
+ if (pt.flip_time > tMax) tMax = pt.flip_time;
851
+ }
852
+ }
853
+ }
854
+
855
+ return { gridSize, thetaRange, flipTimes, tMax };
856
+ }
857
+
858
+ function generateDemoData(gridSize) {
859
+ const n3 = gridSize ** 3;
860
+ const flipTimes = new Float32Array(n3);
861
+ const tMax = 15;
862
+ for (let i = 0; i < n3; i++) {
863
+ flipTimes[i] = Math.random() < 0.20 ? NaN : Math.random() * tMax;
864
+ }
865
+ return { gridSize, thetaRange: [-170, 170], flipTimes, tMax };
866
+ }
867
+
868
+ // ─── Three.js setup ───────────────────────────────────────────────────────────
869
+
870
+ function initThree() {
871
+ const container = document.getElementById('canvas-container');
872
+
873
+ state.scene = new THREE.Scene();
874
+ state.scene.background = new THREE.Color(0x0a0a0f);
875
+
876
+ state.camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.001, 100);
877
+ state.camera.position.set(2.2, 1.6, 2.2);
878
+
879
+ state.renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: 'high-performance' });
880
+ state.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
881
+ state.renderer.setSize(container.clientWidth, container.clientHeight);
882
+ container.appendChild(state.renderer.domElement);
883
+
884
+ state.controls = new OrbitControls(state.camera, state.renderer.domElement);
885
+ state.controls.enableDamping = true;
886
+ state.controls.dampingFactor = 0.08;
887
+ state.controls.minDistance = 0.5;
888
+ state.controls.maxDistance = 10;
889
+
890
+ // Subtle axis helper
891
+ state.axesHelper = new THREE.AxesHelper(1.2);
892
+ state.axesHelper.material.opacity = 0.35;
893
+ state.axesHelper.material.transparent = true;
894
+ state.axesHelper.visible = document.getElementById('show-axes').checked;
895
+ state.scene.add(state.axesHelper);
896
+
897
+ window.addEventListener('resize', onResize);
898
+ }
899
+
900
+ function onResize() {
901
+ const container = document.getElementById('canvas-container');
902
+ const w = container.clientWidth;
903
+ const h = container.clientHeight;
904
+ state.camera.aspect = w / h;
905
+ state.camera.updateProjectionMatrix();
906
+ state.renderer.setSize(w, h);
907
+ }
908
+
909
+ // ─── Point cloud construction ─────────────────────────────────────────────────
910
+
911
+ function computeColors() {
912
+ const n3 = state.pointCount;
913
+ const colors = state.rawColors;
914
+ const flipTimes = state.flipTimes;
915
+ const isStable = state.isStable;
916
+ const tMax = state.tMax;
917
+ const mapName = state.currentColormap;
918
+
919
+ const stableColor = sampleColormap(0.02, mapName);
920
+
921
+ for (let idx = 0; idx < n3; idx++) {
922
+ const ft = flipTimes[idx];
923
+ const stable = isStable[idx] === 1;
924
+
925
+ let rgb;
926
+ if (stable) {
927
+ rgb = stableColor;
928
+ } else {
929
+ const normalizedT = tMax > 0 ? ft / tMax : 0;
930
+ rgb = sampleColormap(normalizedT, mapName);
931
+ }
932
+
933
+ colors[idx * 3 + 0] = rgb[0];
934
+ colors[idx * 3 + 1] = rgb[1];
935
+ colors[idx * 3 + 2] = rgb[2];
936
+ }
937
+ }
938
+
939
+ function buildPointCloud(data) {
940
+ const { gridSize, thetaRange, flipTimes, tMax } = data;
941
+ Object.assign(state, { gridSize, thetaRange, flipTimes, tMax });
942
+
943
+ const n3 = gridSize ** 3;
944
+ const positions = new Float32Array(n3 * 3);
945
+ const colors = new Float32Array(n3 * 3);
946
+ const isStable = new Uint8Array(n3);
947
+
948
+ const thetaMin = thetaRange[0];
949
+ const thetaSpan = thetaRange[1] - thetaRange[0];
950
+
951
+ for (let idx = 0; idx < n3; idx++) {
952
+ const ix = idx % gridSize;
953
+ const iy = Math.floor(idx / gridSize) % gridSize;
954
+ const iz = Math.floor(idx / (gridSize * gridSize));
955
+
956
+ // Normalize to [-1, 1]
957
+ const nx = (ix / (gridSize - 1)) * 2 - 1;
958
+ const ny = (iy / (gridSize - 1)) * 2 - 1;
959
+ const nz = (iz / (gridSize - 1)) * 2 - 1;
960
+
961
+ positions[idx * 3 + 0] = nx;
962
+ positions[idx * 3 + 1] = ny;
963
+ positions[idx * 3 + 2] = nz;
964
+
965
+ const ft = flipTimes[idx];
966
+ isStable[idx] = !isFinite(ft) ? 1 : 0;
967
+ }
968
+
969
+ state.positions = positions;
970
+ state.rawColors = colors;
971
+ state.isStable = isStable;
972
+ state.visibleFlags = new Uint8Array(n3);
973
+ state.pointCount = n3;
974
+
975
+ // Compute colors using selected colormap
976
+ computeColors();
977
+
978
+ // Build geometry
979
+ const geometry = new THREE.BufferGeometry();
980
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
981
+
982
+ const displayColors = new Float32Array(colors);
983
+ geometry.setAttribute('color', new THREE.BufferAttribute(displayColors, 3));
984
+
985
+ const material = new THREE.PointsMaterial({
986
+ size: 0.013,
987
+ vertexColors: true,
988
+ blending: THREE.AdditiveBlending,
989
+ depthWrite: false,
990
+ transparent: true,
991
+ opacity: 0.05,
992
+ sizeAttenuation: true,
993
+ });
994
+
995
+ if (state.points) {
996
+ state.scene.remove(state.points);
997
+ state.geometry.dispose();
998
+ }
999
+
1000
+ state.geometry = geometry;
1001
+ state.points = new THREE.Points(geometry, material);
1002
+ state.scene.add(state.points);
1003
+
1004
+ return geometry;
1005
+ }
1006
+
1007
+ // ─── Filter & visibility ──────────────────────────────────────────────────────
1008
+
1009
+ function applyFilters() {
1010
+ if (!state.points) return;
1011
+
1012
+ const n3 = state.pointCount;
1013
+ const gridSize = state.gridSize;
1014
+ const thetaRange = state.thetaRange;
1015
+ const thetaMin = thetaRange[0];
1016
+ const thetaSpan = thetaRange[1] - thetaRange[0];
1017
+
1018
+ const timeMin = parseFloat(document.getElementById('time-min').value) * state.tMax;
1019
+ const timeMax = parseFloat(document.getElementById('time-max').value) * state.tMax;
1020
+ const showStable = document.getElementById('show-stable').checked;
1021
+ const sliceThickness = parseFloat(document.getElementById('slice-thickness').value);
1022
+
1023
+ const xEn = document.getElementById('slice-x-en').checked;
1024
+ const yEn = document.getElementById('slice-y-en').checked;
1025
+ const zEn = document.getElementById('slice-z-en').checked;
1026
+ const xVal = parseFloat(document.getElementById('slice-x').value);
1027
+ const yVal = parseFloat(document.getElementById('slice-y').value);
1028
+ const zVal = parseFloat(document.getElementById('slice-z').value);
1029
+
1030
+ // Octant visibility
1031
+ const octantVisible = new Array(8);
1032
+ for (let octantIdx = 0; octantIdx < 8; octantIdx++) {
1033
+ octantVisible[octantIdx] = document.getElementById('octant-' + (octantIdx + 1)).checked;
1034
+ }
1035
+
1036
+ const colorAttr = state.geometry.getAttribute('color');
1037
+ const displayColors = colorAttr.array;
1038
+ const rawColors = state.rawColors;
1039
+ const flipTimes = state.flipTimes;
1040
+ const isStable = state.isStable;
1041
+
1042
+ let showing = 0;
1043
+ let totalFlipped = 0;
1044
+
1045
+ for (let idx = 0; idx < n3; idx++) {
1046
+ const ix = idx % gridSize;
1047
+ const iy = Math.floor(idx / gridSize) % gridSize;
1048
+ const iz = Math.floor(idx / (gridSize * gridSize));
1049
+
1050
+ const t1 = thetaMin + (ix / (gridSize - 1)) * thetaSpan;
1051
+ const t2 = thetaMin + (iy / (gridSize - 1)) * thetaSpan;
1052
+ const t3 = thetaMin + (iz / (gridSize - 1)) * thetaSpan;
1053
+
1054
+ const stable = isStable[idx] === 1;
1055
+ const ft = flipTimes[idx];
1056
+
1057
+ if (!stable) totalFlipped++;
1058
+
1059
+ let visible = true;
1060
+
1061
+ // Slice filtering
1062
+ if (xEn && Math.abs(t1 - xVal) > sliceThickness) visible = false;
1063
+ if (visible && yEn && Math.abs(t2 - yVal) > sliceThickness) visible = false;
1064
+ if (visible && zEn && Math.abs(t3 - zVal) > sliceThickness) visible = false;
1065
+
1066
+ // Octant filtering
1067
+ if (visible) {
1068
+ const nx = state.positions[idx * 3 + 0];
1069
+ const ny = state.positions[idx * 3 + 1];
1070
+ const nz = state.positions[idx * 3 + 2];
1071
+ const octantIndex = (nx >= 0 ? 0 : 4) + (ny >= 0 ? 0 : 2) + (nz >= 0 ? 0 : 1);
1072
+ if (!octantVisible[octantIndex]) visible = false;
1073
+ }
1074
+
1075
+ if (visible) {
1076
+ if (stable) {
1077
+ visible = showStable;
1078
+ } else {
1079
+ if (ft < timeMin || ft > timeMax) visible = false;
1080
+ }
1081
+ }
1082
+
1083
+ if (visible) {
1084
+ showing++;
1085
+ displayColors[idx * 3 + 0] = rawColors[idx * 3 + 0];
1086
+ displayColors[idx * 3 + 1] = rawColors[idx * 3 + 1];
1087
+ displayColors[idx * 3 + 2] = rawColors[idx * 3 + 2];
1088
+ } else {
1089
+ displayColors[idx * 3 + 0] = 0;
1090
+ displayColors[idx * 3 + 1] = 0;
1091
+ displayColors[idx * 3 + 2] = 0;
1092
+ }
1093
+ }
1094
+
1095
+ colorAttr.needsUpdate = true;
1096
+ updateStats(showing, n3, totalFlipped);
1097
+ }
1098
+
1099
+ function updateStats(showing, total, totalFlipped) {
1100
+ const pct = total > 0 ? ((totalFlipped / total) * 100).toFixed(1) : '0.0';
1101
+ document.getElementById('stats').innerHTML =
1102
+ `Showing <span>${showing.toLocaleString()}</span> / ${total.toLocaleString()} points<br>` +
1103
+ `<span>${pct}%</span> flipped`;
1104
+ }
1105
+
1106
+ // ─── Dataset switching ───────────────────────────────────────────────────────
1107
+
1108
+ async function reloadDataset() {
1109
+ const backend = document.getElementById('backend-select').value;
1110
+ const select = document.getElementById('resolution-select');
1111
+ const selectedLabel = select.options[select.selectedIndex]?.textContent || '';
1112
+
1113
+ const loading = document.getElementById('loading');
1114
+ loading.style.display = 'flex';
1115
+ loading.classList.remove('hidden');
1116
+ document.getElementById('loading-text').textContent = `Loading ${backend.toUpperCase()} ${selectedLabel} data...`;
1117
+ document.getElementById('demo-badge').style.display = 'none';
1118
+
1119
+ await new Promise(r => setTimeout(r, 50));
1120
+
1121
+ const data = await loadData();
1122
+ buildPointCloud(data);
1123
+ initSliders();
1124
+ applyFilters();
1125
+
1126
+ loading.classList.add('hidden');
1127
+ setTimeout(() => { loading.style.display = 'none'; }, 500);
1128
+ }
1129
+
1130
+ function changeBackend() {
1131
+ const backend = document.getElementById('backend-select').value;
1132
+ populateResolutionSelect(backend);
1133
+ reloadDataset();
1134
+ }
1135
+
1136
+ function changeResolution() {
1137
+ reloadDataset();
1138
+ }
1139
+
1140
+ // ─── Colormap switching ──────────────────────────────────────────────────────
1141
+
1142
+ function changeColormap() {
1143
+ const mapName = document.getElementById('colormap-select').value;
1144
+ state.currentColormap = mapName;
1145
+ if (state.pointCount > 0) {
1146
+ computeColors();
1147
+ applyFilters();
1148
+ }
1149
+ drawLegend();
1150
+ }
1151
+
1152
+ function toggleInvertColormap() {
1153
+ state.invertColormap = !state.invertColormap;
1154
+ const btn = document.getElementById('invert-btn');
1155
+ btn.style.borderColor = state.invertColormap ? 'var(--text-accent)' : '';
1156
+ btn.style.color = state.invertColormap ? 'var(--text-accent)' : '';
1157
+ if (state.pointCount > 0) {
1158
+ computeColors();
1159
+ applyFilters();
1160
+ }
1161
+ drawLegend();
1162
+ }
1163
+
1164
+ // ─── Control handlers ─────────────────────────────────────────────────────────
1165
+
1166
+ function updatePointSize() {
1167
+ const val = parseFloat(document.getElementById('point-size').value);
1168
+ document.getElementById('point-size-val').textContent = val;
1169
+ if (state.points) {
1170
+ state.points.material.size = val * 0.013;
1171
+ state.points.material.needsUpdate = true;
1172
+ }
1173
+ }
1174
+
1175
+ function updateOpacity() {
1176
+ const val = parseFloat(document.getElementById('opacity').value);
1177
+ document.getElementById('opacity-val').textContent = val.toFixed(2);
1178
+ if (state.points) {
1179
+ state.points.material.opacity = val;
1180
+ state.points.material.needsUpdate = true;
1181
+ }
1182
+ }
1183
+
1184
+ function updateTimeFilter() {
1185
+ let minVal = parseFloat(document.getElementById('time-min').value);
1186
+ let maxVal = parseFloat(document.getElementById('time-max').value);
1187
+
1188
+ if (minVal > maxVal) {
1189
+ minVal = maxVal;
1190
+ document.getElementById('time-min').value = minVal;
1191
+ }
1192
+
1193
+ const minSec = (minVal * state.tMax).toFixed(1);
1194
+ const maxSec = maxVal >= 1.0 ? (state.tMax > 0 ? state.tMax.toFixed(1) : '--') : (maxVal * state.tMax).toFixed(1);
1195
+ document.getElementById('time-min-val').textContent = minSec + ' s';
1196
+ document.getElementById('time-max-val').textContent = maxSec + ' s';
1197
+
1198
+ applyFilters();
1199
+ }
1200
+
1201
+ function updateSliceThickness() {
1202
+ const val = parseInt(document.getElementById('slice-thickness').value);
1203
+ document.getElementById('slice-thickness-val').textContent = val + 'Β°';
1204
+ applyFilters();
1205
+ }
1206
+
1207
+ function updateSliceLabel(axis) {
1208
+ const val = parseInt(document.getElementById('slice-' + axis).value);
1209
+ document.getElementById('slice-' + axis + '-val').textContent = val + 'Β°';
1210
+ applyFilters();
1211
+ }
1212
+
1213
+ function togglePanel() {
1214
+ document.getElementById('controls').classList.toggle('collapsed');
1215
+ }
1216
+
1217
+ function showAllOctants() {
1218
+ for (let octantIdx = 1; octantIdx <= 8; octantIdx++) {
1219
+ document.getElementById('octant-' + octantIdx).checked = true;
1220
+ }
1221
+ applyFilters();
1222
+ }
1223
+
1224
+ function hideAllOctants() {
1225
+ for (let octantIdx = 1; octantIdx <= 8; octantIdx++) {
1226
+ document.getElementById('octant-' + octantIdx).checked = false;
1227
+ }
1228
+ applyFilters();
1229
+ }
1230
+
1231
+ function toggleAutoRotate() {
1232
+ state.controls.autoRotate = document.getElementById('auto-rotate').checked;
1233
+ state.controls.autoRotateSpeed = parseFloat(document.getElementById('rotate-speed').value);
1234
+ }
1235
+
1236
+ function updateRotateSpeed() {
1237
+ const val = parseFloat(document.getElementById('rotate-speed').value);
1238
+ document.getElementById('rotate-speed-val').textContent = val.toFixed(1);
1239
+ state.controls.autoRotateSpeed = val;
1240
+ }
1241
+
1242
+ function toggleAxes() {
1243
+ if (state.axesHelper) state.axesHelper.visible = document.getElementById('show-axes').checked;
1244
+ }
1245
+
1246
+ function resetView() {
1247
+ state.camera.position.set(2.2, 1.6, 2.2);
1248
+ state.controls.target.set(0, 0, 0);
1249
+ state.controls.update();
1250
+ }
1251
+
1252
+ // Expose to inline handlers
1253
+ window.toggleAutoRotate = toggleAutoRotate;
1254
+ window.updateRotateSpeed = updateRotateSpeed;
1255
+ window.toggleAxes = toggleAxes;
1256
+ window.resetView = resetView;
1257
+ window.updatePointSize = updatePointSize;
1258
+ window.updateOpacity = updateOpacity;
1259
+ window.updateTimeFilter = updateTimeFilter;
1260
+ window.updateSliceThickness = updateSliceThickness;
1261
+ window.updateSliceLabel = updateSliceLabel;
1262
+ window.applyFilters = applyFilters;
1263
+ window.togglePanel = togglePanel;
1264
+ window.showAllOctants = showAllOctants;
1265
+ window.hideAllOctants = hideAllOctants;
1266
+ window.changeColormap = changeColormap;
1267
+ window.toggleInvertColormap = toggleInvertColormap;
1268
+ window.changeBackend = changeBackend;
1269
+ window.changeResolution = changeResolution;
1270
+
1271
+ // ─── Render loop ──────────────────────────────────────────────────────────────
1272
+
1273
+ function animate() {
1274
+ requestAnimationFrame(animate);
1275
+ state.controls.update();
1276
+ state.renderer.render(state.scene, state.camera);
1277
+ }
1278
+
1279
+ // ─── Initialise time-range sliders from real data ────────────────────────────
1280
+
1281
+ function initSliders() {
1282
+ // Time filter defaults are applied by applyConfigTimeFilter()
1283
+ }
1284
+
1285
+ // ─── Config ───────────────────────────────────────────────────────────────────
1286
+
1287
+ let CONFIG = null;
1288
+
1289
+ async function loadConfig() {
1290
+ try {
1291
+ const res = await fetch('viewer-config.json');
1292
+ if (res.ok) CONFIG = await res.json();
1293
+ } catch (e) {
1294
+ console.warn('Could not load viewer-config.json, using HTML defaults');
1295
+ }
1296
+ }
1297
+
1298
+ function applyConfig() {
1299
+ if (!CONFIG) return;
1300
+
1301
+ const d = CONFIG.dataset || {};
1302
+ const r = CONFIG.rendering || {};
1303
+ const t = CONFIG.timeFilter || {};
1304
+ const disp = CONFIG.display || {};
1305
+
1306
+ // Dataset
1307
+ if (d.backend) document.getElementById('backend-select').value = d.backend;
1308
+
1309
+ // Rendering controls
1310
+ if (r.pointSize != null) {
1311
+ document.getElementById('point-size').value = r.pointSize;
1312
+ document.getElementById('point-size-val').textContent = r.pointSize;
1313
+ }
1314
+ if (r.opacity != null) {
1315
+ document.getElementById('opacity').value = r.opacity;
1316
+ document.getElementById('opacity-val').textContent = r.opacity;
1317
+ }
1318
+ if (r.colormap) document.getElementById('colormap-select').value = r.colormap;
1319
+
1320
+ // Display
1321
+ if (disp.showStable === false) document.getElementById('show-stable').checked = false;
1322
+ if (disp.showStable === true) document.getElementById('show-stable').checked = true;
1323
+ if (disp.showAxes === false) document.getElementById('show-axes').checked = false;
1324
+ if (disp.showAxes === true) document.getElementById('show-axes').checked = true;
1325
+ if (disp.autoRotate != null) document.getElementById('auto-rotate').checked = !!disp.autoRotate;
1326
+ if (disp.autoRotateSpeed != null) {
1327
+ document.getElementById('rotate-speed').value = disp.autoRotateSpeed;
1328
+ document.getElementById('rotate-speed-val').textContent = disp.autoRotateSpeed.toFixed(1);
1329
+ }
1330
+ if (disp.sliceThickness != null) {
1331
+ document.getElementById('slice-thickness').value = disp.sliceThickness;
1332
+ document.getElementById('slice-thickness-val').textContent = disp.sliceThickness;
1333
+ }
1334
+ }
1335
+
1336
+ function applyConfigToMaterial() {
1337
+ if (!CONFIG) return;
1338
+ const r = CONFIG.rendering || {};
1339
+ const disp = CONFIG.display || {};
1340
+ if (state.points) {
1341
+ if (r.pointSize != null) state.points.material.size = r.pointSize * 0.013;
1342
+ if (r.opacity != null) state.points.material.opacity = r.opacity;
1343
+ }
1344
+ if (state.controls && disp.autoRotate) {
1345
+ state.controls.autoRotate = true;
1346
+ state.controls.autoRotateSpeed = disp.autoRotateSpeed || 1.0;
1347
+ }
1348
+ if (state.axesHelper && disp.showAxes != null) {
1349
+ state.axesHelper.visible = disp.showAxes;
1350
+ }
1351
+ }
1352
+
1353
+ function applyConfigTimeFilter() {
1354
+ if (!CONFIG) return;
1355
+ const t = CONFIG.timeFilter || {};
1356
+ const tMax = state.tMax;
1357
+ if (tMax <= 0) return;
1358
+ if (t.minSeconds != null) {
1359
+ const norm = Math.min(t.minSeconds / tMax, 1.0);
1360
+ document.getElementById('time-min').value = norm;
1361
+ document.getElementById('time-min-val').textContent = (norm * tMax).toFixed(1) + ' s';
1362
+ }
1363
+ if (t.maxSeconds != null) {
1364
+ const norm = Math.min(t.maxSeconds / tMax, 1.0);
1365
+ document.getElementById('time-max').value = norm;
1366
+ document.getElementById('time-max-val').textContent = (norm * tMax).toFixed(1) + ' s';
1367
+ }
1368
+ }
1369
+
1370
+ // ─── Main ─────────────────────────────────────────────────────────────────────
1371
+
1372
+ async function main() {
1373
+ await loadConfig();
1374
+ applyConfig();
1375
+
1376
+ initThree();
1377
+ animate();
1378
+ drawLegend();
1379
+
1380
+ const defaultResolution = CONFIG?.dataset?.defaultResolution || null;
1381
+ populateResolutionSelect(document.getElementById('backend-select').value, defaultResolution);
1382
+ const data = await loadData();
1383
+ buildPointCloud(data);
1384
+ applyConfigToMaterial();
1385
+ applyConfigTimeFilter();
1386
+ applyFilters();
1387
+
1388
+ // Hide loading screen
1389
+ const loading = document.getElementById('loading');
1390
+ loading.classList.add('hidden');
1391
+ setTimeout(() => { loading.style.display = 'none'; }, 500);
1392
+ }
1393
+
1394
+ main();
1395
+ </script>
1396
+ </body>
1397
  </html>