mirror of
https://github.com/storytold/spark.git
synced 2026-10-09 00:09:53 +00:00
696 lines
23 KiB
TypeScript
696 lines
23 KiB
TypeScript
import * as THREE from "three";
|
|
|
|
// Forge controls for keyboard + mouse, game pad, or mobile multi-touch
|
|
|
|
const DEFAULT_MOVEMENT_SPEED = 1.0;
|
|
const DEFAULT_ROLL_SPEED = 2.0;
|
|
const DEFAULT_ROTATE_SPEED = 0.002;
|
|
const DEFAULT_SLIDE_SPEED = 0.006;
|
|
const DEFAULT_SCROLL_SPEED = 0.0015;
|
|
const DEFAULT_ROLL_SPRING = 0.0;
|
|
const DEFAULT_ROTATE_INERTIA = 0.15;
|
|
const DEFAULT_MOVE_INERTIA = 0.15;
|
|
const DEFAULT_STICK_THRESHOLD = 0.1;
|
|
const DEFAULT_FPS_ROTATE_SPEED = 2.0;
|
|
|
|
// Time limit for double-finger press (pinch etc)
|
|
const DUAL_PRESS_MS = 200;
|
|
// Time limit for double-click/double-tap
|
|
const DOUBLE_PRESS_LIMIT_MS = 400;
|
|
// Distance limit for double-click
|
|
const DOUBLE_PRESS_DISTANCE = 50;
|
|
|
|
// Standard WASD movement keys with R+F for up/down
|
|
const WASD_KEYCODE_MOVE = {
|
|
KeyW: new THREE.Vector3(0, 0, -1),
|
|
KeyS: new THREE.Vector3(0, 0, 1),
|
|
KeyA: new THREE.Vector3(-1, 0, 0),
|
|
KeyD: new THREE.Vector3(1, 0, 0),
|
|
KeyR: new THREE.Vector3(0, 1, 0),
|
|
KeyF: new THREE.Vector3(0, -1, 0),
|
|
};
|
|
|
|
// Arrow key movement with PageUp/PageDown
|
|
const ARROW_KEYCODE_MOVE = {
|
|
ArrowUp: new THREE.Vector3(0, 0, -1),
|
|
ArrowDown: new THREE.Vector3(0, 0, 1),
|
|
ArrowLeft: new THREE.Vector3(-1, 0, 0),
|
|
ArrowRight: new THREE.Vector3(1, 0, 0),
|
|
PageUp: new THREE.Vector3(0, 1, 0),
|
|
PageDown: new THREE.Vector3(0, -1, 0),
|
|
};
|
|
|
|
// Rolling with Q/E
|
|
const QE_KEYCODE_ROTATE = {
|
|
KeyQ: new THREE.Vector3(0, 0, 1),
|
|
KeyE: new THREE.Vector3(0, 0, -1),
|
|
};
|
|
|
|
// Home/End/Insert/Delete for rotation
|
|
const ARROW_KEYCODE_ROTATE = {
|
|
Home: new THREE.Vector3(0, -1, 0),
|
|
End: new THREE.Vector3(0, 1, 0),
|
|
Insert: new THREE.Vector3(-1, 0, 0),
|
|
Delete: new THREE.Vector3(1, 0, 0),
|
|
};
|
|
|
|
// ForgeControls provides simple, intuitive controls for navigating 3D space that
|
|
// use the keyboard + mouse, game pad, or mobile multi-touch. Internally it
|
|
// instantiates and updates a `FpsMovement` and `PointerControls` instance.
|
|
|
|
export class ForgeControls {
|
|
fpsMovement: FpsMovement;
|
|
pointerControls: PointerControls;
|
|
lastTime = 0;
|
|
|
|
constructor({ canvas }: { canvas: HTMLCanvasElement }) {
|
|
this.fpsMovement = new FpsMovement({});
|
|
this.pointerControls = new PointerControls({ canvas });
|
|
}
|
|
|
|
update(control: THREE.Object3D) {
|
|
const time = performance.now();
|
|
const deltaTime = (time - (this.lastTime || time)) / 1000;
|
|
this.lastTime = time;
|
|
|
|
this.fpsMovement.update(deltaTime, control);
|
|
this.pointerControls.update(deltaTime, control);
|
|
}
|
|
}
|
|
|
|
// FpsMovement implements controls that will be familiar to anyone who plays
|
|
// First Person Shooters using keyboard + mouse or a gamepad. Creating a FpsMovement
|
|
// instance provides many parameters for configuring the controls.
|
|
//
|
|
// When gamepads are connected, FpsMovement will always use gamepad index 0
|
|
// for twin-stick movement and rotation.
|
|
//
|
|
// If xr is passed in, the WebXR controllers can be used as a split gamepad
|
|
// to control movement and rotation. (tested on Quest 3)
|
|
|
|
export class FpsMovement {
|
|
moveSpeed: number;
|
|
rollSpeed: number;
|
|
stickThreshold: number;
|
|
rotateSpeed: number;
|
|
keycodeMoveMapping: { [key: string]: THREE.Vector3 };
|
|
keycodeRotateMapping: { [key: string]: THREE.Vector3 };
|
|
gamepadMapping: {
|
|
[button: number]: "shift" | "ctrl" | "rollLeft" | "rollRight";
|
|
};
|
|
capsMultiplier: number;
|
|
shiftMultiplier: number;
|
|
ctrlMultiplier: number;
|
|
xr?: THREE.WebXRManager;
|
|
// Enable/disable controls updates
|
|
enable = true;
|
|
|
|
// Currently active event.key values
|
|
keydown: { [key: string]: boolean };
|
|
// Currently active event.code values
|
|
keycode: { [key: string]: boolean };
|
|
|
|
constructor({
|
|
moveSpeed,
|
|
rollSpeed,
|
|
stickThreshold,
|
|
rotateSpeed,
|
|
keycodeMoveMapping,
|
|
keycodeRotateMapping,
|
|
gamepadMapping,
|
|
capsMultiplier,
|
|
shiftMultiplier,
|
|
ctrlMultiplier,
|
|
xr,
|
|
}: {
|
|
// Base movement speed (default DEFAULT_MOVEMENT_SPEED)
|
|
moveSpeed?: number;
|
|
// Base roll speed (default DEFAULT_ROLL_SPEED)
|
|
rollSpeed?: number;
|
|
// Stick threshold (default DEFAULT_STICK_THRESHOLD)
|
|
stickThreshold?: number;
|
|
// Speed of rotation when using gamepad or keys (default DEFAULT_FPS_ROTATE_SPEED)
|
|
rotateSpeed?: number;
|
|
// Maps keyboard keys to movement directions
|
|
// (default {...WASD_KEYCODE_MOVE, ...ARROW_KEYCODE_MOVE})
|
|
keycodeMoveMapping?: { [key: string]: THREE.Vector3 };
|
|
// Maps keyboard keys to rotation directions
|
|
// (default {...QE_KEYCODE_ROTATE, ...ARROW_KEYCODE_ROTATE})
|
|
keycodeRotateMapping?: { [key: string]: THREE.Vector3 };
|
|
// Maps gamepad buttons to control actions
|
|
// (default {4: "rollLeft", 5: "rollRight", 6: "ctrl", 7: "shift"})
|
|
gamepadMapping?: {
|
|
[button: number]: "shift" | "ctrl" | "rollLeft" | "rollRight";
|
|
};
|
|
// Speed multiplier when Caps Lock is active (default: 10)
|
|
capsMultiplier?: number;
|
|
// Speed multiplier when Shift is active (default: 5)
|
|
shiftMultiplier?: number;
|
|
// Speed multiplier when Ctrl is active (default: 1/5)
|
|
ctrlMultiplier?: number;
|
|
// Optional WebXR manager for XR controller stick support
|
|
xr?: THREE.WebXRManager;
|
|
} = {}) {
|
|
this.moveSpeed = moveSpeed ?? DEFAULT_MOVEMENT_SPEED;
|
|
this.rollSpeed = rollSpeed ?? DEFAULT_ROLL_SPEED;
|
|
this.stickThreshold = stickThreshold ?? DEFAULT_STICK_THRESHOLD;
|
|
this.rotateSpeed = rotateSpeed ?? DEFAULT_FPS_ROTATE_SPEED;
|
|
this.keycodeMoveMapping = keycodeMoveMapping ?? {
|
|
...WASD_KEYCODE_MOVE,
|
|
...ARROW_KEYCODE_MOVE,
|
|
};
|
|
this.keycodeRotateMapping = keycodeRotateMapping ?? {
|
|
...QE_KEYCODE_ROTATE,
|
|
...ARROW_KEYCODE_ROTATE,
|
|
};
|
|
this.gamepadMapping = gamepadMapping ?? {
|
|
4: "rollLeft",
|
|
5: "rollRight",
|
|
6: "ctrl",
|
|
7: "shift",
|
|
};
|
|
this.capsMultiplier = capsMultiplier ?? 10.0;
|
|
this.shiftMultiplier = shiftMultiplier ?? 5.0;
|
|
this.ctrlMultiplier = ctrlMultiplier ?? 1.0 / 5.0;
|
|
|
|
this.xr = xr;
|
|
|
|
this.keydown = {};
|
|
this.keycode = {};
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
this.keydown[event.key] = true;
|
|
this.keycode[event.code] = true;
|
|
});
|
|
document.addEventListener("keyup", (event) => {
|
|
this.keydown[event.key] = false;
|
|
this.keycode[event.code] = false;
|
|
});
|
|
}
|
|
|
|
// Call this method in your render loop with `control` set to the object to control
|
|
// (`THREE.Camera` or a `THREE.Object3D` that contains it), with `deltaTime`
|
|
// in seconds since the last update.
|
|
update(deltaTime: number, control: THREE.Object3D) {
|
|
if (!this.enable) {
|
|
return;
|
|
}
|
|
|
|
// Update gamepad / XR controllers
|
|
|
|
const sticks = [new THREE.Vector2(), new THREE.Vector2()];
|
|
const gamepad = navigator.getGamepads()[0];
|
|
if (gamepad) {
|
|
sticks[0].set(gamepad.axes[0], gamepad.axes[1]);
|
|
sticks[1].set(gamepad.axes[2], gamepad.axes[3]);
|
|
}
|
|
const gamepadButtons =
|
|
gamepad?.buttons.map((button) => button.pressed) || [];
|
|
|
|
const xrSources = Array.from(this.xr?.getSession()?.inputSources ?? []);
|
|
for (const source of xrSources) {
|
|
const gamepad = source.gamepad;
|
|
if (gamepad) {
|
|
switch (source.handedness) {
|
|
case "none": {
|
|
sticks[0].x += gamepad.axes[0];
|
|
sticks[0].y += gamepad.axes[1];
|
|
sticks[1].x += gamepad.axes[2];
|
|
sticks[1].y += gamepad.axes[3];
|
|
break;
|
|
}
|
|
case "left": {
|
|
sticks[0].x += gamepad.axes[2];
|
|
sticks[0].y += gamepad.axes[3];
|
|
break;
|
|
}
|
|
case "right": {
|
|
sticks[1].x += gamepad.axes[2];
|
|
sticks[1].y += gamepad.axes[3];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const stick of sticks) {
|
|
stick.x = Math.abs(stick.x) >= this.stickThreshold ? stick.x : 0;
|
|
stick.y = Math.abs(stick.y) >= this.stickThreshold ? stick.y : 0;
|
|
}
|
|
|
|
// Rotation
|
|
|
|
const rotate = new THREE.Vector3(
|
|
sticks[1].x,
|
|
sticks[1].y,
|
|
0,
|
|
).multiplyScalar(this.rotateSpeed);
|
|
|
|
for (const [keycode, rot] of Object.entries(this.keycodeRotateMapping)) {
|
|
if (this.keycode[keycode]) {
|
|
rotate.add(rot);
|
|
}
|
|
}
|
|
for (const button in this.gamepadMapping) {
|
|
if (gamepadButtons[Number.parseInt(button)]) {
|
|
switch (this.gamepadMapping[button]) {
|
|
case "rollLeft":
|
|
rotate.z += 1;
|
|
break;
|
|
case "rollRight":
|
|
rotate.z -= 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
rotate.multiply(
|
|
new THREE.Vector3(this.rotateSpeed, this.rotateSpeed, this.rollSpeed),
|
|
);
|
|
|
|
if (rotate.manhattanLength() > 0.0) {
|
|
rotate.multiplyScalar(deltaTime);
|
|
const eulers = new THREE.Euler().setFromQuaternion(
|
|
control.quaternion,
|
|
"YXZ",
|
|
);
|
|
eulers.y -= rotate.x;
|
|
eulers.x = Math.max(
|
|
-Math.PI / 2,
|
|
Math.min(Math.PI / 2, eulers.x - rotate.y),
|
|
);
|
|
eulers.z = Math.max(-Math.PI, Math.min(Math.PI, eulers.z + rotate.z));
|
|
control.quaternion.setFromEuler(eulers);
|
|
}
|
|
|
|
// Movement
|
|
|
|
const moveVector = new THREE.Vector3(sticks[0].x, 0, sticks[0].y);
|
|
|
|
for (const [keycode, move] of Object.entries(this.keycodeMoveMapping)) {
|
|
if (this.keycode[keycode]) {
|
|
moveVector.add(move);
|
|
}
|
|
}
|
|
|
|
let speedMultiplier = 1.0;
|
|
if (this.keydown.CapsLock) {
|
|
speedMultiplier *= this.capsMultiplier;
|
|
}
|
|
if (this.keycode.ShiftLeft || this.keycode.ShiftRight) {
|
|
speedMultiplier *= this.shiftMultiplier;
|
|
}
|
|
if (this.keycode.ControlLeft || this.keycode.ControlRight) {
|
|
speedMultiplier *= this.ctrlMultiplier;
|
|
}
|
|
for (const button in this.gamepadMapping) {
|
|
if (gamepadButtons[Number.parseInt(button)]) {
|
|
switch (this.gamepadMapping[button]) {
|
|
case "shift":
|
|
speedMultiplier *= this.shiftMultiplier;
|
|
break;
|
|
case "ctrl":
|
|
speedMultiplier *= this.ctrlMultiplier;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply movement in view direction
|
|
moveVector.applyQuaternion(control.quaternion);
|
|
control.position.add(
|
|
moveVector.multiplyScalar(this.moveSpeed * speedMultiplier * deltaTime),
|
|
);
|
|
}
|
|
}
|
|
|
|
type PointerState = {
|
|
initial: THREE.Vector2;
|
|
last: THREE.Vector2;
|
|
position: THREE.Vector2;
|
|
pointerId: number;
|
|
button?: number;
|
|
timeStamp: DOMHighResTimeStamp;
|
|
};
|
|
|
|
// `PointerControls` implements pointer/mouse/touch controls on the canvas,
|
|
// for both desktop and mobile web applications.
|
|
|
|
export class PointerControls {
|
|
canvas: HTMLCanvasElement;
|
|
rotateSpeed: number;
|
|
slideSpeed: number;
|
|
scrollSpeed: number;
|
|
swapRotateSlide: boolean;
|
|
reverseRotate: boolean;
|
|
reverseSlide: boolean;
|
|
reverseSwipe: boolean;
|
|
reverseScroll: boolean;
|
|
moveInertia: number;
|
|
rotateInertia: number;
|
|
// Enable/disable controls updates
|
|
enable = true;
|
|
|
|
doublePress: ({
|
|
position,
|
|
intervalMs,
|
|
}: { position: THREE.Vector2; intervalMs: number }) => void;
|
|
// Time limit for double press (default DOUBLE_PRESS_LIMIT_MS)
|
|
doublePressLimitMs: number;
|
|
// Distance limit for double press (default DOUBLE_PRESS_DISTANCE)
|
|
doublePressDistance: number;
|
|
// Last pointer up event (default: null)
|
|
lastUp: { position: THREE.Vector2; time: number } | null;
|
|
|
|
// Pointer state for currently active rotating pointer
|
|
rotating: PointerState | null;
|
|
// Pointer state for currently active sliding pointer
|
|
sliding: PointerState | null;
|
|
// Whether we pressed two pointers at the same time
|
|
dualPress: boolean;
|
|
// Cumulative scroll movement
|
|
scroll: THREE.Vector3;
|
|
|
|
// Current rotation velocity
|
|
rotateVelocity: THREE.Vector3;
|
|
// Current movement velocity
|
|
moveVelocity: THREE.Vector3;
|
|
|
|
constructor({
|
|
// The HTML canvas element to attach pointer events to
|
|
canvas,
|
|
// Speed of rotation (default DEFAULT_ROTATE_SPEED)
|
|
rotateSpeed,
|
|
// Speed of sliding when dragging with right/middle mouse button or two fingers
|
|
// (default DEFAULT_SLIDE_SPEED)
|
|
slideSpeed,
|
|
// Speed of movement when using mouse scroll wheel (default DEFAULT_SCROLL_SPEED)
|
|
scrollSpeed,
|
|
// Swap the direction of rotation and sliding (default: false)
|
|
swapRotateSlide,
|
|
// Reverse the direction of rotation (default: false)
|
|
reverseRotate,
|
|
// Reverse the direction of sliding (default: false)
|
|
reverseSlide,
|
|
// Reverse the direction of swipe gestures (default: false)
|
|
reverseSwipe,
|
|
// Reverse the direction of scroll wheel movement (default: false)
|
|
reverseScroll,
|
|
// Inertia factor for movement (default: DEFAULT_MOVE_INERTIA)
|
|
moveInertia,
|
|
// Inertia factor for rotation (default: DEFAULT_ROTATE_INERTIA)
|
|
rotateInertia,
|
|
// Callback for double press events (default: () => {})
|
|
doublePress,
|
|
}: {
|
|
canvas: HTMLCanvasElement;
|
|
rotateSpeed?: number;
|
|
slideSpeed?: number;
|
|
scrollSpeed?: number;
|
|
swapRotateSlide?: boolean;
|
|
reverseRotate?: boolean;
|
|
reverseSlide?: boolean;
|
|
reverseSwipe?: boolean;
|
|
reverseScroll?: boolean;
|
|
moveInertia?: number;
|
|
rotateInertia?: number;
|
|
doublePress?: ({
|
|
position,
|
|
intervalMs,
|
|
}: { position: THREE.Vector2; intervalMs: number }) => void;
|
|
}) {
|
|
this.canvas = canvas;
|
|
this.rotateSpeed = rotateSpeed ?? DEFAULT_ROTATE_SPEED;
|
|
this.slideSpeed = slideSpeed ?? DEFAULT_SLIDE_SPEED;
|
|
this.scrollSpeed = scrollSpeed ?? DEFAULT_SCROLL_SPEED;
|
|
this.swapRotateSlide = swapRotateSlide ?? false;
|
|
this.reverseRotate = reverseRotate ?? false;
|
|
this.reverseSlide = reverseSlide ?? false;
|
|
this.reverseSwipe = reverseSwipe ?? false;
|
|
this.reverseScroll = reverseScroll ?? false;
|
|
this.moveInertia = moveInertia ?? DEFAULT_MOVE_INERTIA;
|
|
this.rotateInertia = rotateInertia ?? DEFAULT_ROTATE_INERTIA;
|
|
|
|
this.doublePress = doublePress ?? (() => {});
|
|
this.doublePressLimitMs = DOUBLE_PRESS_LIMIT_MS;
|
|
this.doublePressDistance = DOUBLE_PRESS_DISTANCE;
|
|
this.lastUp = null;
|
|
|
|
this.rotating = null;
|
|
this.sliding = null;
|
|
this.dualPress = false;
|
|
this.scroll = new THREE.Vector3();
|
|
|
|
this.rotateVelocity = new THREE.Vector3();
|
|
this.moveVelocity = new THREE.Vector3();
|
|
|
|
canvas.addEventListener("pointerdown", (event: PointerEvent) => {
|
|
const position = this.getPointerPosition(event);
|
|
const initial = position.clone();
|
|
const last = position.clone();
|
|
|
|
// Determine if we're starting a rotation pointer action
|
|
const isRotate =
|
|
(!this.swapRotateSlide &&
|
|
!this.rotating &&
|
|
(event.pointerType !== "mouse" || event.button === 0)) ||
|
|
(this.swapRotateSlide &&
|
|
this.sliding &&
|
|
!this.rotating &&
|
|
(event.pointerType !== "mouse" || event.button === 1));
|
|
// const isRotate =
|
|
// !this.rotating && (event.pointerType !== "mouse" || event.button === 0);
|
|
const { pointerId, timeStamp } = event;
|
|
|
|
if (isRotate) {
|
|
this.rotating = { initial, last, position, pointerId, timeStamp };
|
|
// Capture the pointer so events continue to be delivered even if it leaves the canvas.
|
|
canvas.setPointerCapture(event.pointerId);
|
|
|
|
this.dualPress = false;
|
|
} else if (!this.sliding) {
|
|
// If it's not a rotation action and we're not yet sliding, the next
|
|
// pointer activates a sliding action
|
|
const button = event.pointerType === "mouse" ? event.button : undefined;
|
|
this.sliding = {
|
|
initial,
|
|
last,
|
|
position,
|
|
pointerId,
|
|
button,
|
|
timeStamp,
|
|
};
|
|
// Capture the pointer so events continue to be delivered even if it leaves the canvas.
|
|
canvas.setPointerCapture(event.pointerId);
|
|
|
|
// Check if we pressed both pointers at roughly the same time
|
|
this.dualPress =
|
|
this.rotating != null &&
|
|
timeStamp - this.rotating.timeStamp < DUAL_PRESS_MS;
|
|
}
|
|
});
|
|
|
|
const pointerUp = (event: PointerEvent) => {
|
|
if (this.rotating?.pointerId === event.pointerId) {
|
|
this.rotating = null;
|
|
canvas.releasePointerCapture(event.pointerId);
|
|
if (this.dualPress && this.sliding) {
|
|
canvas.releasePointerCapture(this.sliding.pointerId);
|
|
this.sliding = null;
|
|
}
|
|
} else if (this.sliding?.pointerId === event.pointerId) {
|
|
this.sliding = null;
|
|
canvas.releasePointerCapture(event.pointerId);
|
|
if (this.dualPress && this.rotating) {
|
|
canvas.releasePointerCapture(this.rotating.pointerId);
|
|
this.rotating = null;
|
|
}
|
|
}
|
|
|
|
const position = this.getPointerPosition(event);
|
|
const lastUp = this.lastUp;
|
|
this.lastUp = { position, time: event.timeStamp };
|
|
if (lastUp) {
|
|
const distance = lastUp.position.distanceTo(position);
|
|
if (distance < this.doublePressDistance) {
|
|
const intervalMs = event.timeStamp - lastUp.time;
|
|
if (intervalMs < this.doublePressLimitMs) {
|
|
// We pressed and release twice within the time and distance limits
|
|
this.lastUp = null;
|
|
this.doublePress({ position, intervalMs });
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
document.addEventListener("pointerup", pointerUp);
|
|
document.addEventListener("pointercancel", pointerUp);
|
|
|
|
document.addEventListener("pointermove", (event: PointerEvent) => {
|
|
if (this.rotating?.pointerId === event.pointerId) {
|
|
this.rotating.position = this.getPointerPosition(event);
|
|
} else if (this.sliding?.pointerId === event.pointerId) {
|
|
this.sliding.position = this.getPointerPosition(event);
|
|
}
|
|
});
|
|
|
|
canvas.addEventListener("contextmenu", (event: MouseEvent) => {
|
|
// Prevent context menu appearing on right click
|
|
event.preventDefault();
|
|
});
|
|
|
|
canvas.addEventListener("wheel", (event: WheelEvent) => {
|
|
this.scroll.add(
|
|
new THREE.Vector3(event.deltaX, event.deltaY, event.deltaZ),
|
|
);
|
|
event.preventDefault();
|
|
});
|
|
}
|
|
|
|
getPointerPosition(event: PointerEvent): THREE.Vector2 {
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
return new THREE.Vector2(
|
|
event.clientX - rect.left,
|
|
event.clientY - rect.top,
|
|
);
|
|
}
|
|
|
|
update(deltaTime: number, control: THREE.Object3D) {
|
|
if (!this.enable) {
|
|
return;
|
|
}
|
|
|
|
if (this.dualPress && this.rotating && this.sliding) {
|
|
// We pressed both pointers at the same time, either pinching or sliding
|
|
const motion = [
|
|
this.rotating.position.clone().sub(this.rotating.last),
|
|
this.sliding.position.clone().sub(this.sliding.last),
|
|
];
|
|
const coincidence = motion[0].dot(motion[1]);
|
|
|
|
if (coincidence >= 0.2) {
|
|
// Similar directions so slide the camera on the XY plane
|
|
const totalMotion = motion[0].clone().add(motion[1]);
|
|
const slide = new THREE.Vector3(totalMotion.x, -totalMotion.y, 0);
|
|
slide.multiplyScalar(this.slideSpeed * (this.reverseSwipe ? 1 : -1));
|
|
slide.applyQuaternion(control.quaternion);
|
|
control.position.add(slide);
|
|
this.moveVelocity = slide.clone().multiplyScalar(1 / deltaTime);
|
|
} else if (coincidence <= -0.2) {
|
|
// Opposite directions so either pinch or roll motion
|
|
const deltaDir = this.sliding.last.clone().sub(this.rotating.last);
|
|
const deltaDist = deltaDir.length();
|
|
deltaDir.multiplyScalar(1 / deltaDist).normalize();
|
|
|
|
const orthoDir = new THREE.Vector2(-deltaDir.y, deltaDir.x);
|
|
const motionDir = [motion[0].dot(deltaDir), motion[1].dot(deltaDir)];
|
|
const motionOrtho = [motion[0].dot(orthoDir), motion[1].dot(orthoDir)];
|
|
|
|
// Pinching motion
|
|
const midpoint = this.rotating.last
|
|
.clone()
|
|
.add(this.sliding.last)
|
|
.multiplyScalar(0.5);
|
|
let midpointDir = new THREE.Vector3();
|
|
if (control instanceof THREE.Camera) {
|
|
const ndcMidpoint = new THREE.Vector2(
|
|
(midpoint.x / this.canvas.clientWidth) * 2 - 1,
|
|
-(midpoint.y / this.canvas.clientHeight) * 2 + 1,
|
|
);
|
|
const raycaster = new THREE.Raycaster();
|
|
raycaster.setFromCamera(ndcMidpoint, control);
|
|
midpointDir = raycaster.ray.direction;
|
|
}
|
|
const pinchOut = motionDir[1] - motionDir[0];
|
|
const slide = midpointDir.multiplyScalar(pinchOut * this.slideSpeed);
|
|
control.position.add(slide);
|
|
this.moveVelocity = slide.clone().multiplyScalar(1 / deltaTime);
|
|
|
|
// Rolling motion
|
|
// Calculate angle of orthogonal motion change over distance deltaDist/2
|
|
// motionOrtho[0] and 1 are already in float distance
|
|
const angles = [
|
|
Math.atan(motionOrtho[0] / (-0.5 * deltaDist)),
|
|
Math.atan(motionOrtho[1] / (0.5 * deltaDist)),
|
|
];
|
|
const rotate = 0.5 * (angles[0] + angles[1]);
|
|
const eulers = new THREE.Euler().setFromQuaternion(
|
|
control.quaternion,
|
|
"YXZ",
|
|
);
|
|
eulers.z = Math.max(
|
|
-Math.PI,
|
|
Math.min(Math.PI, eulers.z + 0.5 * rotate),
|
|
);
|
|
control.quaternion.setFromEuler(eulers);
|
|
}
|
|
|
|
this.rotating.last.copy(this.rotating.position);
|
|
this.sliding.last.copy(this.sliding.position);
|
|
} else {
|
|
// Didn't press both pointers at the same time, so we're in rotating
|
|
// or FPS mode
|
|
const rotate = new THREE.Vector3();
|
|
if (this.rotating && !this.dualPress) {
|
|
const delta = this.rotating.position.clone().sub(this.rotating.last);
|
|
this.rotating.last.copy(this.rotating.position);
|
|
rotate.set(delta.x, delta.y, 0);
|
|
rotate.multiplyScalar(this.rotateSpeed * (this.reverseRotate ? -1 : 1));
|
|
// Update rotation velocity from last delta
|
|
this.rotateVelocity = rotate.clone().multiplyScalar(1 / deltaTime);
|
|
} else {
|
|
// Continue to rotate with inertia
|
|
this.rotateVelocity.multiplyScalar(
|
|
Math.exp(-deltaTime / this.rotateInertia),
|
|
);
|
|
rotate.addScaledVector(this.rotateVelocity, deltaTime);
|
|
}
|
|
|
|
// Apply rotation in Euler angles space
|
|
const eulers = new THREE.Euler().setFromQuaternion(
|
|
control.quaternion,
|
|
"YXZ",
|
|
);
|
|
eulers.y -= rotate.x;
|
|
eulers.x = Math.max(
|
|
-Math.PI / 2,
|
|
Math.min(Math.PI / 2, eulers.x - rotate.y),
|
|
);
|
|
eulers.z *= Math.exp(-DEFAULT_ROLL_SPRING * deltaTime);
|
|
control.quaternion.setFromEuler(eulers);
|
|
|
|
if (this.sliding && !this.dualPress) {
|
|
const delta = this.sliding.position.clone().sub(this.sliding.last);
|
|
this.sliding.last.copy(this.sliding.position);
|
|
|
|
// Slide on plane depending on center/right mouse button
|
|
const slide =
|
|
this.sliding.button !== 2
|
|
? new THREE.Vector3(delta.x, 0, delta.y)
|
|
: new THREE.Vector3(delta.x, -delta.y, 0);
|
|
slide.multiplyScalar(this.slideSpeed * (this.reverseSlide ? -1 : 1));
|
|
|
|
slide.applyQuaternion(control.quaternion);
|
|
control.position.add(slide);
|
|
// Update movement velocity from last delta
|
|
this.moveVelocity = slide.clone().multiplyScalar(1 / deltaTime);
|
|
} else {
|
|
// Continue to move with inertia
|
|
this.moveVelocity.multiplyScalar(
|
|
Math.exp(-deltaTime / this.moveInertia),
|
|
);
|
|
control.position.addScaledVector(this.moveVelocity, deltaTime);
|
|
}
|
|
}
|
|
|
|
const scroll = this.scroll.multiplyScalar(this.scrollSpeed);
|
|
scroll.set(scroll.x, scroll.z, scroll.y);
|
|
if (this.reverseScroll) {
|
|
scroll.multiplyScalar(-1);
|
|
}
|
|
scroll.applyQuaternion(control.quaternion);
|
|
control.position.add(scroll);
|
|
this.scroll.set(0, 0, 0);
|
|
}
|
|
}
|