storysplat-viewer 2.9.7 → 2.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/demo/4dgs-flipbook-demo.html +406 -0
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/storysplat-viewer.bundled.umd.js +1 -1
- package/dist/storysplat-viewer.bundled.umd.js.map +1 -1
- package/dist/storysplat-viewer.umd.js +1 -1
- package/dist/storysplat-viewer.umd.js.map +1 -1
- package/dist/types/dynamic-viewer/FrameSequencePlayer.d.ts +21 -9
- package/dist/types/types/index.d.ts +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,406 @@
|
|
|
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>StorySplat 4DGS Flipbook Demo</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg-primary: #0d0d1a;
|
|
10
|
+
--bg-secondary: #1a1a2e;
|
|
11
|
+
--bg-tertiary: #252540;
|
|
12
|
+
--accent-primary: #6366f1;
|
|
13
|
+
--accent-secondary: #818cf8;
|
|
14
|
+
--accent-success: #22c55e;
|
|
15
|
+
--text-primary: #ffffff;
|
|
16
|
+
--text-secondary: #a1a1aa;
|
|
17
|
+
--text-muted: #71717a;
|
|
18
|
+
--border-color: #3f3f5a;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
22
|
+
|
|
23
|
+
body {
|
|
24
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
25
|
+
background: var(--bg-primary);
|
|
26
|
+
color: var(--text-primary);
|
|
27
|
+
height: 100vh;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#viewer-container {
|
|
34
|
+
flex: 1;
|
|
35
|
+
position: relative;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Loading overlay */
|
|
39
|
+
#loading-overlay {
|
|
40
|
+
position: absolute;
|
|
41
|
+
inset: 0;
|
|
42
|
+
background: var(--bg-primary);
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
align-items: center;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
z-index: 10;
|
|
48
|
+
transition: opacity 0.4s ease;
|
|
49
|
+
}
|
|
50
|
+
#loading-overlay.hidden {
|
|
51
|
+
opacity: 0;
|
|
52
|
+
pointer-events: none;
|
|
53
|
+
}
|
|
54
|
+
.loading-title {
|
|
55
|
+
font-size: 1.4rem;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
margin-bottom: 1rem;
|
|
58
|
+
}
|
|
59
|
+
.loading-subtitle {
|
|
60
|
+
color: var(--text-secondary);
|
|
61
|
+
font-size: 0.9rem;
|
|
62
|
+
margin-bottom: 1.5rem;
|
|
63
|
+
}
|
|
64
|
+
.progress-bar-track {
|
|
65
|
+
width: 300px;
|
|
66
|
+
height: 6px;
|
|
67
|
+
background: var(--bg-tertiary);
|
|
68
|
+
border-radius: 3px;
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
}
|
|
71
|
+
.progress-bar-fill {
|
|
72
|
+
height: 100%;
|
|
73
|
+
background: var(--accent-primary);
|
|
74
|
+
border-radius: 3px;
|
|
75
|
+
width: 0%;
|
|
76
|
+
transition: width 0.3s ease;
|
|
77
|
+
}
|
|
78
|
+
.loading-status {
|
|
79
|
+
color: var(--text-muted);
|
|
80
|
+
font-size: 0.8rem;
|
|
81
|
+
margin-top: 0.75rem;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Controls bar */
|
|
85
|
+
#controls {
|
|
86
|
+
background: var(--bg-secondary);
|
|
87
|
+
border-top: 1px solid var(--border-color);
|
|
88
|
+
padding: 12px 20px;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 12px;
|
|
92
|
+
flex-wrap: wrap;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.ctrl-btn {
|
|
96
|
+
background: var(--bg-tertiary);
|
|
97
|
+
border: 1px solid var(--border-color);
|
|
98
|
+
color: var(--text-primary);
|
|
99
|
+
padding: 6px 12px;
|
|
100
|
+
border-radius: 6px;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
font-size: 0.85rem;
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
gap: 4px;
|
|
106
|
+
transition: background 0.15s, border-color 0.15s;
|
|
107
|
+
}
|
|
108
|
+
.ctrl-btn:hover { background: var(--accent-primary); border-color: var(--accent-primary); }
|
|
109
|
+
.ctrl-btn.active { background: var(--accent-primary); border-color: var(--accent-secondary); }
|
|
110
|
+
.ctrl-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
111
|
+
.ctrl-btn:disabled:hover { background: var(--bg-tertiary); border-color: var(--border-color); }
|
|
112
|
+
|
|
113
|
+
.ctrl-separator {
|
|
114
|
+
width: 1px;
|
|
115
|
+
height: 24px;
|
|
116
|
+
background: var(--border-color);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Frame slider */
|
|
120
|
+
#frame-slider {
|
|
121
|
+
flex: 1;
|
|
122
|
+
min-width: 120px;
|
|
123
|
+
-webkit-appearance: none;
|
|
124
|
+
appearance: none;
|
|
125
|
+
height: 6px;
|
|
126
|
+
background: var(--bg-tertiary);
|
|
127
|
+
border-radius: 3px;
|
|
128
|
+
outline: none;
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
}
|
|
131
|
+
#frame-slider::-webkit-slider-thumb {
|
|
132
|
+
-webkit-appearance: none;
|
|
133
|
+
width: 14px;
|
|
134
|
+
height: 14px;
|
|
135
|
+
background: var(--accent-primary);
|
|
136
|
+
border-radius: 50%;
|
|
137
|
+
cursor: grab;
|
|
138
|
+
}
|
|
139
|
+
#frame-slider::-moz-range-thumb {
|
|
140
|
+
width: 14px;
|
|
141
|
+
height: 14px;
|
|
142
|
+
background: var(--accent-primary);
|
|
143
|
+
border-radius: 50%;
|
|
144
|
+
border: none;
|
|
145
|
+
cursor: grab;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.ctrl-label {
|
|
149
|
+
color: var(--text-secondary);
|
|
150
|
+
font-size: 0.8rem;
|
|
151
|
+
white-space: nowrap;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#frame-info {
|
|
155
|
+
color: var(--text-primary);
|
|
156
|
+
font-size: 0.85rem;
|
|
157
|
+
font-variant-numeric: tabular-nums;
|
|
158
|
+
min-width: 100px;
|
|
159
|
+
text-align: center;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* FPS input */
|
|
163
|
+
#fps-input {
|
|
164
|
+
width: 50px;
|
|
165
|
+
background: var(--bg-tertiary);
|
|
166
|
+
border: 1px solid var(--border-color);
|
|
167
|
+
color: var(--text-primary);
|
|
168
|
+
padding: 4px 6px;
|
|
169
|
+
border-radius: 4px;
|
|
170
|
+
font-size: 0.85rem;
|
|
171
|
+
text-align: center;
|
|
172
|
+
}
|
|
173
|
+
#fps-input:focus { outline: none; border-color: var(--accent-primary); }
|
|
174
|
+
|
|
175
|
+
/* Loop checkbox */
|
|
176
|
+
.loop-toggle {
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
gap: 6px;
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
font-size: 0.8rem;
|
|
182
|
+
color: var(--text-secondary);
|
|
183
|
+
}
|
|
184
|
+
.loop-toggle input[type="checkbox"] {
|
|
185
|
+
accent-color: var(--accent-primary);
|
|
186
|
+
cursor: pointer;
|
|
187
|
+
}
|
|
188
|
+
</style>
|
|
189
|
+
</head>
|
|
190
|
+
<body>
|
|
191
|
+
|
|
192
|
+
<div id="viewer-container">
|
|
193
|
+
<div id="loading-overlay">
|
|
194
|
+
<div class="loading-title">4DGS Flipbook Demo</div>
|
|
195
|
+
<div class="loading-subtitle">Loading basketball capture (149 frames)</div>
|
|
196
|
+
<div class="progress-bar-track">
|
|
197
|
+
<div class="progress-bar-fill" id="progress-fill"></div>
|
|
198
|
+
</div>
|
|
199
|
+
<div class="loading-status" id="loading-status">Initializing...</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div id="controls">
|
|
204
|
+
<button class="ctrl-btn" id="play-btn" title="Play/Pause">Play</button>
|
|
205
|
+
<button class="ctrl-btn" id="stop-btn" title="Stop">Stop</button>
|
|
206
|
+
|
|
207
|
+
<div class="ctrl-separator"></div>
|
|
208
|
+
|
|
209
|
+
<button class="ctrl-btn" id="prev-btn" title="Previous frame">Prev</button>
|
|
210
|
+
<button class="ctrl-btn" id="next-btn" title="Next frame">Next</button>
|
|
211
|
+
|
|
212
|
+
<div class="ctrl-separator"></div>
|
|
213
|
+
|
|
214
|
+
<input type="range" id="frame-slider" min="0" max="148" value="0">
|
|
215
|
+
<span id="frame-info">Frame: 1 / 149</span>
|
|
216
|
+
|
|
217
|
+
<div class="ctrl-separator"></div>
|
|
218
|
+
|
|
219
|
+
<span class="ctrl-label">FPS:</span>
|
|
220
|
+
<input type="number" id="fps-input" value="30" min="1" max="120">
|
|
221
|
+
|
|
222
|
+
<label class="loop-toggle">
|
|
223
|
+
<input type="checkbox" id="loop-checkbox" checked>
|
|
224
|
+
Loop
|
|
225
|
+
</label>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<script src="../dist/storysplat-viewer.bundled.umd.js"></script>
|
|
229
|
+
<script>
|
|
230
|
+
// Generate 149 basketball frame URLs (001.compressed.ply through 149.compressed.ply)
|
|
231
|
+
var BASE_URL = 'https://code.playcanvas.com/examples_data/example_basketball_02/';
|
|
232
|
+
var TOTAL_FRAMES = 149;
|
|
233
|
+
var frameUrls = [];
|
|
234
|
+
for (var i = 1; i <= TOTAL_FRAMES; i++) {
|
|
235
|
+
frameUrls.push(BASE_URL + String(i).padStart(3, '0') + '.compressed.ply');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// DOM references
|
|
239
|
+
var playBtn = document.getElementById('play-btn');
|
|
240
|
+
var stopBtn = document.getElementById('stop-btn');
|
|
241
|
+
var prevBtn = document.getElementById('prev-btn');
|
|
242
|
+
var nextBtn = document.getElementById('next-btn');
|
|
243
|
+
var frameSlider = document.getElementById('frame-slider');
|
|
244
|
+
var frameInfo = document.getElementById('frame-info');
|
|
245
|
+
var fpsInput = document.getElementById('fps-input');
|
|
246
|
+
var loopCheckbox = document.getElementById('loop-checkbox');
|
|
247
|
+
var loadingOverlay = document.getElementById('loading-overlay');
|
|
248
|
+
var progressFill = document.getElementById('progress-fill');
|
|
249
|
+
var loadingStatus = document.getElementById('loading-status');
|
|
250
|
+
|
|
251
|
+
var isUserSeeking = false; // Prevent slider fighting with frameChange events
|
|
252
|
+
|
|
253
|
+
// Create viewer — matching PlayCanvas splat-flipbook example settings:
|
|
254
|
+
// - rotation [0, -90, 180] corrects the basketball capture's coordinate system
|
|
255
|
+
// - 'explore' mode with 'orbit' sub-mode enables mouse orbit/pan/zoom
|
|
256
|
+
var viewer = StorySplatViewer.createViewer(
|
|
257
|
+
document.getElementById('viewer-container'),
|
|
258
|
+
{
|
|
259
|
+
name: '4DGS Basketball Demo',
|
|
260
|
+
frameSequence: {
|
|
261
|
+
frameUrls: frameUrls,
|
|
262
|
+
fps: 30,
|
|
263
|
+
loop: true,
|
|
264
|
+
preloadCount: 10,
|
|
265
|
+
autoplay: false,
|
|
266
|
+
rotation: [0, -90, 180],
|
|
267
|
+
},
|
|
268
|
+
defaultCameraMode: 'explore',
|
|
269
|
+
initialSplatExploreMode: 'orbit',
|
|
270
|
+
cameraRotationSensitivity: 4000,
|
|
271
|
+
showUI: false,
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// --- Loading progress ---
|
|
276
|
+
viewer.on('progress', function(data) {
|
|
277
|
+
if (data && typeof data.progress === 'number') {
|
|
278
|
+
var pct = Math.round(data.progress * 100);
|
|
279
|
+
progressFill.style.width = pct + '%';
|
|
280
|
+
loadingStatus.textContent = 'Loading... ' + pct + '%';
|
|
281
|
+
if (data.progress >= 1) {
|
|
282
|
+
loadingOverlay.classList.add('hidden');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Hide loading once first frame renders
|
|
288
|
+
viewer.on('frameChange', function onFirstFrame() {
|
|
289
|
+
loadingOverlay.classList.add('hidden');
|
|
290
|
+
viewer.off('frameChange', onFirstFrame);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Fallback: hide loading after a timeout
|
|
294
|
+
setTimeout(function() {
|
|
295
|
+
loadingOverlay.classList.add('hidden');
|
|
296
|
+
}, 15000);
|
|
297
|
+
|
|
298
|
+
// --- Frame change event ---
|
|
299
|
+
viewer.on('frameChange', function(frame, total) {
|
|
300
|
+
if (!isUserSeeking) {
|
|
301
|
+
frameSlider.value = frame;
|
|
302
|
+
frameSlider.max = total - 1;
|
|
303
|
+
}
|
|
304
|
+
frameInfo.textContent = 'Frame: ' + (frame + 1) + ' / ' + total;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// --- Playback controls ---
|
|
308
|
+
var playing = false;
|
|
309
|
+
|
|
310
|
+
function updatePlayButton() {
|
|
311
|
+
if (playing) {
|
|
312
|
+
playBtn.textContent = 'Pause';
|
|
313
|
+
playBtn.classList.add('active');
|
|
314
|
+
} else {
|
|
315
|
+
playBtn.textContent = 'Play';
|
|
316
|
+
playBtn.classList.remove('active');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
playBtn.addEventListener('click', function() {
|
|
321
|
+
if (playing) {
|
|
322
|
+
viewer.pause();
|
|
323
|
+
playing = false;
|
|
324
|
+
} else {
|
|
325
|
+
viewer.play();
|
|
326
|
+
playing = true;
|
|
327
|
+
}
|
|
328
|
+
updatePlayButton();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
stopBtn.addEventListener('click', function() {
|
|
332
|
+
viewer.stop();
|
|
333
|
+
playing = false;
|
|
334
|
+
updatePlayButton();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
prevBtn.addEventListener('click', function() {
|
|
338
|
+
var current = viewer.getCurrentFrame();
|
|
339
|
+
var prev = current - 1;
|
|
340
|
+
if (prev < 0) prev = loopCheckbox.checked ? TOTAL_FRAMES - 1 : 0;
|
|
341
|
+
viewer.setFrame(prev);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
nextBtn.addEventListener('click', function() {
|
|
345
|
+
var current = viewer.getCurrentFrame();
|
|
346
|
+
var next = current + 1;
|
|
347
|
+
if (next >= TOTAL_FRAMES) next = loopCheckbox.checked ? 0 : TOTAL_FRAMES - 1;
|
|
348
|
+
viewer.setFrame(next);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// --- Frame slider ---
|
|
352
|
+
frameSlider.addEventListener('mousedown', function() { isUserSeeking = true; });
|
|
353
|
+
frameSlider.addEventListener('touchstart', function() { isUserSeeking = true; });
|
|
354
|
+
|
|
355
|
+
frameSlider.addEventListener('input', function() {
|
|
356
|
+
viewer.setFrame(parseInt(frameSlider.value, 10));
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
frameSlider.addEventListener('mouseup', function() { isUserSeeking = false; });
|
|
360
|
+
frameSlider.addEventListener('touchend', function() { isUserSeeking = false; });
|
|
361
|
+
frameSlider.addEventListener('change', function() { isUserSeeking = false; });
|
|
362
|
+
|
|
363
|
+
// --- FPS control ---
|
|
364
|
+
fpsInput.addEventListener('change', function() {
|
|
365
|
+
var fps = parseInt(fpsInput.value, 10);
|
|
366
|
+
if (fps > 0 && fps <= 120) {
|
|
367
|
+
viewer.setFps(fps);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// --- Playback events ---
|
|
372
|
+
viewer.on('playbackStart', function() {
|
|
373
|
+
playing = true;
|
|
374
|
+
updatePlayButton();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
viewer.on('playbackStop', function() {
|
|
378
|
+
playing = false;
|
|
379
|
+
updatePlayButton();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// --- Keyboard shortcuts ---
|
|
383
|
+
document.addEventListener('keydown', function(e) {
|
|
384
|
+
if (e.target.tagName === 'INPUT') return;
|
|
385
|
+
|
|
386
|
+
switch (e.code) {
|
|
387
|
+
case 'Space':
|
|
388
|
+
e.preventDefault();
|
|
389
|
+
playBtn.click();
|
|
390
|
+
break;
|
|
391
|
+
case 'ArrowLeft':
|
|
392
|
+
e.preventDefault();
|
|
393
|
+
prevBtn.click();
|
|
394
|
+
break;
|
|
395
|
+
case 'ArrowRight':
|
|
396
|
+
e.preventDefault();
|
|
397
|
+
nextBtn.click();
|
|
398
|
+
break;
|
|
399
|
+
case 'KeyR':
|
|
400
|
+
stopBtn.click();
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
</script>
|
|
405
|
+
</body>
|
|
406
|
+
</html>
|