Files
yawn/addons/handles/src/camera/FreeCamera.ts
T

105 lines
3.6 KiB
TypeScript

import type { Scene } from "../Scene";
import { Camera, type CameraOptions } from "./Camera";
export type FreeCameraControls = {
element: HTMLElement;
keyboard?: boolean;
pointer?: boolean;
controller?: boolean;
speed?: number;
sensitivity?: number;
};
export type FreeCameraOptions = CameraOptions & { controls?: FreeCameraControls };
/** Spectator-style WASD/mouse/gamepad camera backed entirely by shared camera and transform rows. */
export class FreeCamera extends Camera {
#controls?: FreeCameraControls;
#keys = new Set<string>();
#frame = 0;
#last = 0;
constructor(scene: Scene, options: FreeCameraOptions = {}) {
super(scene, options);
const cameraReady = this.ready;
this.ready = cameraReady.then(() => {
this.cameraRow()[19] = 2;
if (options.controls) this.attachControls(options.controls);
return this;
});
}
attachControls(controls: FreeCameraControls) {
this.detachControls();
this.#controls = controls;
if (controls.keyboard !== false) {
addEventListener("keydown", this.#keyDown);
addEventListener("keyup", this.#keyUp);
}
if (controls.pointer !== false) {
controls.element.addEventListener("click", this.#lock);
addEventListener("mousemove", this.#mouse);
}
this.#last = performance.now();
this.#frame = requestAnimationFrame(this.#update);
return this;
}
detachControls() {
const element = this.#controls?.element;
if (element) element.removeEventListener("click", this.#lock);
removeEventListener("keydown", this.#keyDown);
removeEventListener("keyup", this.#keyUp);
removeEventListener("mousemove", this.#mouse);
cancelAnimationFrame(this.#frame);
this.#frame = 0;
this.#keys.clear();
this.#controls = undefined;
}
#keyDown = (event: KeyboardEvent) => this.#keys.add(event.code);
#keyUp = (event: KeyboardEvent) => this.#keys.delete(event.code);
#lock = () => this.#controls?.element.requestPointerLock?.();
#mouse = (event: MouseEvent) => {
if (!this.#controls || document.pointerLockElement !== this.#controls.element) return;
const sensitivity = this.#controls.sensitivity ?? 0.002;
this.cameraRow()[13] -= event.movementX * sensitivity;
this.cameraRow()[14] = Math.min(1.55, Math.max(-1.55, this.cameraRow()[14] - event.movementY * sensitivity));
this.#writeRotor();
};
#writeRotor() {
const yaw = this.cameraRow()[13];
const pitch = this.cameraRow()[14];
const sy = Math.sin(yaw / 2), cy = Math.cos(yaw / 2);
const sx = Math.sin(pitch / 2), cx = Math.cos(pitch / 2);
this.setRotor([sx * cy, cx * sy, -sx * sy, cx * cy]);
}
#update = (time: number) => {
if (!this.#controls) return;
const delta = Math.min(0.1, (time - this.#last) / 1000);
this.#last = time;
let x = Number(this.#keys.has("KeyD")) - Number(this.#keys.has("KeyA"));
let y = Number(this.#keys.has("Space")) - Number(this.#keys.has("ControlLeft"));
let z = Number(this.#keys.has("KeyW")) - Number(this.#keys.has("KeyS"));
if (this.#controls.controller) {
const pad = navigator.getGamepads?.().find(Boolean);
if (pad) { x += pad.axes[0] ?? 0; y -= pad.axes[3] ?? 0; z -= pad.axes[1] ?? 0; }
}
const speed = (this.#controls.speed ?? 4) * delta;
const yaw = this.cameraRow()[13];
const position = this.position;
this.setPosition([
position.x + (x * Math.cos(yaw) + z * Math.sin(yaw)) * speed,
position.y + y * speed,
position.z + (x * -Math.sin(yaw) + z * Math.cos(yaw)) * speed,
]);
this.#frame = requestAnimationFrame(this.#update);
};
override async dispose() {
this.detachControls();
await super.dispose();
}
}