@wonderlandengine/components
Version:
Wonderland Engine's official component library.
292 lines • 12.4 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Component } from '@wonderlandengine/api';
import { property } from '@wonderlandengine/api/decorators.js';
import { deg2rad, rad2deg } from './utils/utils.js';
import { quat, vec3 } from 'gl-matrix';
const preventDefault = (e) => {
e.preventDefault();
};
const tempVec = [0, 0, 0];
const tempquat = quat.create();
const tempquat2 = quat.create();
const tempVec3 = vec3.create();
/**
* OrbitalCamera component allows the user to orbit around a target point, which
* is the position of the object itself. It rotates at the specified distance.
*
* @remarks
* The component works using mouse or touch. Therefor it does not work in VR.
*/
class OrbitalCamera extends Component {
static TypeName = 'orbital-camera';
mouseButtonIndex = 0;
radial = 5;
minElevation = 0.0;
maxElevation = 89.99;
minZoom = 0.01;
maxZoom = 10.0;
xSensitivity = 0.5;
ySensitivity = 0.5;
zoomSensitivity = 0.02;
damping = 0.9;
_mouseDown = false;
_origin = [0, 0, 0];
_azimuth = 0;
_polar = 45;
_initialPinchDistance = 0;
_touchStartX = 0;
_touchStartY = 0;
_azimuthSpeed = 0;
_polarSpeed = 0;
init() {
this.object.getPositionWorld(this._origin);
}
start() {
this._polar = Math.min(this.maxElevation, Math.max(this.minElevation, this._polar));
this._updateCamera();
}
onActivate() {
const canvas = this.engine.canvas;
if (this.mouseButtonIndex === 2) {
canvas.addEventListener('contextmenu', preventDefault, { passive: false });
}
canvas.addEventListener('mousedown', this._onMouseDown);
canvas.addEventListener('wheel', this._onMouseScroll, { passive: false });
canvas.addEventListener('touchstart', this._onTouchStart, { passive: false });
canvas.addEventListener('touchmove', this._onTouchMove, { passive: false });
canvas.addEventListener('touchend', this._onTouchEnd);
}
onDeactivate() {
const canvas = this.engine.canvas;
if (this.mouseButtonIndex === 2) {
canvas.removeEventListener('contextmenu', preventDefault);
}
canvas.removeEventListener('mousemove', this._onMouseMove);
canvas.removeEventListener('mousedown', this._onMouseDown);
canvas.removeEventListener('wheel', this._onMouseScroll);
canvas.removeEventListener('touchstart', this._onTouchStart);
canvas.removeEventListener('touchmove', this._onTouchMove);
canvas.removeEventListener('touchend', this._onTouchEnd);
/* Reset state to make sure nothing gets stuck */
this._mouseDown = false;
this._initialPinchDistance = 0;
this._touchStartX = 0;
this._touchStartY = 0;
this._azimuthSpeed = 0;
this._polarSpeed = 0;
}
update() {
/* Always apply damping, because there's no event for stop moving */
this._azimuthSpeed *= this.damping;
this._polarSpeed *= this.damping;
/* Stop completely if the speed is very low to avoid endless tiny movements */
if (Math.abs(this._azimuthSpeed) < 0.01)
this._azimuthSpeed = 0;
if (Math.abs(this._polarSpeed) < 0.01)
this._polarSpeed = 0;
/* Apply the speed to the camera angles */
this._azimuth += this._azimuthSpeed;
this._polar += this._polarSpeed;
/* Clamp the polar angle */
this._polar = Math.min(this.maxElevation, Math.max(this.minElevation, this._polar));
/* Update the camera if there's any speed */
if (this._azimuthSpeed !== 0 || this._polarSpeed !== 0) {
this._updateCamera();
}
}
/**
* Get the closest position to the given position on the orbit sphere of the camera.
* This can be used to get a position and rotation to transition to.
*
* You pass this a position object. The method calculates the closest positition and updates the position parameter.
* It also sets the rotation parameter to reflect the rotate the camera will have when it is on the orbit sphere,
* pointing towards the center.
* @param position the position to get the closest position to
* @param rotation the rotation to get the closest position to
*/
getClosestPosition(position, rotation) {
/* It's a bit hacky, but the easiest way to get the rotation of the camera is just briefly
change the rotation to look at the center and then get the rotation. */
this.object.getRotationWorld(tempquat);
this.object.lookAt(this._origin);
this.object.getRotationWorld(tempquat2);
if (quat.dot(tempquat, tempquat2) < 0) {
quat.scale(tempquat2, tempquat2, -1); /* Negate to ensure shortest path */
}
this.object.setRotationWorld(tempquat);
/* Calculate the direction from the center of orbit to the current camera position */
const directionToCamera = vec3.create();
vec3.subtract(directionToCamera, position, this._origin);
vec3.normalize(directionToCamera, directionToCamera);
/* Scale this direction by the radius of your orbital sphere to get the nearest point on the sphere */
const nearestPointOnSphere = vec3.create();
vec3.scale(nearestPointOnSphere, directionToCamera, this.radial);
vec3.add(nearestPointOnSphere, nearestPointOnSphere, this._origin);
vec3.copy(position, nearestPointOnSphere);
quat.copy(rotation, tempquat2);
}
/**
* Set the camera position based on the given position and calculate the rotation.
* @param cameraPosition the position to set the camera to
*/
setPosition(cameraPosition) {
const centerOfOrbit = this._origin;
/* Compute the direction vector */
vec3.subtract(tempVec3, cameraPosition, centerOfOrbit);
vec3.normalize(tempVec3, tempVec3);
/* Compute the azimuth angle (in radians) */
const azimuth = Math.atan2(tempVec3[0], tempVec3[2]);
/* Compute the polar angle (in radians) */
const polar = Math.acos(tempVec3[1]);
const azimuthDeg = rad2deg(azimuth);
/* Polar is inverted to match the orbital camera */
const polarDeg = 90 - rad2deg(polar);
this._azimuth = azimuthDeg;
this._polar = polarDeg;
}
/**
* Update the camera position based on the current azimuth,
* polar and radial values
*/
_updateCamera() {
const azimuthInRadians = deg2rad(this._azimuth);
const polarInRadians = deg2rad(this._polar);
tempVec[0] = this.radial * Math.sin(azimuthInRadians) * Math.cos(polarInRadians);
tempVec[1] = this.radial * Math.sin(polarInRadians);
tempVec[2] = this.radial * Math.cos(azimuthInRadians) * Math.cos(polarInRadians);
this.object.setPositionWorld(tempVec);
this.object.translateWorld(this._origin);
this.object.lookAt(this._origin);
}
/* Mouse Event Handlers */
_onMouseDown = (e) => {
window.addEventListener('mouseup', this._onMouseUp);
window.addEventListener('mousemove', this._onMouseMove);
if (e.button === this.mouseButtonIndex) {
this._mouseDown = true;
document.body.style.cursor = 'grabbing';
if (e.button === 1) {
e.preventDefault(); /* to prevent scrolling */
return false;
}
}
};
_onMouseUp = (e) => {
window.removeEventListener('mouseup', this._onMouseUp);
window.removeEventListener('mousemove', this._onMouseMove);
if (e.button === this.mouseButtonIndex) {
this._mouseDown = false;
document.body.style.cursor = 'initial';
}
};
_onMouseMove = (e) => {
if (this.active && this._mouseDown) {
if (this.active && this._mouseDown) {
this._azimuthSpeed = -(e.movementX * this.xSensitivity);
this._polarSpeed = e.movementY * this.ySensitivity;
}
}
};
_onMouseScroll = (e) => {
e.preventDefault(); /* to prevent scrolling */
this.radial *= 1 - e.deltaY * this.zoomSensitivity * -0.001;
this.radial = Math.min(this.maxZoom, Math.max(this.minZoom, this.radial));
this._updateCamera();
};
/* Touch event handlers */
_onTouchStart = (e) => {
if (e.touches.length === 1) {
/* to prevent scrolling and allow us to track touch movement */
e.preventDefault();
this._touchStartX = e.touches[0].clientX;
this._touchStartY = e.touches[0].clientY;
this._mouseDown = true; /* Treat touch like mouse down */
}
else if (e.touches.length === 2) {
/* Calculate initial pinch distance */
this._initialPinchDistance = this._getDistanceBetweenTouches(e.touches);
e.preventDefault(); /* Prevent default pinch actions */
}
};
_onTouchMove = (e) => {
if (!this.active || !this._mouseDown) {
return;
}
e.preventDefault(); /* to prevent moving the page */
if (e.touches.length === 1) {
const deltaX = e.touches[0].clientX - this._touchStartX;
const deltaY = e.touches[0].clientY - this._touchStartY;
this._azimuthSpeed = -(deltaX * this.xSensitivity);
this._polarSpeed = deltaY * this.ySensitivity;
this._touchStartX = e.touches[0].clientX;
this._touchStartY = e.touches[0].clientY;
}
else if (e.touches.length === 2) {
/* Handle pinch zoom */
const currentPinchDistance = this._getDistanceBetweenTouches(e.touches);
const pinchScale = this._initialPinchDistance / currentPinchDistance;
this.radial *= pinchScale;
this.radial = Math.min(this.maxZoom, Math.max(this.minZoom, this.radial));
this._updateCamera();
/* Update initial pinch distance for next move */
this._initialPinchDistance = currentPinchDistance;
}
};
_onTouchEnd = (e) => {
if (e.touches.length < 2) {
this._mouseDown = false; /* Treat touch end like mouse up */
}
if (e.touches.length === 1) {
/* Prepare for possible single touch movement */
this._touchStartX = e.touches[0].clientX;
this._touchStartY = e.touches[0].clientY;
}
};
/**
* Helper function to calculate the distance between two touch points
* @param touches list of touch points
* @returns distance between the two touch points
*/
_getDistanceBetweenTouches(touches) {
const dx = touches[0].clientX - touches[1].clientX;
const dy = touches[0].clientY - touches[1].clientY;
return Math.sqrt(dx * dx + dy * dy);
}
}
__decorate([
property.int()
], OrbitalCamera.prototype, "mouseButtonIndex", void 0);
__decorate([
property.float(5)
], OrbitalCamera.prototype, "radial", void 0);
__decorate([
property.float()
], OrbitalCamera.prototype, "minElevation", void 0);
__decorate([
property.float(89.99)
], OrbitalCamera.prototype, "maxElevation", void 0);
__decorate([
property.float()
], OrbitalCamera.prototype, "minZoom", void 0);
__decorate([
property.float(10.0)
], OrbitalCamera.prototype, "maxZoom", void 0);
__decorate([
property.float(0.5)
], OrbitalCamera.prototype, "xSensitivity", void 0);
__decorate([
property.float(0.5)
], OrbitalCamera.prototype, "ySensitivity", void 0);
__decorate([
property.float(0.02)
], OrbitalCamera.prototype, "zoomSensitivity", void 0);
__decorate([
property.float(0.9)
], OrbitalCamera.prototype, "damping", void 0);
export { OrbitalCamera };
//# sourceMappingURL=orbital-camera.js.map