Redo controls, try making them somewhat modular

This commit is contained in:
Blue (Lukas Rieger) 2021-02-17 15:48:27 +01:00
parent a4ee07aa5c
commit d16b71765f
No known key found for this signature in database
GPG Key ID: 904C4995F9E1F800
29 changed files with 2420 additions and 612 deletions

View File

@ -8,6 +8,7 @@
</head>
<body style="margin: 0; padding: 0;">
<div id="map-container" style="position: absolute; width: 100%; height: 100%;"></div>
<!--<div id="debug-div" style="position: fixed; top: 10px; left: 10px; background-color: #111; opacity: 0.8; color: white; padding: 10px"></div>-->
<script type="text/javascript" src="js/three.js"></script>
<script type="text/javascript" src="js/hammer.js"></script>
@ -19,6 +20,9 @@
// show stats panel
bluemap.stats.showPanel(1);
let mapControls = bluemap.controlsManager.controls;
let freeControls = new BlueMap.FreeFlightControls(bluemap.rootElement);
// load map
let maps = [];
let markerManager = null;

View File

@ -1,10 +1,11 @@
import {FileLoader, Object3D} from "three";
import {Map} from "./map/Map";
export { MapViewer } from "./MapViewer";
export * from "./util/Utils";
export { MarkerFileManager } from "./markers/MarkerFileManager";
export { PlayerMarkerManager } from "./markers/PlayerMarkerManager";
export * from "./MapViewer";
export * from "./markers/MarkerFileManager";
export * from "./markers/PlayerMarkerManager";
export * from "./controls/map/MapControls";
export * from "./controls/freeflight/FreeFlightControls";
/**
* Loads and returns a promise with an array of Maps loaded from that root-path.<br>

View File

@ -1,12 +1,8 @@
import {
PerspectiveCamera,
WebGLRenderer,
Vector2, Raycaster, Layers, Scene
} from "three";
import {Layers, PerspectiveCamera, Raycaster, Scene, Vector2, WebGLRenderer} from "three";
import {Map} from "./map/Map";
import {SkyboxScene} from "./skybox/SkyboxScene";
import {ControlsManager} from "./controls/ControlsManager";
import {MapControls} from "./controls/MapControls";
import {MapControls} from "./controls/map/MapControls";
import Stats from "./util/Stats";
import {alert, dispatchEvent, elementOffset, htmlToElement} from "./util/Utils";
import {TileManager} from "./map/TileManager";
@ -16,6 +12,7 @@ import {LOWRES_VERTEX_SHADER} from "./map/lowres/LowresVertexShader";
import {LOWRES_FRAGMENT_SHADER} from "./map/lowres/LowresFragmentShader";
import {CombinedCamera} from "./util/CombinedCamera";
import {CSS2DRenderer} from "./util/CSS2DRenderer";
import {FreeFlightControls} from "./controls/freeflight/FreeFlightControls";
export class MapViewer {
@ -84,7 +81,7 @@ export class MapViewer {
this.initializeHammer();
this.controlsManager = new ControlsManager(this, this.camera);
this.controlsManager.controls = new MapControls(this.rootElement, this.hammer, this.events);
this.controlsManager.controls = new MapControls(this.rootElement);
this.raycaster = new Raycaster();
this.raycaster.layers.enableAll();
@ -218,7 +215,7 @@ export class MapViewer {
requestAnimationFrame(this.renderLoop);
// calculate delta time
if (this.lastFrame <= 0) { this.lastFrame = now; }
if (this.lastFrame <= 0) this.lastFrame = now;
let delta = now - this.lastFrame;
this.lastFrame = now;
@ -228,7 +225,6 @@ export class MapViewer {
// update controls
if (this.map != null) {
this.controlsManager.update(delta, this.map);
this.controlsManager.updateCamera();
}
// render

View File

@ -6,7 +6,7 @@ export class ControlsManager {
/**
* @param mapViewer {MapViewer}
* @param camera {THREE.Camera}
* @param camera {CombinedCamera}
*/
constructor(mapViewer, camera) {
Object.defineProperty( this, 'isControlsManager', { value: true } );
@ -14,19 +14,31 @@ export class ControlsManager {
this.mapViewer = mapViewer;
this.camera = camera;
this.positionValue = new Vector3(0, 0, 0);
this.position = new Vector3(0, 0, 0);
this.rotation = 0;
this.angle = 0;
this.tilt = 0;
this.rotationValue = 0;
this.angleValue = 0;
this.lastPosition = this.position.clone();
this.lastRotation = this.rotation;
this.lastAngle = this.angle;
this.lastDistance = this.distance;
this.lastOrtho = this.ortho;
this.lastTilt = this.tilt;
this.distanceValue = 500;
this.lastMapUpdatePosition = this.position.clone();
this.orthoValue = 0;
this.averageDeltaTime = 16;
this.valueChanged = true;
this.lastMapUpdatePosition = this.positionValue.clone();
this._controls = null;
this.controlsValue = null;
// start
this.distance = 300;
this.position.set(0, 0, 0);
this.rotation = 0;
this.angle = 0;
this.tilt = 0;
this.ortho = 0;
this.updateCamera();
}
@ -37,48 +49,60 @@ export class ControlsManager {
*/
update(deltaTime, map) {
if (deltaTime > 50) deltaTime = 50; // assume min 20 UPS
this.averageDeltaTime = this.averageDeltaTime * 0.9 + deltaTime * 0.1; // average delta-time to avoid choppy controls on lag-spikes
if (this.controlsValue && typeof this.controlsValue.update === "function")
this.controlsValue.update(deltaTime, map);
if (this._controls) this._controls.update(this.averageDeltaTime, map);
this.updateCamera();
}
updateCamera() {
if (this.valueChanged) {
// prevent problems with the rotation when the angle is 0 (top-down) or distance is 0 (first-person)
let rotatableAngle = this.angleValue;
if (Math.abs(rotatableAngle) <= 0.0001) rotatableAngle = 0.0001;
let rotatableDistance = this.distanceValue;
if (Math.abs(rotatableDistance) <= 0.0001) rotatableDistance = -0.0001;
let valueChanged = this.isValueChanged();
// fix distance for ortho-effect
if (this.orthoValue > 0) {
rotatableDistance = MathUtils.lerp(rotatableDistance, Math.max(rotatableDistance, 300), Math.pow(this.orthoValue, 8));
if (valueChanged) {
this.resetValueChanged();
// wrap rotation
while (this.rotation >= Math.PI) this.rotation -= Math.PI * 2;
while (this.rotation <= -Math.PI) this.rotation += Math.PI * 2;
// prevent problems with the rotation when the angle is 0 (top-down) or distance is 0 (first-person)
let rotatableAngle = this.angle;
if (Math.abs(rotatableAngle) <= 0.0001) rotatableAngle = 0.0001;
else if (Math.abs(rotatableAngle) - Math.PI <= 0.0001) rotatableAngle = rotatableAngle - 0.0001;
let rotatableDistance = this.distance;
if (Math.abs(rotatableDistance) <= 0.0001) rotatableDistance = 0.0001;
// fix distance for orthogonal-camera
if (this.ortho > 0) {
rotatableDistance = MathUtils.lerp(rotatableDistance, Math.max(rotatableDistance, 300), Math.pow(this.ortho, 8));
}
// calculate rotationVector
let rotationVector = new Vector3(Math.sin(this.rotationValue), 0, -Math.cos(this.rotationValue)); // 0 is towards north
let rotationVector = new Vector3(Math.sin(this.rotation), 0, -Math.cos(this.rotation)); // 0 is towards north
let angleRotationAxis = new Vector3(0, 1, 0).cross(rotationVector);
rotationVector.applyAxisAngle(angleRotationAxis, (Math.PI / 2) - rotatableAngle);
rotationVector.multiplyScalar(rotatableDistance);
// position camera
this.camera.position.copy(this.positionValue).sub(rotationVector);
this.camera.lookAt(this.positionValue);
// update ortho
this.camera.distance = this.distanceValue;
this.camera.ortho = this.orthoValue;
this.camera.rotation.set(0, 0, 0);
this.camera.position.copy(this.position).sub(rotationVector);
this.camera.lookAt(this.position);
this.camera.rotateZ(this.tilt + rotatableAngle < 0 ? Math.PI : 0);
// optimize far/near planes
if (this.orthoValue <= 0) {
let near = MathUtils.clamp(this.distanceValue / 1000, 0.01, 1);
let far = MathUtils.clamp(this.distanceValue * 2, Math.max(near + 1, 2000), this.distanceValue + 5000);
if (this.ortho <= 0) {
let near = MathUtils.clamp(rotatableDistance / 1000, 0.01, 1);
let far = MathUtils.clamp(rotatableDistance * 2, Math.max(near + 1, 2000), rotatableDistance + 5000);
if (far - near > 10000) near = far - 10000;
this.camera.near = near;
this.camera.far = far;
} else {
} else if (this.angle === 0) {
this.camera.near = 1;
this.camera.far = rotatableDistance + 300;
} else {
this.camera.near = 1;
this.camera.far = 100000;
}
// event
@ -91,171 +115,85 @@ export class ControlsManager {
// if the position changed, update map to show new position
if (this.mapViewer.map) {
let triggerDistance = 1;
if (this.valueChanged) {
triggerDistance = this.mapViewer.loadedHiresViewDistance * 0.8;
if (valueChanged) {
triggerDistance = this.mapViewer.loadedLowresViewDistance * 0.8;
}
if (
Math.abs(this.lastMapUpdatePosition.x - this.positionValue.x) >= triggerDistance ||
Math.abs(this.lastMapUpdatePosition.z - this.positionValue.z) >= triggerDistance
Math.abs(this.lastMapUpdatePosition.x - this.position.x) >= triggerDistance ||
Math.abs(this.lastMapUpdatePosition.z - this.position.z) >= triggerDistance
) {
this.lastMapUpdatePosition = this.positionValue.clone();
this.mapViewer.loadMapArea(this.positionValue.x, this.positionValue.z);
this.lastMapUpdatePosition = this.position.clone();
this.mapViewer.loadMapArea(this.position.x, this.position.z);
}
}
this.valueChanged = false;
}
handleValueChange() {
this.valueChanged = true;
isValueChanged() {
return !(
this.position.equals(this.lastPosition) &&
this.rotation === this.lastRotation &&
this.angle === this.lastAngle &&
this.distance === this.lastDistance &&
this.ortho === this.lastOrtho &&
this.tilt === this.lastTilt
);
}
/**
* @returns {number}
*/
get x() {
return this.positionValue.x;
}
/**
* @param x {number}
*/
set x(x) {
this.positionValue.x = x;
this.handleValueChange();
}
/**
* @returns {number}
*/
get y() {
return this.positionValue.y;
}
/**
* @param y {number}
*/
set y(y) {
this.positionValue.y = y;
this.handleValueChange();
}
/**
* @returns {number}
*/
get z() {
return this.positionValue.z;
}
/**
* @param z {number}
*/
set z(z) {
this.positionValue.z = z;
this.handleValueChange();
}
/**
* @returns {Vector3}
*/
get position() {
return this.positionValue;
}
/**
* @param position {Vector3}
*/
set position(position) {
this.position.copy(position);
this.handleValueChange();
}
/**
* @returns {number}
*/
get rotation() {
return this.rotationValue;
}
/**
* @param rotation {number}
*/
set rotation(rotation) {
this.rotationValue = rotation;
this.handleValueChange();
}
/**
* @returns {number}
*/
get angle() {
return this.angleValue;
}
/**
* @param angle {number}
*/
set angle(angle) {
this.angleValue = angle;
this.handleValueChange();
}
/**
* @returns {number}
*/
get distance() {
return this.distanceValue;
}
/**
* @param distance {number}
*/
set distance(distance) {
this.distanceValue = distance;
this.handleValueChange();
resetValueChanged() {
this.lastPosition.copy(this.position);
this.lastRotation = this.rotation;
this.lastAngle = this.angle;
this.lastDistance = this.distance;
this.lastOrtho = this.ortho;
this.lastTilt = this.tilt;
}
/**
* @returns {number}
*/
get ortho() {
return this.orthoValue;
return this.camera.ortho;
}
/**
* @param ortho {number}
*/
set ortho(ortho) {
this.orthoValue = ortho;
this.handleValueChange();
this.camera.ortho = ortho;
}
/**
* @param controls {{
get distance() {
return this.camera.distance;
}
set distance(distance) {
this.camera.distance = distance;
}
/** @typedef ControlsLike {{
* start: function(controls: ControlsManager),
* stop: function(),
* update: function(deltaTime: number, map: Map)
* }}
/**
* @param controls {ControlsLike}
*/
set controls(controls) {
if (this.controlsValue && typeof this.controlsValue.stop === "function")
this.controlsValue.stop();
if (this._controls && this._controls.stop)
this._controls.stop();
this.controlsValue = controls;
this._controls = controls;
if (this.controlsValue && typeof this.controlsValue.start === "function")
this.controlsValue.start(this);
if (this._controls && this._controls.start)
this._controls.start(this);
}
/**
* @returns {{
* start: function(controls: ControlsManager),
* stop: function(),
* update: function(deltaTime: number, map: Map)
* }}
* @returns {ControlsLike}
*/
get controls() {
return this.controlsValue;
return this._controls;
}
}

View File

@ -0,0 +1,53 @@
export class KeyCombination {
static CTRL = 0;
static SHIFT = 1;
static ALT = 2;
/**
* @param code {string}
* @param modifiers {...number}
*/
constructor(code, ...modifiers) {
this.code = code;
this.ctrl = modifiers.includes(KeyCombination.CTRL) || this.code === "CtrlLeft" || this.code === "CtrlRight";
this.shift = modifiers.includes(KeyCombination.SHIFT) || this.code === "ShiftLeft" || this.code === "ShiftRight";
this.alt = modifiers.includes(KeyCombination.ALT) || this.code === "AltLeft" || this.code === "AltRight";
}
/**
* @param evt {KeyboardEvent}
* @returns {boolean}
*/
testDown(evt) {
return this.code === evt.code &&
this.ctrl === evt.ctrlKey &&
this.shift === evt.shiftKey &&
this.alt === evt.altKey;
}
/**
* @param evt {KeyboardEvent}
* @returns {boolean}
*/
testUp(evt) {
return this.code === evt.code;
}
static oneDown(evt, ...combinations) {
for (let combination of combinations){
if (combination.testDown(evt)) return true;
}
return false;
}
static oneUp(evt, ...combinations) {
for (let combination of combinations){
if (combination.testUp(evt)) return true;
}
return false;
}
}

View File

@ -1,435 +0,0 @@
import {MathUtils, MOUSE, Vector2, Vector3} from "three";
import {alert} from "../util/Utils";
export class MapControls {
static STATES = {
NONE: 0,
MOVE: 1,
ORBIT: 2
};
static KEYS = {
LEFT: ["ArrowLeft", "a", "A", 37, 65],
UP: ["ArrowUp", "w", "W", 38, 87],
RIGHT: ["ArrowRight", "d", "D", 39, 68],
DOWN: ["ArrowDown", "s", "S", 40, 83],
ZOOM_IN: ["+"],
ZOOM_OUT: ["-"]
};
static BUTTONS = {
ORBIT: [MOUSE.RIGHT],
MOVE: [MOUSE.LEFT]
};
static VECTOR2_ZERO = new Vector2(0, 0);
/**
* @param rootElement {Element}
* @param hammerLib {Hammer.Manager}
* @param events {EventTarget}
*/
constructor(rootElement, hammerLib, events = null) {
Object.defineProperty( this, 'isMapControls', { value: true } );
this.rootElement = rootElement;
this.hammer = hammerLib;
this.events = events;
/** @type {ControlsManager} */
this.controls = null;
this.targetPosition = new Vector3();
this.positionTerrainHeight = false;
this.targetDistance = 400;
this.minDistance = 10;
this.maxDistance = 10000;
this.targetRotation = 0;
this.targetAngle = 0;
this.minAngle = 0;
this.maxAngle = Math.PI / 2;
this.maxAngleForZoom = this.maxAngle;
this.state = MapControls.STATES.NONE;
this.mouse = new Vector2();
this.lastMouse = new Vector2();
this.keyStates = {};
this.touchStart = new Vector2();
this.touchTiltStart = 0;
this.lastTouchRotation = 0;
this.touchZoomStart = 0;
}
/**
* @param controls {ControlsManager}
*/
start(controls) {
this.controls = controls;
this.targetPosition.copy(this.controls.position);
this.positionTerrainHeight = false;
this.targetDistance = this.controls.distance;
this.targetDistance = MathUtils.clamp(this.targetDistance, this.minDistance, this.maxDistance);
this.targetRotation = this.controls.rotation;
this.targetAngle = this.controls.angle;
this.updateZoom();
// add events
this.rootElement.addEventListener("wheel", this.onWheel, {passive: true})
this.hammer.on('zoomstart', this.onTouchZoomDown);
this.hammer.on('zoommove', this.onTouchZoomMove);
this.rootElement.addEventListener('mousedown', this.onMouseDown);
window.addEventListener('mousemove', this.onMouseMove);
window.addEventListener('mouseup', this.onMouseUp);
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
this.hammer.on('movestart', this.onTouchDown);
this.hammer.on('movemove', this.onTouchMove);
this.hammer.on('moveend', this.onTouchUp);
this.hammer.on('movecancel', this.onTouchUp);
this.hammer.on('tiltstart', this.onTouchTiltDown);
this.hammer.on('tiltmove', this.onTouchTiltMove);
this.hammer.on('tiltend', this.onTouchTiltUp);
this.hammer.on('tiltcancel', this.onTouchTiltUp);
this.hammer.on('rotatestart', this.onTouchRotateDown);
this.hammer.on('rotatemove', this.onTouchRotateMove);
this.hammer.on('rotateend', this.onTouchRotateUp);
this.hammer.on('rotatecancel', this.onTouchRotateUp);
window.addEventListener('contextmenu', this.onContextMenu);
}
stop() {
// remove events
this.rootElement.removeEventListener("wheel", this.onWheel)
this.hammer.off('zoomstart', this.onTouchZoomDown);
this.hammer.off('zoommove', this.onTouchZoomMove);
this.rootElement.addEventListener('mousedown', this.onMouseDown);
window.removeEventListener('mousemove', this.onMouseMove);
window.removeEventListener('mouseup', this.onMouseUp);
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
this.hammer.on('movestart', this.onTouchDown);
this.hammer.off('movemove', this.onTouchMove);
this.hammer.off('moveend', this.onTouchUp);
this.hammer.off('movecancel', this.onTouchUp);
this.hammer.off('tiltstart', this.onTouchTiltDown);
this.hammer.off('tiltmove', this.onTouchTiltMove);
this.hammer.off('tiltend', this.onTouchTiltUp);
this.hammer.off('tiltcancel', this.onTouchTiltUp);
this.hammer.off('rotatestart', this.onTouchRotateDown);
this.hammer.off('rotatemove', this.onTouchRotateMove);
this.hammer.off('rotateend', this.onTouchRotateUp);
this.hammer.off('rotatecancel', this.onTouchRotateUp);
window.removeEventListener('contextmenu', this.onContextMenu);
}
/**
* @param deltaTime {number}
* @param map {Map}
*/
update(deltaTime, map) {
// == process mouse movements ==
let deltaMouse = this.lastMouse.clone().sub(this.mouse);
let moveDelta = new Vector2();
// zoom keys
if (this.keyStates.ZOOM_IN) {
this.targetDistance *= 1 - 0.003 * deltaTime;
this.updateZoom();
}
if (this.keyStates.ZOOM_OUT){
this.targetDistance *= 1 + 0.003 * deltaTime;
this.updateZoom();
}
// move
if (this.state === MapControls.STATES.MOVE) {
moveDelta.copy(deltaMouse);
} else {
if (this.keyStates.UP) moveDelta.y -= 20;
if (this.keyStates.DOWN) moveDelta.y += 20;
if (this.keyStates.LEFT) moveDelta.x -= 20;
if (this.keyStates.RIGHT) moveDelta.x += 20;
}
if (moveDelta.x !== 0 || moveDelta.y !== 0) {
moveDelta.rotateAround(MapControls.VECTOR2_ZERO, this.controls.rotation);
this.targetPosition.set(
this.targetPosition.x + (moveDelta.x * this.targetDistance / this.rootElement.clientHeight * 1.5),
this.targetPosition.y,
this.targetPosition.z + (moveDelta.y * this.targetDistance / this.rootElement.clientHeight * 1.5)
);
}
this.updatePositionTerrainHeight(map);
// tilt/pan
if (this.state === MapControls.STATES.ORBIT) {
if (deltaMouse.x !== 0) {
this.targetRotation -= (deltaMouse.x / this.rootElement.clientHeight * Math.PI);
this.wrapRotation();
}
if (deltaMouse.y !== 0) {
this.targetAngle += (deltaMouse.y / this.rootElement.clientHeight * Math.PI);
this.targetAngle = MathUtils.clamp(this.targetAngle, this.minAngle, this.maxAngleForZoom + 0.1);
}
}
if (this.targetAngle > this.maxAngleForZoom) this.targetAngle -= (this.targetAngle - this.maxAngleForZoom) * 0.3;
// == Smoothly apply target values ==
let somethingChanged = false;
// move
let deltaPosition = this.targetPosition.clone().sub(this.controls.position);
if (Math.abs(deltaPosition.x) > 0.01 || Math.abs(deltaPosition.y) > 0.001 || Math.abs(deltaPosition.z) > 0.01) {
this.controls.position = this.controls.position.add(deltaPosition.multiplyScalar(0.015 * deltaTime));
somethingChanged = true;
}
// rotation
let deltaRotation = this.targetRotation - this.controls.rotation;
if (Math.abs(deltaRotation) > 0.0001) {
this.controls.rotation += deltaRotation * 0.015 * deltaTime;
somethingChanged = true;
}
// angle
let deltaAngle = this.targetAngle - this.controls.angle;
if (Math.abs(deltaAngle) > 0.0001) {
this.controls.angle += deltaAngle * 0.015 * deltaTime;
somethingChanged = true;
}
// zoom
let deltaDistance = this.targetDistance - this.controls.distance
if (Math.abs(deltaDistance) > 0.001) {
this.controls.distance += deltaDistance * 0.01 * deltaTime;
somethingChanged = true;
}
// == Adjust camera height to terrain ==
if (somethingChanged) {
let y = 0;
if (this.positionTerrainHeight !== false) {
y = this.targetPosition.y;
let deltaY = this.positionTerrainHeight - y;
if (Math.abs(deltaY) > 0.001) {
y += deltaY * 0.01 * deltaTime;
}
}
let minCameraHeight = map.terrainHeightAt(this.controls.camera.position.x, this.controls.camera.position.z) + ((this.minDistance - this.targetDistance) * 0.4) + 1;
if (minCameraHeight > y) y = minCameraHeight;
this.targetPosition.y = y;
}
// == Fix NaN's as a fail-safe ==
if (isNaN(this.targetPosition.x)){
alert(this.events, `Invalid targetPosition x: ${this.targetPosition.x}`, "warning");
this.targetPosition.x = 0;
}
if (isNaN(this.targetPosition.y)){
alert(this.events, `Invalid targetPosition y: ${this.targetPosition.y}`, "warning");
this.targetPosition.y = 0;
}
if (isNaN(this.targetPosition.z)){
alert(this.events, `Invalid targetPosition z: ${this.targetPosition.z}`, "warning");
this.targetPosition.z = 0;
}
if (isNaN(this.targetDistance)){
alert(this.events, `Invalid targetDistance: ${this.targetDistance}`, "warning");
this.targetDistance = this.minDistance;
}
if (isNaN(this.targetRotation)){
alert(this.events, `Invalid targetRotation: ${this.targetRotation}`, "warning");
this.targetRotation = 0;
}
if (isNaN(this.targetAngle)){
alert(this.events, `Invalid targetAngle: ${this.targetAngle}`, "warning");
this.targetAngle = this.minAngle;
}
// == Remember last processed state ==
this.lastMouse.copy(this.mouse);
}
updateZoom() {
this.targetDistance = MathUtils.clamp(this.targetDistance, this.minDistance, this.maxDistance);
this.updateMaxAngleForZoom();
this.targetAngle = MathUtils.clamp(this.targetAngle, this.minAngle, this.maxAngleForZoom);
}
updateMaxAngleForZoom() {
this.maxAngleForZoom =
MathUtils.clamp(
(1 - Math.pow((this.targetDistance - this.minDistance) / (500 - this.minDistance), 0.5)) * this.maxAngle,
this.minAngle,
this.maxAngle
);
}
updatePositionTerrainHeight(map) {
this.positionTerrainHeight = map.terrainHeightAt(this.targetPosition.x, this.targetPosition.z);
}
wrapRotation() {
while (this.targetRotation >= Math.PI) {
this.targetRotation -= Math.PI * 2;
this.controls.rotation -= Math.PI * 2;
}
while (this.targetRotation <= -Math.PI) {
this.targetRotation += Math.PI * 2;
this.controls.rotation += Math.PI * 2;
}
}
onKeyDown = evt => {
let key = evt.key || evt.keyCode;
for (let action in MapControls.KEYS){
if (!MapControls.KEYS.hasOwnProperty(action)) continue;
if (MapControls.KEYS[action].includes(key)){
this.keyStates[action] = true;
}
}
};
onKeyUp = evt => {
let key = evt.key || evt.keyCode;
for (let action in MapControls.KEYS){
if (!MapControls.KEYS.hasOwnProperty(action)) continue;
if (MapControls.KEYS[action].includes(key)){
this.keyStates[action] = false;
}
}
};
onWheel = evt => {
let delta = evt.deltaY;
if (evt.deltaMode === WheelEvent.DOM_DELTA_PIXEL) delta *= 0.01;
if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) delta *= 0.33;
this.targetDistance *= Math.pow(1.5, delta);
this.updateZoom();
}
onMouseDown = evt => {
if (this.state !== MapControls.STATES.NONE) return;
if (MapControls.BUTTONS.MOVE.includes(evt.button)) {
this.state = MapControls.STATES.MOVE;
evt.preventDefault();
}
if (MapControls.BUTTONS.ORBIT.includes(evt.button)) {
this.state = MapControls.STATES.ORBIT;
evt.preventDefault();
}
};
onMouseMove = evt => {
this.mouse.set(evt.clientX, evt.clientY);
if (this.state !== MapControls.STATES.NONE){
evt.preventDefault();
}
};
onMouseUp = evt => {
if (this.state === MapControls.STATES.NONE) return;
if (MapControls.BUTTONS.MOVE.includes(evt.button)) {
if (this.state === MapControls.STATES.MOVE) this.state = MapControls.STATES.NONE;
evt.preventDefault();
}
if (MapControls.BUTTONS.ORBIT.includes(evt.button)) {
if (this.state === MapControls.STATES.ORBIT) this.state = MapControls.STATES.NONE;
evt.preventDefault();
}
};
onTouchDown = evt => {
if (evt.pointerType === "mouse") return;
this.touchStart.set(this.targetPosition.x, this.targetPosition.z);
this.state = MapControls.STATES.MOVE;
};
onTouchMove = evt => {
if (evt.pointerType === "mouse") return;
if (this.state !== MapControls.STATES.MOVE) return;
let touchDelta = new Vector2(evt.deltaX, evt.deltaY);
if (touchDelta.x !== 0 || touchDelta.y !== 0) {
touchDelta.rotateAround(MapControls.VECTOR2_ZERO, this.controls.rotation);
this.targetPosition.x = this.touchStart.x - (touchDelta.x * this.targetDistance / this.rootElement.clientHeight * 1.5);
this.targetPosition.z = this.touchStart.y - (touchDelta.y * this.targetDistance / this.rootElement.clientHeight * 1.5);
}
};
onTouchUp = evt => {
if (evt.pointerType === "mouse") return;
this.state = MapControls.STATES.NONE;
};
onTouchTiltDown = () => {
this.touchTiltStart = this.targetAngle;
this.state = MapControls.STATES.ORBIT;
};
onTouchTiltMove = evt => {
if (this.state !== MapControls.STATES.ORBIT) return;
this.targetAngle = this.touchTiltStart - (evt.deltaY / this.rootElement.clientHeight * Math.PI);
this.targetAngle = MathUtils.clamp(this.targetAngle, this.minAngle, this.maxAngleForZoom + 0.1);
};
onTouchTiltUp = () => {
this.state = MapControls.STATES.NONE;
};
onTouchRotateDown = evt => {
this.lastTouchRotation = evt.rotation;
this.state = MapControls.STATES.ORBIT;
};
onTouchRotateMove = evt => {
if (this.state !== MapControls.STATES.ORBIT) return;
let delta = evt.rotation - this.lastTouchRotation;
this.lastTouchRotation = evt.rotation;
if (delta > 180) delta -= 360;
if (delta < -180) delta += 360;
this.targetRotation -= (delta * (Math.PI / 180)) * 1.4;
this.wrapRotation();
};
onTouchRotateUp = () => {
this.state = MapControls.STATES.NONE;
};
onTouchZoomDown = () => {
this.touchZoomStart = this.targetDistance;
};
onTouchZoomMove = evt => {
this.targetDistance = this.touchZoomStart / evt.scale;
this.updateZoom();
};
onContextMenu = evt => {
evt.preventDefault();
}
}

View File

@ -0,0 +1,148 @@
import {MathUtils, Vector2} from "three";
import {animate, EasingFunctions, softMin} from "../../util/Utils";
import {KeyMoveControls} from "./keyboard/KeyMoveControls";
import {MouseRotateControls} from "./mouse/MouseRotateControls";
import {MouseAngleControls} from "./mouse/MouseAngleControls";
import {KeyHeightControls} from "./keyboard/KeyHeightControls";
import {TouchPanControls} from "./touch/TouchPanControls";
export class FreeFlightControls {
/**
* @param target {Element}
*/
constructor(target) {
this.target = target;
this.manager = null;
this.hammer = new Hammer.Manager(this.target);
this.initializeHammer();
this.keyMove = new KeyMoveControls(this.target, 0.5, 0.1);
this.keyHeight = new KeyHeightControls(this.target, 0.5, 0.2);
this.mouseRotate = new MouseRotateControls(this.target, 0.002, -0.003, -0.002, 0.5);
this.mouseAngle = new MouseAngleControls(this.target, 0.002, -0.003, -0.002, 0.5);
this.touchPan = new TouchPanControls(this.hammer, 0.005, 0.15);
this.started = false;
this.clickStart = new Vector2();
this.moveSpeed = 0.5;
this.animationTargetHeight = 0;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.keyMove.start(manager);
this.keyHeight.start(manager);
this.mouseRotate.start(manager);
this.mouseAngle.start(manager);
this.touchPan.start(manager);
this.target.addEventListener("contextmenu", this.onContextMenu);
this.target.addEventListener("mousedown", this.onMouseDown);
this.target.addEventListener("mouseup", this.onMouseUp);
window.addEventListener("wheel", this.onWheel, {passive: true});
let startOrtho = this.manager.ortho;
let startDistance = this.manager.distance;
let startAngle = this.manager.angle;
let startY = this.manager.position.y;
let targetAngle = Math.PI / 2;
animate(progress => {
let smoothProgress = EasingFunctions.easeInOutQuad(progress);
this.manager.ortho = MathUtils.lerp(startOrtho, 0, progress);
this.manager.distance = MathUtils.lerp(startDistance, 0, smoothProgress);
this.manager.angle = MathUtils.lerp(startAngle, targetAngle, smoothProgress);
this.manager.position.y = MathUtils.lerp(startY, this.animationTargetHeight, smoothProgress);
}, 500, () => {
this.started = true;
});
}
stop() {
this.keyMove.stop();
this.keyHeight.stop();
this.mouseRotate.stop();
this.mouseAngle.stop();
this.touchPan.stop();
this.started = false;
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (!this.started) {
this.animationTargetHeight = map.terrainHeightAt(this.manager.position.x, this.manager.position.z) + 10;
return;
}
this.keyMove.update(delta, map);
this.keyHeight.update(delta, map);
this.mouseRotate.update(delta, map);
this.mouseAngle.update(delta, map);
this.touchPan.update(delta, map);
this.manager.angle = MathUtils.clamp(this.manager.angle, 0, Math.PI);
}
initializeHammer() {
let touchTap = new Hammer.Tap({ event: 'tap', pointers: 1, taps: 1, threshold: 2 });
let touchMove = new Hammer.Pan({ event: 'move', pointers: 1, direction: Hammer.DIRECTION_ALL, threshold: 0 });
let touchTilt = new Hammer.Pan({ event: 'tilt', pointers: 2, direction: Hammer.DIRECTION_VERTICAL, threshold: 0 });
let touchRotate = new Hammer.Rotate({ event: 'rotate', pointers: 2, threshold: 0 });
let touchZoom = new Hammer.Pinch({ event: 'zoom', pointers: 2, threshold: 0 });
touchMove.recognizeWith(touchRotate);
touchMove.recognizeWith(touchTilt);
touchMove.recognizeWith(touchZoom);
touchTilt.recognizeWith(touchRotate);
touchTilt.recognizeWith(touchZoom);
touchRotate.recognizeWith(touchZoom);
this.hammer.add(touchTap);
this.hammer.add(touchTilt);
this.hammer.add(touchMove);
this.hammer.add(touchRotate);
this.hammer.add(touchZoom);
}
onContextMenu = evt => {
evt.preventDefault();
}
onMouseDown = evt => {
this.clickStart.set(evt.x, evt.y);
}
onMouseUp = evt => {
if (this.clickStart.x !== evt.x) return;
if (this.clickStart.y !== evt.y) return;
this.target.requestPointerLock();
}
onWheel = evt => {
let delta = evt.deltaY;
if (evt.deltaMode === WheelEvent.DOM_DELTA_PIXEL) delta *= 0.01;
if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) delta *= 0.33;
this.moveSpeed *= Math.pow(1.5, -delta * 0.25);
this.moveSpeed = MathUtils.clamp(this.moveSpeed, 0.05, 5);
this.keyMove.speed = this.moveSpeed;
this.keyHeight.speed = this.moveSpeed;
}
}

View File

@ -0,0 +1,97 @@
import {MathUtils} from "three";
import {KeyCombination} from "../../KeyCombination";
export class KeyHeightControls {
static KEYS = {
UP: [
new KeyCombination("Space"),
new KeyCombination("PageUp")
],
DOWN: [
new KeyCombination("ShiftLeft"),
new KeyCombination("PageDown")
],
}
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.deltaY = 0;
this.up = false;
this.down = false;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
stop() {
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.up) this.deltaY += 1;
if (this.down) this.deltaY -= 1;
if (this.deltaY === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.position.y += this.deltaY * smoothing * this.speed * delta * 0.06;
this.deltaY *= 1 - smoothing;
if (Math.abs(this.deltaY) < 0.0001) {
this.deltaY = 0;
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyDown = evt => {
if (KeyCombination.oneUp(evt, ...KeyHeightControls.KEYS.UP)){
this.up = true;
evt.preventDefault();
}
else if (KeyCombination.oneUp(evt, ...KeyHeightControls.KEYS.DOWN)){
this.down = true;
evt.preventDefault();
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyUp = evt => {
if (KeyCombination.oneUp(evt, ...KeyHeightControls.KEYS.UP)){
this.up = false;
}
if (KeyCombination.oneUp(evt, ...KeyHeightControls.KEYS.DOWN)){
this.down = false;
}
}
}

View File

@ -0,0 +1,130 @@
import {MathUtils, Vector2} from "three";
import {VEC2_ZERO} from "../../../util/Utils";
import {KeyCombination} from "../../KeyCombination";
export class KeyMoveControls {
static KEYS = {
LEFT: [
new KeyCombination("ArrowLeft"),
new KeyCombination("KeyA")
],
UP: [
new KeyCombination("ArrowUp"),
new KeyCombination("KeyW")
],
RIGHT: [
new KeyCombination("ArrowRight"),
new KeyCombination("KeyD")
],
DOWN: [
new KeyCombination("ArrowDown"),
new KeyCombination("KeyS")
],
}
static temp_v2 = new Vector2();
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.deltaPosition = new Vector2();
this.up = false;
this.down = false;
this.left = false;
this.right = false;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
stop() {
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.up) this.deltaPosition.y -= 1;
if (this.down) this.deltaPosition.y += 1;
if (this.left) this.deltaPosition.x -= 1;
if (this.right) this.deltaPosition.x += 1;
if (this.deltaPosition.x === 0 && this.deltaPosition.y === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
let rotatedDelta = KeyMoveControls.temp_v2.copy(this.deltaPosition);
rotatedDelta.rotateAround(VEC2_ZERO, this.manager.rotation);
this.manager.position.x += rotatedDelta.x * smoothing * this.speed * delta * 0.06;
this.manager.position.z += rotatedDelta.y * smoothing * this.speed * delta * 0.06;
this.deltaPosition.multiplyScalar(1 - smoothing);
if (this.deltaPosition.lengthSq() < 0.0001) {
this.deltaPosition.set(0, 0);
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyDown = evt => {
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.UP)){
this.up = true;
evt.preventDefault();
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.DOWN)){
this.down = true;
evt.preventDefault();
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.LEFT)){
this.left = true;
evt.preventDefault();
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.RIGHT)){
this.right = true;
evt.preventDefault();
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyUp = evt => {
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.UP)){
this.up = false;
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.DOWN)){
this.down = false;
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.LEFT)){
this.left = false;
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.RIGHT)){
this.right = false;
}
}
}

View File

@ -0,0 +1,103 @@
import {MathUtils} from "three";
export class MouseAngleControls {
/**
* @param target {EventTarget}
* @param speedLeft {number}
* @param speedRight {number}
* @param speedCapture {number}
* @param stiffness {number}
*/
constructor(target, speedLeft, speedRight, speedCapture, stiffness) {
this.target = target;
this.manager = null;
this.moving = false;
this.lastY = 0;
this.deltaAngle = 0;
this.speedLeft = speedLeft;
this.speedRight = speedRight;
this.speedCapture = speedCapture;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.target.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
}
stop() {
this.target.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("mousemove", this.onMouseMove);
window.removeEventListener("mouseup", this.onMouseUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaAngle === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.angle += this.deltaAngle * smoothing;
this.deltaAngle *= 1 - smoothing;
if (Math.abs(this.deltaAngle) < 0.0001) {
this.deltaAngle = 0;
}
}
reset() {
this.deltaAngle = 0;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseDown = evt => {
this.moving = true;
this.deltaAngle = 0;
this.lastY = evt.y;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseMove = evt => {
if (document.pointerLockElement) {
this.deltaAngle += evt.movementY * this.speedCapture;
}
else if(this.moving){
if (evt.buttons === 1) {
this.deltaAngle += (evt.y - this.lastY) * this.speedLeft;
} else {
this.deltaAngle += (evt.y - this.lastY) * this.speedRight;
}
}
this.lastY = evt.y;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseUp = evt => {
this.moving = false;
}
}

View File

@ -0,0 +1,103 @@
import {MathUtils} from "three";
export class MouseRotateControls {
/**
* @param target {EventTarget}
* @param speedLeft {number}
* @param speedRight {number}
* @param speedCapture {number}
* @param stiffness {number}
*/
constructor(target, speedLeft, speedRight, speedCapture, stiffness) {
this.target = target;
this.manager = null;
this.moving = false;
this.lastX = 0;
this.deltaRotation = 0;
this.speedLeft = speedLeft;
this.speedRight = speedRight;
this.speedCapture = speedCapture;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.target.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
}
stop() {
this.target.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("mousemove", this.onMouseMove);
window.removeEventListener("mouseup", this.onMouseUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaRotation === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.rotation += this.deltaRotation * smoothing;
this.deltaRotation *= 1 - smoothing;
if (Math.abs(this.deltaRotation) < 0.0001) {
this.deltaRotation = 0;
}
}
reset() {
this.deltaRotation = 0;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseDown = evt => {
this.moving = true;
this.deltaRotation = 0;
this.lastX = evt.x;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseMove = evt => {
if (document.pointerLockElement) {
this.deltaRotation -= evt.movementX * this.speedCapture;
}
else if(this.moving){
if (evt.buttons === 1) {
this.deltaRotation -= (evt.x - this.lastX) * this.speedLeft;
} else {
this.deltaRotation -= (evt.x - this.lastX) * this.speedRight;
}
}
this.lastX = evt.x;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseUp = evt => {
this.moving = false;
}
}

View File

@ -0,0 +1,104 @@
import {MathUtils, Vector2} from "three";
export class TouchPanControls {
static tempVec2_1 = new Vector2();
/**
* @param hammer {Hammer.Manager}
* @param speed {number}
* @param stiffness {number}
*/
constructor(hammer, speed, stiffness) {
this.hammer = hammer;
this.manager = null;
this.moving = false;
this.lastPosition = new Vector2();
this.deltaPosition = new Vector2();
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.hammer.on("movestart", this.onTouchDown);
this.hammer.on("movemove", this.onTouchMove);
this.hammer.on("moveend", this.onTouchUp);
this.hammer.on("movecancel", this.onTouchUp);
}
stop() {
this.hammer.off("movestart", this.onTouchDown);
this.hammer.off("movemove", this.onTouchMove);
this.hammer.off("moveend", this.onTouchUp);
this.hammer.off("movecancel", this.onTouchUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaPosition.x === 0 && this.deltaPosition.y === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.rotation += this.deltaPosition.x * this.speed * this.stiffness;
this.manager.angle -= this.deltaPosition.y * this.speed * this.stiffness;
this.deltaPosition.multiplyScalar(1 - smoothing);
if (this.deltaPosition.lengthSq() < 0.0001) {
this.deltaPosition.set(0, 0);
}
}
reset() {
this.deltaPosition.set(0, 0);
}
/**
* @private
* @param evt {object}
*/
onTouchDown = evt => {
if (evt.pointerType === "mouse") return;
this.moving = true;
this.deltaPosition.set(0, 0);
this.lastPosition.set(evt.center.x, evt.center.y);
}
/**
* @private
* @param evt {object}
*/
onTouchMove = evt => {
if (evt.pointerType === "mouse") return;
let position = TouchPanControls.tempVec2_1.set(evt.center.x, evt.center.y);
if(this.moving){
this.deltaPosition.sub(position).add(this.lastPosition);
}
this.lastPosition.copy(position);
}
/**
* @private
* @param evt {object}
*/
onTouchUp = evt => {
if (evt.pointerType === "mouse") return;
this.moving = false;
}
}

View File

@ -0,0 +1,241 @@
import {MouseMoveControls} from "./mouse/MouseMoveControls";
import {MouseZoomControls} from "./mouse/MouseZoomControls";
import {MouseRotateControls} from "./mouse/MouseRotateControls";
import {MouseAngleControls} from "./mouse/MouseAngleControls";
import {MathUtils} from "three";
import {animate, EasingFunctions, softClamp, softMin} from "../../util/Utils";
import {MapHeightControls} from "./MapHeightControls";
import {KeyMoveControls} from "./keyboard/KeyMoveControls";
import {KeyAngleControls} from "./keyboard/KeyAngleControls";
import {KeyRotateControls} from "./keyboard/KeyRotateControls";
import {KeyZoomControls} from "./keyboard/KeyZoomControls";
import {TouchMoveControls} from "./touch/TouchMoveControls";
import {TouchRotateControls} from "./touch/TouchRotateControls";
import {TouchAngleControls} from "./touch/TouchAngleControls";
import {TouchZoomControls} from "./touch/TouchZoomControls";
const HALF_PI = Math.PI * 0.5;
export class MapControls {
/**
* @param rootElement {EventTarget}
*/
constructor(rootElement) {
this.rootElement = rootElement;
this.manager = null;
this.started = false;
this.hammer = new Hammer.Manager(this.rootElement);
this.initializeHammer();
//controls
this.mouseMove = new MouseMoveControls(this.rootElement, 0.002,0.3);
this.mouseRotate = new MouseRotateControls(this.rootElement, 0.004, 0.3);
this.mouseAngle = new MouseAngleControls(this.rootElement, 0.004, 0.3);
this.mouseZoom = new MouseZoomControls(this.rootElement, 1, 0.2);
this.keyMove = new KeyMoveControls(this.rootElement, 0.025, 0.2);
this.keyRotate = new KeyRotateControls(this.rootElement, 0.06, 0.15);
this.keyAngle = new KeyAngleControls(this.rootElement, 0.04, 0.15);
this.keyZoom = new KeyZoomControls(this.rootElement, 0.2, 0.15);
this.touchMove = new TouchMoveControls(this.hammer, 0.002,0.3);
this.touchRotate = new TouchRotateControls(this.hammer, 0.0174533, 0.3);
this.touchAngle = new TouchAngleControls(this.hammer, 0.01, 0.3);
this.touchZoom = new TouchZoomControls(this.hammer);
this.mapHeight = new MapHeightControls(0.2, 0.1);
this.animationTargetHeight = 0;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.rootElement.addEventListener("contextmenu", this.onContextMenu);
this.mouseMove.start(manager);
this.mouseRotate.start(manager);
this.mouseAngle.start(manager);
this.mouseZoom.start(manager);
this.keyMove.start(manager);
this.keyRotate.start(manager);
this.keyAngle.start(manager);
this.keyZoom.start(manager);
this.touchMove.start(manager);
this.touchRotate.start(manager);
this.touchAngle.start(manager);
this.touchZoom.start(manager);
this.mapHeight.start(manager);
let startOrtho = this.manager.ortho;
let startDistance = this.manager.distance;
let startAngle = this.manager.angle;
let startY = this.manager.position.y;
let targetDistance = MathUtils.clamp(this.manager.distance, 100, 10000);
let targetAngle = Math.min(startAngle, this.getMaxPerspectiveAngleForDistance(targetDistance));
animate(progress => {
let smoothProgress = EasingFunctions.easeInOutQuad(progress);
this.manager.ortho = MathUtils.lerp(startOrtho, 0, progress);
this.manager.distance = MathUtils.lerp(startDistance, targetDistance, smoothProgress);
this.manager.angle = MathUtils.lerp(startAngle, targetAngle, smoothProgress);
this.manager.position.y = MathUtils.lerp(startY, this.animationTargetHeight, smoothProgress);
}, 500, () => this.started = true);
}
stop() {
this.rootElement.removeEventListener("contextmenu", this.onContextMenu);
this.mouseMove.stop();
this.mouseRotate.stop();
this.mouseAngle.stop();
this.mouseZoom.stop();
this.keyMove.stop();
this.keyRotate.stop();
this.keyAngle.stop();
this.keyZoom.stop();
this.touchMove.stop();
this.touchRotate.stop();
this.touchAngle.stop();
this.touchZoom.stop();
this.mapHeight.stop();
this.started = false;
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (!this.started){
this.mapHeight.updateHeights(delta, map);
this.animationTargetHeight = this.mapHeight.getSuggestedHeight();
return;
}
// move and zoom
this.mouseMove.update(delta, map);
this.mouseZoom.update(delta, map);
this.keyMove.update(delta, map);
this.keyZoom.update(delta, map);
this.touchMove.update(delta, map);
this.touchZoom.update(delta, map);
this.manager.distance = softClamp(this.manager.distance, 5, 10000, 0.8);
// max angle for current distance
let maxAngleForZoom = this.getMaxPerspectiveAngleForDistance(this.manager.distance);
// rotation
if (this.manager.ortho === 0) {
this.mouseRotate.update(delta, map);
this.keyRotate.update(delta, map);
this.touchRotate.update(delta, map);
}
// tilt
if (this.manager.ortho === 0) {
this.mouseAngle.update(delta, map);
this.keyAngle.update(delta, map);
this.touchAngle.update(delta, map);
this.manager.angle = softClamp(this.manager.angle, 0, maxAngleForZoom, 0.8);
}
// target height
if (this.manager.ortho === 0 || this.manager.angle === 0) {
this.manager.position.y = 0;
this.mapHeight.maxAngle = maxAngleForZoom;
this.mapHeight.update(delta, map);
}
}
reset() {
this.mouseMove.reset();
this.mouseRotate.reset();
this.mouseAngle.reset();
this.mouseZoom.reset();
this.touchMove.reset();
this.touchRotate.reset();
this.touchAngle.reset();
this.touchZoom.reset();
}
getMaxPerspectiveAngleForDistance(distance) {
return MathUtils.clamp((1 - Math.pow(Math.max(distance - 5, 0.001) / 500, 0.5)) * HALF_PI,0, HALF_PI)
}
setPerspectiveView() {
this.reset();
let startOrtho = this.manager.ortho;
let startAngle = this.manager.angle;
let targetAngle = Math.min(startAngle, this.getMaxPerspectiveAngleForDistance(this.manager.distance));
animate(progress => {
let smoothProgress = EasingFunctions.easeInOutQuad(progress);
this.manager.ortho = MathUtils.lerp(startOrtho, 0, progress);
this.manager.angle = MathUtils.lerp(startAngle, targetAngle, smoothProgress);
}, 500);
}
setOrthographicView(targetRotation = 0, targetAngle = 0) {
this.reset();
let startOrtho = this.manager.ortho;
let startAngle = this.manager.angle;
let startRotation = this.manager.rotation;
animate(progress => {
let smoothProgress = EasingFunctions.easeInOutQuad(progress);
this.manager.ortho = MathUtils.lerp(startOrtho, 1, progress);
this.manager.angle = MathUtils.lerp(startAngle, targetAngle, smoothProgress);
this.manager.rotation = MathUtils.lerp(startRotation, targetRotation, smoothProgress);
}, 500);
}
initializeHammer() {
let touchTap = new Hammer.Tap({ event: 'tap', pointers: 1, taps: 1, threshold: 2 });
let touchMove = new Hammer.Pan({ event: 'move', pointers: 1, direction: Hammer.DIRECTION_ALL, threshold: 0 });
let touchTilt = new Hammer.Pan({ event: 'tilt', pointers: 2, direction: Hammer.DIRECTION_VERTICAL, threshold: 0 });
let touchRotate = new Hammer.Rotate({ event: 'rotate', pointers: 2, threshold: 0 });
let touchZoom = new Hammer.Pinch({ event: 'zoom', pointers: 2, threshold: 0 });
touchMove.recognizeWith(touchRotate);
touchMove.recognizeWith(touchTilt);
touchMove.recognizeWith(touchZoom);
touchTilt.recognizeWith(touchRotate);
touchTilt.recognizeWith(touchZoom);
touchRotate.recognizeWith(touchZoom);
this.hammer.add(touchTap);
this.hammer.add(touchTilt);
this.hammer.add(touchMove);
this.hammer.add(touchRotate);
this.hammer.add(touchZoom);
}
onContextMenu = evt => {
evt.preventDefault();
}
}

View File

@ -0,0 +1,86 @@
import {MathUtils, Vector2} from "three";
export class MapHeightControls {
/**
* @param cameraHeightStiffness {number}
* @param targetHeightStiffness {number}
*/
constructor(cameraHeightStiffness, targetHeightStiffness) {
this.manager = null;
this.cameraHeightStiffness = cameraHeightStiffness;
this.targetHeightStiffness = targetHeightStiffness;
this.maxAngle = Math.PI / 2;
this.targetHeight = 0;
this.cameraHeight = 0;
this.lastTarget = new Vector2();
this.lastTargetTerrainHeight = 0;
this.minCameraHeight = 0;
this.distanceTagretHeight = 0;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
}
stop() {}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
// adjust target height
this.updateHeights(delta, map);
this.manager.position.y = Math.max(this.manager.position.y, this.getSuggestedHeight());
}
updateHeights(delta, map) {
//target height
let targetSmoothing = this.targetHeightStiffness / (16.666 / delta);
targetSmoothing = MathUtils.clamp(targetSmoothing, 0, 1);
let targetTerrainHeight = this.lastTargetTerrainHeight;
if (this.lastTarget.x !== this.manager.position.x || this.lastTarget.y !== this.manager.position.z){
targetTerrainHeight = map.terrainHeightAt(this.manager.position.x, this.manager.position.z) || 0;
this.lastTargetTerrainHeight = targetTerrainHeight;
this.lastTarget.set(this.manager.position.x, this.manager.position.z);
}
let targetDelta = targetTerrainHeight - this.targetHeight;
this.targetHeight += targetDelta * targetSmoothing;
if (Math.abs(targetDelta) < 0.001) this.targetHeight = targetTerrainHeight;
// camera height
this.minCameraHeight = 0;
if (this.maxAngle >= 0.1) {
let cameraSmoothing = this.cameraHeightStiffness / (16.666 / delta);
cameraSmoothing = MathUtils.clamp(cameraSmoothing, 0, 1);
let cameraTerrainHeight = map.terrainHeightAt(this.manager.camera.position.x, this.manager.camera.position.z) || 0;
let cameraDelta = cameraTerrainHeight - this.cameraHeight;
this.cameraHeight += cameraDelta * cameraSmoothing;
if (Math.abs(cameraDelta) < 0.001) this.cameraHeight = cameraTerrainHeight;
let maxAngleHeight = Math.cos(this.maxAngle) * this.manager.distance;
this.minCameraHeight = this.cameraHeight - maxAngleHeight + 1;
}
// adjust targetHeight by distance
this.distanceTagretHeight = Math.max(MathUtils.lerp(this.targetHeight, 0, this.manager.distance / 500), 0);
}
getSuggestedHeight() {
return Math.max(this.distanceTagretHeight, this.minCameraHeight);
}
}

View File

@ -0,0 +1,99 @@
import {MathUtils} from "three";
import {KeyCombination} from "../../KeyCombination";
export class KeyAngleControls {
static KEYS = {
UP: [
new KeyCombination("ArrowUp", KeyCombination.ALT),
new KeyCombination("KeyW", KeyCombination.ALT),
new KeyCombination("PageUp")
],
DOWN: [
new KeyCombination("ArrowDown", KeyCombination.ALT),
new KeyCombination("KeyS", KeyCombination.ALT),
new KeyCombination("PageDown")
],
}
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.deltaAngle = 0;
this.up = false;
this.down = false;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
stop() {
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.up) this.deltaAngle -= 1;
if (this.down) this.deltaAngle += 1;
if (this.deltaAngle === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.angle += this.deltaAngle * smoothing * this.speed * delta * 0.06;
this.deltaAngle *= 1 - smoothing;
if (Math.abs(this.deltaAngle) < 0.0001) {
this.deltaAngle = 0;
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyDown = evt => {
if (KeyCombination.oneDown(evt, ...KeyAngleControls.KEYS.UP)){
this.up = true;
evt.preventDefault();
}
if (KeyCombination.oneDown(evt, ...KeyAngleControls.KEYS.DOWN)){
this.down = true;
evt.preventDefault();
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyUp = evt => {
if (KeyCombination.oneUp(evt, ...KeyAngleControls.KEYS.UP)){
this.up = false;
}
if (KeyCombination.oneUp(evt, ...KeyAngleControls.KEYS.DOWN)){
this.down = false;
}
}
}

View File

@ -0,0 +1,130 @@
import {MathUtils, Vector2} from "three";
import {VEC2_ZERO} from "../../../util/Utils";
import {KeyCombination} from "../../KeyCombination";
export class KeyMoveControls {
static KEYS = {
LEFT: [
new KeyCombination("ArrowLeft"),
new KeyCombination("KeyA")
],
UP: [
new KeyCombination("ArrowUp"),
new KeyCombination("KeyW")
],
RIGHT: [
new KeyCombination("ArrowRight"),
new KeyCombination("KeyD")
],
DOWN: [
new KeyCombination("ArrowDown"),
new KeyCombination("KeyS")
],
}
static temp_v2 = new Vector2();
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.deltaPosition = new Vector2();
this.up = false;
this.down = false;
this.left = false;
this.right = false;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
stop() {
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.up) this.deltaPosition.y -= 1;
if (this.down) this.deltaPosition.y += 1;
if (this.left) this.deltaPosition.x -= 1;
if (this.right) this.deltaPosition.x += 1;
if (this.deltaPosition.x === 0 && this.deltaPosition.y === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
let rotatedDelta = KeyMoveControls.temp_v2.copy(this.deltaPosition);
rotatedDelta.rotateAround(VEC2_ZERO, this.manager.rotation);
this.manager.position.x += rotatedDelta.x * smoothing * this.manager.distance * this.speed * delta * 0.06;
this.manager.position.z += rotatedDelta.y * smoothing * this.manager.distance * this.speed * delta * 0.06;
this.deltaPosition.multiplyScalar(1 - smoothing);
if (this.deltaPosition.lengthSq() < 0.0001) {
this.deltaPosition.set(0, 0);
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyDown = evt => {
if (KeyCombination.oneDown(evt, ...KeyMoveControls.KEYS.UP)){
this.up = true;
evt.preventDefault();
}
if (KeyCombination.oneDown(evt, ...KeyMoveControls.KEYS.DOWN)){
this.down = true;
evt.preventDefault();
}
if (KeyCombination.oneDown(evt, ...KeyMoveControls.KEYS.LEFT)){
this.left = true;
evt.preventDefault();
}
if (KeyCombination.oneDown(evt, ...KeyMoveControls.KEYS.RIGHT)){
this.right = true;
evt.preventDefault();
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyUp = evt => {
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.UP)){
this.up = false;
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.DOWN)){
this.down = false;
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.LEFT)){
this.left = false;
}
if (KeyCombination.oneUp(evt, ...KeyMoveControls.KEYS.RIGHT)){
this.right = false;
}
}
}

View File

@ -0,0 +1,99 @@
import {MathUtils} from "three";
import {KeyCombination} from "../../KeyCombination";
export class KeyRotateControls {
static KEYS = {
LEFT: [
new KeyCombination("ArrowLeft", KeyCombination.ALT),
new KeyCombination("KeyA", KeyCombination.ALT),
new KeyCombination("Delete"),
],
RIGHT: [
new KeyCombination("ArrowRight", KeyCombination.ALT),
new KeyCombination("KeyD", KeyCombination.ALT),
new KeyCombination("End"),
],
}
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.deltaRotation = 0;
this.left = false;
this.right = false;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
stop() {
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.left) this.deltaRotation += 1;
if (this.right) this.deltaRotation -= 1;
if (this.deltaRotation === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.rotation += this.deltaRotation * smoothing * this.speed * delta * 0.06;
this.deltaRotation *= 1 - smoothing;
if (Math.abs(this.deltaRotation) < 0.0001) {
this.deltaRotation = 0;
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyDown = evt => {
if (KeyCombination.oneDown(evt, ...KeyRotateControls.KEYS.LEFT)){
this.left = true;
evt.preventDefault();
}
if (KeyCombination.oneDown(evt, ...KeyRotateControls.KEYS.RIGHT)){
this.right = true;
evt.preventDefault();
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyUp = evt => {
if (KeyCombination.oneUp(evt, ...KeyRotateControls.KEYS.LEFT)){
this.left = false;
}
if (KeyCombination.oneUp(evt, ...KeyRotateControls.KEYS.RIGHT)){
this.right = false;
}
}
}

View File

@ -0,0 +1,95 @@
import {MathUtils} from "three";
import {KeyCombination} from "../../KeyCombination";
export class KeyZoomControls {
static KEYS = {
IN: [
new KeyCombination("NumpadAdd"),
],
OUT: [
new KeyCombination("NumpadSubtract"),
],
}
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.deltaZoom = 0;
this.in = false;
this.out = false;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
stop() {
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.in) this.deltaZoom += 1;
if (this.out) this.deltaZoom -= 1;
if (this.deltaZoom === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.distance *= Math.pow(1.5, this.deltaZoom * smoothing * this.speed * delta * 0.06);
this.deltaZoom *= 1 - smoothing;
if (Math.abs(this.deltaZoom) < 0.0001) {
this.deltaZoom = 0;
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyDown = evt => {
if (KeyCombination.oneDown(evt, ...KeyZoomControls.KEYS.IN)){
this.in = true;
evt.preventDefault();
}
if (KeyCombination.oneDown(evt, ...KeyZoomControls.KEYS.OUT)){
this.out = true;
evt.preventDefault();
}
}
/**
* @param evt {KeyboardEvent}
*/
onKeyUp = evt => {
if (KeyCombination.oneUp(evt, ...KeyZoomControls.KEYS.IN)){
this.in = false;
}
if (KeyCombination.oneUp(evt, ...KeyZoomControls.KEYS.OUT)){
this.out = false;
}
}
}

View File

@ -0,0 +1,94 @@
import {MathUtils} from "three";
export class MouseAngleControls {
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.moving = false;
this.lastY = 0;
this.deltaAngle = 0;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.target.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
}
stop() {
this.target.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("mousemove", this.onMouseMove);
window.removeEventListener("mouseup", this.onMouseUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaAngle === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.angle += this.deltaAngle * smoothing * this.speed;
this.deltaAngle *= 1 - smoothing;
if (Math.abs(this.deltaAngle) < 0.0001) {
this.deltaAngle = 0;
}
}
reset() {
this.deltaAngle = 0;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseDown = evt => {
if ((evt.buttons !== undefined ? evt.buttons === 2 : evt.button === 2) ||
((evt.altKey || evt.ctrlKey) && (evt.buttons !== undefined ? evt.buttons === 1 : evt.button === 0))) {
this.moving = true;
this.deltaAngle = 0;
this.lastY = evt.y;
}
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseMove = evt => {
if(this.moving){
this.deltaAngle -= evt.y - this.lastY;
}
this.lastY = evt.y;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseUp = evt => {
this.moving = false;
}
}

View File

@ -0,0 +1,102 @@
import {MathUtils, Vector2} from "three";
import {VEC2_ZERO} from "../../../util/Utils";
export class MouseMoveControls {
static tempVec2_1 = new Vector2();
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.moving = false;
this.lastPosition = new Vector2();
this.deltaPosition = new Vector2();
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.target.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
}
stop() {
this.target.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("mousemove", this.onMouseMove);
window.removeEventListener("mouseup", this.onMouseUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaPosition.x === 0 && this.deltaPosition.y === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
let directionDelta = MouseMoveControls.tempVec2_1.copy(this.deltaPosition);
directionDelta.rotateAround(VEC2_ZERO, this.manager.rotation);
this.manager.position.x += directionDelta.x * smoothing * this.manager.distance * this.speed;
this.manager.position.z += directionDelta.y * smoothing * this.manager.distance * this.speed;
this.deltaPosition.multiplyScalar(1 - smoothing);
if (this.deltaPosition.lengthSq() < 0.0001) {
this.deltaPosition.set(0, 0);
}
}
reset() {
this.deltaPosition.set(0, 0);
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseDown = evt => {
if ((evt.buttons !== undefined ? evt.buttons === 1 : evt.button === 0) && !evt.altKey) {
this.moving = true;
this.deltaPosition.set(0, 0);
this.lastPosition.set(evt.x, evt.y);
}
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseMove = evt => {
let position = MouseMoveControls.tempVec2_1.set(evt.x, evt.y);
if(this.moving){
this.deltaPosition.sub(position).add(this.lastPosition);
}
this.lastPosition.copy(position);
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseUp = evt => {
if (evt.button === 0) this.moving = false;
}
}

View File

@ -0,0 +1,94 @@
import {MathUtils} from "three";
export class MouseRotateControls {
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.moving = false;
this.lastX = 0;
this.deltaRotation = 0;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.target.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp);
}
stop() {
this.target.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("mousemove", this.onMouseMove);
window.removeEventListener("mouseup", this.onMouseUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaRotation === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.rotation += this.deltaRotation * smoothing * this.speed;
this.deltaRotation *= 1 - smoothing;
if (Math.abs(this.deltaRotation) < 0.0001) {
this.deltaRotation = 0;
}
}
reset() {
this.deltaRotation = 0;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseDown = evt => {
if ((evt.buttons !== undefined ? evt.buttons === 2 : evt.button === 2) ||
((evt.altKey || evt.ctrlKey) && (evt.buttons !== undefined ? evt.buttons === 1 : evt.button === 0))) {
this.moving = true;
this.deltaRotation = 0;
this.lastX = evt.x;
}
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseMove = evt => {
if(this.moving){
this.deltaRotation += evt.x - this.lastX;
}
this.lastX = evt.x;
}
/**
* @private
* @param evt {MouseEvent}
*/
onMouseUp = evt => {
this.moving = false;
}
}

View File

@ -0,0 +1,67 @@
import {MathUtils} from "three";
export class MouseZoomControls {
/**
* @param target {EventTarget}
* @param speed {number}
* @param stiffness {number}
*/
constructor(target, speed, stiffness) {
this.target = target;
this.manager = null;
this.stiffness = stiffness;
this.speed = speed;
this.deltaZoom = 0;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.target.addEventListener("wheel", this.onMouseWheel, {passive: true});
}
stop() {
this.target.removeEventListener("wheel", this.onMouseWheel);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaZoom === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.distance *= Math.pow(1.5, this.deltaZoom * smoothing * this.speed);
this.deltaZoom *= 1 - smoothing;
if (Math.abs(this.deltaZoom) < 0.0001) {
this.deltaZoom = 0;
}
}
reset() {
this.deltaZoom = 0;
}
/**
* @private
* @param evt {WheelEvent}
*/
onMouseWheel = evt => {
let delta = evt.deltaY;
if (evt.deltaMode === WheelEvent.DOM_DELTA_PIXEL) delta *= 0.01;
if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) delta *= 0.33;
this.deltaZoom += delta;
}
}

View File

@ -0,0 +1,93 @@
import {MathUtils} from "three";
export class TouchAngleControls {
/**
* @param hammer {Hammer.Manager}
* @param speed {number}
* @param stiffness {number}
*/
constructor(hammer, speed, stiffness) {
this.hammer = hammer;
this.manager = null;
this.moving = false;
this.lastY = 0;
this.deltaAngle = 0;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.hammer.on("tiltstart", this.onTouchDown);
this.hammer.on("tiltmove", this.onTouchMove);
this.hammer.on("tiltend", this.onTouchUp);
this.hammer.on("tiltcancel", this.onTouchUp);
}
stop() {
this.hammer.off("tiltstart", this.onTouchDown);
this.hammer.off("tiltmove", this.onTouchMove);
this.hammer.off("tiltend", this.onTouchUp);
this.hammer.off("tiltcancel", this.onTouchUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaAngle === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.angle += this.deltaAngle * smoothing * this.speed;
this.deltaAngle *= 1 - smoothing;
if (Math.abs(this.deltaAngle) < 0.0001) {
this.deltaAngle = 0;
}
}
reset() {
this.deltaAngle = 0;
}
/**
* @private
* @param evt {object}
*/
onTouchDown = evt => {
this.moving = true;
this.deltaAngle = 0;
this.lastY = evt.center.y;
}
/**
* @private
* @param evt {object}
*/
onTouchMove = evt => {
if(this.moving){
this.deltaAngle -= evt.center.y - this.lastY;
}
this.lastY = evt.center.y;
}
/**
* @private
* @param evt {object}
*/
onTouchUp = evt => {
this.moving = false;
}
}

View File

@ -0,0 +1,108 @@
import {MathUtils, Vector2} from "three";
import {VEC2_ZERO} from "../../../util/Utils";
export class TouchMoveControls {
static tempVec2_1 = new Vector2();
/**
* @param hammer {Hammer.Manager}
* @param speed {number}
* @param stiffness {number}
*/
constructor(hammer, speed, stiffness) {
this.hammer = hammer;
this.manager = null;
this.moving = false;
this.lastPosition = new Vector2();
this.deltaPosition = new Vector2();
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.hammer.on("movestart", this.onTouchDown);
this.hammer.on("movemove", this.onTouchMove);
this.hammer.on("moveend", this.onTouchUp);
this.hammer.on("movecancel", this.onTouchUp);
}
stop() {
this.hammer.off("movestart", this.onTouchDown);
this.hammer.off("movemove", this.onTouchMove);
this.hammer.off("moveend", this.onTouchUp);
this.hammer.off("movecancel", this.onTouchUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaPosition.x === 0 && this.deltaPosition.y === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
let directionDelta = TouchMoveControls.tempVec2_1.copy(this.deltaPosition);
directionDelta.rotateAround(VEC2_ZERO, this.manager.rotation);
this.manager.position.x += directionDelta.x * smoothing * this.manager.distance * this.speed;
this.manager.position.z += directionDelta.y * smoothing * this.manager.distance * this.speed;
this.deltaPosition.multiplyScalar(1 - smoothing);
if (this.deltaPosition.lengthSq() < 0.0001) {
this.deltaPosition.set(0, 0);
}
}
reset() {
this.deltaPosition.set(0, 0);
}
/**
* @private
* @param evt {object}
*/
onTouchDown = evt => {
if (evt.pointerType === "mouse") return;
this.moving = true;
this.deltaPosition.set(0, 0);
this.lastPosition.set(evt.center.x, evt.center.y);
}
/**
* @private
* @param evt {object}
*/
onTouchMove = evt => {
if (evt.pointerType === "mouse") return;
let position = TouchMoveControls.tempVec2_1.set(evt.center.x, evt.center.y);
if(this.moving){
this.deltaPosition.sub(position).add(this.lastPosition);
}
this.lastPosition.copy(position);
}
/**
* @private
* @param evt {object}
*/
onTouchUp = evt => {
if (evt.pointerType === "mouse") return;
this.moving = false;
}
}

View File

@ -0,0 +1,97 @@
import {MathUtils} from "three";
export class TouchRotateControls {
/**
* @param hammer {Hammer.Manager}
* @param speed {number}
* @param stiffness {number}
*/
constructor(hammer, speed, stiffness) {
this.hammer = hammer;
this.manager = null;
this.moving = false;
this.lastRotation = 0;
this.deltaRotation = 0;
this.speed = speed;
this.stiffness = stiffness;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.hammer.on("rotatestart", this.onTouchDown);
this.hammer.on("rotatemove", this.onTouchMove);
this.hammer.on("rotateend", this.onTouchUp);
this.hammer.on("rotatecancel", this.onTouchUp);
}
stop() {
this.hammer.off("rotatestart", this.onTouchDown);
this.hammer.off("rotatemove", this.onTouchMove);
this.hammer.off("rotateend", this.onTouchUp);
this.hammer.off("rotatecancel", this.onTouchUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaRotation === 0) return;
let smoothing = this.stiffness / (16.666 / delta);
smoothing = MathUtils.clamp(smoothing, 0, 1);
this.manager.rotation += this.deltaRotation * smoothing * this.speed;
this.deltaRotation *= 1 - smoothing;
if (Math.abs(this.deltaRotation) < 0.0001) {
this.deltaRotation = 0;
}
}
reset() {
this.deltaRotation = 0;
}
/**
* @private
* @param evt {object}
*/
onTouchDown = evt => {
this.moving = true;
this.deltaRotation = 0;
this.lastRotation = evt.rotation;
}
/**
* @private
* @param evt {object}
*/
onTouchMove = evt => {
if(this.moving){
let delta = evt.rotation - this.lastRotation;
if (delta > 180) delta -= 360;
if (delta < -180) delta += 360;
this.deltaRotation -= delta;
}
this.lastRotation = evt.rotation;
}
/**
* @private
* @param evt {object}
*/
onTouchUp = evt => {
this.moving = false;
}
}

View File

@ -0,0 +1,80 @@
import {MathUtils} from "three";
export class TouchZoomControls {
/**
* @param hammer {Hammer.Manager}
*/
constructor(hammer) {
this.hammer = hammer;
this.manager = null;
this.moving = false;
this.deltaZoom = 1;
this.lastZoom = 1;
}
/**
* @param manager {ControlsManager}
*/
start(manager) {
this.manager = manager;
this.hammer.on("zoomstart", this.onTouchDown);
this.hammer.on("zoommove", this.onTouchMove);
this.hammer.on("zoomend", this.onTouchUp);
this.hammer.on("zoomcancel", this.onTouchUp);
}
stop() {
this.hammer.off("zoomstart", this.onTouchDown);
this.hammer.off("zoommove", this.onTouchMove);
this.hammer.off("zoomend", this.onTouchUp);
this.hammer.off("zoomcancel", this.onTouchUp);
}
/**
* @param delta {number}
* @param map {Map}
*/
update(delta, map) {
if (this.deltaZoom === 1) return;
this.manager.distance /= this.deltaZoom;
this.deltaZoom = 1;
}
reset() {
this.deltaZoom = 1;
}
/**
* @private
* @param evt {object}
*/
onTouchDown = evt => {
this.moving = true;
this.lastZoom = 1;
}
/**
* @private
* @param evt {object}
*/
onTouchMove = evt => {
if(this.moving){
this.deltaZoom *= evt.scale / this.lastZoom;
}
this.lastZoom = evt.scale;
}
/**
* @private
* @param evt {object}
*/
onTouchUp = evt => {
this.moving = false;
}
}

View File

@ -84,7 +84,7 @@ export class LabelPopup extends CSS2DObject {
if (autoClose) {
let removeHandler = evt => {
if (evt.path.includes(this.element)) return;
if (evt.composedPath().includes(this.element)) return;
inAnimation.cancel();
this.close();

View File

@ -47,8 +47,8 @@ export class CombinedCamera extends PerspectiveCamera {
if ( skew !== 0 ) left += near * skew / this.getFilmWidth();
// this part different to PerspectiveCamera
let normalizedOrtho = -Math.pow(this.ortho - 1, 4) + 1;
let orthoTop = this.distance * Math.tan( MathUtils.DEG2RAD * 0.5 * this.fov ) / this.zoom;
let normalizedOrtho = -Math.pow(this.ortho - 1, 6) + 1;
let orthoTop = Math.max(this.distance, 0.0001) * Math.tan( MathUtils.DEG2RAD * 0.5 * this.fov ) / this.zoom;
let orthoHeight = 2 * orthoTop;
let orthoWidth = this.aspect * orthoHeight;
let orthoLeft = - 0.5 * orthoWidth;

View File

@ -3,8 +3,19 @@
* @param string {string}
* @returns {HTMLElement}
*/
import {MathUtils} from "three";
import {MathUtils, Vector2, Vector3} from "three";
export const VEC2_ZERO = new Vector2(0, 0);
export const VEC3_ZERO = new Vector3(0, 0, 0);
export const VEC3_X = new Vector3(1, 0, 0);
export const VEC3_Y = new Vector3(0, 1, 0);
export const VEC3_Z = new Vector3(0, 0, 1);
/**
* Converts a url-encoded image string to an actual image-element
* @param string {string}
* @returns {HTMLElement}
*/
export const stringToImage = string => {
let image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img');
image.src = string;
@ -269,3 +280,73 @@ export const deepEquals = (object1, object2) => {
return true;
}
/**
* Adds one listener to multiple events
* @param target {EventTarget}
* @param types {string|string[]}
* @param listener {EventListenerOrEventListenerObject | null}
* @param options {boolean | AddEventListenerOptions?}
*/
export const addEventListeners = (target, types, listener, options) => {
if (!Array.isArray(types)){
types = types.trim().split(" ");
}
types.forEach(type => target.addEventListener(type, listener, options));
}
/**
* Removes one listener to multiple events
* @param target {EventTarget}
* @param types {string|string[]}
* @param listener {EventListenerOrEventListenerObject | null}
* @param options {boolean | EventListenerOptions?}
*/
export const removeEventListeners = (target, types, listener, options) => {
if (!Array.isArray(types)){
types = types.trim().split(" ");
}
types.forEach(type => target.removeEventListener(type, listener, options));
}
/**
* Softly clamps towards a minimum value
* @param value {number}
* @param min {number}
* @param stiffness {number}
* @returns {number}
*/
export const softMin = (value, min, stiffness) => {
if (value >= min) return value;
let delta = min - value;
if (delta < 0.0001) return min;
return value + delta * stiffness;
}
/**
* Softly clamps towards a maximum value
* @param value {number}
* @param max {number}
* @param stiffness {number}
* @returns {number}
*/
export const softMax = (value, max, stiffness) => {
if (value <= max) return value;
let delta = value - max;
if (delta < 0.0001) return max;
return value - delta * stiffness;
}
/**
* Softly clamps towards a minimum and maximum value
* @param value {number}
* @param min {number}
* @param max {number}
* @param stiffness {number}
* @returns {number}
*/
export const softClamp = (value, min, max, stiffness) => {
return softMax(softMin(value, min, stiffness), max, stiffness);
}