Skip to content

Controls Module Architecture

The controls module sits between UI events and camera state. It keeps event handling, math, and state management separate so each layer stays testable and predictable.

Layered flow

DOM events �?input.ts �?CameraController / FPSController �?orbit.ts math �?PerspectiveCamera
  • input.ts �?pure functions (processKeyboardInput, processMouseInput, processScrollInput) map raw events to “intent�?vectors (amount, rotation, shift, scroll).
  • controller.ts / fps-controller.ts �?hold state, accumulate input, call orbit math helpers, and finally mutate PerspectiveCamera.positionV / rotationQ each frame.
  • orbit.ts �?math primitives for basis computation, log zoom, panning, rotation with pole protection, lookAtW2C, decay, etc.

Orbit controller pipeline

State

center   // orbit focus point (vec3)
rotation // accumulated yaw/pitch/roll (vec3)
shift    // accumulated pan (vec2)
scroll   // logarithmic zoom scalar (number)
orbitUp  // stable up vector carried between frames (vec3)

Update steps

  1. Basis construction �?calculateOrbitBasis(cam.positionV, center, orbitUp) returns forward, right, yawAxis. forward is normalized (center - position). yawAxis is the orbit up vector re-projected onto the plane orthogonal to forward, then re-orthogonalised with right = forward × yawAxis.
  2. Log zoom �?applyDistanceScaling uses dist�?= exp(log(dist₀) + scroll × dt × 10 × speed) and clamps between [MIN_DISTANCE, MAX_DISTANCE].
  3. Panning �?applyPanning(center, shift, right, yawAxis, dt, speed, distance) offsets center proportionally to distance and speed, so panning feels consistent regardless of zoom level.
  4. Rotation �?applyRotation(forward, right, yawAxis, yaw, pitch, roll) applies yaw around yawAxis, pitch around right, and optional roll (Alt drag). Pole protection clamps pitch when forward approaches ±yawAxis within 5°.
  5. Position/orientation �?cam.positionV = center - forward * distance. cam.rotationQ = lookAtW2C(forward, orbitUpRef) builds a world→camera quaternion from the new basis.
  6. Decay �?applyDecay(rotation, shift, scroll, dt) multiplies components by pow(0.8, dt * 60) (60 fps baseline) and zeroes out tiny values.
  7. Orbit-up maintenance �?the cleaned yawAxis becomes the orbitUp for the next frame so the camera maintains a stable reference up vector even after multiple rotations.

Input helpers

Keyboard

processKeyboardInput(code, pressed, amount, rotation, sensitivity) updates amount (KeyWASD, Space, ShiftLeft) and rotation[2] (KeyQ/E roll). It returns true if the key was handled.

Mouse

processMouseInput(dx, dy, leftPressed, rightPressed, rotation, shift) adds deltas to rotation (left button) or shift (right button). The controller multiplies those deltas by sensitivity and deltaTime later.

Scroll

processScrollInput(delta) simply scales the wheel delta (default ×3) so the controller can accumulate smooth log zoom steps.

Orbit math internals (orbit.ts)

  • lookAtW2C(forward, up) constructs a world→camera quaternion by aligning +Z to forward, then twisting around forward until +Y aligns with the desired up vector. It handles degenerate cases (forward �?up) by falling back to another axis.
  • projectOntoPlaneNormed(v, n, fallback) projects v onto the plane with normal n and normalizes the result with an epsilon-safe fallback.
  • Pole protection compares the dot product between the would-be pitch result and yawAxis. If the new vector would exceed the pole guard (cos(5°)), the pitch is scaled down so the camera never hits the singularity.
  • Constants (WORLD_UP, MIN_POLE_ANGLE, MIN_DISTANCE, etc.) are exported for reuse by custom controllers.

FPS controller overview

  • Maintains yaw/pitch angles, inertia for rotation and translation, and keyboard/mouse state maps.
  • Mouse deltas update yaw/pitch when the left button is pressed; otherwise rotation inertia decays exponentially (rotateInertia).
  • Keyboard WASDQE/PageUp/PageDown produce a movement vector in camera space, which is transformed into world space via the inverted camera quaternion. flyMode toggles between full 6DoF (Y axis free) and ground-only (movement confined to the XZ plane).
  • Scroll input feeds additional forward/backward movement.
  • Speed multipliers: Caps Lock (×10), Shift (×50), Ctrl (×0.2).

Integration blueprint

DOM events (UIController)
  ├─ keydown/up �?controller.processKeyboard(code, pressed)
  ├─ mousedown/up �?set leftMousePressed/rightMousePressed
  ├─ mousemove �?controller.processMouse(dx, dy)
  └─ wheel �?controller.processScroll(delta)

Per frame:
  controller.update(perspectiveCamera, deltaTime)
  renderer uploads new camera view/proj matrices

Controllers expose userInput so host apps know if any new interaction occurred (useful for pausing auto-rotation, etc.).

Extensibility

  • Implement another control scheme by adhering to IController and reusing input.ts / orbit.ts helpers.
  • Touch/gesture support can be layered on by converting touch events into the same rotation, shift, and scroll accumulators.
  • Blend between controllers by interpolating camera state or switching based on user mode (e.g., orbit �?FPS).

By isolating input parsing, mathematical application, and camera mutation, the controls module provides predictable behaviour whether it is driven by the default UI, a Three.js scene, or custom tooling.