Skip to content

Hosting a spinner

Warning

Online spinner publishing is available to PRO subscribers.

The Webex Spinner SDK renders exported one-axis or two-axis product-image sequences directly into a canvas. Keep the SDK and image URLs on HTTPS, and pin the SDK to an immutable release.

Add the SDK

<script src="https://cdn.jsdelivr.net/gh/randomcontrol/[email protected]/webex-spinner.min.js"></script>

The classic build exposes one global: WebexSpinner. Package consumers can instead import the default class and the events constant from @randomcontrol/webex-spinner.

Create the canvas

<div class="spinner-shell" aria-busy="true">
  <canvas id="product-spinner" width="800" height="800"></canvas>
  <progress id="spinner-progress" max="1" value="0"></progress>
</div>
.spinner-shell { max-width: 800px; margin-inline: auto; }
.spinner-shell canvas { display: block; width: 100%; height: auto; cursor: grab; }
.spinner-shell[aria-busy="true"] canvas { opacity: 0.45; pointer-events: none; }

Mount the spinner

const canvas = document.querySelector('#product-spinner');
const shell = document.querySelector('.spinner-shell');
const progress = document.querySelector('#spinner-progress');

const images = Array.from({ length: 34 }, (_, index) =>
  `/products/ring/atlas_${String(index).padStart(4, '0')}.webp`
);

const spinner = new WebexSpinner(canvas, {
  images,
  atlasColumns: 4,
  atlasRows: 1,
  poster: '/products/ring/poster.webp',
  framesPerSecond: 30,
  autoPlay: true,
  loop: 'auto',
  draggable: true,
  preloadAmount: 8,
});

spinner.addEventListener(WebexSpinner.events.loadProgress, ({ detail }) => {
  progress.value = detail.progress;
});

spinner.addEventListener(WebexSpinner.events.loadError, ({ detail }) => {
  console.error(`Could not load ${detail.url}`);
});

await spinner.ready;
shell.removeAttribute('aria-busy');
progress.remove();

Events belong to the Spinner instance, not the canvas. The ready promise resolves after every image in the active sequence has either loaded or reported an error.

Two-axis sequences

Pass one atlas array per pitch row and describe the angular grid:

const spinner = new WebexSpinner(canvas, {
  images: [
    ['/ring/pitch-30-atlas-1.webp', '/ring/pitch-30-atlas-2.webp'],
    ['/ring/pitch-20-atlas-1.webp', '/ring/pitch-20-atlas-2.webp'],
    ['/ring/pitch-10-atlas-1.webp', '/ring/pitch-10-atlas-2.webp'],
  ],
  atlasColumns: 4,
  atlasRows: 1,
  pitchMinDegrees: -30,
  yawMinDegrees: -180,
  stepDegrees: 10,
  initialPitchIndex: 1,
});

Every pitch row must contain the same number of atlases. stepDegrees is shared by yaw and pitch.

Replace a sequence

Material or product changes do not require rebuilding the instance:

await spinner.setSequence({
  images: roseGoldImages,
  atlasColumns: 4,
  atlasRows: 1,
  poster: '/products/ring/rose-gold-poster.webp',
});

setSequence() validates before changing state, cancels the old preload, preserves the nearest view, and returns the new readiness promise.

API reference

Options

Option Default Description
images required Atlas URLs as string[] or pitch rows as string[][].
atlasColumns 4 Horizontal tiles per atlas.
atlasRows 1 Vertical tiles per atlas.
poster false Still shown before the first sequence frame.
pitchMinDegrees 0 Pitch represented by row zero; required for multiple pitch rows.
yawMinDegrees 0 Yaw represented by frame zero.
stepDegrees 0 Angular step; required for multiple pitch rows.
initialYawIndex null Initial yaw index.
initialPitchIndex null Initial pitch index.
preloadAmount 0 Atlas count in the preview batch.
framesPerSecond 30 Automatic playback rate.
loop 'auto' Automatic, forced, or disabled yaw wrapping.
autoPlay true Play when ready.
reverse false Begin in reverse.
draggable true Enable pointer interaction.
dragSensitivity 0.5 Horizontal drag sensitivity.
pitchDragSensitivity null Vertical sensitivity; null matches angular sensitivity automatically.
touchSensitivityMultiplier 1 Touch-input multiplier.
axisLockThreshold 10 Pixels before a two-axis gesture locks to its dominant axis.
invertHorizontalDrag false Reverse horizontal drag.
invertVerticalDrag false Reverse vertical drag.
touchBehavior 'auto' Preserve page scrolling where possible, or use 'capture'.
pageScrollLockDuration 1500 Milliseconds to guard against an accidental page scroll after interaction.
crossOrigin null Image CORS mode.

Methods and state

  • Playback: play(), pause(), setDirection(), setFrameRate(), setLoop().
  • Interaction: setDraggable(), setPitch(), resetPitch().
  • Content: setSequence() and destroy().
  • Readonly state: ready, currentFrame, totalFrames, totalImages, running, dragging, loaded, loadedWithErrors, yawIndex, yawAngle, pitchIndex, pitchAngle, yawCount, and pitchCount.

Events

Use WebexSpinner.events.loadProgress, loadError, previewReady, ready, and posterReady. loadError includes the URL, pitch index, atlas index, and attempt count.

Troubleshooting

  • If images fail across origins, configure the image host's CORS policy and set crossOrigin: 'anonymous' when canvas pixel reads are required.
  • Match atlasColumns and atlasRows to the exported atlas layout.
  • Keep the canvas responsive with CSS; its width and height attributes control render resolution.
  • Call destroy() before permanently removing a spinner from the page.