playcanvas
Version:
PlayCanvas WebGL game engine
1,163 lines (1,024 loc) • 29.7 kB
JavaScript
import { Vec2, Vec3, Ray, Plane, Mat4, Quat, Script, math } from 'playcanvas';
/** @import { AppBase, Entity, CameraComponent } from 'playcanvas' */
/**
* @typedef {object} ScriptArgs
* @property {AppBase} app - The app.
* @property {Entity} entity - The entity.
* @property {boolean} [enabled] - The enabled state.
* @property {object} [attributes] - The attributes.
*/
const tmpVa = new Vec2();
const tmpV1 = new Vec3();
const tmpV2 = new Vec3();
const tmpM1 = new Mat4();
const tmpQ1 = new Quat();
const tmpR1 = new Ray();
const tmpP1 = new Plane();
/** @type {AddEventListenerOptions & EventListenerOptions} */
const PASSIVE = { passive: false };
const ZOOM_SCALE_SCENE_MULT = 10;
const EPSILON = 0.0001;
/**
* Calculate the lerp rate.
*
* @param {number} damping - The damping.
* @param {number} dt - The delta time.
* @returns {number} - The lerp rate.
*/
const lerpRate = (damping, dt) => 1 - Math.pow(damping, dt * 1000);
class CameraControls extends Script {
/**
* Fired to clamp the position (Vec3).
*
* @event
* @example
* cameraControls.on('clamp:position', (position) => {
* position.y = Math.max(0, position.y);
* });
*/
static EVENT_CLAMP_POSITION = 'clamp:position';
/**
* Fired to clamp the angles (Vec2).
*
* @event
* @example
* cameraControls.on('clamp:angles', (angles) => {
* angles.x = Math.max(-90, Math.min(90, angles.x));
* });
*/
static EVENT_CLAMP_ANGLES = 'clamp:angles';
/**
* @private
* @type {CameraComponent | null}
*/
_camera = null;
/**
* @private
* @type {Vec3}
*/
_origin = new Vec3();
/**
* @private
* @type {Vec3}
*/
_position = new Vec3();
/**
* @private
* @type {Vec2}
*/
_dir = new Vec2();
/**
* @private
* @type {Vec3}
*/
_angles = new Vec3();
/**
* @private
* @type {Vec2}
*/
_pitchRange = new Vec2(-360, 360);
/**
* @private
* @type {number}
*/
_zoomMin = 0;
/**
* @private
* @type {number}
*/
_zoomMax = 0;
/**
* @type {number}
* @private
*/
_zoomDist = 0;
/**
* @type {number}
* @private
*/
_cameraDist = 0;
/**
* @type {Map<number, PointerEvent>}
* @private
*/
_pointerEvents = new Map();
/**
* @type {number}
* @private
*/
_lastPinchDist = -1;
/**
* @type {Vec2}
* @private
*/
_lastPosition = new Vec2();
/**
* @type {boolean}
* @private
*/
_dragging = false;
/**
* @type {boolean}
* @private
*/
_orbiting = true;
/**
* @type {boolean}
* @private
*/
_panning = false;
/**
* @type {boolean}
* @private
*/
_flying = false;
/**
* @type {boolean}
* @private
*/
_moving = false;
/**
* @type {boolean}
* @private
*/
_focusing = false;
/**
* @type {Record<string, boolean>}
* @private
*/
_key = {
forward: false,
backward: false,
left: false,
right: false,
up: false,
down: false,
sprint: false,
crouch: false
};
/**
* @type {HTMLElement}
* @private
*/
_element = this.app.graphicsDevice.canvas;
/**
* @type {Mat4}
* @private
*/
_cameraTransform = new Mat4();
/**
* @type {Mat4}
* @private
*/
_baseTransform = new Mat4();
/**
* @attribute
* @title Scene Size
* @description The scene size. The zoom, pan and fly speeds are relative to this size.
* @type {number}
*/
sceneSize = 100;
/**
* Enable orbit camera controls.
*
* @attribute
* @title Enable Orbit
* @description Enable orbit camera controls.
* @type {boolean}
*/
enableOrbit = true;
/**
* @attribute
* @title Enable Pan
* @description Enable pan camera controls.
* @type {boolean}
*/
enablePan = true;
/**
* @attribute
* @title Enable Fly
* @description Enable fly camera controls.
* @type {boolean}
*/
enableFly = true;
/**
* @attribute
* @title Focus Damping
* @description The damping applied when calling {@link CameraControls#focus}. A higher value means
* more damping. A value of 0 means no damping.
* @type {number}
*/
focusDamping = 0.98;
/**
* @attribute
* @title Rotate Speed
* @description The rotation speed.
* @enabledif {enableOrbit}
* @type {number}
*/
rotateSpeed = 0.2;
/**
* @attribute
* @title Rotate Damping
* @description The rotation damping. A higher value means more damping. A value of 0 means no damping.
* @enabledif {enableOrbit}
* @type {number}
*/
rotateDamping = 0.98;
/**
* @attribute
* @title Move Speed
* @description The fly move speed relative to the scene size.
* @enabledif {enableFly || enablePan}
* @type {number}
*/
moveSpeed = 2;
/**
* @attribute
* @title Move Fast Speed
* @description The fast fly move speed relative to the scene size.
* @enabledif {enableFly || enablePan}
* @type {number}
*/
moveFastSpeed = 4;
/**
* @attribute
* @title Move Slow Speed
* @description The slow fly move speed relative to the scene size.
* @enabledif {enableFly || enablePan}
* @type {number}
*/
moveSlowSpeed = 1;
/**
* @attribute
* @title Move Damping
* @description The movement damping. A higher value means more damping. A value of 0 means no damping.
* @enabledif {enableFly || enablePan}
* @type {number}
*/
moveDamping = 0.98;
/**
* @attribute
* @title Zoom Speed
* @description The zoom speed relative to the scene size.
* @type {number}
*/
zoomSpeed = 0.005;
/**
* @attribute
* @title Zoom Pinch Sensitivity
* @description The touch zoom pinch sensitivity.
* @type {number}
*/
zoomPinchSens = 5;
/**
* @attribute
* @title Zoom Damping
* @description The zoom damping. A higher value means more damping. A value of 0 means no damping.
* @type {number}
*/
zoomDamping = 0.98;
/**
* @attribute
* @title Zoom Scale Min
* @description The minimum scale the camera can zoom (absolute value).
* @type {number}
*/
zoomScaleMin = 0;
initialize() {
this._onWheel = this._onWheel.bind(this);
this._onKeyDown = this._onKeyDown.bind(this);
this._onKeyUp = this._onKeyUp.bind(this);
this._onPointerDown = this._onPointerDown.bind(this);
this._onPointerMove = this._onPointerMove.bind(this);
this._onPointerUp = this._onPointerUp.bind(this);
this._onContextMenu = this._onContextMenu.bind(this);
if (!this.entity.camera) {
throw new Error('CameraControls script requires a camera component');
}
this.attach(this.entity.camera);
this.focusPoint = this._origin ?? this.focusPoint;
this.pitchRange = this._pitchRange ?? this.pitchRange;
this.zoomMin = this._zoomMin ?? this.zoomMin;
this.zoomMax = this._zoomMax ?? this.zoomMax;
this.on('destroy', this.destroy, this);
}
/**
* The element to attach the camera controls to.
*
* @type {HTMLElement}
*/
set element(value) {
this._element = value;
const camera = this._camera;
this.detach();
if (camera) {
this.attach(camera);
}
}
get element() {
return this._element;
}
/**
* @attribute
* @title Focus Point
* @description The camera's focus point.
* @type {Vec3}
* @default [0, 0, 0]
*/
set focusPoint(point) {
if (!this._camera) {
if (point instanceof Vec3) {
this._origin.copy(point);
}
return;
}
this.focus(point, this.entity.getPosition(), false);
}
get focusPoint() {
return this._origin;
}
/**
* @attribute
* @title Pitch Range
* @description The camera's pitch range. Having a value of -360 means no minimum pitch and 360
* means no maximum pitch.
* @type {Vec2}
* @default [-360, 360]
*/
set pitchRange(value) {
if (!(value instanceof Vec2)) {
return;
}
this._pitchRange.copy(value);
this._clampAngles(this._dir);
this._smoothTransform(-1);
}
get pitchRange() {
return this._pitchRange;
}
/**
* @attribute
* @title Zoom Min
* @description The minimum zoom distance relative to the scene size.
* @type {number}
* @default 0
*/
set zoomMin(value) {
this._zoomMin = value ?? this._zoomMin;
this._zoomDist = this._clampZoom(this._zoomDist);
this._smoothZoom(-1);
}
get zoomMin() {
return this._zoomMin;
}
/**
* @attribute
* @title Zoom Max
* @description The maximum zoom distance relative to the scene size. Having a value less than
* or equal to zoomMin means no maximum zoom.
* @type {number}
* @default 0
*/
set zoomMax(value) {
this._zoomMax = value ?? this._zoomMax;
this._zoomDist = this._clampZoom(this._zoomDist);
this._smoothZoom(-1);
}
get zoomMax() {
return this._zoomMax;
}
/**
* @param {Vec3} out - The output vector.
* @returns {Vec3} - The focus vector.
*/
_focusDir(out) {
return out.copy(this.entity.forward).mulScalar(this._zoomDist);
}
/**
* @private
* @param {Vec2} angles - The value to clamp.
*/
_clampAngles(angles) {
const min = this._pitchRange.x === -360 ? -Infinity : this._pitchRange.x;
const max = this._pitchRange.y === 360 ? Infinity : this._pitchRange.y;
angles.x = math.clamp(angles.x, min, max);
// emit clamp event
this.fire(CameraControls.EVENT_CLAMP_ANGLES, angles);
}
/**
* @private
* @param {Vec3} position - The position to clamp.
*/
_clampPosition(position) {
if (this._flying) {
tmpV1.set(0, 0, 0);
} else {
this._focusDir(tmpV1);
}
// emit clamp event
position.sub(tmpV1);
this.fire(CameraControls.EVENT_CLAMP_POSITION, position);
position.add(tmpV1);
}
/**
* @private
* @param {number} value - The value to clamp.
* @returns {number} - The clamped value.
*/
_clampZoom(value) {
const min = (this._camera?.nearClip ?? 0) + this.zoomMin * this.sceneSize;
const max = this.zoomMax <= this.zoomMin ? Infinity : this.zoomMax * this.sceneSize;
return math.clamp(value, min, max);
}
/**
* @private
* @param {MouseEvent} event - The mouse event.
*/
_onContextMenu(event) {
event.preventDefault();
}
/**
* @private
* @param {PointerEvent} event - The pointer event.
* @returns {boolean} Whether the mouse pan should start.
*/
_isStartMousePan(event) {
if (!this.enablePan) {
return false;
}
if (event.shiftKey) {
return true;
}
if (!this.enableOrbit && !this.enableFly) {
return event.button === 0 || event.button === 1 || event.button === 2;
}
if (!this.enableOrbit || !this.enableFly) {
return event.button === 1 || event.button === 2;
}
return event.button === 1;
}
/**
* @private
* @param {PointerEvent} event - The pointer event.
* @returns {boolean} Whether the fly should start.
*/
_isStartFly(event) {
if (!this.enableFly) {
return false;
}
if (!this.enableOrbit && !this.enablePan) {
return event.button === 0 || event.button === 1 || event.button === 2;
}
if (!this.enableOrbit) {
return event.button === 0;
}
return event.button === 2;
}
/**
* @param {PointerEvent} event - The pointer event.
* @returns {boolean} Whether the orbit should start.
* @private
*/
_isStartOrbit(event) {
if (!this.enableOrbit) {
return false;
}
if (!this.enableFly && !this.enablePan) {
return event.button === 0 || event.button === 1 || event.button === 2;
}
return event.button === 0;
}
/**
* @private
* @returns {boolean} Whether the switch to orbit was successful.
*/
_switchToOrbit() {
if (!this.enableOrbit) {
return false;
}
if (this._flying) {
this._flying = false;
this._focusDir(tmpV1);
this._origin.add(tmpV1);
this._position.add(tmpV1);
}
this._orbiting = true;
return true;
}
/**
* @private
* @returns {boolean} Whether the switch to fly was successful.
*/
_switchToFly() {
if (!this.enableFly) {
return false;
}
if (this._orbiting) {
this._orbiting = false;
this._zoomDist = this._cameraDist;
this._origin.copy(this.entity.getPosition());
this._position.copy(this._origin);
this._cameraTransform.setTranslate(0, 0, 0);
}
this._flying = true;
return true;
}
/**
* @private
* @param {PointerEvent} event - The pointer event.
*/
_onPointerDown(event) {
if (!this._camera) {
return;
}
this._element.setPointerCapture(event.pointerId);
this._pointerEvents.set(event.pointerId, event);
const startTouchPan = this.enablePan && this._pointerEvents.size === 2;
const startMousePan = this._isStartMousePan(event);
const startFly = this._isStartFly(event);
const startOrbit = this._isStartOrbit(event);
if (this._focusing) {
this._cancelSmoothTransform();
this._focusing = false;
}
if (startTouchPan) {
// start touch pan
this._lastPinchDist = this._getPinchDist();
this._getMidPoint(this._lastPosition);
this._panning = true;
}
if (startMousePan) {
// start mouse pan
this._lastPosition.set(event.clientX, event.clientY);
this._panning = true;
this._dragging = true;
}
if (startFly) {
// start fly
this._switchToFly();
this._dragging = true;
}
if (startOrbit) {
// start orbit
this._switchToOrbit();
this._dragging = true;
}
}
/**
* @private
* @param {PointerEvent} event - The pointer event.
*/
_onPointerMove(event) {
if (this._pointerEvents.size === 0) {
return;
}
this._pointerEvents.set(event.pointerId, event);
if (this._focusing) {
this._cancelSmoothTransform();
this._focusing = false;
}
if (this._pointerEvents.size === 1) {
if (this._panning) {
// mouse pan
this._pan(tmpVa.set(event.clientX, event.clientY));
} else if (this._orbiting || this._flying) {
this._look(event);
}
return;
}
if (this._pointerEvents.size === 2) {
// touch pan
if (this._panning) {
this._pan(this._getMidPoint(tmpVa));
}
// pinch zoom
const pinchDist = this._getPinchDist();
if (this._lastPinchDist > 0) {
this._zoom((this._lastPinchDist - pinchDist) * this.zoomPinchSens);
}
this._lastPinchDist = pinchDist;
}
}
/**
* @private
* @param {PointerEvent} event - The pointer event.
*/
_onPointerUp(event) {
this._element.releasePointerCapture(event.pointerId);
this._pointerEvents.delete(event.pointerId);
if (this._pointerEvents.size < 2) {
this._lastPinchDist = -1;
this._panning = false;
}
if (this._panning) {
this._panning = false;
}
if (this._dragging) {
this._dragging = false;
}
}
/**
* @private
* @param {WheelEvent} event - The wheel event.
*/
_onWheel(event) {
event.preventDefault();
this._zoom(event.deltaY);
}
/**
* @private
* @param {KeyboardEvent} event - The keyboard event.
*/
_onKeyDown(event) {
event.stopPropagation();
switch (event.key.toLowerCase()) {
case 'w':
case 'arrowup':
this._key.forward = true;
break;
case 's':
case 'arrowdown':
this._key.backward = true;
break;
case 'a':
case 'arrowleft':
this._key.left = true;
break;
case 'd':
case 'arrowright':
this._key.right = true;
break;
case 'q':
this._key.up = true;
break;
case 'e':
this._key.down = true;
break;
case 'shift':
this._key.sprint = true;
break;
case 'control':
this._key.crouch = true;
break;
}
}
/**
* @private
* @param {KeyboardEvent} event - The keyboard event.
*/
_onKeyUp(event) {
event.stopPropagation();
switch (event.key.toLowerCase()) {
case 'w':
case 'arrowup':
this._key.forward = false;
break;
case 's':
case 'arrowdown':
this._key.backward = false;
break;
case 'a':
case 'arrowleft':
this._key.left = false;
break;
case 'd':
case 'arrowright':
this._key.right = false;
break;
case 'q':
this._key.up = false;
break;
case 'e':
this._key.down = false;
break;
case 'shift':
this._key.sprint = false;
break;
case 'control':
this._key.crouch = false;
break;
}
}
/**
* @private
* @param {PointerEvent} event - The pointer event.
*/
_look(event) {
if (event.target !== this.app.graphicsDevice.canvas) {
return;
}
const movementX = event.movementX || 0;
const movementY = event.movementY || 0;
this._dir.x -= movementY * this.rotateSpeed;
this._dir.y -= movementX * this.rotateSpeed;
this._clampAngles(this._dir);
}
/**
* @param {number} dt - The delta time.
*/
_move(dt) {
if (!this.enableFly) {
return;
}
tmpV1.set(0, 0, 0);
if (this._key.forward) {
tmpV1.add(this.entity.forward);
}
if (this._key.backward) {
tmpV1.sub(this.entity.forward);
}
if (this._key.left) {
tmpV1.sub(this.entity.right);
}
if (this._key.right) {
tmpV1.add(this.entity.right);
}
if (this._key.up) {
tmpV1.add(this.entity.up);
}
if (this._key.down) {
tmpV1.sub(this.entity.up);
}
tmpV1.normalize();
this._moving = tmpV1.length() > 0;
const speed = this._key.crouch ? this.moveSlowSpeed : this._key.sprint ? this.moveFastSpeed : this.moveSpeed;
tmpV1.mulScalar(this.sceneSize * speed * dt);
this._origin.add(tmpV1);
// clamp movement if locked
if (this._moving) {
if (this._focusing) {
this._cancelSmoothTransform();
this._focusing = false;
}
this._clampPosition(this._origin);
}
}
/**
* @private
* @param {Vec2} out - The output vector.
* @returns {Vec2} The mid point.
*/
_getMidPoint(out) {
const [a, b] = this._pointerEvents.values();
const dx = a.clientX - b.clientX;
const dy = a.clientY - b.clientY;
return out.set(b.clientX + dx * 0.5, b.clientY + dy * 0.5);
}
/**
* @private
* @returns {number} The pinch distance.
*/
_getPinchDist() {
const [a, b] = this._pointerEvents.values();
const dx = a.clientX - b.clientX;
const dy = a.clientY - b.clientY;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* @private
* @param {Vec2} pos - The screen position.
* @param {Vec3} point - The output point.
*/
_screenToWorldPan(pos, point) {
if (!this._camera) {
return;
}
const mouseW = this._camera.screenToWorld(pos.x, pos.y, 1);
const cameraPos = this.entity.getPosition();
const focusDir = this._focusDir(tmpV1);
const focalPos = tmpV2.add2(cameraPos, focusDir);
const planeNormal = focusDir.mulScalar(-1).normalize();
const plane = tmpP1.setFromPointNormal(focalPos, planeNormal);
const ray = tmpR1.set(cameraPos, mouseW.sub(cameraPos).normalize());
plane.intersectsRay(ray, point);
}
/**
* @private
* @param {Vec2} pos - The screen position.
*/
_pan(pos) {
if (!this.enablePan) {
return;
}
const start = new Vec3();
const end = new Vec3();
this._screenToWorldPan(this._lastPosition, start);
this._screenToWorldPan(pos, end);
tmpV1.sub2(start, end);
this._origin.add(tmpV1);
this._lastPosition.copy(pos);
}
/**
* @private
* @param {number} delta - The delta.
*/
_zoom(delta) {
if (!this.enableOrbit && !this.enablePan) {
return;
}
if (this._flying) {
if (this._dragging) {
return;
}
if (!this._switchToOrbit()) {
return;
}
}
if (!this._camera) {
return;
}
const distNormalized = this._zoomDist / (ZOOM_SCALE_SCENE_MULT * this.sceneSize);
const scale = math.clamp(distNormalized, this.zoomScaleMin, 1);
this._zoomDist += (delta * this.zoomSpeed * this.sceneSize * scale);
this._zoomDist = this._clampZoom(this._zoomDist);
}
/**
* @private
* @param {number} dt - The delta time.
*/
_smoothZoom(dt) {
const a = dt === -1 ? 1 : lerpRate(this.zoomDamping, dt);
this._cameraDist = math.lerp(this._cameraDist, this._zoomDist, a);
this._cameraTransform.setTranslate(0, 0, this._cameraDist);
}
/**
* @private
* @param {number} dt - The delta time.
*/
_smoothTransform(dt) {
const ar = dt === -1 ? 1 : lerpRate(this._focusing ? this.focusDamping : this.rotateDamping, dt);
const am = dt === -1 ? 1 : lerpRate(this._focusing ? this.focusDamping : this.moveDamping, dt);
this._angles.x = math.lerpAngle(this._angles.x % 360, this._dir.x % 360, ar);
this._angles.y = math.lerpAngle(this._angles.y % 360, this._dir.y % 360, ar);
this._position.lerp(this._position, this._origin, am);
this._baseTransform.setTRS(this._position, tmpQ1.setFromEulerAngles(this._angles), Vec3.ONE);
const focusDelta = this._position.distance(this._origin) +
Math.abs(this._angles.x - this._dir.x) +
Math.abs(this._angles.y - this._dir.y);
if (this._focusing && focusDelta < EPSILON) {
this._focusing = false;
}
}
/**
* @private
*/
_cancelSmoothZoom() {
this._cameraDist = this._zoomDist;
}
/**
* @private
*/
_cancelSmoothTransform() {
this._origin.copy(this._position);
this._dir.set(this._angles.x, this._angles.y);
}
/**
* @private
*/
_updateTransform() {
tmpM1.copy(this._baseTransform).mul(this._cameraTransform);
this.entity.setPosition(tmpM1.getTranslation());
this.entity.setEulerAngles(tmpM1.getEulerAngles());
}
/**
* Focus the camera on a point.
*
* @param {Vec3} point - The focus point.
* @param {Vec3} [start] - The camera start position.
* @param {boolean} [smooth] - Whether to smooth the focus.
*/
focus(point, start, smooth = true) {
if (!this._camera) {
return;
}
if (this._flying) {
if (this._dragging) {
return;
}
if (!this._switchToOrbit()) {
return;
}
}
if (start) {
tmpV1.sub2(start, point);
const elev = Math.atan2(tmpV1.y, Math.sqrt(tmpV1.x * tmpV1.x + tmpV1.z * tmpV1.z)) * math.RAD_TO_DEG;
const azim = Math.atan2(tmpV1.x, tmpV1.z) * math.RAD_TO_DEG;
this._clampAngles(this._dir.set(-elev, azim));
this._origin.copy(point);
this._cameraTransform.setTranslate(0, 0, 0);
const pos = this.entity.getPosition();
const rot = this.entity.getRotation();
this._baseTransform.setTRS(pos, rot, Vec3.ONE);
this._zoomDist = this._clampZoom(tmpV1.length());
if (!smooth) {
this._smoothZoom(-1);
this._smoothTransform(-1);
}
this._updateTransform();
} else {
this._origin.copy(point);
if (!smooth) {
this._position.copy(point);
}
}
if (smooth) {
this._focusing = true;
}
}
/**
* Reset the zoom. For orbit and panning only.
*
* @param {number} [zoomDist] - The zoom distance.
* @param {boolean} [smooth] - Whether to smooth the zoom.
*/
resetZoom(zoomDist = 0, smooth = true) {
this._zoomDist = zoomDist;
if (!smooth) {
this._cameraDist = zoomDist;
}
}
/**
* Refocus the camera.
*
* @param {Vec3} point - The point.
* @param {Vec3} [start] - The start.
* @param {number} [zoomDist] - The zoom distance.
* @param {boolean} [smooth] - Whether to smooth the refocus.
*/
refocus(point, start, zoomDist, smooth = true) {
if (typeof zoomDist === 'number') {
this.resetZoom(zoomDist, smooth);
}
this.focus(point, start, smooth);
}
/**
* @param {CameraComponent} camera - The camera component.
*/
attach(camera) {
if (this._camera === camera) {
return;
}
this._camera = camera;
// Attach events to canvas instead of window
this._element.addEventListener('wheel', this._onWheel, PASSIVE);
this._element.addEventListener('pointerdown', this._onPointerDown);
this._element.addEventListener('pointermove', this._onPointerMove);
this._element.addEventListener('pointerup', this._onPointerUp);
this._element.addEventListener('contextmenu', this._onContextMenu);
// These can stay on window since they're keyboard events
window.addEventListener('keydown', this._onKeyDown, false);
window.addEventListener('keyup', this._onKeyUp, false);
}
detach() {
if (!this._camera) {
return;
}
// Remove from canvas instead of window
this._element.removeEventListener('wheel', this._onWheel, PASSIVE);
this._element.removeEventListener('pointermove', this._onPointerMove);
this._element.removeEventListener('pointerdown', this._onPointerDown);
this._element.removeEventListener('pointerup', this._onPointerUp);
this._element.removeEventListener('contextmenu', this._onContextMenu);
// Remove keyboard events from window
window.removeEventListener('keydown', this._onKeyDown, false);
window.removeEventListener('keyup', this._onKeyUp, false);
this._camera = null;
this._cancelSmoothZoom();
this._cancelSmoothTransform();
this._pointerEvents.clear();
this._lastPinchDist = -1;
this._panning = false;
this._key = {
forward: false,
backward: false,
left: false,
right: false,
up: false,
down: false,
sprint: false,
crouch: false
};
}
/**
* @param {number} dt - The delta time.
*/
update(dt) {
if (this.app.xr?.active) {
return;
}
if (!this._camera) {
return;
}
this._move(dt);
if (!this._flying) {
this._smoothZoom(dt);
}
this._smoothTransform(dt);
this._updateTransform();
}
destroy() {
this.detach();
}
}
export { CameraControls };