{
  "schemaVersion": 1,
  "id": "orbit-camera",
  "version": "1.1.0",
  "status": "published",
  "kind": "camera",
  "title": "Smooth Orbit Camera",
  "summary": "A camera that glides around your subject with a gentle rise and fall. Loops perfectly.",
  "description": "Circle any object with a calm, cinematic camera move. Set how far away it sits, how high, how many turns per clip and how much it gently bobs or breathes in and out. It is plain math (a position and a look-at point for each moment of the clip), so it works around any subject in any 3D scene. The demo circles a glossy ceramic capsule with chrome details in a studio.",
  "role": "Camera motion: position and look-at target for any 3D camera over the clip.",
  "collections": [
    "product-reveals",
    "chrome-glass",
    "satisfying"
  ],
  "tags": [
    "camera",
    "orbit",
    "turntable",
    "rotate",
    "cinematic",
    "camera move",
    "360",
    "3d",
    "loop",
    "smooth"
  ],
  "synonyms": [
    "rotate the camera",
    "spin around",
    "circle around",
    "camera around",
    "turntable",
    "fly around",
    "orbiting",
    "slow pan",
    "camera movement"
  ],
  "useCases": [
    "Rotate the camera around a product or sculpture",
    "Make a static 3D scene feel alive",
    "Seamless 360° loops"
  ],
  "featured": 6,
  "publishedAt": "2026-10-03",
  "implementation": {
    "target": "js",
    "runtime": "any",
    "language": "javascript",
    "module": "esm",
    "entry": "src/orbit-camera.js",
    "exports": [
      {
        "name": "orbitCamera",
        "signature": "orbitCamera(progress, { target?, radius?, height?, turns?, startAngle?, heightWave?, heightWaves?, radiusWave?, radiusWaves?, targetLift?, easing? }) => { position, target, angle }",
        "description": "Camera position and look-at target for a progress value 0…1."
      },
      {
        "name": "applyOrbit",
        "signature": "applyOrbit(camera, state) => camera",
        "description": "Applies a state to any camera with position.set() and lookAt() (Three.js cameras)."
      },
      {
        "name": "fitRadius",
        "signature": "fitRadius(boundingRadius, fovDegrees?, margin?) => number",
        "description": "Distance that keeps an object of a given size in frame."
      }
    ],
    "reusable": [
      "src/orbit-camera.js"
    ],
    "demo": "demo.js",
    "harness": "index.html"
  },
  "dependencies": [
    {
      "id": "three",
      "version": "0.185.1",
      "scope": "demo",
      "reason": "Demo scene only. src/orbit-camera.js has no dependencies."
    },
    {
      "id": "apw-runtime",
      "version": "1.0.0",
      "scope": "demo",
      "reason": "Demo harness."
    },
    {
      "id": "chrome-material",
      "version": "1.0.0",
      "scope": "demo",
      "reason": "Demo scene: studio environment, chrome and contact shadow. src/orbit-camera.js has no dependencies."
    }
  ],
  "params": [
    {
      "key": "turns",
      "label": "Turns per clip",
      "type": "integer",
      "min": 1,
      "max": 3,
      "step": 1,
      "default": 1
    },
    {
      "key": "direction",
      "label": "Direction",
      "type": "select",
      "default": "counterclockwise",
      "options": [
        {
          "value": "counterclockwise",
          "label": "Left to right"
        },
        {
          "value": "clockwise",
          "label": "Right to left"
        }
      ]
    },
    {
      "key": "radius",
      "label": "Distance",
      "help": "How far the camera stays from the subject.",
      "type": "number",
      "min": 4,
      "max": 14,
      "step": 0.1,
      "default": 8.5
    },
    {
      "key": "height",
      "label": "Height",
      "type": "number",
      "min": 0.3,
      "max": 6,
      "step": 0.1,
      "default": 2.2
    },
    {
      "key": "bob",
      "label": "Rise and fall",
      "type": "number",
      "min": 0,
      "max": 1.5,
      "step": 0.05,
      "default": 0.35
    },
    {
      "key": "breathe",
      "label": "Move in and out",
      "type": "number",
      "min": 0,
      "max": 2,
      "step": 0.05,
      "default": 0.4
    },
    {
      "key": "easing",
      "label": "Speed",
      "help": "Steady loops seamlessly. Ease in & out starts and ends slowly (no seamless loop).",
      "type": "select",
      "default": "linear",
      "options": [
        {
          "value": "linear",
          "label": "Steady (loops)"
        },
        {
          "value": "smooth",
          "label": "Ease in & out"
        }
      ]
    },
    {
      "key": "fov",
      "label": "Lens (field of view)",
      "type": "number",
      "min": 20,
      "max": 70,
      "step": 1,
      "default": 38
    },
    {
      "key": "accent",
      "label": "Product colour",
      "type": "color",
      "default": "#d84a2c"
    },
    {
      "key": "background",
      "label": "Background",
      "type": "color",
      "default": "#0f1117"
    }
  ],
  "presets": [
    {
      "id": "calm",
      "label": "Calm turntable",
      "params": {}
    },
    {
      "id": "low-hero",
      "label": "Low hero angle",
      "params": {
        "height": 0.6,
        "radius": 7.5,
        "bob": 0.1,
        "fov": 30,
        "accent": "#1c3cff"
      }
    },
    {
      "id": "double",
      "label": "Two fast turns",
      "params": {
        "turns": 2,
        "breathe": 1.1,
        "fov": 48,
        "accent": "#f1e9d6"
      }
    }
  ],
  "output": {
    "width": 1080,
    "height": 1080,
    "fps": 30,
    "durationSeconds": 8,
    "posterSeconds": 1.4,
    "alpha": false,
    "motion": "procedural",
    "loop": {
      "seamless": true,
      "cycleSeconds": 8,
      "method": "Whole turns and whole bob/breathe cycles. Only seamless with the default Steady speed; Ease in & out is not loopable."
    },
    "seed": null
  },
  "composition": {
    "inputs": "Clip progress (frame / durationFrames) and a target point.",
    "outputs": "Camera position [x, y, z] and look-at target [x, y, z].",
    "coordinates": "Y-up. Angle 0 is on the +Z side of the target, increasing towards +X. Units match your scene.",
    "lighting": "None. Keep your scene's lights.",
    "timing": "Driven by progress 0…1, so the same path stretches to any duration.",
    "compatibleWith": [
      "chrome-material",
      "glass-container",
      "bouncing-balls",
      "iridescent-blob"
    ],
    "notes": [
      "Pick radius with fitRadius(boundingRadius, fov) to keep the subject framed."
    ]
  },
  "reuse": [
    "Circle the glass box of Bouncing Marbles while the balls fall (see the Bouncing Marbles in a Box example).",
    "Feed the position into a 2D perspective projection to orbit a canvas particle field.",
    "Use easing 'smooth' and turns 0.25 for a slow quarter-turn product reveal."
  ],
  "integration": {
    "newProject": "Import orbitCamera and applyOrbit, compute progress = frame / totalFrames each frame and call applyOrbit(camera, orbitCamera(progress, options)).",
    "existingProject": "Replace only your camera update with orbitCamera(progress, options). Pass your subject's centre as target. Leave the rest of the scene untouched."
  },
  "performance": {
    "notes": "A few trig calls per frame.",
    "limits": "None."
  },
  "limitations": [
    "Does not avoid obstacles; choose a radius that clears your scene."
  ],
  "license": {
    "spdx": "MIT",
    "notice": "Copyright (c) 2026 AI Pixel Wizard. Released under the MIT License. See LICENSE in this folder.",
    "thirdParty": [
      {
        "name": "three.js",
        "version": "0.185.1",
        "license": "MIT",
        "url": "https://github.com/mrdoob/three.js",
        "scope": "demo"
      }
    ]
  },
  "provenance": "Original code written for AI Pixel Wizard in October 2026. Demo scene rebuilt in October 2026 to the catalog quality bar (docs/QUALITY.md); the reusable source is unchanged.",
  "changelog": [
    {
      "version": "1.1.0",
      "date": "2026-10-03",
      "notes": "New demo: a product shot (glossy ceramic capsule with chrome details on a polished floor in the shared studio) instead of primitives on a grid; the orbit is fitted to the subject for any lens. Camera math unchanged."
    },
    {
      "version": "1.0.0",
      "date": "2026-10-02",
      "notes": "Initial release."
    }
  ]
}
