File: /home/creaqbdc/public_html/wp-content/uploads/ac_assets/morph_slider/vendor/ogl/src/extras/Orbit.js
// Based from ThreeJS' OrbitControls class, rewritten using es6 with some additions and subtractions.
// TODO: abstract event handlers so can be fed from other sources
// TODO: make scroll zoom more accurate than just >/< zero
// TODO: be able to pass in new camera position
import { Vec3 } from '../math/Vec3.js';
import { Vec2 } from '../math/Vec2.js';
const STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, DOLLY_PAN: 3 };
const tempVec3 = new Vec3();
const tempVec2a = new Vec2();
const tempVec2b = new Vec2();
export function Orbit(
object,
{
element = document,
enabled = true,
target = new Vec3(),
ease = 0.25,
inertia = 0.85,
enableRotate = true,
rotateSpeed = 0.1,
autoRotate = false,
autoRotateSpeed = 1.0,
enableZoom = true,
zoomSpeed = 1,
zoomStyle = 'dolly',
enablePan = true,
panSpeed = 0.1,
minPolarAngle = 0,
maxPolarAngle = Math.PI,
minAzimuthAngle = -Infinity,
maxAzimuthAngle = Infinity,
minDistance = 0,
maxDistance = Infinity,
} = {}
) {
this.enabled = enabled;
this.target = target;
this.zoomStyle = zoomStyle;
// Catch attempts to disable - set to 1 so has no effect
ease = ease || 1;
inertia = inertia || 0;
this.minDistance = minDistance;
this.maxDistance = maxDistance;
// current position in sphericalTarget coordinates
const sphericalDelta = { radius: 1, phi: 0, theta: 0 };
const sphericalTarget = { radius: 1, phi: 0, theta: 0 };
const spherical = { radius: 1, phi: 0, theta: 0 };
const panDelta = new Vec3();
// Grab initial position values
const offset = new Vec3();
offset.copy(object.position).sub(this.target);
spherical.radius = sphericalTarget.radius = offset.distance();
spherical.theta = sphericalTarget.theta = Math.atan2(offset.x, offset.z);
spherical.phi = sphericalTarget.phi = Math.acos(Math.min(Math.max(offset.y / sphericalTarget.radius, -1), 1));
this.offset = offset;
this.update = () => {
if (autoRotate) {
handleAutoRotate();
}
// apply delta
sphericalTarget.radius *= sphericalDelta.radius;
sphericalTarget.theta += sphericalDelta.theta;
sphericalTarget.phi += sphericalDelta.phi;
// apply boundaries
sphericalTarget.theta = Math.max(minAzimuthAngle, Math.min(maxAzimuthAngle, sphericalTarget.theta));
sphericalTarget.phi = Math.max(minPolarAngle, Math.min(maxPolarAngle, sphericalTarget.phi));
sphericalTarget.radius = Math.max(this.minDistance, Math.min(this.maxDistance, sphericalTarget.radius));
// ease values
spherical.phi += (sphericalTarget.phi - spherical.phi) * ease;
spherical.theta += (sphericalTarget.theta - spherical.theta) * ease;
spherical.radius += (sphericalTarget.radius - spherical.radius) * ease;
// apply pan to target. As offset is relative to target, it also shifts
this.target.add(panDelta);
// apply rotation to offset
let sinPhiRadius = spherical.radius * Math.sin(Math.max(0.000001, spherical.phi));
offset.x = sinPhiRadius * Math.sin(spherical.theta);
offset.y = spherical.radius * Math.cos(spherical.phi);
offset.z = sinPhiRadius * Math.cos(spherical.theta);
// Apply updated values to object
object.position.copy(this.target).add(offset);
object.lookAt(this.target);
// Apply inertia to values
sphericalDelta.theta *= inertia;
sphericalDelta.phi *= inertia;
panDelta.multiply(inertia);
// Reset scale every frame to avoid applying scale multiple times
sphericalDelta.radius = 1;
};
// Updates internals with new position
this.forcePosition = () => {
offset.copy(object.position).sub(this.target);
spherical.radius = sphericalTarget.radius = offset.distance();
spherical.theta = sphericalTarget.theta = Math.atan2(offset.x, offset.z);
spherical.phi = sphericalTarget.phi = Math.acos(Math.min(Math.max(offset.y / sphericalTarget.radius, -1), 1));
object.lookAt(this.target);
};
// Everything below here just updates panDelta and sphericalDelta
// Using those two objects' values, the orbit is calculated
const rotateStart = new Vec2();
const panStart = new Vec2();
const dollyStart = new Vec2();
let state = STATE.NONE;
this.mouseButtons = { ORBIT: 0, ZOOM: 1, PAN: 2 };
function getZoomScale() {
return Math.pow(0.95, zoomSpeed);
}
function panLeft(distance, m) {
tempVec3.set(m[0], m[1], m[2]);
tempVec3.multiply(-distance);
panDelta.add(tempVec3);
}
function panUp(distance, m) {
tempVec3.set(m[4], m[5], m[6]);
tempVec3.multiply(distance);
panDelta.add(tempVec3);
}
const pan = (deltaX, deltaY) => {
let el = element === document ? document.body : element;
tempVec3.copy(object.position).sub(this.target);
let targetDistance = tempVec3.distance();
targetDistance *= Math.tan((((object.fov || 45) / 2) * Math.PI) / 180.0);
panLeft((2 * deltaX * targetDistance) / el.clientHeight, object.matrix);
panUp((2 * deltaY * targetDistance) / el.clientHeight, object.matrix);
};
const dolly = (dollyScale) => {
if (this.zoomStyle === 'dolly') sphericalDelta.radius /= dollyScale;
else {
object.fov /= dollyScale;
if (object.type === 'orthographic') object.orthographic();
else object.perspective();
}
};
function handleAutoRotate() {
const angle = ((2 * Math.PI) / 60 / 60) * autoRotateSpeed;
sphericalDelta.theta -= angle;
}
function handleMoveRotate(x, y) {
tempVec2a.set(x, y);
tempVec2b.sub(tempVec2a, rotateStart).multiply(rotateSpeed);
let el = element === document ? document.body : element;
sphericalDelta.theta -= (2 * Math.PI * tempVec2b.x) / el.clientHeight;
sphericalDelta.phi -= (2 * Math.PI * tempVec2b.y) / el.clientHeight;
rotateStart.copy(tempVec2a);
}
function handleMouseMoveDolly(e) {
tempVec2a.set(e.clientX, e.clientY);
tempVec2b.sub(tempVec2a, dollyStart);
if (tempVec2b.y > 0) {
dolly(getZoomScale());
} else if (tempVec2b.y < 0) {
dolly(1 / getZoomScale());
}
dollyStart.copy(tempVec2a);
}
function handleMovePan(x, y) {
tempVec2a.set(x, y);
tempVec2b.sub(tempVec2a, panStart).multiply(panSpeed);
pan(tempVec2b.x, tempVec2b.y);
panStart.copy(tempVec2a);
}
function handleTouchStartDollyPan(e) {
if (enableZoom) {
let dx = e.touches[0].pageX - e.touches[1].pageX;
let dy = e.touches[0].pageY - e.touches[1].pageY;
let distance = Math.sqrt(dx * dx + dy * dy);
dollyStart.set(0, distance);
}
if (enablePan) {
let x = 0.5 * (e.touches[0].pageX + e.touches[1].pageX);
let y = 0.5 * (e.touches[0].pageY + e.touches[1].pageY);
panStart.set(x, y);
}
}
function handleTouchMoveDollyPan(e) {
if (enableZoom) {
let dx = e.touches[0].pageX - e.touches[1].pageX;
let dy = e.touches[0].pageY - e.touches[1].pageY;
let distance = Math.sqrt(dx * dx + dy * dy);
tempVec2a.set(0, distance);
tempVec2b.set(0, Math.pow(tempVec2a.y / dollyStart.y, zoomSpeed));
dolly(tempVec2b.y);
dollyStart.copy(tempVec2a);
}
if (enablePan) {
let x = 0.5 * (e.touches[0].pageX + e.touches[1].pageX);
let y = 0.5 * (e.touches[0].pageY + e.touches[1].pageY);
handleMovePan(x, y);
}
}
const onMouseDown = (e) => {
if (!this.enabled) return;
switch (e.button) {
case this.mouseButtons.ORBIT:
if (enableRotate === false) return;
rotateStart.set(e.clientX, e.clientY);
state = STATE.ROTATE;
break;
case this.mouseButtons.ZOOM:
if (enableZoom === false) return;
dollyStart.set(e.clientX, e.clientY);
state = STATE.DOLLY;
break;
case this.mouseButtons.PAN:
if (enablePan === false) return;
panStart.set(e.clientX, e.clientY);
state = STATE.PAN;
break;
}
if (state !== STATE.NONE) {
window.addEventListener('mousemove', onMouseMove, false);
window.addEventListener('mouseup', onMouseUp, false);
}
};
const onMouseMove = (e) => {
if (!this.enabled) return;
switch (state) {
case STATE.ROTATE:
if (enableRotate === false) return;
handleMoveRotate(e.clientX, e.clientY);
break;
case STATE.DOLLY:
if (enableZoom === false) return;
handleMouseMoveDolly(e);
break;
case STATE.PAN:
if (enablePan === false) return;
handleMovePan(e.clientX, e.clientY);
break;
}
};
const onMouseUp = () => {
window.removeEventListener('mousemove', onMouseMove, false);
window.removeEventListener('mouseup', onMouseUp, false);
state = STATE.NONE;
};
const onMouseWheel = (e) => {
if (!this.enabled || !enableZoom || (state !== STATE.NONE && state !== STATE.ROTATE)) return;
e.stopPropagation();
e.preventDefault();
if (e.deltaY < 0) {
dolly(1 / getZoomScale());
} else if (e.deltaY > 0) {
dolly(getZoomScale());
}
};
const onTouchStart = (e) => {
if (!this.enabled) return;
e.preventDefault();
switch (e.touches.length) {
case 1:
if (enableRotate === false) return;
rotateStart.set(e.touches[0].pageX, e.touches[0].pageY);
state = STATE.ROTATE;
break;
case 2:
if (enableZoom === false && enablePan === false) return;
handleTouchStartDollyPan(e);
state = STATE.DOLLY_PAN;
break;
default:
state = STATE.NONE;
}
};
const onTouchMove = (e) => {
if (!this.enabled) return;
e.preventDefault();
e.stopPropagation();
switch (e.touches.length) {
case 1:
if (enableRotate === false) return;
handleMoveRotate(e.touches[0].pageX, e.touches[0].pageY);
break;
case 2:
if (enableZoom === false && enablePan === false) return;
handleTouchMoveDollyPan(e);
break;
default:
state = STATE.NONE;
}
};
const onTouchEnd = () => {
if (!this.enabled) return;
state = STATE.NONE;
};
const onContextMenu = (e) => {
if (!this.enabled) return;
e.preventDefault();
};
function addHandlers() {
element.addEventListener('contextmenu', onContextMenu, false);
element.addEventListener('mousedown', onMouseDown, false);
element.addEventListener('wheel', onMouseWheel, { passive: false });
element.addEventListener('touchstart', onTouchStart, { passive: false });
element.addEventListener('touchend', onTouchEnd, false);
element.addEventListener('touchmove', onTouchMove, { passive: false });
}
this.remove = function () {
element.removeEventListener('contextmenu', onContextMenu);
element.removeEventListener('mousedown', onMouseDown);
element.removeEventListener('wheel', onMouseWheel);
element.removeEventListener('touchstart', onTouchStart);
element.removeEventListener('touchend', onTouchEnd);
element.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
addHandlers();
}