1/**
2 * @author Eberhard Graether / http://egraether.com/
3 * @author Mark Lundin 	/ http://mark-lundin.com
4 * @author Simone Manini / http://daron1337.github.io
5 * @author Luca Antiga 	/ http://lantiga.github.io
6 */
7
8THREE.TrackballControls = function ( object, domElement ) {
9
10	var _this = this;
11	var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };
12
13	this.object = object;
14	this.domElement = ( domElement !== undefined ) ? domElement : document;
15
16	// API
17
18	this.enabled = true;
19
20	this.screen = { left: 0, top: 0, width: 0, height: 0 };
21
22	this.rotateSpeed = 1.0;
23	this.zoomSpeed = 1.2;
24	this.panSpeed = 0.3;
25
26	this.noRotate = false;
27	this.noZoom = false;
28	this.noPan = false;
29
30	this.staticMoving = false;
31	this.dynamicDampingFactor = 0.2;
32
33	this.minDistance = 0;
34	this.maxDistance = Infinity;
35
36	this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ];
37
38	// internals
39
40	this.target = new THREE.Vector3();
41
42	var EPS = 0.000001;
43
44	var lastPosition = new THREE.Vector3();
45
46	var _state = STATE.NONE,
47		_prevState = STATE.NONE,
48
49		_eye = new THREE.Vector3(),
50
51		_movePrev = new THREE.Vector2(),
52		_moveCurr = new THREE.Vector2(),
53
54		_lastAxis = new THREE.Vector3(),
55		_lastAngle = 0,
56
57		_zoomStart = new THREE.Vector2(),
58		_zoomEnd = new THREE.Vector2(),
59
60		_touchZoomDistanceStart = 0,
61		_touchZoomDistanceEnd = 0,
62
63		_panStart = new THREE.Vector2(),
64		_panEnd = new THREE.Vector2();
65
66	// for reset
67
68	this.target0 = this.target.clone();
69	this.position0 = this.object.position.clone();
70	this.up0 = this.object.up.clone();
71
72	// events
73
74	var changeEvent = { type: 'change' };
75	var startEvent = { type: 'start' };
76	var endEvent = { type: 'end' };
77
78
79	// methods
80
81	this.handleResize = function () {
82
83		if ( this.domElement === document ) {
84
85			this.screen.left = 0;
86			this.screen.top = 0;
87			this.screen.width = window.innerWidth;
88			this.screen.height = window.innerHeight;
89
90		} else {
91
92			var box = this.domElement.getBoundingClientRect();
93			// adjustments come from similar code in the jquery offset() function
94			var d = this.domElement.ownerDocument.documentElement;
95			this.screen.left = box.left + window.pageXOffset - d.clientLeft;
96			this.screen.top = box.top + window.pageYOffset - d.clientTop;
97			this.screen.width = box.width;
98			this.screen.height = box.height;
99
100		}
101
102	};
103
104	var getMouseOnScreen = ( function () {
105
106		var vector = new THREE.Vector2();
107
108		return function getMouseOnScreen( pageX, pageY ) {
109
110			vector.set(
111				( pageX - _this.screen.left ) / _this.screen.width,
112				( pageY - _this.screen.top ) / _this.screen.height
113			);
114
115			return vector;
116
117		};
118
119	}() );
120
121	var getMouseOnCircle = ( function () {
122
123		var vector = new THREE.Vector2();
124
125		return function getMouseOnCircle( pageX, pageY ) {
126
127			vector.set(
128				( ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / ( _this.screen.width * 0.5 ) ),
129				( ( _this.screen.height + 2 * ( _this.screen.top - pageY ) ) / _this.screen.width ) // screen.width intentional
130			);
131
132			return vector;
133
134		};
135
136	}() );
137
138	this.rotateCamera = ( function () {
139
140		var axis = new THREE.Vector3(),
141			quaternion = new THREE.Quaternion(),
142			eyeDirection = new THREE.Vector3(),
143			objectUpDirection = new THREE.Vector3(),
144			objectSidewaysDirection = new THREE.Vector3(),
145			moveDirection = new THREE.Vector3(),
146			angle;
147
148		return function rotateCamera() {
149
150			moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 );
151			angle = moveDirection.length();
152
153			if ( angle ) {
154
155				_eye.copy( _this.object.position ).sub( _this.target );
156
157				eyeDirection.copy( _eye ).normalize();
158				objectUpDirection.copy( _this.object.up ).normalize();
159				objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize();
160
161				objectUpDirection.setLength( _moveCurr.y - _movePrev.y );
162				objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x );
163
164				moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) );
165
166				axis.crossVectors( moveDirection, _eye ).normalize();
167
168				angle *= _this.rotateSpeed;
169				quaternion.setFromAxisAngle( axis, angle );
170
171				_eye.applyQuaternion( quaternion );
172				_this.object.up.applyQuaternion( quaternion );
173
174				_lastAxis.copy( axis );
175				_lastAngle = angle;
176
177			} else if ( ! _this.staticMoving && _lastAngle ) {
178
179				_lastAngle *= Math.sqrt( 1.0 - _this.dynamicDampingFactor );
180				_eye.copy( _this.object.position ).sub( _this.target );
181				quaternion.setFromAxisAngle( _lastAxis, _lastAngle );
182				_eye.applyQuaternion( quaternion );
183				_this.object.up.applyQuaternion( quaternion );
184
185			}
186
187			_movePrev.copy( _moveCurr );
188
189		};
190
191	}() );
192
193
194	this.zoomCamera = function () {
195
196		var factor;
197
198		if ( _state === STATE.TOUCH_ZOOM_PAN ) {
199
200			factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
201			_touchZoomDistanceStart = _touchZoomDistanceEnd;
202			_eye.multiplyScalar( factor );
203
204		} else {
205
206			factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed;
207
208			if ( factor !== 1.0 && factor > 0.0 ) {
209
210				_eye.multiplyScalar( factor );
211
212			}
213
214			if ( _this.staticMoving ) {
215
216				_zoomStart.copy( _zoomEnd );
217
218			} else {
219
220				_zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor;
221
222			}
223
224		}
225
226	};
227
228	this.panCamera = ( function () {
229
230		var mouseChange = new THREE.Vector2(),
231			objectUp = new THREE.Vector3(),
232			pan = new THREE.Vector3();
233
234		return function panCamera() {
235
236			mouseChange.copy( _panEnd ).sub( _panStart );
237
238			if ( mouseChange.lengthSq() ) {
239
240				mouseChange.multiplyScalar( _eye.length() * _this.panSpeed );
241
242				pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x );
243				pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) );
244
245				_this.object.position.add( pan );
246				_this.target.add( pan );
247
248				if ( _this.staticMoving ) {
249
250					_panStart.copy( _panEnd );
251
252				} else {
253
254					_panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) );
255
256				}
257
258			}
259
260		};
261
262	}() );
263
264	this.checkDistances = function () {
265
266		if ( ! _this.noZoom || ! _this.noPan ) {
267
268			if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) {
269
270				_this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) );
271				_zoomStart.copy( _zoomEnd );
272
273			}
274
275			if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) {
276
277				_this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) );
278				_zoomStart.copy( _zoomEnd );
279
280			}
281
282		}
283
284	};
285
286	this.update = function () {
287
288		_eye.subVectors( _this.object.position, _this.target );
289
290		if ( ! _this.noRotate ) {
291
292			_this.rotateCamera();
293
294		}
295
296		if ( ! _this.noZoom ) {
297
298			_this.zoomCamera();
299
300		}
301
302		if ( ! _this.noPan ) {
303
304			_this.panCamera();
305
306		}
307
308		_this.object.position.addVectors( _this.target, _eye );
309
310		_this.checkDistances();
311
312		_this.object.lookAt( _this.target );
313
314		if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) {
315
316			_this.dispatchEvent( changeEvent );
317
318			lastPosition.copy( _this.object.position );
319
320		}
321
322	};
323
324	this.reset = function () {
325
326		_state = STATE.NONE;
327		_prevState = STATE.NONE;
328
329		_this.target.copy( _this.target0 );
330		_this.object.position.copy( _this.position0 );
331		_this.object.up.copy( _this.up0 );
332
333		_eye.subVectors( _this.object.position, _this.target );
334
335		_this.object.lookAt( _this.target );
336
337		_this.dispatchEvent( changeEvent );
338
339		lastPosition.copy( _this.object.position );
340
341	};
342
343	// listeners
344
345	function keydown( event ) {
346
347		if ( _this.enabled === false ) return;
348
349		window.removeEventListener( 'keydown', keydown );
350
351		_prevState = _state;
352
353		if ( _state !== STATE.NONE ) {
354
355			return;
356
357		} else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && ! _this.noRotate ) {
358
359			_state = STATE.ROTATE;
360
361		} else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && ! _this.noZoom ) {
362
363			_state = STATE.ZOOM;
364
365		} else if ( event.keyCode === _this.keys[ STATE.PAN ] && ! _this.noPan ) {
366
367			_state = STATE.PAN;
368
369		}
370
371	}
372
373	function keyup( event ) {
374
375		if ( _this.enabled === false ) return;
376
377		_state = _prevState;
378
379		window.addEventListener( 'keydown', keydown, false );
380
381	}
382
383	function mousedown( event ) {
384
385		if ( _this.enabled === false ) return;
386
387		event.preventDefault();
388		event.stopPropagation();
389
390		if ( _state === STATE.NONE ) {
391
392			_state = event.button;
393
394		}
395
396		if ( _state === STATE.ROTATE && ! _this.noRotate ) {
397
398			_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
399			_movePrev.copy( _moveCurr );
400
401		} else if ( _state === STATE.ZOOM && ! _this.noZoom ) {
402
403			_zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
404			_zoomEnd.copy( _zoomStart );
405
406		} else if ( _state === STATE.PAN && ! _this.noPan ) {
407
408			_panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
409			_panEnd.copy( _panStart );
410
411		}
412
413		document.addEventListener( 'mousemove', mousemove, false );
414		document.addEventListener( 'mouseup', mouseup, false );
415
416		_this.dispatchEvent( startEvent );
417
418	}
419
420	function mousemove( event ) {
421
422		if ( _this.enabled === false ) return;
423
424		event.preventDefault();
425		event.stopPropagation();
426
427		if ( _state === STATE.ROTATE && ! _this.noRotate ) {
428
429			_movePrev.copy( _moveCurr );
430			_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
431
432		} else if ( _state === STATE.ZOOM && ! _this.noZoom ) {
433
434			_zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
435
436		} else if ( _state === STATE.PAN && ! _this.noPan ) {
437
438			_panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
439
440		}
441
442	}
443
444	function mouseup( event ) {
445
446		if ( _this.enabled === false ) return;
447
448		event.preventDefault();
449		event.stopPropagation();
450
451		_state = STATE.NONE;
452
453		document.removeEventListener( 'mousemove', mousemove );
454		document.removeEventListener( 'mouseup', mouseup );
455		_this.dispatchEvent( endEvent );
456
457	}
458
459	function mousewheel( event ) {
460
461		if ( _this.enabled === false ) return;
462
463		if ( _this.noZoom === true ) return;
464
465		event.preventDefault();
466		event.stopPropagation();
467
468		switch ( event.deltaMode ) {
469
470			case 2:
471				// Zoom in pages
472				_zoomStart.y -= event.deltaY * 0.025;
473				break;
474
475			case 1:
476				// Zoom in lines
477				_zoomStart.y -= event.deltaY * 0.01;
478				break;
479
480			default:
481				// undefined, 0, assume pixels
482				_zoomStart.y -= event.deltaY * 0.00025;
483				break;
484
485		}
486
487		_this.dispatchEvent( startEvent );
488		_this.dispatchEvent( endEvent );
489
490	}
491
492	function touchstart( event ) {
493
494		if ( _this.enabled === false ) return;
495
496		event.preventDefault();
497
498		switch ( event.touches.length ) {
499
500			case 1:
501				_state = STATE.TOUCH_ROTATE;
502				_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
503				_movePrev.copy( _moveCurr );
504				break;
505
506			default: // 2 or more
507				_state = STATE.TOUCH_ZOOM_PAN;
508				var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
509				var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
510				_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
511
512				var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
513				var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
514				_panStart.copy( getMouseOnScreen( x, y ) );
515				_panEnd.copy( _panStart );
516				break;
517
518		}
519
520		_this.dispatchEvent( startEvent );
521
522	}
523
524	function touchmove( event ) {
525
526		if ( _this.enabled === false ) return;
527
528		event.preventDefault();
529		event.stopPropagation();
530
531		switch ( event.touches.length ) {
532
533			case 1:
534				_movePrev.copy( _moveCurr );
535				_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
536				break;
537
538			default: // 2 or more
539				var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
540				var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
541				_touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );
542
543				var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
544				var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
545				_panEnd.copy( getMouseOnScreen( x, y ) );
546				break;
547
548		}
549
550	}
551
552	function touchend( event ) {
553
554		if ( _this.enabled === false ) return;
555
556		switch ( event.touches.length ) {
557
558			case 0:
559				_state = STATE.NONE;
560				break;
561
562			case 1:
563				_state = STATE.TOUCH_ROTATE;
564				_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
565				_movePrev.copy( _moveCurr );
566				break;
567
568		}
569
570		_this.dispatchEvent( endEvent );
571
572	}
573
574	function contextmenu( event ) {
575
576		if ( _this.enabled === false ) return;
577
578		event.preventDefault();
579
580	}
581
582	this.dispose = function () {
583
584		this.domElement.removeEventListener( 'contextmenu', contextmenu, false );
585		this.domElement.removeEventListener( 'mousedown', mousedown, false );
586		this.domElement.removeEventListener( 'wheel', mousewheel, false );
587
588		this.domElement.removeEventListener( 'touchstart', touchstart, false );
589		this.domElement.removeEventListener( 'touchend', touchend, false );
590		this.domElement.removeEventListener( 'touchmove', touchmove, false );
591
592		document.removeEventListener( 'mousemove', mousemove, false );
593		document.removeEventListener( 'mouseup', mouseup, false );
594
595		window.removeEventListener( 'keydown', keydown, false );
596		window.removeEventListener( 'keyup', keyup, false );
597
598	};
599
600	this.domElement.addEventListener( 'contextmenu', contextmenu, false );
601	this.domElement.addEventListener( 'mousedown', mousedown, false );
602	this.domElement.addEventListener( 'wheel', mousewheel, false );
603
604	this.domElement.addEventListener( 'touchstart', touchstart, false );
605	this.domElement.addEventListener( 'touchend', touchend, false );
606	this.domElement.addEventListener( 'touchmove', touchmove, false );
607
608	window.addEventListener( 'keydown', keydown, false );
609	window.addEventListener( 'keyup', keyup, false );
610
611	this.handleResize();
612
613	// force an update at start
614	this.update();
615
616};
617
618THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype );
619THREE.TrackballControls.prototype.constructor = THREE.TrackballControls;