Spaces:
Running
Running
File size: 3,797 Bytes
84b2c31 4d6f475 84b2c31 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | import * as THREE from 'three';
const OFFSET_BEHIND = 10;
const OFFSET_ABOVE = 4.5;
const LOOK_AHEAD = 6;
const LERP_POS = 0.1;
const LERP_LOOK = 0.08;
export class ChaseCamera {
constructor(camera) {
this.camera = camera;
this.targetPos = new THREE.Vector3();
this.targetLook = new THREE.Vector3();
this._tmpPos = new THREE.Vector3();
this._tmpLook = new THREE.Vector3();
// 360-degree free-look variables
this.orbitYaw = 0; // horizontal rotation offset
this.orbitPitch = 0; // vertical rotation offset
this.isDragging = false;
this.lastMouseX = 0;
this.lastMouseY = 0;
this._bindEvents();
}
_bindEvents() {
window.addEventListener('mousedown', (e) => {
this.isDragging = true;
this.lastMouseX = e.clientX;
this.lastMouseY = e.clientY;
});
window.addEventListener('mousemove', (e) => {
if (!this.isDragging) return;
const dx = e.clientX - this.lastMouseX;
const dy = e.clientY - this.lastMouseY;
this.lastMouseX = e.clientX;
this.lastMouseY = e.clientY;
this.orbitYaw += dx * 0.01;
this.orbitPitch += dy * 0.01;
// Clamp pitch so they don't look upside down
this.orbitPitch = Math.max(-0.5, Math.min(1.5, this.orbitPitch));
});
window.addEventListener('mouseup', () => {
this.isDragging = false;
});
// Touch support for mobile
window.addEventListener('touchstart', (e) => {
if (e.touches.length > 0) {
this.isDragging = true;
this.lastMouseX = e.touches[0].clientX;
this.lastMouseY = e.touches[0].clientY;
}
});
window.addEventListener('touchmove', (e) => {
if (!this.isDragging || e.touches.length === 0) return;
const dx = e.touches[0].clientX - this.lastMouseX;
const dy = e.touches[0].clientY - this.lastMouseY;
this.lastMouseX = e.touches[0].clientX;
this.lastMouseY = e.touches[0].clientY;
this.orbitYaw += dx * 0.01;
this.orbitPitch += dy * 0.01;
this.orbitPitch = Math.max(-0.5, Math.min(1.5, this.orbitPitch));
});
window.addEventListener('touchend', () => {
this.isDragging = false;
});
}
update(carMesh, dt) {
const carPos = carMesh.position;
const heading = carMesh.rotation.y;
// If not dragging, smoothly return the camera to the default forward-facing angle
if (!this.isDragging) {
this.orbitYaw *= 0.95;
this.orbitPitch *= 0.95;
}
// Apply the free-look orbit rotation
const baseHeading = heading + this.orbitYaw;
// Desired camera position: behind and above the car
const behindDir = new THREE.Vector3(
Math.sin(baseHeading),
0,
Math.cos(baseHeading)
);
// Calculate height offset based on pitch
const currentAbove = OFFSET_ABOVE + this.orbitPitch * 5;
this._tmpPos.copy(carPos)
.addScaledVector(behindDir, -OFFSET_BEHIND)
.setY(carPos.y + currentAbove);
// Lerp camera position
this.camera.position.lerp(this._tmpPos, LERP_POS);
// Look-at target: we look directly at the car, but adjust height for pitch
this._tmpLook.copy(carPos).setY(carPos.y + 1.5 - this.orbitPitch * 2);
// Lerp look target
this.targetLook.lerp(this._tmpLook, LERP_LOOK);
this.camera.lookAt(this.targetLook);
}
snapTo(carMesh) {
const carPos = carMesh.position;
const heading = carMesh.rotation.y;
const behindDir = new THREE.Vector3(Math.sin(heading), 0, Math.cos(heading));
this.camera.position.copy(carPos).addScaledVector(behindDir, OFFSET_BEHIND).setY(carPos.y + OFFSET_ABOVE);
this.targetLook.copy(carPos).addScaledVector(behindDir, -LOOK_AHEAD).setY(carPos.y + 1.0);
this.camera.lookAt(this.targetLook);
}
}
|