UNPKG

@wonderlandengine/components

Version:

Wonderland Engine's official component library.

292 lines 12.4 kB
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