Skip to content

Controls Module

src/controls/ houses the input �?camera pipeline for Visionary. It exposes orbit-style controls (default CameraController), an FPS-style controller, pure input helpers, and orbit math utilities. The code mirrors the behaviour of the Rust/Spark stack and plugs directly into the camera module (PerspectiveCamera).

Responsibilities

  • Convert DOM events (mouse/keyboard/wheel) into smooth orbiting, panning, and zooming motions.
  • Maintain numerically stable basis vectors so the camera never “flips�?near the poles.
  • Support alternative control schemes (currently an experimental FPSController).
  • Provide stateless helper functions so UI layers can generate custom interactions without touching controller internals.

Components

Component Path Purpose
CameraController controller.ts Default orbit controller used by the WebGPU viewer. Accumulates input state and updates PerspectiveCamera.
FPSController fps-controller.ts WASD + mouse-look controller with inertia, optional ground/fly modes.
input.ts Pure functions that translate keyboard/mouse/scroll deltas into movement vectors (amount, rotation, shift, scroll).
orbit.ts Math helpers: orbit basis calculation, logarithmic zoom, panning, rotation with pole protection, lookAtW2C.
base-controller.ts Interface (IController) shared by both controllers; defines update, processKeyboard, processMouse, processScroll.
index.ts Re-exports controllers, input helpers, and orbit utilities.

Data Flow

UI events �?input.ts �?controller state �?orbit.ts math �?camera position/rotation

CameraController stores state vectors (rotation, shift, scroll, center, orbit up). Each frame update(): 1. Builds orbit basis (calculateOrbitBasis). 2. Applies logarithmic distance scaling for zoom. 3. Applies panning to adjust the orbit center. 4. Applies yaw/pitch/roll with pole protection. 5. Recomputes camera position and rotation quaternion via lookAtW2C. 6. Decays state vectors (applyDecay) for smooth motion.

Integration

  • Camera module �?controllers mutate PerspectiveCamera.positionV and rotationQ. Projection data remains untouched so renderer/preprocess can reuse it.
  • Renderer/preprocess �?after controllers update the camera, the renderer uploads fresh view/projection matrices and the preprocess step uses the same camera interface.
  • UI layer �?UIController (in src/app) wires DOM events to controller methods: processKeyboard, processMouse, processScroll, and updates leftMousePressed/rightMousePressed flags.

Usage Examples

Orbit controller

import { CameraController } from 'src/controls';
const controller = new CameraController(0.2, 0.1);
const camera = PerspectiveCamera.default();

function frame(dt: number) {
  controller.update(camera, dt);
  renderer.prepareMulti(...); // camera now contains new view/proj
}

Attach events:

canvas.addEventListener('mousedown', (e) => {
  if (e.button === 0) controller.leftMousePressed = true;
  if (e.button === 2) controller.rightMousePressed = true;
});
canvas.addEventListener('mouseup', () => {
  controller.leftMousePressed = controller.rightMousePressed = false;
});
canvas.addEventListener('mousemove', (e) => controller.processMouse(e.movementX, e.movementY));
window.addEventListener('keydown', (e) => controller.processKeyboard(e.code, true));
window.addEventListener('keyup',   (e) => controller.processKeyboard(e.code, false));
canvas.addEventListener('wheel', (e) => controller.processScroll(e.deltaY));

FPS controller

import { FPSController } from 'src/controls';
const fps = new FPSController(5.0, 0.002);

function frame(dt: number) {
  fps.update(camera, dt);
}

FPSController automatically listens to document-level key events and uses mouse deltas when leftMousePressed is true. Call fps.setFlyMode(false) to restrict movement to the XZ plane.

Orbit math at a glance

  • Basis vectors: forward = normalize(center - cameraPos), yawAxis = project orbitUp onto the plane orthogonal to forward, right = forward × yawAxis. Re-orthogonalised every frame.
  • Log zoom: distance updates via exp(log(dist) + scroll * dt * 10 * speed) so zoom feels consistent at any scale.
  • Pole protection: pitch rotation clamps when forward approaches yawAxis within 5°, preventing gimbal lock.
  • Decay: rotation/shift/scroll values shrink by pow(0.8, dt * 60) and snap to zero when tiny.

Notes

  • All math uses gl-matrix. Temporary vectors are reused to avoid GC pressure.
  • Controllers expose resetUp, resetOrientation, and getControllerType() for host apps that switch between orbit/FPS modes.
  • Helper functions (processKeyboardInput, processMouseInput, processScrollInput, calculateOrbitBasis, etc.) are exported so custom controllers can build on the same primitives.

See Architecture for a deeper dive into the math, and API Reference for method signatures.

  • Architecture – Orbit math derivations, controller state machines, and decay tuning.
  • API Reference – Controller constructors, input helpers, and exported utility types.
  • Camera Module – Describes the PerspectiveCamera interface the controllers mutate.
  • Three Integration – Shows how UI layers wire DOM events into the control system.