166 lines
4.5 KiB
TypeScript
166 lines
4.5 KiB
TypeScript
import {
|
|
Object3D,
|
|
PerspectiveCamera,
|
|
Quaternion,
|
|
Vector2,
|
|
Vector3,
|
|
} from 'three';
|
|
import { clamp } from 'three/src/math/MathUtils';
|
|
import { deg2rad } from '../../common/convert';
|
|
|
|
export class ThirdPersonCamera {
|
|
private currentPosition = new Vector3();
|
|
private currentLookAt = new Vector3();
|
|
private offsetFromPlayer = new Vector3();
|
|
private mousePos = new Vector2();
|
|
private prevMousePos = new Vector2();
|
|
|
|
private angleAroundPlayer = 180;
|
|
private pitch = -45;
|
|
private distance = 3;
|
|
|
|
private panning = false;
|
|
private pinching = false;
|
|
private previousPinchLength = 0;
|
|
|
|
constructor(
|
|
private camera: PerspectiveCamera,
|
|
private target: Object3D,
|
|
private eventTarget: HTMLElement,
|
|
) {}
|
|
|
|
private dragEvent = (x: number, y: number) => {
|
|
this.prevMousePos.copy(this.mousePos);
|
|
this.mousePos = this.mousePos.fromArray([x, y]);
|
|
|
|
if (this.panning) {
|
|
const offset = this.prevMousePos.clone().sub(this.mousePos);
|
|
|
|
this.angleAroundPlayer =
|
|
this.angleAroundPlayer + ((offset.x * 0.3) % 360);
|
|
this.pitch = clamp(this.pitch + offset.y * 0.3, -90, 90);
|
|
|
|
this.calculateCameraOffset();
|
|
}
|
|
};
|
|
|
|
events = {
|
|
contextmenu: (e: MouseEvent) => e.preventDefault(),
|
|
mousedown: (e: MouseEvent) => {
|
|
if (e.button === 2) {
|
|
this.panning = true;
|
|
}
|
|
},
|
|
mouseup: (e: MouseEvent) => {
|
|
if (e.button === 2) {
|
|
this.panning = false;
|
|
}
|
|
},
|
|
mouseleave: (e: MouseEvent) => {
|
|
this.panning = false;
|
|
},
|
|
mousemove: (e: MouseEvent) => this.dragEvent(e.clientX, e.clientY),
|
|
wheel: (e: WheelEvent) => {
|
|
e.deltaY < 0 ? (this.distance /= 1.2) : (this.distance *= 1.2);
|
|
this.distance = clamp(this.distance, 4, 20);
|
|
this.calculateCameraOffset();
|
|
},
|
|
// mobile
|
|
touchstart: (ev: TouchEvent) => {
|
|
ev.preventDefault();
|
|
const touch = ev.touches[0] || ev.changedTouches[0];
|
|
this.mousePos.fromArray([touch.pageX, touch.pageY]);
|
|
this.panning = true;
|
|
|
|
if (ev.touches.length === 2) {
|
|
this.pinching = true;
|
|
}
|
|
},
|
|
touchmove: (ev: TouchEvent) => {
|
|
ev.preventDefault();
|
|
|
|
if (ev.touches.length === 2 && this.pinching) {
|
|
const pinchLength = Math.hypot(
|
|
ev.touches[0].pageX - ev.touches[1].pageX,
|
|
ev.touches[0].pageY - ev.touches[1].pageY,
|
|
);
|
|
|
|
if (this.previousPinchLength) {
|
|
const delta = pinchLength / this.previousPinchLength;
|
|
delta > 0 ? (this.distance *= delta) : (this.distance /= delta);
|
|
this.distance = clamp(this.distance, 4, 20);
|
|
this.calculateCameraOffset();
|
|
}
|
|
|
|
this.previousPinchLength = pinchLength;
|
|
} else {
|
|
this.dragEvent(ev.touches[0].clientX, ev.touches[0].clientY);
|
|
}
|
|
},
|
|
touchend: (ev: TouchEvent) => {
|
|
this.pinching = false;
|
|
this.previousPinchLength = 0;
|
|
|
|
if (!ev.touches?.length) {
|
|
this.panning = false;
|
|
}
|
|
},
|
|
};
|
|
|
|
initialize() {
|
|
Object.keys(this.events).forEach((key) => {
|
|
this.eventTarget.addEventListener(key, this.events[key]);
|
|
});
|
|
this.calculateCameraOffset();
|
|
}
|
|
|
|
dispose() {
|
|
Object.keys(this.events).forEach((key) => {
|
|
this.eventTarget.removeEventListener(key, this.events[key]);
|
|
});
|
|
}
|
|
|
|
update(dt: number) {
|
|
const offset = this.getTargetOffset();
|
|
const lookAt = this.getTargetLookAt();
|
|
|
|
// https://www.youtube.com/watch?v=UuNPHOJ_V5o
|
|
const factor = 1.0 - Math.pow(0.001, dt);
|
|
|
|
this.currentPosition.lerp(offset, factor);
|
|
this.currentLookAt.lerp(lookAt, factor);
|
|
|
|
this.camera.position.copy(this.currentPosition);
|
|
this.camera.lookAt(this.currentLookAt);
|
|
}
|
|
|
|
private calculateCameraOffset() {
|
|
const hdist = this.distance * Math.cos(deg2rad(this.pitch));
|
|
const vdist = this.distance * Math.sin(deg2rad(this.pitch));
|
|
|
|
this.offsetFromPlayer.set(
|
|
hdist * Math.sin(deg2rad(this.angleAroundPlayer)),
|
|
-vdist,
|
|
hdist * Math.cos(deg2rad(this.angleAroundPlayer)),
|
|
);
|
|
}
|
|
|
|
private getTargetOffset(): Vector3 {
|
|
const offset = this.offsetFromPlayer.clone();
|
|
const quat = new Quaternion();
|
|
this.target.getWorldQuaternion(quat);
|
|
offset.applyQuaternion(quat);
|
|
offset.add(this.target.position);
|
|
return offset;
|
|
}
|
|
|
|
private getTargetLookAt(): Vector3 {
|
|
const offset = new Vector3(0, 1.5, 0.5);
|
|
const quat = new Quaternion();
|
|
this.target.getWorldQuaternion(quat);
|
|
offset.applyQuaternion(quat);
|
|
offset.add(this.target.position);
|
|
return offset;
|
|
}
|
|
}
|