Skip to content

Events

The StorySplat Viewer emits events that you can listen to for building interactive experiences.

Subscribing to Events

// Subscribe
viewer.on('ready', () => {
  console.log('Viewer is ready');
});

// Unsubscribe
const callback = () => console.log('Frame changed');
viewer.on('frameChange', callback);
viewer.off('frameChange', callback);

Available Events

ready

Fired when the viewer is fully initialized and ready.

viewer.on('ready', () => {
  console.log('Viewer ready!');
  viewer.play(); // Safe to call methods now
});

loaded

Fired when the splat or frame sequence has finished loading.

viewer.on('loaded', () => {
  console.log('Content loaded');
});

progress

Fired during loading with progress updates.

viewer.on('progress', ({ progress, text }) => {
  console.log(`Loading: ${(progress * 100).toFixed(0)}% - ${text}`);
});

error

Fired when an error occurs.

viewer.on('error', (error: Error) => {
  console.error('Viewer error:', error.message);
});

waypointChange

Fired when the current waypoint changes.

viewer.on('waypointChange', ({ index, waypoint }) => {
  console.log(`Arrived at waypoint ${index}: ${waypoint.name}`);
  updateUI(waypoint);
});

playbackStart

Fired when playback begins.

viewer.on('playbackStart', () => {
  playButton.textContent = 'Pause';
});

playbackStop

Fired when playback stops or pauses.

viewer.on('playbackStop', () => {
  playButton.textContent = 'Play';
});

frameChange

Fired when the current frame changes (4DGS only).

viewer.on('frameChange', (frame: number, total: number) => {
  progressBar.value = (frame / (total - 1)) * 100;
  frameCounter.textContent = `${frame + 1} / ${total}`;
});

frameComplete

Fired when 4DGS playback reaches the end (non-looping mode only).

viewer.on('frameComplete', () => {
  console.log('Playback finished');
  showReplayButton();
});

modeChange

Fired when camera mode changes.

viewer.on('modeChange', ({ mode }) => {
  console.log(`Camera mode: ${mode}`); // 'orbit', 'explore', 'tour'
});

Example: Custom Progress UI

const progressBar = document.getElementById('progress');
const statusText = document.getElementById('status');

viewer.on('progress', ({ progress, text }) => {
  progressBar.style.width = `${progress * 100}%`;
  statusText.textContent = text;
});

viewer.on('ready', () => {
  progressBar.parentElement.style.display = 'none';
});

Example: 4DGS Playback Controls

const playBtn = document.getElementById('play');
const slider = document.getElementById('timeline');
const counter = document.getElementById('counter');

playBtn.onclick = () => {
  if (viewer.isPlaying()) {
    viewer.pause();
  } else {
    viewer.play();
  }
};

slider.oninput = (e) => {
  viewer.setFrameProgress(e.target.value / 100);
};

viewer.on('frameChange', (frame, total) => {
  counter.textContent = `${frame + 1} / ${total}`;
  slider.value = (frame / (total - 1)) * 100;
});

viewer.on('playbackStart', () => {
  playBtn.textContent = '⏸ Pause';
});

viewer.on('playbackStop', () => {
  playBtn.textContent = '▶ Play';
});

Example: Waypoint Tour UI

const waypointName = document.getElementById('waypoint-name');
const waypointDesc = document.getElementById('waypoint-desc');
const prevBtn = document.getElementById('prev');
const nextBtn = document.getElementById('next');

prevBtn.onclick = () => viewer.prevWaypoint();
nextBtn.onclick = () => viewer.nextWaypoint();

viewer.on('waypointChange', ({ index, waypoint }) => {
  waypointName.textContent = waypoint.name;
  waypointDesc.textContent = waypoint.description || '';

  // Update button states
  prevBtn.disabled = index === 0;
  nextBtn.disabled = index === viewer.getWaypointCount() - 1;
});