Replace addons with conventional handles

Provide the single-loadout Scene API, SAB-backed meshes, cameras, materials, lights, workers, post effects, and tutorial playgrounds. Batch matching row growth so active GPU loadouts refresh once.

Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-08-20 06:39:30 +00:00
co-authored by heaust
parent 239b1d25b0
commit 9768765d74
62 changed files with 2749 additions and 798 deletions
+101
View File
@@ -0,0 +1,101 @@
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.#writeQuaternion();
};
#writeQuaternion() {
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.quaternion = [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];
this.position[0] += (x * Math.cos(yaw) + z * Math.sin(yaw)) * speed;
this.position[1] += y * speed;
this.position[2] += (x * -Math.sin(yaw) + z * Math.cos(yaw)) * speed;
this.#frame = requestAnimationFrame(this.#update);
};
override async dispose() {
this.detachControls();
await super.dispose();
}
}