storysplat-viewer 2.0.0 → 2.2.0
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/README.md +284 -502
- 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.umd.js +1 -1
- package/dist/storysplat-viewer.umd.js.map +1 -1
- package/dist/types/dynamic-viewer/AnimatedGifHelper.d.ts +83 -0
- package/dist/types/dynamic-viewer/CameraControls.d.ts +8 -3
- package/dist/types/dynamic-viewer/CharacterController.d.ts +138 -0
- package/dist/types/dynamic-viewer/CustomScriptSystem.d.ts +87 -0
- package/dist/types/dynamic-viewer/HtmlMeshHelper.d.ts +148 -0
- package/dist/types/dynamic-viewer/createViewerFromSceneId.d.ts +100 -0
- package/dist/types/dynamic-viewer/viewerUI.d.ts +25 -2
- package/dist/types/index.d.ts +2 -1
- package/dist/types/transformers/sceneToConfig.d.ts +14 -1
- package/dist/types/types/index.d.ts +389 -12
- package/docs/USAGE_GUIDE.md +1196 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,639 +1,427 @@
|
|
|
1
1
|
# StorySplat Viewer
|
|
2
2
|
|
|
3
|
-
A powerful npm package for embedding and interacting with 3D Gaussian Splatting scenes in web applications. StorySplat Viewer provides a complete solution for displaying, navigating, and interacting with .splat files with built-in support for waypoints, hotspots, and multiple camera modes.
|
|
3
|
+
A powerful npm package for embedding and interacting with 3D Gaussian Splatting scenes in web applications. StorySplat Viewer provides a complete solution for displaying, navigating, and interacting with .splat, .ply, and .sog files with built-in support for waypoints, hotspots, and multiple camera modes.
|
|
4
4
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
7
|
-
- [Overview](#overview)
|
|
8
7
|
- [Installation](#installation)
|
|
9
8
|
- [Quick Start](#quick-start)
|
|
9
|
+
- [Loading Scenes](#loading-scenes)
|
|
10
|
+
- [From Scene ID (Live-Linked)](#option-1-from-scene-id-live-linked)
|
|
11
|
+
- [From JSON File (Version-Controlled)](#option-2-from-json-file-version-controlled)
|
|
12
|
+
- [From URL](#option-3-from-url)
|
|
10
13
|
- [API Reference](#api-reference)
|
|
11
14
|
- [Configuration Options](#configuration-options)
|
|
12
|
-
- [
|
|
13
|
-
- [
|
|
14
|
-
- [Styling](#styling)
|
|
15
|
-
- [Migration Guide](#migration-guide)
|
|
15
|
+
- [Events](#events)
|
|
16
|
+
- [React Integration](#react-integration)
|
|
16
17
|
- [Troubleshooting](#troubleshooting)
|
|
17
18
|
|
|
18
|
-
## Overview
|
|
19
|
-
|
|
20
|
-
StorySplat Viewer is a TypeScript/JavaScript library that enables seamless integration of 3D Gaussian Splatting scenes into web applications. It provides:
|
|
21
|
-
|
|
22
|
-
- 🎥 Multiple camera modes (orbit, free, first-person)
|
|
23
|
-
- 🎯 Interactive hotspots with rich content
|
|
24
|
-
- 🚶 Waypoint-based navigation system
|
|
25
|
-
- 📱 Mobile-friendly touch controls
|
|
26
|
-
- 🎨 Customizable UI and styling
|
|
27
|
-
- 🔧 Comprehensive API for programmatic control
|
|
28
|
-
- ⚡ Optimized performance with BabylonJS engine
|
|
29
|
-
|
|
30
19
|
## Installation
|
|
31
20
|
|
|
32
21
|
```bash
|
|
33
|
-
npm install
|
|
22
|
+
npm install storysplat-viewer
|
|
34
23
|
```
|
|
35
24
|
|
|
36
25
|
Or using yarn:
|
|
37
26
|
|
|
38
27
|
```bash
|
|
39
|
-
yarn add
|
|
28
|
+
yarn add storysplat-viewer
|
|
40
29
|
```
|
|
41
30
|
|
|
42
31
|
## Quick Start
|
|
43
32
|
|
|
44
33
|
```javascript
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
targetFps: 60,
|
|
53
|
-
fov: 60,
|
|
54
|
-
cameraMode: 'orbit'
|
|
55
|
-
});
|
|
34
|
+
import { createViewerFromSceneId } from 'storysplat-viewer';
|
|
35
|
+
|
|
36
|
+
// Create a viewer from your StorySplat scene ID
|
|
37
|
+
const viewer = await createViewerFromSceneId(
|
|
38
|
+
document.getElementById('viewer'),
|
|
39
|
+
'YOUR_SCENE_ID'
|
|
40
|
+
);
|
|
56
41
|
|
|
57
|
-
//
|
|
58
|
-
|
|
42
|
+
// Control playback
|
|
43
|
+
viewer.play();
|
|
44
|
+
viewer.pause();
|
|
59
45
|
|
|
60
|
-
//
|
|
61
|
-
viewer.
|
|
46
|
+
// Navigate between waypoints
|
|
47
|
+
viewer.nextWaypoint();
|
|
48
|
+
viewer.goToWaypoint(2);
|
|
49
|
+
|
|
50
|
+
// Listen for events
|
|
51
|
+
viewer.on('ready', () => console.log('Viewer ready!'));
|
|
62
52
|
```
|
|
63
53
|
|
|
64
|
-
##
|
|
54
|
+
## Loading Scenes
|
|
65
55
|
|
|
66
|
-
|
|
56
|
+
There are three ways to load scenes into the viewer:
|
|
67
57
|
|
|
68
|
-
|
|
58
|
+
### Option 1: From Scene ID (Live-Linked)
|
|
69
59
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
60
|
+
Best for: Live content that updates when you edit in the StorySplat editor.
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
import { createViewerFromSceneId } from 'storysplat-viewer';
|
|
64
|
+
|
|
65
|
+
// Scene ID from your StorySplat dashboard
|
|
66
|
+
const viewer = await createViewerFromSceneId(
|
|
67
|
+
document.getElementById('viewer'),
|
|
68
|
+
'YOUR_SCENE_ID'
|
|
69
|
+
);
|
|
76
70
|
```
|
|
77
71
|
|
|
78
|
-
|
|
79
|
-
- `element` (HTMLElement): The DOM element to render the viewer into
|
|
80
|
-
- `data` (StorySplatData): Scene data including splat URL and configuration
|
|
81
|
-
- `options` (Partial<ViewerOptions>): Optional viewer configuration
|
|
72
|
+
The scene is fetched from the StorySplat API and always reflects your latest changes.
|
|
82
73
|
|
|
83
|
-
**
|
|
74
|
+
**Getting your Scene ID:**
|
|
75
|
+
1. Open your scene in the StorySplat editor
|
|
76
|
+
2. Click "Upload" or "Update"
|
|
77
|
+
3. Copy the Scene ID from the "Developer Export" section
|
|
84
78
|
|
|
85
|
-
###
|
|
79
|
+
### Option 2: From JSON File (Version-Controlled)
|
|
86
80
|
|
|
87
|
-
|
|
81
|
+
Best for: Production apps where you want version control over scene data.
|
|
88
82
|
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
setCameraMode(mode: 'orbit' | 'free' | 'firstPerson'): void;
|
|
98
|
-
getCameraMode(): string;
|
|
99
|
-
setCameraPosition(x: number, y: number, z: number): void;
|
|
100
|
-
getCameraPosition(): { x: number; y: number; z: number };
|
|
101
|
-
setCameraTarget(x: number, y: number, z: number): void;
|
|
102
|
-
getCameraTarget(): { x: number; y: number; z: number };
|
|
103
|
-
|
|
104
|
-
// Navigation
|
|
105
|
-
navigateToWaypoint(waypointId: string): Promise<void>;
|
|
106
|
-
getWaypoints(): Waypoint[];
|
|
107
|
-
startWaypointTour(waypointIds?: string[]): void;
|
|
108
|
-
stopWaypointTour(): void;
|
|
109
|
-
|
|
110
|
-
// Hotspots
|
|
111
|
-
getHotspots(): Hotspot[];
|
|
112
|
-
showHotspot(hotspotId: string): void;
|
|
113
|
-
hideHotspot(hotspotId: string): void;
|
|
114
|
-
updateHotspot(hotspotId: string, data: Partial<Hotspot>): void;
|
|
115
|
-
|
|
116
|
-
// Rendering
|
|
117
|
-
setRenderingEnabled(enabled: boolean): void;
|
|
118
|
-
takeScreenshot(options?: ScreenshotOptions): Promise<Blob>;
|
|
119
|
-
|
|
120
|
-
// Scene Properties
|
|
121
|
-
setBackgroundColor(color: string): void;
|
|
122
|
-
setSkybox(skyboxUrl: string): void;
|
|
123
|
-
setFog(enabled: boolean, density?: number): void;
|
|
124
|
-
|
|
125
|
-
// Events
|
|
126
|
-
on(event: string, callback: Function): void;
|
|
127
|
-
off(event: string, callback: Function): void;
|
|
128
|
-
}
|
|
83
|
+
```javascript
|
|
84
|
+
import { createViewer } from 'storysplat-viewer';
|
|
85
|
+
import sceneConfig from './my-scene.json'; // Downloaded from editor
|
|
86
|
+
|
|
87
|
+
const viewer = createViewer(
|
|
88
|
+
document.getElementById('viewer'),
|
|
89
|
+
sceneConfig
|
|
90
|
+
);
|
|
129
91
|
```
|
|
130
92
|
|
|
131
|
-
|
|
93
|
+
**Downloading scene JSON:**
|
|
94
|
+
1. Open your scene in the StorySplat editor
|
|
95
|
+
2. Click "Export" or "Upload"
|
|
96
|
+
3. In the "Developer Export" section, click "Download Scene JSON"
|
|
97
|
+
4. Save the file in your project
|
|
132
98
|
|
|
133
|
-
|
|
99
|
+
### Option 3: From URL
|
|
134
100
|
|
|
135
|
-
|
|
136
|
-
interface StorySplatData {
|
|
137
|
-
splatUrl: string;
|
|
138
|
-
waypoints?: Waypoint[];
|
|
139
|
-
hotspots?: Hotspot[];
|
|
140
|
-
cameraSettings?: CameraSettings;
|
|
141
|
-
environmentSettings?: EnvironmentSettings;
|
|
142
|
-
meshes?: CustomMesh[];
|
|
143
|
-
particleSystems?: ParticleSystemConfig[];
|
|
144
|
-
}
|
|
145
|
-
```
|
|
101
|
+
Load scene configuration from any URL:
|
|
146
102
|
|
|
147
|
-
|
|
103
|
+
```javascript
|
|
104
|
+
import { createViewerFromUrl } from 'storysplat-viewer';
|
|
148
105
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
fov?: number;
|
|
154
|
-
initialCameraPosition?: { x: number; y: number; z: number };
|
|
155
|
-
initialCameraTarget?: { x: number; y: number; z: number };
|
|
156
|
-
|
|
157
|
-
// Rendering
|
|
158
|
-
targetFps?: number;
|
|
159
|
-
backgroundColor?: string;
|
|
160
|
-
skyboxUrl?: string;
|
|
161
|
-
|
|
162
|
-
// UI Options
|
|
163
|
-
showToolbar?: boolean;
|
|
164
|
-
showMobileControls?: boolean;
|
|
165
|
-
showLoadingIndicator?: boolean;
|
|
166
|
-
watermarkText?: string;
|
|
167
|
-
|
|
168
|
-
// Interaction
|
|
169
|
-
enableHotspots?: boolean;
|
|
170
|
-
enableWaypoints?: boolean;
|
|
171
|
-
enableCollisions?: boolean;
|
|
172
|
-
|
|
173
|
-
// Callbacks
|
|
174
|
-
onReady?: () => void;
|
|
175
|
-
onError?: (error: Error) => void;
|
|
176
|
-
onProgress?: (progress: number) => void;
|
|
177
|
-
onHotspotClick?: (hotspot: Hotspot) => void;
|
|
178
|
-
onWaypointReached?: (waypoint: Waypoint) => void;
|
|
179
|
-
}
|
|
106
|
+
const viewer = await createViewerFromUrl(
|
|
107
|
+
document.getElementById('viewer'),
|
|
108
|
+
'https://example.com/my-scene.json'
|
|
109
|
+
);
|
|
180
110
|
```
|
|
181
111
|
|
|
182
|
-
|
|
112
|
+
## API Reference
|
|
113
|
+
|
|
114
|
+
### createViewerFromSceneId
|
|
115
|
+
|
|
116
|
+
Create a viewer by fetching scene data from the StorySplat API.
|
|
183
117
|
|
|
184
118
|
```typescript
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
duration?: number; // Animation duration in ms
|
|
191
|
-
easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
|
|
192
|
-
}
|
|
119
|
+
async function createViewerFromSceneId(
|
|
120
|
+
container: HTMLElement,
|
|
121
|
+
sceneId: string,
|
|
122
|
+
options?: ViewerFromSceneIdOptions
|
|
123
|
+
): Promise<ViewerInstance>
|
|
193
124
|
```
|
|
194
125
|
|
|
195
|
-
|
|
126
|
+
**Parameters:**
|
|
127
|
+
- `container` - HTML element to render the viewer into
|
|
128
|
+
- `sceneId` - Your StorySplat scene ID
|
|
129
|
+
- `options` - Optional viewer configuration
|
|
196
130
|
|
|
131
|
+
**Options:**
|
|
197
132
|
```typescript
|
|
198
|
-
interface
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
133
|
+
interface ViewerFromSceneIdOptions {
|
|
134
|
+
// API configuration
|
|
135
|
+
baseUrl?: string; // Default: 'https://discover.storysplat.com'
|
|
136
|
+
apiKey?: string; // For private scenes (future feature)
|
|
137
|
+
|
|
138
|
+
// Viewer options
|
|
139
|
+
autoPlay?: boolean;
|
|
140
|
+
showUI?: boolean;
|
|
141
|
+
backgroundColor?: string;
|
|
142
|
+
revealEffect?: 'fast' | 'medium' | 'slow' | 'none';
|
|
143
|
+
lazyLoad?: boolean;
|
|
144
|
+
lazyLoadThumbnail?: string;
|
|
145
|
+
lazyLoadButtonText?: string;
|
|
210
146
|
}
|
|
211
147
|
```
|
|
212
148
|
|
|
213
|
-
|
|
149
|
+
### createViewer
|
|
214
150
|
|
|
215
|
-
|
|
151
|
+
Create a viewer from scene data object.
|
|
216
152
|
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
initialCameraPosition: { x: 0, y: 5, z: 10 },
|
|
224
|
-
initialCameraTarget: { x: 0, y: 0, z: 0 }
|
|
225
|
-
});
|
|
153
|
+
```typescript
|
|
154
|
+
function createViewer(
|
|
155
|
+
container: HTMLElement,
|
|
156
|
+
scene: SceneData,
|
|
157
|
+
options?: ViewerOptions
|
|
158
|
+
): ViewerInstance
|
|
226
159
|
```
|
|
227
160
|
|
|
228
|
-
###
|
|
161
|
+
### createViewerFromUrl
|
|
229
162
|
|
|
230
|
-
|
|
231
|
-
const viewer = initializeViewer(element, data, {
|
|
232
|
-
targetFps: 60, // Target frame rate
|
|
233
|
-
backgroundColor: '#000000', // Background color
|
|
234
|
-
skyboxUrl: '/assets/skybox.dds', // Optional skybox
|
|
235
|
-
showLoadingIndicator: true // Show loading progress
|
|
236
|
-
});
|
|
237
|
-
```
|
|
163
|
+
Create a viewer by fetching scene data from a URL.
|
|
238
164
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
watermarkText: 'My Scene' // Custom watermark
|
|
246
|
-
});
|
|
165
|
+
```typescript
|
|
166
|
+
async function createViewerFromUrl(
|
|
167
|
+
container: HTMLElement,
|
|
168
|
+
url: string,
|
|
169
|
+
options?: ViewerOptions
|
|
170
|
+
): Promise<ViewerInstance>
|
|
247
171
|
```
|
|
248
172
|
|
|
249
|
-
|
|
173
|
+
### fetchSceneMeta
|
|
250
174
|
|
|
251
|
-
|
|
175
|
+
Fetch scene metadata without creating a viewer (useful for previews).
|
|
252
176
|
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return viewer;
|
|
268
|
-
}
|
|
177
|
+
```typescript
|
|
178
|
+
async function fetchSceneMeta(
|
|
179
|
+
sceneId: string,
|
|
180
|
+
options?: { baseUrl?: string; apiKey?: string }
|
|
181
|
+
): Promise<{
|
|
182
|
+
name: string;
|
|
183
|
+
description: string;
|
|
184
|
+
thumbnailUrl: string;
|
|
185
|
+
userName: string;
|
|
186
|
+
views: number;
|
|
187
|
+
tags: string[];
|
|
188
|
+
createdAt: string | null;
|
|
189
|
+
}>
|
|
269
190
|
```
|
|
270
191
|
|
|
271
|
-
###
|
|
192
|
+
### ViewerInstance
|
|
272
193
|
|
|
273
|
-
|
|
274
|
-
// Create camera mode buttons
|
|
275
|
-
const viewer = initializeViewer(element, { splatUrl: 'scene.splat' });
|
|
194
|
+
The viewer instance returned by all create functions.
|
|
276
195
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
196
|
+
```typescript
|
|
197
|
+
interface ViewerInstance {
|
|
198
|
+
// PlayCanvas app reference
|
|
199
|
+
app: any;
|
|
200
|
+
canvas: HTMLCanvasElement;
|
|
280
201
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
202
|
+
// Navigation
|
|
203
|
+
goToWaypoint: (index: number) => void;
|
|
204
|
+
nextWaypoint: () => void;
|
|
205
|
+
prevWaypoint: () => void;
|
|
206
|
+
getCurrentWaypointIndex: () => number;
|
|
207
|
+
getWaypointCount: () => number;
|
|
208
|
+
|
|
209
|
+
// Camera
|
|
210
|
+
setPosition: (x: number, y: number, z: number) => void;
|
|
211
|
+
setRotation: (x: number, y: number, z: number) => void;
|
|
212
|
+
getPosition: () => { x: number; y: number; z: number };
|
|
213
|
+
getRotation: () => { x: number; y: number; z: number };
|
|
214
|
+
|
|
215
|
+
// Playback
|
|
216
|
+
play: () => void;
|
|
217
|
+
pause: () => void;
|
|
218
|
+
stop: () => void;
|
|
219
|
+
isPlaying: () => boolean;
|
|
220
|
+
|
|
221
|
+
// Lifecycle
|
|
222
|
+
destroy: () => void;
|
|
223
|
+
resize: () => void;
|
|
284
224
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
225
|
+
// Events
|
|
226
|
+
on: (event: ViewerEvent, callback: (...args: any[]) => void) => void;
|
|
227
|
+
off: (event: ViewerEvent, callback: (...args: any[]) => void) => void;
|
|
228
|
+
}
|
|
288
229
|
```
|
|
289
230
|
|
|
290
|
-
|
|
231
|
+
## Configuration Options
|
|
291
232
|
|
|
292
|
-
|
|
293
|
-
const viewer = initializeViewer(element, {
|
|
294
|
-
splatUrl: '/models/scene.splat',
|
|
295
|
-
waypoints: [
|
|
296
|
-
{ id: 'entrance', name: 'Entrance', position: { x: 0, y: 0, z: 5 } },
|
|
297
|
-
{ id: 'center', name: 'Center', position: { x: 0, y: 0, z: 0 } },
|
|
298
|
-
{ id: 'exit', name: 'Exit', position: { x: 0, y: 0, z: -5 } }
|
|
299
|
-
]
|
|
300
|
-
});
|
|
233
|
+
### ViewerOptions
|
|
301
234
|
|
|
302
|
-
|
|
303
|
-
|
|
235
|
+
```typescript
|
|
236
|
+
interface ViewerOptions {
|
|
237
|
+
// Template style
|
|
238
|
+
template?: 'standard' | 'minimal' | 'pro';
|
|
304
239
|
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
```
|
|
240
|
+
// Playback
|
|
241
|
+
autoPlay?: boolean;
|
|
308
242
|
|
|
309
|
-
|
|
243
|
+
// UI
|
|
244
|
+
showUI?: boolean;
|
|
245
|
+
backgroundColor?: string;
|
|
310
246
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
splatUrl: 'scene.splat',
|
|
314
|
-
hotspots: [
|
|
315
|
-
{
|
|
316
|
-
id: 'info-1',
|
|
317
|
-
position: { x: 2, y: 1, z: 0 },
|
|
318
|
-
type: 'info',
|
|
319
|
-
title: 'Information',
|
|
320
|
-
content: 'This is an information hotspot'
|
|
321
|
-
}
|
|
322
|
-
]
|
|
323
|
-
}, {
|
|
324
|
-
onHotspotClick: (hotspot) => {
|
|
325
|
-
console.log('Hotspot clicked:', hotspot);
|
|
326
|
-
// Custom hotspot handling
|
|
327
|
-
if (hotspot.type === 'link') {
|
|
328
|
-
window.open(hotspot.linkUrl, '_blank');
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
});
|
|
247
|
+
// Loading animation
|
|
248
|
+
revealEffect?: 'fast' | 'medium' | 'slow' | 'none';
|
|
332
249
|
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
content: 'Added at runtime',
|
|
339
|
-
color: '#ff0000'
|
|
340
|
-
});
|
|
250
|
+
// Lazy loading
|
|
251
|
+
lazyLoad?: boolean;
|
|
252
|
+
lazyLoadThumbnail?: string;
|
|
253
|
+
lazyLoadButtonText?: string;
|
|
254
|
+
}
|
|
341
255
|
```
|
|
342
256
|
|
|
343
|
-
###
|
|
257
|
+
### Lazy Loading
|
|
258
|
+
|
|
259
|
+
Show a thumbnail with a start button before loading the full viewer:
|
|
344
260
|
|
|
345
261
|
```javascript
|
|
346
|
-
const viewer =
|
|
347
|
-
|
|
262
|
+
const viewer = await createViewerFromSceneId(container, sceneId, {
|
|
263
|
+
lazyLoad: true,
|
|
264
|
+
lazyLoadThumbnail: '/preview.jpg', // Optional custom thumbnail
|
|
265
|
+
lazyLoadButtonText: 'Start Tour' // Default: "Start Experience"
|
|
348
266
|
});
|
|
349
|
-
|
|
350
|
-
// Load additional splats
|
|
351
|
-
async function loadScenes() {
|
|
352
|
-
await viewer.loadSplat('/models/building.splat');
|
|
353
|
-
await viewer.loadSplat('/models/furniture.splat', {
|
|
354
|
-
position: { x: 0, y: 0, z: 0 },
|
|
355
|
-
scale: 1.5,
|
|
356
|
-
rotation: { x: 0, y: 90, z: 0 }
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Clear and load new scene
|
|
361
|
-
async function switchScene() {
|
|
362
|
-
viewer.clearScene();
|
|
363
|
-
await viewer.loadSplat('/models/new-scene.splat');
|
|
364
|
-
}
|
|
365
267
|
```
|
|
366
268
|
|
|
367
|
-
###
|
|
269
|
+
### Reveal Effects
|
|
270
|
+
|
|
271
|
+
Control how the splat appears when loaded:
|
|
368
272
|
|
|
369
273
|
```javascript
|
|
370
|
-
const viewer =
|
|
371
|
-
|
|
274
|
+
const viewer = createViewer(container, sceneData, {
|
|
275
|
+
revealEffect: 'medium' // 'fast' | 'medium' | 'slow' | 'none'
|
|
372
276
|
});
|
|
373
|
-
|
|
374
|
-
// Create custom controls
|
|
375
|
-
const customUI = {
|
|
376
|
-
updateCameraInfo() {
|
|
377
|
-
const pos = viewer.getCameraPosition();
|
|
378
|
-
document.getElementById('camera-info').textContent =
|
|
379
|
-
`Camera: ${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}, ${pos.z.toFixed(2)}`;
|
|
380
|
-
},
|
|
381
|
-
|
|
382
|
-
takePhoto() {
|
|
383
|
-
viewer.takeScreenshot({ width: 1920, height: 1080 })
|
|
384
|
-
.then(blob => {
|
|
385
|
-
const url = URL.createObjectURL(blob);
|
|
386
|
-
window.open(url, '_blank');
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
// Update camera info on render
|
|
392
|
-
viewer.on('render', customUI.updateCameraInfo);
|
|
393
277
|
```
|
|
394
278
|
|
|
395
|
-
## Events
|
|
396
|
-
|
|
397
|
-
### Available Events
|
|
279
|
+
## Events
|
|
398
280
|
|
|
399
281
|
```javascript
|
|
282
|
+
// Viewer is ready
|
|
400
283
|
viewer.on('ready', () => {
|
|
401
|
-
console.log('Viewer initialized
|
|
284
|
+
console.log('Viewer initialized');
|
|
402
285
|
});
|
|
403
286
|
|
|
404
|
-
|
|
405
|
-
|
|
287
|
+
// Scene loaded
|
|
288
|
+
viewer.on('loaded', () => {
|
|
289
|
+
console.log('Scene fully loaded');
|
|
406
290
|
});
|
|
407
291
|
|
|
408
|
-
|
|
409
|
-
|
|
292
|
+
// Loading progress
|
|
293
|
+
viewer.on('progress', ({ loaded, total, percent }) => {
|
|
294
|
+
console.log(`Loading: ${percent}%`);
|
|
410
295
|
});
|
|
411
296
|
|
|
412
|
-
|
|
413
|
-
|
|
297
|
+
// Waypoint changed
|
|
298
|
+
viewer.on('waypointChange', ({ index, waypoint }) => {
|
|
299
|
+
console.log(`Now at waypoint ${index}: ${waypoint.name}`);
|
|
414
300
|
});
|
|
415
301
|
|
|
302
|
+
// Playback state
|
|
303
|
+
viewer.on('playbackStart', () => console.log('Playing'));
|
|
304
|
+
viewer.on('playbackStop', () => console.log('Stopped'));
|
|
305
|
+
|
|
306
|
+
// Errors
|
|
416
307
|
viewer.on('error', (error) => {
|
|
417
308
|
console.error('Viewer error:', error);
|
|
418
309
|
});
|
|
419
|
-
|
|
420
|
-
viewer.on('cameraMove', (position) => {
|
|
421
|
-
console.log('Camera moved to:', position);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
viewer.on('hotspotClick', (hotspot) => {
|
|
425
|
-
console.log('Hotspot clicked:', hotspot);
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
viewer.on('waypointReached', (waypoint) => {
|
|
429
|
-
console.log('Reached waypoint:', waypoint);
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
viewer.on('render', (deltaTime) => {
|
|
433
|
-
// Called every frame
|
|
434
|
-
});
|
|
435
310
|
```
|
|
436
311
|
|
|
437
|
-
|
|
312
|
+
## React Integration
|
|
438
313
|
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
viewer
|
|
442
|
-
|
|
443
|
-
// Later...
|
|
444
|
-
viewer.off('cameraMove', handler);
|
|
445
|
-
```
|
|
314
|
+
```jsx
|
|
315
|
+
import { useEffect, useRef } from 'react';
|
|
316
|
+
import { createViewerFromSceneId } from 'storysplat-viewer';
|
|
446
317
|
|
|
447
|
-
|
|
318
|
+
function StorySplatViewer({ sceneId }) {
|
|
319
|
+
const containerRef = useRef(null);
|
|
320
|
+
const viewerRef = useRef(null);
|
|
448
321
|
|
|
449
|
-
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
let mounted = true;
|
|
450
324
|
|
|
451
|
-
|
|
325
|
+
async function initViewer() {
|
|
326
|
+
if (!containerRef.current) return;
|
|
452
327
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
--storysplat-toolbar-bg: rgba(0, 0, 0, 0.8);
|
|
459
|
-
--storysplat-button-hover: #0056b3;
|
|
460
|
-
--storysplat-hotspot-color: #ffc107;
|
|
461
|
-
}
|
|
462
|
-
```
|
|
328
|
+
try {
|
|
329
|
+
const viewer = await createViewerFromSceneId(
|
|
330
|
+
containerRef.current,
|
|
331
|
+
sceneId
|
|
332
|
+
);
|
|
463
333
|
|
|
464
|
-
|
|
334
|
+
if (mounted) {
|
|
335
|
+
viewerRef.current = viewer;
|
|
336
|
+
} else {
|
|
337
|
+
viewer.destroy();
|
|
338
|
+
}
|
|
339
|
+
} catch (error) {
|
|
340
|
+
console.error('Failed to create viewer:', error);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
465
343
|
|
|
466
|
-
|
|
467
|
-
/* Override toolbar styles */
|
|
468
|
-
.storysplat-toolbar {
|
|
469
|
-
background: linear-gradient(to bottom, rgba(0,0,0,0.9), rgba(0,0,0,0.7));
|
|
470
|
-
border-radius: 8px;
|
|
471
|
-
margin: 10px;
|
|
472
|
-
}
|
|
344
|
+
initViewer();
|
|
473
345
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
346
|
+
return () => {
|
|
347
|
+
mounted = false;
|
|
348
|
+
viewerRef.current?.destroy();
|
|
349
|
+
};
|
|
350
|
+
}, [sceneId]);
|
|
478
351
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
352
|
+
return (
|
|
353
|
+
<div
|
|
354
|
+
ref={containerRef}
|
|
355
|
+
style={{ width: '100%', height: '500px' }}
|
|
356
|
+
/>
|
|
357
|
+
);
|
|
483
358
|
}
|
|
484
359
|
|
|
485
|
-
|
|
486
|
-
.storysplat-mobile-controls {
|
|
487
|
-
opacity: 0.8;
|
|
488
|
-
}
|
|
360
|
+
export default StorySplatViewer;
|
|
489
361
|
```
|
|
490
362
|
|
|
491
|
-
|
|
363
|
+
## Error Handling
|
|
492
364
|
|
|
493
|
-
|
|
365
|
+
The package exports error classes for specific error handling:
|
|
494
366
|
|
|
495
367
|
```javascript
|
|
496
|
-
import
|
|
368
|
+
import {
|
|
369
|
+
createViewerFromSceneId,
|
|
370
|
+
SceneNotFoundError,
|
|
371
|
+
SceneApiError
|
|
372
|
+
} from 'storysplat-viewer';
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const viewer = await createViewerFromSceneId(container, sceneId);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (error instanceof SceneNotFoundError) {
|
|
378
|
+
console.error('Scene not found or not public');
|
|
379
|
+
} else if (error instanceof SceneApiError) {
|
|
380
|
+
console.error(`API error (${error.statusCode}): ${error.message}`);
|
|
381
|
+
} else {
|
|
382
|
+
console.error('Unknown error:', error);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
497
385
|
```
|
|
498
386
|
|
|
499
|
-
|
|
387
|
+
## Comparison: Scene ID vs JSON File
|
|
500
388
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
## Migration Guide
|
|
506
|
-
|
|
507
|
-
### From Exported HTML to NPM Package
|
|
508
|
-
|
|
509
|
-
If you're migrating from StorySplat's exported HTML files to the npm package:
|
|
510
|
-
|
|
511
|
-
1. **Extract Scene Data**
|
|
512
|
-
```javascript
|
|
513
|
-
// From your exported HTML's scene configuration
|
|
514
|
-
const sceneData = {
|
|
515
|
-
splatUrl: 'path/to/your.splat',
|
|
516
|
-
waypoints: [...], // Copy waypoints array
|
|
517
|
-
hotspots: [...], // Copy hotspots array
|
|
518
|
-
cameraSettings: {
|
|
519
|
-
position: { x: 0, y: 5, z: 10 },
|
|
520
|
-
target: { x: 0, y: 0, z: 0 }
|
|
521
|
-
}
|
|
522
|
-
};
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
2. **Initialize Viewer**
|
|
526
|
-
```javascript
|
|
527
|
-
const element = document.getElementById('viewer-container');
|
|
528
|
-
const viewer = initializeViewer(element, sceneData, {
|
|
529
|
-
// Match your export settings
|
|
530
|
-
backgroundColor: '#000000',
|
|
531
|
-
targetFps: 60,
|
|
532
|
-
cameraMode: 'orbit'
|
|
533
|
-
});
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
3. **Update Event Handlers**
|
|
537
|
-
```javascript
|
|
538
|
-
// Old: onclick handlers in HTML
|
|
539
|
-
// New: Event listeners
|
|
540
|
-
viewer.on('hotspotClick', handleHotspotClick);
|
|
541
|
-
viewer.on('waypointReached', handleWaypointReached);
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
4. **Handle Assets**
|
|
545
|
-
```javascript
|
|
546
|
-
// Ensure all assets use absolute URLs or are properly bundled
|
|
547
|
-
const sceneData = {
|
|
548
|
-
splatUrl: '/assets/scene.splat',
|
|
549
|
-
skyboxUrl: '/assets/skybox.dds'
|
|
550
|
-
};
|
|
551
|
-
```
|
|
389
|
+
| Approach | Best For | Pros | Cons |
|
|
390
|
+
|----------|----------|------|------|
|
|
391
|
+
| **Scene ID** | Live content, CMS | Always latest, no redeploy | Needs internet, API dependency |
|
|
392
|
+
| **JSON File** | Version control, offline | Full control, git-tracked | Manual updates needed |
|
|
552
393
|
|
|
553
394
|
## Troubleshooting
|
|
554
395
|
|
|
555
|
-
###
|
|
396
|
+
### Container Needs Dimensions
|
|
556
397
|
|
|
557
|
-
|
|
398
|
+
The container element must have explicit dimensions:
|
|
558
399
|
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
const container = document.getElementById('storysplat-viewer');
|
|
562
|
-
if (!container) {
|
|
563
|
-
console.error('Container element not found');
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// Ensure container has explicit dimensions
|
|
567
|
-
#storysplat-viewer {
|
|
400
|
+
```css
|
|
401
|
+
#viewer {
|
|
568
402
|
width: 100%;
|
|
569
403
|
height: 500px; /* Must have explicit height */
|
|
570
404
|
}
|
|
571
405
|
```
|
|
572
406
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
```javascript
|
|
576
|
-
// For cross-origin splat files, ensure proper CORS headers
|
|
577
|
-
// Or use a proxy:
|
|
578
|
-
const viewer = initializeViewer(element, {
|
|
579
|
-
splatUrl: '/api/proxy?url=' + encodeURIComponent(splatUrl)
|
|
580
|
-
});
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
#### Performance Issues
|
|
584
|
-
|
|
585
|
-
```javascript
|
|
586
|
-
// Reduce target FPS for better performance
|
|
587
|
-
const viewer = initializeViewer(element, data, {
|
|
588
|
-
targetFps: 30, // Lower FPS
|
|
589
|
-
// Disable expensive features
|
|
590
|
-
skyboxUrl: null,
|
|
591
|
-
showLoadingIndicator: false
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
// Optimize rendering
|
|
595
|
-
viewer.setRenderingEnabled(false); // Pause when not visible
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
#### Mobile Touch Controls Not Working
|
|
407
|
+
### CORS Issues
|
|
599
408
|
|
|
600
|
-
|
|
601
|
-
// Ensure mobile controls are enabled
|
|
602
|
-
const viewer = initializeViewer(element, data, {
|
|
603
|
-
showMobileControls: true,
|
|
604
|
-
// Force mobile controls for testing
|
|
605
|
-
forceMobileControls: true
|
|
606
|
-
});
|
|
409
|
+
If loading splats from a different domain, ensure the server has proper CORS headers or use a proxy.
|
|
607
410
|
|
|
608
|
-
|
|
609
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
610
|
-
```
|
|
411
|
+
### Memory Cleanup
|
|
611
412
|
|
|
612
|
-
|
|
413
|
+
Always destroy the viewer when unmounting:
|
|
613
414
|
|
|
614
415
|
```javascript
|
|
615
|
-
//
|
|
416
|
+
// Vanilla JS
|
|
616
417
|
window.addEventListener('beforeunload', () => {
|
|
617
418
|
viewer.destroy();
|
|
618
419
|
});
|
|
619
420
|
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
### Debug Mode
|
|
629
|
-
|
|
630
|
-
Enable debug logging:
|
|
631
|
-
|
|
632
|
-
```javascript
|
|
633
|
-
const viewer = initializeViewer(element, data, {
|
|
634
|
-
debug: true, // Enable debug logging
|
|
635
|
-
onDebug: (message) => console.log('[Debug]', message)
|
|
636
|
-
});
|
|
421
|
+
// React useEffect cleanup
|
|
422
|
+
return () => {
|
|
423
|
+
viewer?.destroy();
|
|
424
|
+
};
|
|
637
425
|
```
|
|
638
426
|
|
|
639
427
|
### Browser Compatibility
|
|
@@ -641,19 +429,13 @@ const viewer = initializeViewer(element, data, {
|
|
|
641
429
|
StorySplat Viewer requires:
|
|
642
430
|
- WebGL 2.0 support
|
|
643
431
|
- Modern JavaScript (ES2015+)
|
|
644
|
-
- Recommended browsers:
|
|
645
|
-
- Chrome 80+
|
|
646
|
-
- Firefox 75+
|
|
647
|
-
- Safari 13+
|
|
648
|
-
- Edge 80+
|
|
432
|
+
- Recommended browsers: Chrome 80+, Firefox 75+, Safari 13+, Edge 80+
|
|
649
433
|
|
|
650
|
-
|
|
434
|
+
## Support
|
|
651
435
|
|
|
652
|
-
|
|
653
|
-
- GitHub Issues: [github.com/storysplat/viewer/issues](https://github.com/storysplat/viewer/issues)
|
|
436
|
+
- GitHub Issues: [github.com/SonnyC56/storysplat-viewer/issues](https://github.com/SonnyC56/storysplat-viewer/issues)
|
|
654
437
|
- Documentation: [docs.storysplat.com](https://docs.storysplat.com)
|
|
655
|
-
- Discord: [discord.gg/storysplat](https://discord.gg/storysplat)
|
|
656
438
|
|
|
657
439
|
## License
|
|
658
440
|
|
|
659
|
-
MIT License - see LICENSE file for details
|
|
441
|
+
MIT License - see LICENSE file for details
|