OV25

📷 Camera Controls

Override camera locks and limits, and listen for horizontal spin and vertical tilt updates from the configurator.

Every product's 3D configurator ships with camera behaviour that was set up alongside the product: whether shoppers can zoom, pan, and rotate, and within what limits. Sometimes your page wants something different - a hero embed that shouldn't hijack scroll-wheel zoom, a "spin only" showroom view, or a completely static presentation.

Camera locks & limits let you override that behaviour at embed time, per page, without touching the product's camera data. Anything you don't override keeps working exactly as the product was set up.

There are two ways to apply an override:

  • URL parameters on the configurator URL - set once, applies from load.
  • A postMessage from your page - change or clear the override at any time.

Quick start

Add locks to the configurator URL:

https://configurator.orbital.vision/{apiKey}/{productId}?cameraLocks=vertical&zoomOutDistance=1

That freezes the vertical tilt at the product's default camera angle and stops the camera zooming out past double its starting distance - while horizontal spin and zooming in keep working as normal.

Using the ov25-ui package? productLink accepts a query string, and it's merged into the iframe URL for you:

injectConfigurator({
  apiKey: () => 'your-api-key',
  productLink: () => '92?cameraLocks=vertical&zoomOutDistance=1',
  // ...the rest of your config
});

Testing tip: the iframe also inherits query parameters from the page it's embedded on, so you can try locks out by appending ?cameraLocks=zoom to your own product page's URL - no code changes needed.


Locks

Locks are the simple switches. Pass them as a comma-separated list in cameraLocks:

?cameraLocks=zoom,pan,vertical
TokenWhat it locks
zoomZooming in/out (scroll wheel, pinch).
panPanning the camera off-centre (right-drag / two-finger drag).
rotateAll orbiting. The model can no longer be spun or tilted at all.
horizontalHorizontal orbit - freezes the spin at its current angle. Tilt still works.
verticalVertical orbit - freezes the tilt at its current angle. Spin still works.

Each token also works as an individual parameter if you prefer: ?lockZoom=1&lockVertical=1 is equivalent to ?cameraLocks=zoom,vertical.

A note on axes: the 3D camera orbits with two degrees of freedom - horizontal (spinning around the product) and vertical (tilting up and down). There is no "roll", so those two locks plus zoom and pan cover everything the shopper can do.


Limits

For finer control than an outright lock, set the underlying camera constraints directly. Angle limits map 1:1 onto three.js OrbitControls properties - angles are in radians.

ParameterTypeWhat it does
zoomInDistance / zoomOutDistanceallowance ≥ 0How far shoppers may zoom in / out from the starting view. 0 = not at all, 10 = lots. See below.
minZoom / maxZoomnumberMagnification range for wheel/pinch zoom, where 1 is the framed view. Also caps initialZoom.
minPolarAngle / maxPolarAngleradiansVertical tilt range. 0 is directly above, π/2 (≈1.5708) is eye level.
minAzimuthAngle / maxAzimuthAngleradiansHorizontal spin range, relative to the default view.
enableZoom / enablePan / enableRotatebooleanExplicitly enable or disable an interaction - including re-enabling one the product's camera setup turned off.

How the zoom allowances work

When a product loads, the configurator automatically frames it - the camera is positioned so the whole product fits comfortably in view. zoomInDistance and zoomOutDistance say how far shoppers may travel from that framed starting view, one per direction:

  • zoomInDistance - zoom in allowance. 0 = can't zoom in at all; each step up allows more: 1 lets the camera get twice as close (the product appears 2× larger), 10 lets it get 11× closer.
  • zoomOutDistance - zoom out allowance, same scale. 1 = up to double the starting distance, 10 = up to 11×.
  • zoomInDistance=0&zoomOutDistance=0 locks the zoom at the framed view entirely.

The two are independent - set just one and the other direction keeps the product's normal behaviour. And because they're relative to the framed view, the same values work for a footstool and a corner sofa: you never need to know the product's real-world size.

Degrees → radians: multiply by π/180. Handy values: 30° ≈ 0.524, 45° ≈ 0.785, 60° ≈ 1.047, 90° ≈ 1.571.

Example - allow a bit of zoom either way, and stop shoppers tilting below eye level:

?zoomInDistance=1&zoomOutDistance=1&maxPolarAngle=1.571

Starting angle

By default the configurator opens on the angle authored in the product's camera group - usually a three-quarter view. initialAzimuth overrides that horizontal angle for the opening view, so every product opens facing the same way regardless of how its camera was set up:

?initialAzimuth=0
ParameterTypeDescription
initialAzimuthdegreesAbsolute horizontal angle for the opening view. 0 faces the front of the product, 90 looks from its right (+X), -90 from its left, 180 from behind. Same convention as cutoutAngles and azimuthDegrees below.

Only the horizontal angle changes. Height, lens and how tightly the product is framed still come from the product's own camera, and shoppers can orbit away from it as normal. The override is applied again whenever the configurator reframes from the authored camera - for example after a product swap. It is not applied by RECENTER_CAMERA, which keeps the shopper's current angle.

Combine it with minAzimuthAngle / maxAzimuthAngle (radians, absolute) if the spin should stay near the new opening angle.


Starting zoom

initialZoom opens the configurator already zoomed in or out from the framed view. It is a magnification factor on the same axis the shopper's scroll wheel and pinch use, so 2 looks exactly like a shopper having zoomed in to twice the size:

?initialZoom=1.5
ParameterTypeDescription
initialZoomfactorOpening magnification. 1 is the framed view (the default), 2 shows the product twice as large, 0.5 half as large. Accepted range is above 0 up to 2; 0, negatives and non-numbers are ignored.

The value is clamped to the embed's zoom limits before it is applied, so it can never open outside what a shopper could reach themselves: an explicit minZoom / maxZoom wins, otherwise the caps derived from zoomInDistance / zoomOutDistance and the product's own distance limits apply. initialZoom=2&zoomInDistance=0 therefore opens at the framed view, and initialZoom=2&maxZoom=1.5 opens at 1.5×.

The starting zoom is the new baseline, not a one-off nudge. Recentering, product swaps, camera switches and live-angle thumbnail tiles all return to it rather than to the framed view, and the pan-recenter treats it as "not zoomed in". It applies to perspective cameras only.


Changing it at runtime

To apply, change, or remove an override after load, post a SET_CAMERA_CONTROLS_OVERRIDE message to the configurator iframe. The payload is a JSON string containing any mix of the lock flags and limit values above:

const iframe = document.getElementById('ov25-configurator-iframe'); // however you hold a reference
 
iframe.contentWindow.postMessage({
  type: 'SET_CAMERA_CONTROLS_OVERRIDE',
  payload: JSON.stringify({ lockRotate: true, zoomOutDistance: 1 }),
}, '*');

Three things to know:

  • Each message replaces the previous override entirely - it doesn't merge with it. Send the full set of locks/limits you want each time.

  • Clear back to the product's own camera behaviour by sending null:

    iframe.contentWindow.postMessage({
      type: 'SET_CAMERA_CONTROLS_OVERRIDE',
      payload: JSON.stringify(null),
    }, '*');
  • The iframe confirms with a message of the same type and payload {"success": true}. A malformed payload gets an ERROR message back, and unknown or wrongly-typed keys are silently dropped.


Listening for camera movement

To drive a parent-page spin or tilt indicator, add reportCameraOrbit=true to the iframe URL before loading it:

https://configurator.orbital.vision/{apiKey}/{productId}?reportCameraOrbit=true

Reporting is off by default; use the literal value true to enable it. This flag observes the camera and does not change its locks or limits. The iframe emits CAMERA_ORBIT_CHANGED with a JSON-string payload containing two numbers:

FieldMeaning
azimuthDegreesHorizontal spin, from 0 inclusive to 360 exclusive. With the normal Y-up camera, 0 is on the +Z side of the target and 90 is on the +X side.
polarDegreesVertical tilt, from 0 to 180: 0 is above the target, 90 is level, and 180 is below. Elevation is 90 - polarDegrees.

Both values are rounded to two decimal places and describe the camera's orbit around its target. They are not cumulative turns, and the authored starting view need not have an azimuth of 0.

The first update arrives when active orbit controls become available. Further updates arrive at roughly 30 Hz while the angles change, including vertical-only movement, programmatic camera moves and damping after pointer release. A trailing update reports the final angle. Unchanged values are suppressed, and replacing the camera/controls sends another initial update.

Register the listener before setting the iframe's src so the first update cannot be missed. This example treats the first bearing as the indicator's zero point and updates a CSS custom property on your own [data-ov25-spin] element:

const iframe = document.getElementById('ov25-configurator-iframe');
const configuratorOrigin = 'https://configurator.orbital.vision';
let previousAzimuth = null;
let spinDegrees = 0;
 
function onCameraOrbit(event) {
  if (event.source !== iframe.contentWindow || event.origin !== configuratorOrigin) return;
  if (event.data?.type !== 'CAMERA_ORBIT_CHANGED') return;
 
  let payload;
  try {
    payload = JSON.parse(event.data.payload);
  } catch {
    return;
  }
  const { azimuthDegrees, polarDegrees } = payload || {};
  if (!Number.isFinite(azimuthDegrees) || azimuthDegrees < 0 || azimuthDegrees >= 360) return;
  if (!Number.isFinite(polarDegrees) || polarDegrees < 0 || polarDegrees > 180) return;
 
  if (previousAzimuth !== null) {
    // Take the shortest turn across 359 → 0 instead of jumping backwards.
    spinDegrees += ((azimuthDegrees - previousAzimuth + 540) % 360) - 180;
  }
  previousAzimuth = azimuthDegrees;
  document.querySelector('[data-ov25-spin]')?.style.setProperty('--ov25-spin-angle', `${spinDegrees}deg`);
}
 
window.addEventListener('message', onCameraOrbit);
iframe.src = `${configuratorOrigin}/YOUR_PUBLIC_ACCESS_KEY/92?reportCameraOrbit=true`;
 
// When destroying the integration:
// window.removeEventListener('message', onCameraOrbit);

If the indicator overlays the iframe, give it pointer-events: none so dragging still reaches the 3D scene. Keep a static hint until a message arrives. Older configurator deployments do not emit this event, and static screenshot captures do not report camera movement. Use IS_LOADING for scene readiness; camera updates are optional and must not block loading or configuration changes.


Recipes

GoalSetup
Hero embed that shouldn't capture scroll-wheel?cameraLocks=zoom
Showroom turntable - spin only, fixed tilt, no drift?cameraLocks=vertical,pan
Completely static presentation?cameraLocks=rotate,zoom,pan
Stop shoppers zooming through the model or into the void?zoomInDistance=1&zoomOutDistance=1
Zoom locked at the framed view, orbit still free?zoomInDistance=0&zoomOutDistance=0
Open face-on for every product?initialAzimuth=0
Open a little closer than the default framing?initialZoom=1.3
Open zoomed in, but never let shoppers zoom out past the default framing?initialZoom=1.5&zoomOutDistance=0
Front-ish views only (±45° spin)?minAzimuthAngle=-0.785&maxAzimuthAngle=0.785
Never look underneath the product?maxPolarAngle=1.571
Animate a parent-page spin/tilt indicator?reportCameraOrbit=true; listen for CAMERA_ORBIT_CHANGED

Override behaviour notes

  • The override sticks for the life of the page: it survives option changes, model swaps, and camera switches. A full page reload clears a postMessage override (URL parameters re-apply themselves, of course).
  • horizontal / vertical freeze the camera at the angle it's at when the override lands - via URL that's the product's default camera angle.
  • The zoom allowances' baseline is the framed starting distance, so if the model being viewed changes size (for example a modular build growing as pieces are added), the configurator reframes and your allowances automatically rescale with it.
  • Negative zoomInDistance / zoomOutDistance values are ignored (0 is valid - it means no zoom in that direction).
  • Anything you don't override still comes from the product's camera setup, including its own zoom limits if it has them.
  • Applies to the standard product configurator (single- and multi-product). The bed, dining, and Snap2 modular configurators have their own embeds and don't support this yet.
  • AR is unaffected - these locks only govern the in-page 3D viewer.

On this page