webgl-3d-viewer
Version:
A WebGL ThreeJS STL viewer implementation written in ES6
416 lines (300 loc) • 10.9 kB
JavaScript
import merge from 'lodash/object/merge';
import sample from 'lodash/collection/sample';
import round from 'lodash/math/round';
import OrbitControls from './OrbitControls';
import TWEEN from 'tween.js';
export default class ModelControls {
constructor(container, camera, group, config = {}) {
this.container = container;
this.camera = camera;
this.group = group;
this._animations = [];
this._zoomTween = null;
this._startUpAnimations = [
{
easing: TWEEN.Easing.Quintic.Out,
to: {x: (5 * 360 + 45)},
time: 3500
},
{
easing: TWEEN.Easing.Exponential.InOut,
to: {x: (3 * 360 + 45)},
time: 2500
},
{
easing: TWEEN.Easing.Elastic.Out,
to: {x: (3 * 360 + 45)},
time: 5500
},
{
easing: TWEEN.Easing.Circular.In,
to: {x: (4 * 360 + 45)},
time: 2500
},
{
easing: TWEEN.Easing.Quintic.InOut,
to: {x: (3 * 360 + 45)},
time: 2500
},
{
easing: TWEEN.Easing.Exponential.Out,
to: {x: (4 * 360 + 45)},
time: 2800
}
];
// Default configuration params
this.controlsConfigDefault = {
startupAnimation: true,
targetRotationX: 0,
targetRotationOnMouseDownX: 0,
targetRotationY: 0,
targetRotationOnMouseDownY: 0,
mouseX: 0,
mouseXOnMouseDown: 0,
mouseY: 0,
mouseYOnMouseDown: 0,
clientHalfX: (this.container.clientWidth / 2),
clientHalfY: (this.container.clientHeight / 2),
finalRotationY: null,
currentRotationX: 0,
currentRotationY: 0,
zoom: false
};
this.controlsConfig = merge({}, this.controlsConfigDefault, config);
this._init();
if (this.controlsConfig.startupAnimation === true) {
this._createStartUpAnimation();
} else {
this.group.rotation.y = this.controlsConfig.currentRotationY;
this.group.rotation.x = this.controlsConfig.currentRotationX;
}
}
update(time) {
const group = this.group;
// Do tweening
if (this.controlsConfig.startupAnimation === true ) {
TWEEN.update(time);
}
if (group) {
const cfg = this.controlsConfig;
group.rotation.y += round(((cfg.targetRotationX - group.rotation.y ) * 0.1), 2);
// vertical rotation
cfg.finalRotationY = round((cfg.targetRotationY - group.rotation.x), 2);
group.rotation.x += round((cfg.finalRotationY * 0.05), 2);
this.controlsConfig.currentRotationX = group.rotation.x;
this.controlsConfig.currentRotationY = group.rotation.y;
}
}
zoomIn(scale = 2) {
TWEEN.remove(this._zoomTween);
const cam = this.camera;
const dollyScale = Math.pow(0.95, scale);
const minZoom = this.controls.minZoom;
const maxZoom = this.controls.maxZoom;
const zoomLvl = Math.max( minZoom, Math.min( maxZoom, cam.zoom / dollyScale ) );
this._dollyZoom(zoomLvl);
}
zoomOut(scale = 2) {
TWEEN.remove(this._zoomTween);
const cam = this.camera;
const dollyScale = Math.pow(0.95, scale);
const minZoom = this.controls.minZoom;
const maxZoom = this.controls.maxZoom;
const zoomLvl = Math.max( minZoom, Math.min( maxZoom, cam.zoom * dollyScale ) );
this._dollyZoom(zoomLvl);
}
_dollyZoom(zoomLvl) {
const cam = this.camera;
const tween = new TWEEN.Tween({zoom: cam.zoom });
tween.to({ zoom: zoomLvl }, 300);
tween.onUpdate(function() {
cam.zoom = this.zoom;
cam.updateProjectionMatrix();
});
tween.start();
this._zoomTween = tween;
return tween;
}
destroy() {
this._removeEventListener();
// Remove orbit control
if (this.controls) {
this.controls.dispose();
}
this._clearAnimations();
this._animations = [];
}
_clearAnimations() {
if (this._animations && this._animations.length > 0) {
while (this._animations.length > 0) {
TWEEN.remove(this._animations.pop());
}
}
TWEEN.remove(this._zoomTween);
}
_createStartUpAnimation() {
const {targetRotationY, targetRotationX} = this.controlsConfig;
const self = this;
const coords = {
x: targetRotationX / Math.PI * 180,
y: targetRotationY / Math.PI * 180
};
this._clearAnimations();
const anim = sample(this._startUpAnimations);
let tween1 = new TWEEN.Tween(coords)
.easing(anim.easing)
.to(anim.to, anim.time)
.onUpdate(function() {
self.controlsConfig.targetRotationX = this.x * Math.PI / 180;
})
.start();
this._animations.push(tween1);
let tween2 = new TWEEN.Tween(coords)
.easing(TWEEN.Easing.Exponential.Out)
.to({ y: 45 }, 1500)
.onUpdate(function() {
self.controlsConfig.targetRotationY = this.y * Math.PI / 180;
})
.start();
this._animations.push(tween2);
}
set zoom(val) {
this.controls.enableZoom = val;
this.controlsConfig.zoom = val;
}
get zoom() {
return this.controlsConfig;
}
_init() {
// Clean registered event listener
this._removeEventListener();
// this.controlsConfig = merge({}, this.controlsConfigDefault);
this._setupListener();
// Delecate to orbit controls
const controls = new OrbitControls(this.camera, this.container);
controls.enableKeys = false;
controls.enableRotate = false;
controls.enablePan = false;
controls.enableDamping = false;
controls.enableZoom = this.controlsConfig.zoom;
let bb = new THREE.Box3();
bb.setFromObject(this.group);
bb.center(controls.target);
this.controls = controls;
}
_setupListener() {
const container = this.container;
// Add resize listener
this._resizeListener = (evt)=>{ this._onWindowResize(evt); };
window.addEventListener('resize', this._resizeListener, false);
// Controls
this._mouseDownListener = (e) => { this._onMouseDown(e); };
this._mouseMoveListener = (e) => { this._onMouseMove(e); };
this._mouseUpListener = (e) => { this._onMouseUp(e); };
this._mouseOutListener = (e) => { this._onMouseOut(e); };
this._touchStartListener = (e) => { this._onTouchStart(e); };
this._touchEndListener = (e) => { this._onTouchEnd(e); };
this._touchMoveListener = (e) => { this._onTouchMove(e); };
// Mouse / Touch events
container.addEventListener( 'mousedown', this._mouseDownListener, false );
container.addEventListener( 'touchstart', this._touchStartListener, false );
container.addEventListener( 'touchmove', this._touchMoveListener, false );
}
_removeEventListener() {
const container = this.container;
// Remove resize listener
window.removeEventListener('resize', this._resizeListener, false);
this._resizeListener = null;
// Remove model spinning controls
container.removeEventListener( 'mouseup', this._mouseUpListener, false );
container.removeEventListener( 'mousemove', this._mouseMoveListener, false );
container.removeEventListener( 'mousedown', this._mouseDownListener, false );
container.removeEventListener( 'touchstart', this._touchStartListener, false );
container.removeEventListener( 'touchmove', this._touchMoveListener, false );
}
_onWindowResize() {
['controlsConfig', 'controlsConfigDefault'].forEach((key)=>{
if (this.hasOwnProperty(key)) {
this[key].clientHalfX = (this.container.clientWidth / 2);
this[key].clientHalfY = (this.container.clientHeight / 2);
}
});
}
_onMouseDown(evt) {
evt.preventDefault();
const container = this.container;
const cfg = this.controlsConfig;
if (container) {
const { clientX, clientY } = evt;
this._clearAnimations();
container.addEventListener('mousemove', this._mouseMoveListener, false);
container.addEventListener('mouseup', this._mouseUpListener, false);
container.addEventListener('mouseout', this._mouseOutListener, false);
cfg.mouseXOnMouseDown = clientX - cfg.clientHalfX;
cfg.targetRotationOnMouseDownX = cfg.targetRotationX;
cfg.mouseYOnMouseDown = clientY - cfg.clientHalfY;
cfg.targetRotationOnMouseDownY = cfg.targetRotationY;
}
}
_onMouseMove(evt) {
if (evt) {
const cfg = this.controlsConfig;
cfg.mouseX = evt.clientX - cfg.clientHalfX;
cfg.mouseY = evt.clientY - cfg.clientHalfY;
cfg.targetRotationY = cfg.targetRotationOnMouseDownY + (cfg.mouseY - cfg.mouseYOnMouseDown) * 0.02;
cfg.targetRotationX = cfg.targetRotationOnMouseDownX + (cfg.mouseX - cfg.mouseXOnMouseDown) * 0.02;
}
}
_onMouseUp() {
const {container} = this;
if (container) {
container.removeEventListener( 'mousemove', this._mouseMoveListener, false );
container.removeEventListener( 'mouseup', this._mouseUpListener, false );
container.removeEventListener( 'mouseout', this._mouseOutListener, false );
}
}
_onMouseOut() {
const {container} = this;
if (container) {
container.removeEventListener('mousemove', this._mouseMoveListener, false);
container.removeEventListener('mouseup', this._mouseUpListener, false);
container.removeEventListener('mouseout', this._mouseOutListener, false);
}
}
_onTouchStart(evt = {touches: [] }) {
const touches = evt.touches;
const cfg = this.controlsConfig;
if (touches.length === 1) {
evt.preventDefault();
const {pageX, pageY} = touches[0];
cfg.mouseXOnMouseDown = pageX - cfg.clientHalfX;
cfg.targetRotationOnMouseDownX = cfg.targetRotationX;
cfg.mouseYOnMouseDown = pageY - cfg.clientHalfY;
cfg.targetRotationOnMouseDownY = cfg.targetRotationY;
}
}
_onTouchEnd(evt = {touches: [] }) {
const touches = evt.touches;
const cfg = this.controlsConfig;
if (touches.length === 1) {
evt.preventDefault();
const {pageX, pageY} = touches[0];
cfg.mouseX = pageX - cfg.clientHalfX;
cfg.targetRotationX = cfg.targetRotationOnMouseDownX + ( cfg.mouseX - cfg.mouseXOnMouseDown ) * 0.05;
cfg.mouseY = pageY - cfg.clientHalfY;
cfg.targetRotationY = cfg.targetRotationOnMouseDownY + (cfg.mouseY - cfg.mouseYOnMouseDown) * 0.05;
}
}
_onTouchMove(evt = {touches: [] }) {
const touches = evt.touches;
const cfg = this.controlsConfig;
if (touches.length === 1 ) {
evt.preventDefault();
const {pageX, pageY} = touches[0];
cfg.mouseX = pageX - cfg.clientHalfX;
cfg.targetRotationX = cfg.targetRotationOnMouseDownX + ( cfg.mouseX - cfg.mouseXOnMouseDown ) * 0.05;
cfg.mouseY = pageY - cfg.clientHalfY;
cfg.targetRotationY = cfg.targetRotationOnMouseDownY + (cfg.mouseY - cfg.mouseYOnMouseDown) * 0.05;
}
}
}