← Cycles
Qmap · c6

camera-focus

archivedcreated 2026-03-23· last activity 2026-08-15

New in this cycle: Click-to-focus camera framing + isometric↔top-down angle control

Goal

Camera control experiments:

  1. Zoom and pan to frame selected actor and its connections
  2. Camera angle slider - transition from isometric to top-down view

Outcome

Camera framing system complete. Click actor → camera animates to frame actor and connections. Unfocus → camera returns to initial view. Integrates cleanly with grouping transitions (re-centers if selection active). Core CameraControls now properly supports arbitrary lookAt targets for future pan/navigation features.

Camera angle slider allows exploration of different viewing perspectives - useful for understanding spatial relationships from above vs the standard isometric view.

Rotation controls provide quick access to cardinal viewing directions without drag interaction - useful for consistent screenshots or comparing layouts from fixed angles.

Focus

  • Automatic camera framing when clicking an actor
  • Smooth animated transitions for camera position and zoom
  • Reset to initial view on background click
  • Camera angle slider (0% isometric → 100% top-down)

Approach

  1. When actor is clicked:
    • Collect positions of clicked actor + all connected actors
    • Calculate bounding box of those positions
    • Compute frustum size needed to frame the bounding box
    • Animate camera to center on and zoom to fit
  2. When background is clicked:
    • Animate camera back to initial state (captured on scene init)

Key Files

New: shared/cameraAnimation.js

  • calculateFocusBounds(positions, padding) - Bounding box from actor positions
  • frustumSizeForBounds(bounds, aspect, padding) - Required frustum to frame bounds
  • animateCameraFocus(camera, controls, config, options) - Animated camera transition
  • captureCameraState(camera, controls) - Snapshot camera state for reset

Modified: src/core/CameraControls.js

  • Added target property (was hardcoded to origin)
  • setTarget(target) / getTarget() methods
  • rotateY() now respects target for proper rotation around focus point

Modified: ExperimentsApp.vue

  • initialCameraState ref - Stores initial camera state
  • focusCameraOnPositions(positions) - Focus camera on actor positions
  • resetCameraToInitial() - Return camera to initial view
  • selectActor() - Now calls focusCameraOnPositions after highlighting
  • clearSelection() - Now calls resetCameraToInitial after reset

Animation Config

{
  focusDuration: 500,    // ms for focus animation
  resetDuration: 500,    // ms for reset animation
  minFrustumSize: 4,     // Don't zoom in more than this
  focusPadding: 2.0,     // Extra padding around framed content
  actorPadding: 1.0,     // Padding around actor bounding box
}

Uses easeOutCubic for quick start, gentle landing.


Notes

  • Reuses animation system from morphAnimation.js (animateValue, lerp, easing functions)
  • Camera maintains isometric angle during pan by offsetting position relative to target
  • Frustum size clamped to CameraControls min/max zoom constraints

Key Learnings

Isometric pan without rotation: Store camera-to-target offset once, then apply to new targets. Moving both camera position and lookAt target by the same delta preserves the viewing angle.

Orthographic framing math:

  • frustumSize = vertical extent (top to bottom)
  • Horizontal extent = frustumSize × aspect
  • Frame bounds by taking max(width/aspect, depth) to ensure both fit

State capture/restore pattern: The same animateCameraFocus() function handles both focus-in and reset—just pass different target configurations (captured initial state vs computed bounds).

CameraControls target property: Extending controls with a mutable target Vector3 enables proper rotation around arbitrary points (not just origin) and panning without losing isometric angle.


Camera Angle Slider

Implementation

Uses spherical coordinates to interpolate camera position:

  • Isometric view (0%): phi = atan(√2) ≈ 54.7° from y-axis
  • Top-down view (100%): phi ≈ 0° (nearly straight down)

The slider maintains:

  • Current horizontal rotation (theta) - so you can rotate then adjust angle
  • Current distance from target (radius)
  • LookAt target position
// Interpolate phi based on slider (0-100)
const t = cameraAngle / 100;
const phi = isometricPhi * (1 - t) + topDownPhi * t;

Rotation Controls

UI Layout

[-] [Reset Rotation] [+]

Functions

  • Reset Rotation - Sets theta to 0 (camera along +Z axis)
  • Rotate Left (-) - Rotates 90° counter-clockwise
  • Rotate Right (+) - Rotates 90° clockwise

Implementation

All rotation controls use spherical coordinates, preserving:

  • Current phi (vertical angle from slider)
  • Current radius (zoom distance)
function rotateBy(angle) {
  const spherical = new THREE.Spherical(
    currentSpherical.radius,
    currentSpherical.phi,
    currentSpherical.theta + angle  // +/- π/2 for 90°
  );
  // ... apply to camera
}

Design Notes

  • Reset goes to theta=0 (not 45°) for predictable "front view" alignment
  • 90° increments allow quick exploration of all four cardinal directions
  • Controls work independently of the angle slider - can rotate at any tilt

Visual record

9 captures — heroes, page slices, and full-page renders. Click to view.

View angle 0 — full isometric
View angle 0 — full isometric
Angle 100 — top-down map
Angle 100 — top-down map
Rotated a step, back at working angle
Rotated a step, back at working angle
Two more rotation steps
Two more rotation steps
Vendors regrouped by security policy under the rotated camera
Vendors regrouped by security policy under the rotated camera
Curve height 8
Curve height 8
c6 legacy
Angle 35 — the working tilt
Angle 35 — the working tilt
Angle 70 — flattening out
Angle 70 — flattening out

Artifacts

/experiments/c6.camera-focus/ (local dev)

Qpoint Brand Style Guide