CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-camera-controls

A camera control for three.js, similar to THREE.OrbitControls yet supports smooth transitions and more features

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

state-management.mddocs/

State Management

Camera state persistence, serialization, restoration, and accessor functionality for saving and loading camera configurations.

Capabilities

State Persistence

Save and restore camera states for later use or reset functionality.

/**
 * Save current camera state as the reset point
 * Stores position, target, zoom, and other camera parameters
 */
saveState(): void;

/**
 * Reset camera to saved state or initial position
 * @param enableTransition - Whether to animate the transition
 * @returns Promise array that resolves when all movements complete
 */
reset(enableTransition?: boolean): Promise<void[]>;

Usage Examples:

// Save initial camera state
cameraControls.saveState();

// Move camera around...
await cameraControls.moveTo(10, 5, 15, true);
await cameraControls.rotateTo(Math.PI / 4, Math.PI / 3, true);

// Reset to saved state with animation
await cameraControls.reset(true);

// Reset instantly
await cameraControls.reset(false);

// Save new state after positioning
await cameraControls.fitToBox(mesh, true);
cameraControls.saveState(); // New reset point

Serialization

Convert camera state to/from JSON for persistence across sessions.

/**
 * Serialize current camera state to JSON string
 * @returns JSON string containing complete camera state
 */
toJSON(): string;

/**
 * Load camera state from JSON string
 * @param json - JSON string from previous toJSON() call
 * @param enableTransition - Whether to animate the transition
 */
fromJSON(json: string, enableTransition?: boolean): void;

Usage Examples:

// Save camera state to localStorage
const cameraState = cameraControls.toJSON();
localStorage.setItem('cameraState', cameraState);

// Load camera state from localStorage
const savedState = localStorage.getItem('cameraState');
if (savedState) {
  cameraControls.fromJSON(savedState, true);
}

// Save to file or database
const stateData = {
  timestamp: Date.now(),
  cameraState: cameraControls.toJSON(),
  sceneName: 'main-scene'
};

// Send to server
fetch('/api/save-camera-state', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(stateData)
});

// Load from server response
const response = await fetch('/api/load-camera-state');
const loadedData = await response.json();
cameraControls.fromJSON(loadedData.cameraState, true);

State Accessors

Retrieve current camera state values for analysis or custom operations.

/**
 * Get current camera target position
 * @param out - Vector3 to write result to
 * @param receiveEndValue - Whether to get end value of transition (true) or current value (false)
 * @returns Vector3 containing target position
 */
getTarget(out: THREE.Vector3, receiveEndValue?: boolean): THREE.Vector3;

/**
 * Get current camera position
 * @param out - Vector3 to write result to  
 * @param receiveEndValue - Whether to get end value of transition (true) or current value (false)
 * @returns Vector3 containing camera position
 */
getPosition(out: THREE.Vector3, receiveEndValue?: boolean): THREE.Vector3;

/**
 * Get current spherical coordinates
 * @param out - Spherical to write result to
 * @param receiveEndValue - Whether to get end value of transition (true) or current value (false)
 * @returns Spherical containing camera's spherical coordinates
 */
getSpherical(out: THREE.Spherical, receiveEndValue?: boolean): THREE.Spherical;

/**
 * Get current focal offset
 * @param out - Vector3 to write result to
 * @param receiveEndValue - Whether to get end value of transition (true) or current value (false)
 * @returns Vector3 containing focal offset
 */
getFocalOffset(out: THREE.Vector3, receiveEndValue?: boolean): THREE.Vector3;

Usage Examples:

import * as THREE from 'three';

// Get current camera state
const currentPosition = new THREE.Vector3();
const currentTarget = new THREE.Vector3();
const currentSpherical = new THREE.Spherical();

cameraControls.getPosition(currentPosition);
cameraControls.getTarget(currentTarget);
cameraControls.getSpherical(currentSpherical);

console.log('Camera Position:', currentPosition);
console.log('Target Position:', currentTarget);
console.log('Spherical Coords:', {
  radius: currentSpherical.radius,
  phi: currentSpherical.phi,
  theta: currentSpherical.theta
});

// Get end values during transition (where camera is moving to)
const endPosition = new THREE.Vector3();
const endTarget = new THREE.Vector3();

cameraControls.getPosition(endPosition, true);  // End position
cameraControls.getTarget(endTarget, true);      // End target

// Calculate distance between current and end positions
const distance = currentPosition.distanceTo(endPosition);
console.log('Remaining distance to travel:', distance);

Advanced State Management

State Comparison and Validation:

// Compare two camera states
function compareCameraStates(state1: string, state2: string): boolean {
  try {
    const parsed1 = JSON.parse(state1);
    const parsed2 = JSON.parse(state2);
    
    // Compare key properties (simplified)
    return (
      parsed1.target && parsed2.target &&
      parsed1.position && parsed2.position &&
      Math.abs(parsed1.target.x - parsed2.target.x) < 0.001 &&
      Math.abs(parsed1.target.y - parsed2.target.y) < 0.001 &&
      Math.abs(parsed1.target.z - parsed2.target.z) < 0.001
    );
  } catch {
    return false;
  }
}

// Validate state before loading
function isValidCameraState(json: string): boolean {
  try {
    const state = JSON.parse(json);
    return state && typeof state === 'object' && 
           state.target && state.position;
  } catch {
    return false;
  }
}

// Safe state loading
const savedState = localStorage.getItem('cameraState');
if (savedState && isValidCameraState(savedState)) {
  cameraControls.fromJSON(savedState, true);
}

Multiple State Management:

// State manager class for multiple camera states
class CameraStateManager {
  private states = new Map<string, string>();
  
  saveState(name: string, controls: CameraControls): void {
    this.states.set(name, controls.toJSON());
  }
  
  loadState(name: string, controls: CameraControls, animate = true): boolean {
    const state = this.states.get(name);
    if (state) {
      controls.fromJSON(state, animate);
      return true;
    }
    return false;
  }
  
  deleteState(name: string): boolean {
    return this.states.delete(name);
  }
  
  listStates(): string[] {
    return Array.from(this.states.keys());
  }
}

// Usage
const stateManager = new CameraStateManager();

// Save named states
stateManager.saveState('overview', cameraControls);
stateManager.saveState('closeup', cameraControls);
stateManager.saveState('top-view', cameraControls);

// Load specific state
stateManager.loadState('overview', cameraControls, true);

Transition Monitoring:

// Monitor state changes during transitions
function monitorTransition() {
  const startPosition = new THREE.Vector3();
  const currentPosition = new THREE.Vector3();
  const endPosition = new THREE.Vector3();
  
  cameraControls.getPosition(startPosition, false);  // Current
  cameraControls.getPosition(endPosition, true);     // Target
  
  const interval = setInterval(() => {
    cameraControls.getPosition(currentPosition, false);
    
    const remainingDistance = currentPosition.distanceTo(endPosition);
    const totalDistance = startPosition.distanceTo(endPosition);
    const progress = 1 - (remainingDistance / totalDistance);
    
    console.log(`Transition progress: ${(progress * 100).toFixed(1)}%`);
    
    if (remainingDistance < 0.01) { // Close enough
      clearInterval(interval);
      console.log('Transition complete');
    }
  }, 100);
}

// Start monitoring before transition
monitorTransition();
await cameraControls.moveTo(10, 5, 15, true);

State Interpolation:

// Interpolate between two saved states
async function interpolateStates(
  state1: string, 
  state2: string, 
  t: number, // 0-1 interpolation factor
  controls: CameraControls,
  animate = true
): Promise<void> {
  const parsed1 = JSON.parse(state1);
  const parsed2 = JSON.parse(state2);
  
  // Interpolate position
  const pos1 = new THREE.Vector3(parsed1.position.x, parsed1.position.y, parsed1.position.z);
  const pos2 = new THREE.Vector3(parsed2.position.x, parsed2.position.y, parsed2.position.z);
  const interpolatedPos = pos1.clone().lerp(pos2, t);
  
  // Interpolate target
  const target1 = new THREE.Vector3(parsed1.target.x, parsed1.target.y, parsed1.target.z);
  const target2 = new THREE.Vector3(parsed2.target.x, parsed2.target.y, parsed2.target.z);
  const interpolatedTarget = target1.clone().lerp(target2, t);
  
  // Apply interpolated state
  await Promise.all([
    controls.setPosition(interpolatedPos.x, interpolatedPos.y, interpolatedPos.z, animate),
    controls.setTarget(interpolatedTarget.x, interpolatedTarget.y, interpolatedTarget.z, animate)
  ]);
}

// Usage: animate between states
for (let i = 0; i <= 10; i++) {
  const t = i / 10;
  await interpolateStates(overviewState, closeupState, t, cameraControls, true);
  await new Promise(resolve => setTimeout(resolve, 200));
}

Install with Tessl CLI

npx tessl i tessl/npm-camera-controls

docs

boundaries-constraints.md

camera-fitting.md

camera-movement.md

core-controls.md

event-system.md

index.md

input-configuration.md

state-management.md

tile.json