export interface CameraOrbitState { yaw: number; pitch: number; } export interface PlanarMovement { x: number; z: number; } export const DEFAULT_CAMERA_YAW = 0; export const DEFAULT_CAMERA_PITCH = Math.atan2(4.45, 7.7); export const CAMERA_ORBIT_DISTANCE = Math.hypot(4.45, 7.7); export const CAMERA_FOCUS_HEIGHT = 0.65; export const CAMERA_LOOK_AHEAD = 2.8; export const CAMERA_YAW_SPEED = 2.25; export const CAMERA_PITCH_SPEED = 1.25; export const MIN_CAMERA_PITCH = 0.24; export const MAX_CAMERA_PITCH = 0.9; /** Updates one reusable orbit state without allocating in the render loop. */ export function updateCameraOrbit( orbit: CameraOrbitState, lookX: number, lookY: number, deltaSeconds: number, ) { orbit.yaw -= lookX * CAMERA_YAW_SPEED * deltaSeconds; orbit.pitch = Math.min( MAX_CAMERA_PITCH, Math.max(MIN_CAMERA_PITCH, orbit.pitch + lookY * CAMERA_PITCH_SPEED * deltaSeconds), ); if (orbit.yaw > Math.PI || orbit.yaw < -Math.PI) { orbit.yaw = Math.atan2(Math.sin(orbit.yaw), Math.cos(orbit.yaw)); } } /** Converts left-stick input into world movement relative to current camera yaw. */ export function setCameraRelativeMovement( output: PlanarMovement, moveX: number, moveY: number, cameraYaw: number, ) { const sinYaw = Math.sin(cameraYaw); const cosYaw = Math.cos(cameraYaw); output.x = moveX * cosYaw + moveY * sinYaw; output.z = -moveX * sinYaw + moveY * cosYaw; }