---
contentHash: sha256:6a4d9f763718be3ab160ad683688b102e9cd58e45282cc27e84fd872f6d7b49e
documentId: element:sk-camera-orbit-control
kind: element
schemaVersion: 1
skenraVersion: 0.1.0-alpha.1
---
# sk-camera-orbit-control

<a id="overview"></a>

## Overview

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.

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.

<a id="example"></a>

## Example

```sk
<!-- 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>
```

<a id="attr-id"></a>

## id

Type: `<id>` · Required: no · Animatable: no

Related: [`type:id`](types/id.md)

<a id="attr-fit-targets"></a>

## fit-targets

Type: `<fit-targets>` · Required: no · Animatable: no · Default: `"none"`

Nodes whose bounds drive an auto-fit: `'auto'`, `'none'`, or id list.

Related: [`type:fit-targets`](types/fit-targets.md)

<a id="attr-fit-margin"></a>

## fit-margin

Type: `<number>` · Required: no · Animatable: no · Default: `0`

Padding fraction applied around fitted bounds.

Related: [`type:number`](types/number.md)

<a id="attr-freeze-on-fit"></a>

## freeze-on-fit

Type: `<boolean>` · Required: no · Animatable: no · Default: `true`

Stop the fit observer after the first frame with real bounds.

Related: [`type:boolean`](types/boolean.md)

<a id="attr-azimuth"></a>

## azimuth

Type: `<angle>` · Required: no · Animatable: yes · Default: `0`

Initial/horizontal orbit angle (radians); animatable.

Related: [`type:angle`](types/angle.md)

<a id="attr-elevation"></a>

## elevation

Type: `<angle>` · Required: no · Animatable: yes · Default: `0`

Initial/vertical orbit angle (radians); animatable.

Related: [`type:angle`](types/angle.md)

<a id="attr-radius"></a>

## radius

Type: `<number>` · Required: no · Animatable: yes · Default: `10`

Orbit radius (distance from pivot); animatable.

Related: [`type:number`](types/number.md)

<a id="attr-orient-toward-pivot"></a>

## orient-toward-pivot

Type: `<boolean>` · Required: no · Animatable: no · Default: `false`

When true, the camera faces the pivot.

Related: [`type:boolean`](types/boolean.md)

<a id="attr-orientation-axis"></a>

## orientation-axis

Type: `<orientation-axis>` · Required: no · Animatable: no · Default: `"-z"`

Optional orientation axis for orientTowardPivot.

Related: [`type:orientation-axis`](types/orientation-axis.md)

<a id="attr-pivot"></a>

## pivot

Type: `<id-ref>` · Required: no · Animatable: no

The point the camera orbits around, as an element id reference (`#id`),

Related: [`type:id-ref`](types/id-ref.md)

<a id="attr-pan-x"></a>

## pan-x

Type: `<length>` · Required: no · Animatable: no

Post-fit horizontal pan (CSS length string), or null.

Related: [`type:length`](types/length.md)

<a id="attr-pan-y"></a>

## pan-y

Type: `<length>` · Required: no · Animatable: no

Post-fit vertical pan (CSS length string), or null.

Related: [`type:length`](types/length.md)

<a id="attr-zoom-factor"></a>

## zoom-factor

Type: `<number>` · Required: no · Animatable: no · Default: `1`

Post-fit zoom multiplier (1 = no change).

Related: [`type:number`](types/number.md)

<a id="attr-orbit-sensitivity"></a>

## orbit-sensitivity

Type: `<number>` · Required: no · Animatable: no · Default: `0.005`

Sensitivity for orbit drag operations (radians per unit delta). Default: 0.005

Related: [`type:number`](types/number.md)

<a id="attr-pan-sensitivity"></a>

## pan-sensitivity

Type: `<number>` · Required: no · Animatable: no · Default: `0.01`

Sensitivity for pan drag operations (world units per unit delta). Default: 0.01

Related: [`type:number`](types/number.md)

<a id="attr-zoom-sensitivity"></a>

## zoom-sensitivity

Type: `<number>` · Required: no · Animatable: no · Default: `0.005`

Sensitivity for zoom drag operations (scale factor per unit delta). Default: 0.005

Related: [`type:number`](types/number.md)

<a id="attr-min-zoom"></a>

## min-zoom

Type: `<number>` · Required: no · Animatable: no · Default: `1`

Minimum zoom level (orthographic height or perspective distance). Default: 1

Related: [`type:number`](types/number.md)

<a id="attr-max-zoom"></a>

## max-zoom

Type: `<number>` · Required: no · Animatable: no · Default: `500`

Maximum zoom level (orthographic height or perspective distance). Default: 500

Related: [`type:number`](types/number.md)

---

Related records: [`type:angle`](types/angle.md), [`type:boolean`](types/boolean.md), [`type:fit-targets`](types/fit-targets.md), [`type:id`](types/id.md), [`type:id-ref`](types/id-ref.md), [`type:length`](types/length.md), [`type:number`](types/number.md), [`type:orientation-axis`](types/orientation-axis.md)
