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 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
- - [Examples](#examples)
13
- - [Events and Callbacks](#events-and-callbacks)
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 @storysplat/viewer
22
+ npm install storysplat-viewer
34
23
  ```
35
24
 
36
25
  Or using yarn:
37
26
 
38
27
  ```bash
39
- yarn add @storysplat/viewer
28
+ yarn add storysplat-viewer
40
29
  ```
41
30
 
42
31
  ## Quick Start
43
32
 
44
33
  ```javascript
45
- import { initializeViewer } from '@storysplat/viewer';
46
- import '@storysplat/viewer/dist/styles/viewer.css';
47
-
48
- // Initialize the viewer
49
- const viewerElement = document.getElementById('storysplat-viewer');
50
- const viewer = initializeViewer(viewerElement, {
51
- splatUrl: 'https://example.com/scene.splat',
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
- // Load additional splats
58
- await viewer.loadSplat('https://example.com/another.splat');
42
+ // Control playback
43
+ viewer.play();
44
+ viewer.pause();
59
45
 
60
- // Switch camera mode
61
- viewer.setCameraMode('free');
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
- ## API Reference
54
+ ## Loading Scenes
65
55
 
66
- ### initializeViewer
56
+ There are three ways to load scenes into the viewer:
67
57
 
68
- Creates and initializes a new StorySplat viewer instance.
58
+ ### Option 1: From Scene ID (Live-Linked)
69
59
 
70
- ```typescript
71
- function initializeViewer(
72
- element: HTMLElement,
73
- data: StorySplatData,
74
- options?: Partial<ViewerOptions>
75
- ): ViewerInstance
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
- **Parameters:**
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
- **Returns:** ViewerInstance object for controlling the viewer
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
- ### ViewerInstance Interface
79
+ ### Option 2: From JSON File (Version-Controlled)
86
80
 
87
- The main interface for interacting with the viewer after initialization.
81
+ Best for: Production apps where you want version control over scene data.
88
82
 
89
- ```typescript
90
- interface ViewerInstance {
91
- // Scene Management
92
- loadSplat(url: string, options?: LoadOptions): Promise<void>;
93
- clearScene(): void;
94
- destroy(): void;
95
-
96
- // Camera Controls
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
- ### Data Types
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
- #### StorySplatData
99
+ ### Option 3: From URL
134
100
 
135
- ```typescript
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
- #### ViewerOptions
103
+ ```javascript
104
+ import { createViewerFromUrl } from 'storysplat-viewer';
148
105
 
149
- ```typescript
150
- interface ViewerOptions {
151
- // Camera settings
152
- cameraMode?: 'orbit' | 'free' | 'firstPerson';
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
- #### Waypoint
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
- interface Waypoint {
186
- id: string;
187
- name: string;
188
- position: { x: number; y: number; z: number };
189
- lookAt?: { x: number; y: number; z: number };
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
- #### Hotspot
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 Hotspot {
199
- id: string;
200
- position: { x: number; y: number; z: number };
201
- type: 'info' | 'link' | 'media' | 'custom';
202
- title?: string;
203
- content?: string;
204
- mediaUrl?: string;
205
- linkUrl?: string;
206
- icon?: string;
207
- color?: string;
208
- size?: number;
209
- alwaysVisible?: boolean;
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
- ## Configuration Options
149
+ ### createViewer
214
150
 
215
- ### Camera Settings
151
+ Create a viewer from scene data object.
216
152
 
217
- ```javascript
218
- const viewer = initializeViewer(element, {
219
- splatUrl: 'scene.splat'
220
- }, {
221
- cameraMode: 'orbit', // 'orbit' | 'free' | 'firstPerson'
222
- fov: 60, // Field of view in degrees
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
- ### Rendering Options
161
+ ### createViewerFromUrl
229
162
 
230
- ```javascript
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
- ### UI Options
240
-
241
- ```javascript
242
- const viewer = initializeViewer(element, data, {
243
- showToolbar: true, // Show control toolbar
244
- showMobileControls: true, // Auto-detect and show mobile controls
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
- ## Examples
173
+ ### fetchSceneMeta
250
174
 
251
- ### Basic Setup
175
+ Fetch scene metadata without creating a viewer (useful for previews).
252
176
 
253
- ```javascript
254
- import { initializeViewer } from '@storysplat/viewer';
255
- import '@storysplat/viewer/dist/styles/viewer.css';
256
-
257
- function setupViewer() {
258
- const element = document.getElementById('storysplat-viewer');
259
-
260
- const viewer = initializeViewer(element, {
261
- splatUrl: '/models/scene.splat'
262
- }, {
263
- onReady: () => console.log('Viewer ready!'),
264
- onError: (error) => console.error('Viewer error:', error)
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
- ### Camera Mode Switching
192
+ ### ViewerInstance
272
193
 
273
- ```javascript
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
- document.getElementById('orbit-btn').onclick = () => {
278
- viewer.setCameraMode('orbit');
279
- };
196
+ ```typescript
197
+ interface ViewerInstance {
198
+ // PlayCanvas app reference
199
+ app: any;
200
+ canvas: HTMLCanvasElement;
280
201
 
281
- document.getElementById('free-btn').onclick = () => {
282
- viewer.setCameraMode('free');
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
- document.getElementById('fps-btn').onclick = () => {
286
- viewer.setCameraMode('firstPerson');
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
- ### Waypoint Navigation
231
+ ## Configuration Options
291
232
 
292
- ```javascript
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
- // Navigate to specific waypoint
303
- await viewer.navigateToWaypoint('center');
235
+ ```typescript
236
+ interface ViewerOptions {
237
+ // Template style
238
+ template?: 'standard' | 'minimal' | 'pro';
304
239
 
305
- // Start automated tour
306
- viewer.startWaypointTour(['entrance', 'center', 'exit']);
307
- ```
240
+ // Playback
241
+ autoPlay?: boolean;
308
242
 
309
- ### Hotspot Interaction
243
+ // UI
244
+ showUI?: boolean;
245
+ backgroundColor?: string;
310
246
 
311
- ```javascript
312
- const viewer = initializeViewer(element, {
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
- // Add custom hotspot dynamically
334
- viewer.updateHotspot('custom-1', {
335
- position: { x: -2, y: 1, z: 0 },
336
- type: 'info',
337
- title: 'Dynamic Hotspot',
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
- ### Dynamic Splat Loading
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 = initializeViewer(element, {
347
- splatUrl: '/models/base.splat'
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
- ### Custom UI Integration
269
+ ### Reveal Effects
270
+
271
+ Control how the splat appears when loaded:
368
272
 
369
273
  ```javascript
370
- const viewer = initializeViewer(element, data, {
371
- showToolbar: false // Hide default toolbar
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 and Callbacks
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 and ready');
284
+ console.log('Viewer initialized');
402
285
  });
403
286
 
404
- viewer.on('loadStart', (url) => {
405
- console.log('Started loading:', url);
287
+ // Scene loaded
288
+ viewer.on('loaded', () => {
289
+ console.log('Scene fully loaded');
406
290
  });
407
291
 
408
- viewer.on('loadProgress', (progress) => {
409
- console.log('Loading progress:', progress + '%');
292
+ // Loading progress
293
+ viewer.on('progress', ({ loaded, total, percent }) => {
294
+ console.log(`Loading: ${percent}%`);
410
295
  });
411
296
 
412
- viewer.on('loadComplete', () => {
413
- console.log('Loading complete');
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
- ### Removing Event Listeners
312
+ ## React Integration
438
313
 
439
- ```javascript
440
- const handler = (position) => console.log(position);
441
- viewer.on('cameraMove', handler);
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
- ## Styling
318
+ function StorySplatViewer({ sceneId }) {
319
+ const containerRef = useRef(null);
320
+ const viewerRef = useRef(null);
448
321
 
449
- ### CSS Variables
322
+ useEffect(() => {
323
+ let mounted = true;
450
324
 
451
- The viewer uses CSS variables for theming:
325
+ async function initViewer() {
326
+ if (!containerRef.current) return;
452
327
 
453
- ```css
454
- #storysplat-viewer {
455
- --storysplat-primary-color: #007bff;
456
- --storysplat-background-color: #000000;
457
- --storysplat-text-color: #ffffff;
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
- ### Custom Styling
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
- ```css
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
- /* Custom hotspot appearance */
475
- .storysplat-hotspot {
476
- animation: pulse 2s infinite;
477
- }
346
+ return () => {
347
+ mounted = false;
348
+ viewerRef.current?.destroy();
349
+ };
350
+ }, [sceneId]);
478
351
 
479
- @keyframes pulse {
480
- 0% { transform: scale(1); }
481
- 50% { transform: scale(1.1); }
482
- 100% { transform: scale(1); }
352
+ return (
353
+ <div
354
+ ref={containerRef}
355
+ style={{ width: '100%', height: '500px' }}
356
+ />
357
+ );
483
358
  }
484
359
 
485
- /* Mobile controls */
486
- .storysplat-mobile-controls {
487
- opacity: 0.8;
488
- }
360
+ export default StorySplatViewer;
489
361
  ```
490
362
 
491
- ### Required CSS
363
+ ## Error Handling
492
364
 
493
- Make sure to import the viewer CSS:
365
+ The package exports error classes for specific error handling:
494
366
 
495
367
  ```javascript
496
- import '@storysplat/viewer/dist/styles/viewer.css';
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
- Or include it in your HTML:
387
+ ## Comparison: Scene ID vs JSON File
500
388
 
501
- ```html
502
- <link rel="stylesheet" href="node_modules/@storysplat/viewer/dist/styles/viewer.css">
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
- ### Common Issues
396
+ ### Container Needs Dimensions
556
397
 
557
- #### Viewer Not Rendering
398
+ The container element must have explicit dimensions:
558
399
 
559
- ```javascript
560
- // Check container exists and has dimensions
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
- #### CORS Errors
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
- ```javascript
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
- // Check viewport meta tag
609
- <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
610
- ```
411
+ ### Memory Cleanup
611
412
 
612
- #### Memory Leaks
413
+ Always destroy the viewer when unmounting:
613
414
 
614
415
  ```javascript
615
- // Always cleanup when done
416
+ // Vanilla JS
616
417
  window.addEventListener('beforeunload', () => {
617
418
  viewer.destroy();
618
419
  });
619
420
 
620
- // In React/Vue/Angular components
621
- componentWillUnmount() {
622
- if (this.viewer) {
623
- this.viewer.destroy();
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
- ### Support
434
+ ## Support
651
435
 
652
- For issues, feature requests, or questions:
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