Turns the camera into an interactive turntable — orbit, pan, zoom, and auto-fit around a pivot — so you frame a scene without hand-placing the camera.
JavaScript API:
CameraOrbitControl
This is the camera-control companion to the AbstractCamera family: rather
than a camera itself, it takes over aiming one. Presence of a camera-orbit-control
alongside the camera opts the scene into interactive orbit / pan / zoom plus an
optional auto-fit that frames the scene for you — the easy way to get a good shot,
since a bare camera has to be positioned and rotated by hand.
Key attributes. The pose is azimuth (turn around the pivot), elevation
(height above it), and radius (distance from it) — all animatable, so an animation
can sweep the camera on a turntable. fit-targets / fit-margin turn on the
auto-fit that keeps the subject framed, and orient-toward-pivot keeps the camera
aimed at the pivot as it moves. Reflection and animation are handled by
the shared SkenraAnimatable machinery, and orbit-pose writes drive the
camera through the underlying Orbit node; the fit-backed properties re-arm
the auto-fit at runtime.
Lifecycle: The browser upgrade path constructs the element with NO
arguments, so it must be valid uninitialized — it has no camera until
attachCamera binds one (the declarative scene bootstrap or an
imperative caller). The canonical Orbit node is created lazily on
connect (so an animation always has a real animatable to
target) and survives camera detach/re-attach, preserving its identity and
pose subscription.
Animation: an animation on this control is redirected onto the canonical
Orbit (see _resolveAnimationEffectTarget), the true
animatable owning azimuth / elevation / radius. Fit-backed properties
(fitTargets, fitMargin, freezeOnFit, pivot, panX, panY,
zoomFactor) are live and scriptable but NOT animatable.
Interactive input composes with animation. Orbit drag, zoom, and inertia
contribute a persistent additive offset through the same effect stack the
animation uses (see CameraOrbitUserOffset), so you can grab and orbit
the camera while a turntable animation is playing and the gesture simply
shifts the framing — the animation keeps running underneath without a
frame-to-frame fight. Because the gesture is a separate effect layer, a drag
does NOT rewrite the authored azimuth / elevation / radius attributes;
the authored pose stays the base the animation and the offset compose onto.
resetView and setViewDirection clear the accumulated offset.
This class owns the orbit setup/lifecycle, pivot management, zoom/radius state, reset state, pan-direction vector extraction, sensitivity configs, and min/max clamping. It owns NO DOM event listeners and has no knowledge of pointer/wheel/touch events.
Serve these docs to run the live example.
WebGPU needs a secure context — open this page via grunt serve
rather than double-clicking the file. The Markup tab works from disk.
<!-- Turntable over a FIXED scene: a ground plane and a 2x2 grid of objects
(two spheres, two cubes) never move. `fit-targets="#ground"` frames the
scene once (setting the underlying orbit radius), then the nested animation
sweeps `azimuth` a full turn and, with `composite="add"`, ADDS a small
in-and-out offset onto that fitted radius — so it is the CAMERA that circles
the grid and dollies closer/farther, not the content spinning or scaling.
Watch the ground grid and the objects' parallax to read the camera's motion.
Drag on the canvas WHILE it plays to orbit — the gesture adds a persistent
offset on top of the animation, so the framing shifts smoothly without
fighting the sweep. -->
<sk-scene>
<sk-camera-orbit-control fit-targets="#ground" fit-margin="0.2"
elevation="28deg" orient-toward-pivot>
<!-- composite="add": radius adds to the fit-computed base, so the dolly is
relative to whatever framing the auto-fit chose — the -20..+20 swing
pulls the camera in below the fitted distance and out above it. azimuth's
base is ~0, so the 0->360 sweep reads as a full orbit. -->
<sk-animation duration="18000ms" iterations="Infinity" composite="add">
<sk-keyframe offset="0" azimuth="0deg" radius="-20"></sk-keyframe>
<sk-keyframe offset="0.5" azimuth="180deg" radius="20"></sk-keyframe>
<sk-keyframe offset="1" azimuth="360deg" radius="-20"></sk-keyframe>
</sk-animation>
</sk-camera-orbit-control>
<!-- A ~50mm-equivalent lens (natural ~40deg field of view) gives real
perspective and parallax, so the camera's orbit and dolly read clearly. -->
<sk-perspective-camera focal-length="50"></sk-perspective-camera>
<sk-directional-light p="45deg" h="30deg" intensity="1.0"></sk-directional-light>
<sk-ambient-light intensity="0.35"></sk-ambient-light>
<!-- Ground uses the standard dark-gray floor with a darker grid edge. -->
<sk-plain-material id="ground-mat" base-color="#3a3a3a"></sk-plain-material>
<sk-plain-material id="grid-mat" base-color="#1c1c1c"></sk-plain-material>
<!-- Harmonious object palette (soft blue, warm gold, terracotta, sage). -->
<sk-plain-material id="blue" base-color="#7fa8d8"></sk-plain-material>
<sk-plain-material id="gold" base-color="#d8a24a"></sk-plain-material>
<sk-plain-material id="terracotta" base-color="#c96f4c"></sk-plain-material>
<sk-plain-material id="sage" base-color="#8bab8a"></sk-plain-material>
<!-- Ground: a +y plane (lies flat in XZ) filled + grid wireframe, the fixed
reference the camera moves against. It is the fit target. -->
<sk-plane id="ground" width="14" height="14" width-segments="7" height-segments="7">
<sk-surface-paint material="#ground-mat"></sk-surface-paint>
<sk-edge-paint material="#grid-mat"></sk-edge-paint>
</sk-plane>
<!-- 2x2 grid on the ground: spheres and cubes on alternating corners, each
resting on the surface (y = half-height). -->
<sk-sphere x="-3" y="1.2" z="-3" radius="1.2" segments="3">
<sk-surface-paint material="#terracotta"></sk-surface-paint>
</sk-sphere>
<sk-cube x="3" y="1.1" z="-3" size="2.2">
<sk-surface-paint material="#sage"></sk-surface-paint>
</sk-cube>
<sk-cube x="-3" y="1.1" z="3" size="2.2">
<sk-surface-paint material="#blue"></sk-surface-paint>
</sk-cube>
<sk-sphere x="3" y="1.2" z="3" radius="1.2" segments="3">
<sk-surface-paint material="#gold"></sk-surface-paint>
</sk-sphere>
</sk-scene>
| Attribute | Type | Default | Description |
|---|---|---|---|
id |
<id> |
— | The element's unique identifier — the standard HTML global id attribute. |
fit-targets |
<fit-targets> ( |
"none" |
Nodes whose bounds drive an auto-fit: 'auto', 'none', or id list. |
fit-margin |
<number> |
0 |
Padding fraction applied around fitted bounds. |
freeze-on-fit |
<boolean> |
true |
Stop the fit observer after the first frame with real bounds. |
azimuthanimatable |
<angle> |
0 |
Initial/horizontal orbit angle (radians); animatable. |
elevationanimatable |
<angle> |
0 |
Initial/vertical orbit angle (radians); animatable. |
radiusanimatable |
<number> |
10 |
Orbit radius (distance from pivot); animatable. |
orient-toward-pivot |
<boolean> |
false |
When true, the camera faces the pivot. |
orientation-axis |
<orientation-axis> ("+x" | "-x" | "+y" | "-y" | "+z" | "-z") |
"-z" |
Optional orientation axis for orientTowardPivot. |
pivot |
<id-ref> |
— | The point the camera orbits around, as an element id reference (#id), |
pan-x |
<length> |
— | Post-fit horizontal pan (CSS length string), or null. |
pan-y |
<length> |
— | Post-fit vertical pan (CSS length string), or null. |
zoom-factor |
<number> |
1 |
Post-fit zoom multiplier (1 = no change). |
orbit-sensitivity |
<number> |
0.005 |
Sensitivity for orbit drag operations (radians per unit delta). Default: 0.005 |
pan-sensitivity |
<number> |
0.01 |
Sensitivity for pan drag operations (world units per unit delta). Default: 0.01 |
zoom-sensitivity |
<number> |
0.005 |
Sensitivity for zoom drag operations (scale factor per unit delta). Default: 0.005 |
min-zoom |
<number> |
1 |
Minimum zoom level (orthographic height or perspective distance). Default: 1 |
max-zoom |
<number> |
500 |
Maximum zoom level (orthographic height or perspective distance). Default: 500 |
Used in guides: Scene basics