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
- 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/rotationQeach 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
- Basis construction �?
calculateOrbitBasis(cam.positionV, center, orbitUp)returnsforward,right,yawAxis.forwardis normalized(center - position).yawAxisis the orbit up vector re-projected onto the plane orthogonal toforward, then re-orthogonalised withright = forward × yawAxis. - Log zoom �?
applyDistanceScalingusesdist�?= exp(log(dist₀) + scroll × dt × 10 × speed)and clamps between[MIN_DISTANCE, MAX_DISTANCE]. - Panning �?
applyPanning(center, shift, right, yawAxis, dt, speed, distance)offsetscenterproportionally to distance and speed, so panning feels consistent regardless of zoom level. - Rotation �?
applyRotation(forward, right, yawAxis, yaw, pitch, roll)applies yaw aroundyawAxis, pitch aroundright, and optional roll (Alt drag). Pole protection clamps pitch whenforwardapproaches±yawAxiswithin 5°. - Position/orientation �?
cam.positionV = center - forward * distance.cam.rotationQ = lookAtW2C(forward, orbitUpRef)builds a world→camera quaternion from the new basis. - Decay �?
applyDecay(rotation, shift, scroll, dt)multiplies components bypow(0.8, dt * 60)(60 fps baseline) and zeroes out tiny values. - Orbit-up maintenance �?the cleaned
yawAxisbecomes theorbitUpfor 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 toforward, then twisting aroundforwarduntil +Y aligns with the desired up vector. It handles degenerate cases (forward �?up) by falling back to another axis.projectOntoPlaneNormed(v, n, fallback)projectsvonto the plane with normalnand 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/pitchangles, inertia for rotation and translation, and keyboard/mouse state maps. - Mouse deltas update
yaw/pitchwhen 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.
flyModetoggles 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
IControllerand reusinginput.ts/orbit.tshelpers. - Touch/gesture support can be layered on by converting touch events into the same
rotation,shift, andscrollaccumulators. - 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.