Spaces:
Running
Running
| import { Camera } from "../cameras/Camera"; | |
| import { Matrix3 } from "../math/Matrix3"; | |
| import { Quaternion } from "../math/Quaternion"; | |
| import { Vector3 } from "../math/Vector3"; | |
| class OrbitControls { | |
| // Vertical (polar) limits: by default, free rotation. | |
| minAngle: number = -Infinity; | |
| maxAngle: number = Infinity; | |
| // Horizontal (azimuth) limits: by default, free rotation. | |
| minAzimuth: number = -Infinity; | |
| maxAzimuth: number = Infinity; | |
| minZoom: number = 0.1; | |
| maxZoom: number = 30; | |
| orbitSpeed: number = 1; | |
| panSpeed: number = 1; | |
| zoomSpeed: number = 1; | |
| dampening: number = 0.12; | |
| setCameraTarget: (newTarget: Vector3) => void = () => {}; | |
| update: () => void; | |
| dispose: () => void; | |
| /** | |
| * @param camera the camera to control. | |
| * @param canvas the element to attach events. | |
| * @param alpha initial horizontal angle (in radians). | |
| * @param beta initial vertical angle (in radians). | |
| * @param radius initial distance from the target. | |
| * @param enableKeyboardControls whether to listen to keyboard events. | |
| * @param inputTarget the point to orbit around. | |
| */ | |
| constructor( | |
| camera: Camera, | |
| canvas: HTMLElement, | |
| alpha: number = 0.5, | |
| beta: number = 0.5, | |
| radius: number = 5, | |
| enableKeyboardControls: boolean = true, | |
| inputTarget: Vector3 = new Vector3(), | |
| // New optional parameters to be set from JSON: | |
| initAlpha?: number, | |
| initBeta?: number, | |
| initRadius?: number, | |
| ) { | |
| // Use the JSON-provided initial values if given. | |
| const defaultAlpha = initAlpha !== undefined ? initAlpha : alpha; | |
| const defaultBeta = initBeta !== undefined ? initBeta : beta; | |
| const defaultRadius = initRadius !== undefined ? initRadius : radius; | |
| // The target (the point the camera orbits around) | |
| let target = inputTarget.clone(); | |
| // Set both the current and desired orbit parameters to the defaults. | |
| let desiredTarget = target.clone(); | |
| let desiredAlpha = defaultAlpha; | |
| let desiredBeta = defaultBeta; | |
| let desiredRadius = defaultRadius; | |
| // We now use separate “current” state variables so that update() can lerp toward desired values. | |
| let currentAlpha = defaultAlpha; | |
| let currentBeta = defaultBeta; | |
| let currentRadius = defaultRadius; | |
| let dragging = false; | |
| let panning = false; | |
| let lastDist = 0; | |
| let lastX = 0; | |
| let lastY = 0; | |
| const keys: { [key: string]: boolean } = {}; | |
| let isUpdatingCamera = false; | |
| // (Optionally you could keep an onCameraChange listener to update the current state when the camera changes, | |
| // but that may override your JSON settings. In this example, we remove it.) | |
| // camera.addEventListener("objectChanged", onCameraChange); | |
| this.setCameraTarget = (newTarget: Vector3) => { | |
| const dx = newTarget.x - camera.position.x; | |
| const dy = newTarget.y - camera.position.y; | |
| const dz = newTarget.z - camera.position.z; | |
| desiredRadius = Math.sqrt(dx * dx + dy * dy + dz * dz); | |
| desiredBeta = Math.atan2(dy, Math.sqrt(dx * dx + dz * dz)); | |
| desiredAlpha = -Math.atan2(dx, dz); | |
| desiredTarget = new Vector3(newTarget.x, newTarget.y, newTarget.z); | |
| }; | |
| const computeZoomNorm = () => { | |
| return 0.1 + (0.9 * (desiredRadius - this.minZoom)) / (this.maxZoom - this.minZoom); | |
| }; | |
| const onKeyDown = (e: KeyboardEvent) => { | |
| keys[e.code] = true; | |
| // Map arrow keys to WASD keys | |
| if (e.code === "ArrowUp") keys["KeyW"] = true; | |
| if (e.code === "ArrowDown") keys["KeyS"] = true; | |
| if (e.code === "ArrowLeft") keys["KeyA"] = true; | |
| if (e.code === "ArrowRight") keys["KeyD"] = true; | |
| }; | |
| const onKeyUp = (e: KeyboardEvent) => { | |
| keys[e.code] = false; | |
| if (e.code === "ArrowUp") keys["KeyW"] = false; | |
| if (e.code === "ArrowDown") keys["KeyS"] = false; | |
| if (e.code === "ArrowLeft") keys["KeyA"] = false; | |
| if (e.code === "ArrowRight") keys["KeyD"] = false; | |
| }; | |
| const onMouseDown = (e: MouseEvent) => { | |
| preventDefault(e); | |
| dragging = true; | |
| panning = e.button === 2; | |
| lastX = e.clientX; | |
| lastY = e.clientY; | |
| window.addEventListener("mouseup", onMouseUp); | |
| }; | |
| const onMouseUp = (e: MouseEvent) => { | |
| preventDefault(e); | |
| dragging = false; | |
| panning = false; | |
| window.removeEventListener("mouseup", onMouseUp); | |
| }; | |
| const onMouseMove = (e: MouseEvent) => { | |
| preventDefault(e); | |
| if (!dragging || !camera) return; | |
| const dx = e.clientX - lastX; | |
| const dy = e.clientY - lastY; | |
| if (panning) { | |
| const zoomNorm = computeZoomNorm(); | |
| const panX = -dx * this.panSpeed * 0.01 * zoomNorm; | |
| const panY = -dy * this.panSpeed * 0.01 * zoomNorm; | |
| const R = Matrix3.RotationFromQuaternion(camera.rotation).buffer; | |
| const right = new Vector3(R[0], R[3], R[6]); | |
| const up = new Vector3(R[1], R[4], R[7]); | |
| desiredTarget = desiredTarget.add(right.multiply(panX)); | |
| desiredTarget = desiredTarget.add(up.multiply(panY)); | |
| } else { | |
| desiredAlpha -= dx * this.orbitSpeed * 0.003; | |
| desiredBeta += dy * this.orbitSpeed * 0.003; | |
| // Clamp vertical angle (beta) if limits are finite | |
| desiredBeta = Math.min( | |
| Math.max(desiredBeta, (this.minAngle * Math.PI) / 180), | |
| (this.maxAngle * Math.PI) / 180, | |
| ); | |
| } | |
| lastX = e.clientX; | |
| lastY = e.clientY; | |
| }; | |
| const onWheel = (e: WheelEvent) => { | |
| preventDefault(e); | |
| const zoomNorm = computeZoomNorm(); | |
| desiredRadius += e.deltaY * this.zoomSpeed * 0.025 * zoomNorm; | |
| desiredRadius = Math.min(Math.max(desiredRadius, this.minZoom), this.maxZoom); | |
| }; | |
| const onTouchStart = (e: TouchEvent) => { | |
| preventDefault(e); | |
| if (e.touches.length === 1) { | |
| dragging = true; | |
| panning = false; | |
| lastX = e.touches[0].clientX; | |
| lastY = e.touches[0].clientY; | |
| lastDist = 0; | |
| } else if (e.touches.length === 2) { | |
| dragging = true; | |
| panning = true; | |
| lastX = (e.touches[0].clientX + e.touches[1].clientX) / 2; | |
| lastY = (e.touches[0].clientY + e.touches[1].clientY) / 2; | |
| const distX = e.touches[0].clientX - e.touches[1].clientX; | |
| const distY = e.touches[0].clientY - e.touches[1].clientY; | |
| lastDist = Math.sqrt(distX * distX + distY * distY); | |
| } | |
| }; | |
| const onTouchEnd = (e: TouchEvent) => { | |
| preventDefault(e); | |
| dragging = false; | |
| panning = false; | |
| }; | |
| const onTouchMove = (e: TouchEvent) => { | |
| preventDefault(e); | |
| if (!dragging || !camera) return; | |
| if (panning) { | |
| const zoomNorm = computeZoomNorm(); | |
| const distX = e.touches[0].clientX - e.touches[1].clientX; | |
| const distY = e.touches[0].clientY - e.touches[1].clientY; | |
| const dist = Math.sqrt(distX * distX + distY * distY); | |
| const delta = lastDist - dist; | |
| desiredRadius += delta * this.zoomSpeed * 0.1 * zoomNorm; | |
| desiredRadius = Math.min(Math.max(desiredRadius, this.minZoom), this.maxZoom); | |
| lastDist = dist; | |
| const touchX = (e.touches[0].clientX + e.touches[1].clientX) / 2; | |
| const touchY = (e.touches[0].clientY + e.touches[1].clientY) / 2; | |
| const dx = touchX - lastX; | |
| const dy = touchY - lastY; | |
| const R = Matrix3.RotationFromQuaternion(camera.rotation).buffer; | |
| const right = new Vector3(R[0], R[3], R[6]); | |
| const up = new Vector3(R[1], R[4], R[7]); | |
| desiredTarget = desiredTarget.add(right.multiply(-dx * this.panSpeed * 0.025 * zoomNorm)); | |
| desiredTarget = desiredTarget.add(up.multiply(-dy * this.panSpeed * 0.025 * zoomNorm)); | |
| lastX = touchX; | |
| lastY = touchY; | |
| } else { | |
| const dx = e.touches[0].clientX - lastX; | |
| const dy = e.touches[0].clientY - lastY; | |
| desiredAlpha -= dx * this.orbitSpeed * 0.003; | |
| desiredBeta += dy * this.orbitSpeed * 0.003; | |
| desiredBeta = Math.min( | |
| Math.max(desiredBeta, (this.minAngle * Math.PI) / 180), | |
| (this.maxAngle * Math.PI) / 180, | |
| ); | |
| lastX = e.touches[0].clientX; | |
| lastY = e.touches[0].clientY; | |
| } | |
| }; | |
| const lerp = (a: number, b: number, t: number) => (1 - t) * a + t * b; | |
| this.update = () => { | |
| isUpdatingCamera = true; | |
| // Lerp the current state toward the desired state. | |
| currentAlpha = lerp(currentAlpha, desiredAlpha, this.dampening); | |
| currentBeta = lerp(currentBeta, desiredBeta, this.dampening); | |
| currentRadius = lerp(currentRadius, desiredRadius, this.dampening); | |
| target = target.lerp(desiredTarget, this.dampening); | |
| // Recompute the camera position from currentAlpha/currentBeta/currentRadius. | |
| const x = target.x + currentRadius * Math.sin(currentAlpha) * Math.cos(currentBeta); | |
| const y = target.y - currentRadius * Math.sin(currentBeta); | |
| const z = target.z - currentRadius * Math.cos(currentAlpha) * Math.cos(currentBeta); | |
| camera.position = new Vector3(x, y, z); | |
| // Compute new rotation so that the camera looks at the target. | |
| const direction = target.subtract(camera.position).normalize(); | |
| const rx = Math.asin(-direction.y); | |
| const ry = Math.atan2(direction.x, direction.z); | |
| camera.rotation = Quaternion.FromEuler(new Vector3(rx, ry, 0)); | |
| const moveSpeed = 0.025; | |
| const rotateSpeed = 0.01; | |
| const R = Matrix3.RotationFromQuaternion(camera.rotation).buffer; | |
| const forward = new Vector3(-R[2], -R[5], -R[8]); | |
| const right = new Vector3(R[0], R[3], R[6]); | |
| if (keys["KeyS"]) desiredTarget = desiredTarget.add(forward.multiply(moveSpeed)); | |
| if (keys["KeyW"]) desiredTarget = desiredTarget.subtract(forward.multiply(moveSpeed)); | |
| if (keys["KeyA"]) desiredTarget = desiredTarget.subtract(right.multiply(moveSpeed)); | |
| if (keys["KeyD"]) desiredTarget = desiredTarget.add(right.multiply(moveSpeed)); | |
| // Horizontal (azimuth) rotation with 'e' and 'q' | |
| if (keys["KeyE"]) desiredAlpha += rotateSpeed; | |
| if (keys["KeyQ"]) desiredAlpha -= rotateSpeed; | |
| desiredAlpha = Math.min( | |
| Math.max(desiredAlpha, (this.minAzimuth * Math.PI) / 180), | |
| (this.maxAzimuth * Math.PI) / 180, | |
| ); | |
| // Vertical (polar) rotation with 'r' and 'f' | |
| if (keys["KeyR"]) desiredBeta += rotateSpeed; | |
| if (keys["KeyF"]) desiredBeta -= rotateSpeed; | |
| desiredBeta = Math.min( | |
| Math.max(desiredBeta, (this.minAngle * Math.PI) / 180), | |
| (this.maxAngle * Math.PI) / 180, | |
| ); | |
| isUpdatingCamera = false; | |
| }; | |
| const preventDefault = (e: Event) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| }; | |
| this.dispose = () => { | |
| canvas.removeEventListener("dragenter", preventDefault); | |
| canvas.removeEventListener("dragover", preventDefault); | |
| canvas.removeEventListener("dragleave", preventDefault); | |
| canvas.removeEventListener("contextmenu", preventDefault); | |
| canvas.removeEventListener("mousedown", onMouseDown); | |
| canvas.removeEventListener("mousemove", onMouseMove); | |
| canvas.removeEventListener("wheel", onWheel); | |
| canvas.removeEventListener("touchstart", onTouchStart); | |
| canvas.removeEventListener("touchend", onTouchEnd); | |
| canvas.removeEventListener("touchmove", onTouchMove); | |
| if (enableKeyboardControls) { | |
| window.removeEventListener("keydown", onKeyDown); | |
| window.removeEventListener("keyup", onKeyUp); | |
| } | |
| }; | |
| if (enableKeyboardControls) { | |
| window.addEventListener("keydown", onKeyDown); | |
| window.addEventListener("keyup", onKeyUp); | |
| } | |
| canvas.addEventListener("dragenter", preventDefault); | |
| canvas.addEventListener("dragover", preventDefault); | |
| canvas.addEventListener("dragleave", preventDefault); | |
| canvas.addEventListener("contextmenu", preventDefault); | |
| canvas.addEventListener("mousedown", onMouseDown); | |
| canvas.addEventListener("mousemove", onMouseMove); | |
| canvas.addEventListener("wheel", onWheel); | |
| canvas.addEventListener("touchstart", onTouchStart); | |
| canvas.addEventListener("touchend", onTouchEnd); | |
| canvas.addEventListener("touchmove", onTouchMove); | |
| this.update(); | |
| } | |
| } | |
| export { OrbitControls }; | |