threex
Version:
Game Extensions for three.js http://www.threejsgames.com/extensions/
367 lines (311 loc) • 12.8 kB
JavaScript
// This THREEx helper makes it easy to handle the mouse events in your 3D scene
//
// * CHANGES NEEDED
// * handle drag/drop
// * notify events not object3D - like DOM
// * so single object with property
// * DONE bubling implement bubling/capturing
// * DONE implement event.stopPropagation()
// * DONE implement event.type = "click" and co
// * DONE implement event.target
//
// # Lets get started
//
// First you include it in your page
//
// ```<script src='threex.domevent.js'></script>```
//
// # use the object oriented api
//
// You bind an event like this
//
// ```mesh.on('click', function(object3d){ ... })```
//
// To unbind an event, just do
//
// ```mesh.off('click', function(object3d){ ... })```
//
// As an alternative, there is another naming closer DOM events.
// Pick the one you like, they are doing the same thing
//
// ```mesh.addEventListener('click', function(object3d){ ... })```
// ```mesh.removeEventListener('click', function(object3d){ ... })```
//
// # Supported Events
//
// Always in a effort to stay close to usual pratices, the events name are the same as in DOM.
// The semantic is the same too.
// Currently, the available events are
// [click, dblclick, mouseup, mousedown](http://www.quirksmode.org/dom/events/click.html),
// [mouseover and mouse out](http://www.quirksmode.org/dom/events/mouseover.html).
//
// # use the standalone api
//
// The object-oriented api modifies THREE.Object3D class.
// It is a global class, so it may be legitimatly considered unclean by some people.
// If this bother you, simply do ```THREEx.DomEvent.noConflict()``` and use the
// standalone API. In fact, the object oriented API is just a thin wrapper
// on top of the standalone API.
//
// First, you instanciate the object
//
// ```var domEvent = new THREEx.DomEvent();```
//
// Then you bind an event like this
//
// ```domEvent.bind(mesh, 'click', function(object3d){ object3d.scale.x *= 2; });```
//
// To unbind an event, just do
//
// ```domEvent.unbind(mesh, 'click', callback);```
//
//
// # Code
//
/** @namespace */
var THREEx = THREEx || {};
// # Constructor
THREEx.DomEvent = function(camera, domElement)
{
this._camera = camera || null;
this._domElement= domElement || document;
this._projector = new THREE.Projector();
this._selected = null;
this._boundObjs = [];
// Bind dom event for mouse and touch
var _this = this;
this._$onClick = function(){ _this._onClick.apply(_this, arguments); };
this._$onDblClick = function(){ _this._onDblClick.apply(_this, arguments); };
this._$onMouseMove = function(){ _this._onMouseMove.apply(_this, arguments); };
this._$onMouseDown = function(){ _this._onMouseDown.apply(_this, arguments); };
this._$onMouseUp = function(){ _this._onMouseUp.apply(_this, arguments); };
this._$onTouchMove = function(){ _this._onTouchMove.apply(_this, arguments); };
this._$onTouchStart = function(){ _this._onTouchStart.apply(_this, arguments); };
this._$onTouchEnd = function(){ _this._onTouchEnd.apply(_this, arguments); };
this._domElement.addEventListener( 'click' , this._$onClick , false );
this._domElement.addEventListener( 'dblclick' , this._$onDblClick , false );
this._domElement.addEventListener( 'mousemove' , this._$onMouseMove , false );
this._domElement.addEventListener( 'mousedown' , this._$onMouseDown , false );
this._domElement.addEventListener( 'mouseup' , this._$onMouseUp , false );
this._domElement.addEventListener( 'touchmove' , this._$onTouchMove , false );
this._domElement.addEventListener( 'touchstart' , this._$onTouchStart , false );
this._domElement.addEventListener( 'touchend' , this._$onTouchEnd , false );
}
// # Destructor
THREEx.DomEvent.prototype.destroy = function()
{
// unBind dom event for mouse and touch
this._domElement.removeEventListener( 'click' , this._$onClick , false );
this._domElement.removeEventListener( 'dblclick' , this._$onDblClick , false );
this._domElement.removeEventListener( 'mousemove' , this._$onMouseMove , false );
this._domElement.removeEventListener( 'mousedown' , this._$onMouseDown , false );
this._domElement.removeEventListener( 'mouseup' , this._$onMouseUp , false );
this._domElement.removeEventListener( 'touchmove' , this._$onTouchMove , false );
this._domElement.removeEventListener( 'touchstart' , this._$onTouchStart , false );
this._domElement.removeEventListener( 'touchend' , this._$onTouchEnd , false );
}
THREEx.DomEvent.eventNames = [
"click",
"dblclick",
"mouseover",
"mouseout",
"mousedown",
"mouseup"
];
/********************************************************************************/
/* domevent context */
/********************************************************************************/
// handle domevent context in object3d instance
THREEx.DomEvent.prototype._objectCtxInit = function(object3d){
object3d._3xDomEvent = {};
}
THREEx.DomEvent.prototype._objectCtxDeinit = function(object3d){
delete object3d._3xDomEvent;
}
THREEx.DomEvent.prototype._objectCtxIsInit = function(object3d){
return object3d._3xDomEvent ? true : false;
}
THREEx.DomEvent.prototype._objectCtxGet = function(object3d){
return object3d._3xDomEvent;
}
/********************************************************************************/
/* */
/********************************************************************************/
/**
* Getter/Setter for camera
*/
THREEx.DomEvent.prototype.camera = function(value)
{
if( value ) this._camera = value;
return this._camera;
}
THREEx.DomEvent.prototype.bind = function(object3d, eventName, callback, useCapture)
{
console.assert( THREEx.DomEvent.eventNames.indexOf(eventName) !== -1, "not available events:"+eventName );
if( !this._objectCtxIsInit(object3d) ) this._objectCtxInit(object3d);
var objectCtx = this._objectCtxGet(object3d);
if( !objectCtx[eventName+'Handlers'] ) objectCtx[eventName+'Handlers'] = [];
objectCtx[eventName+'Handlers'].push({
callback : callback,
useCapture : useCapture
});
// add this object in this._boundObjs
this._boundObjs.push(object3d);
}
THREEx.DomEvent.prototype.unbind = function(object3d, eventName, callback)
{
console.assert( THREEx.DomEvent.eventNames.indexOf(eventName) !== -1, "not available events:"+eventName );
if( !this._objectCtxIsInit(object3d) ) this._objectCtxInit(object3d);
var objectCtx = this._objectCtxGet(object3d);
if( !objectCtx[eventName+'Handlers'] ) objectCtx[eventName+'Handlers'] = [];
var handlers = objectCtx[eventName+'Handlers'];
for(var i = 0; i < handlers.length; i++){
var handler = handlers[i];
if( callback != handler.callback ) continue;
if( useCapture != handler.useCapture ) continue;
handlers.splice(i, 1)
break;
}
// from this object from this._boundObjs
var index = this._boundObjs.indexOf(object3d);
console.assert( index !== -1 );
this._boundObjs.splice(index, 1);
}
THREEx.DomEvent.prototype._bound = function(eventName, object3d)
{
var objectCtx = this._objectCtxGet(object3d);
if( !objectCtx ) return false;
return objectCtx[eventName+'Handlers'] ? true : false;
}
/********************************************************************************/
/* onMove */
/********************************************************************************/
// # handle mousemove kind of events
THREEx.DomEvent.prototype._onMove = function(mouseX, mouseY, origDomEvent)
{
var vector = new THREE.Vector3( mouseX, mouseY, 1 );
this._projector.unprojectVector( vector, this._camera );
var ray = new THREE.Ray( this._camera.position, vector.subSelf( this._camera.position ).normalize() );
var intersects = ray.intersectObjects( this._boundObjs );
var oldSelected = this._selected;
if( intersects.length > 0 ){
var intersect = intersects[ 0 ];
var newSelected = intersect.object;
this._selected = newSelected;
var notifyOver, notifyOut;
if( oldSelected != newSelected ){
// if newSelected bound mouseenter, notify it
notifyOver = this._bound('mouseover', newSelected);
// if there is a oldSelect and oldSelected bound mouseleave, notify it
notifyOut = oldSelected && this._bound('mouseout', oldSelected);
}
}else{
// if there is a oldSelect and oldSelected bound mouseleave, notify it
notifyOut = oldSelected && this._bound('mouseout', oldSelected);
this._selected = null;
}
// notify mouseEnter - done at the end with a copy of the list to allow callback to remove handlers
notifyOver && this._notify('mouseover', newSelected, origDomEvent);
// notify mouseLeave - done at the end with a copy of the list to allow callback to remove handlers
notifyOut && this._notify('mouseout', oldSelected, origDomEvent);
}
/********************************************************************************/
/* onEvent */
/********************************************************************************/
// # handle click kind of events
THREEx.DomEvent.prototype._onEvent = function(eventName, mouseX, mouseY, origDomEvent)
{
var vector = new THREE.Vector3( mouseX, mouseY, 1 );
this._projector.unprojectVector( vector, this._camera );
vector.subSelf( this._camera.position ).normalize()
var ray = new THREE.Ray( this._camera.position, vector );
var intersects = ray.intersectObjects( this._boundObjs );
// if there are no intersections, return now
if( intersects.length === 0 ) return;
// init some vairables
var intersect = intersects[0];
var object3d = intersect.object;
var objectCtx = this._objectCtxGet(object3d);
if( !objectCtx ) return;
// notify handlers
this._notify(eventName, object3d, origDomEvent);
}
THREEx.DomEvent.prototype._notify = function(eventName, object3d, origDomEvent)
{
var objectCtx = this._objectCtxGet(object3d);
var handlers = objectCtx ? objectCtx[eventName+'Handlers'] : null;
// do bubbling
if( !objectCtx || !handlers || handlers.length === 0 ){
object3d.parent && this._notify(eventName, object3d.parent);
return;
}
// notify all handlers
var handlers = objectCtx[eventName+'Handlers'];
for(var i = 0; i < handlers.length; i++){
var handler = handlers[i];
var toPropagate = true;
handler.callback({
type : eventName,
target : object3d,
origDomEvent : origDomEvent,
stopPropagation : function(){
toPropagate = false;
}
});
if( !toPropagate ) continue;
// do bubbling
if( handler.useCapture === false ){
object3d.parent && this._notify(eventName, object3d.parent);
}
}
}
/********************************************************************************/
/* handle mouse events */
/********************************************************************************/
// # handle mouse events
THREEx.DomEvent.prototype._onMouseDown = function(event){ return this._onMouseEvent('mousedown', event); }
THREEx.DomEvent.prototype._onMouseUp = function(event){ return this._onMouseEvent('mouseup' , event); }
THREEx.DomEvent.prototype._onMouseEvent = function(eventName, domEvent)
{
var mouseX = +(domEvent.clientX / window.innerWidth ) * 2 - 1;
var mouseY = -(domEvent.clientY / window.innerHeight) * 2 + 1;
return this._onEvent(eventName, mouseX, mouseY, domEvent);
}
THREEx.DomEvent.prototype._onMouseMove = function(domEvent)
{
var mouseX = +(domEvent.clientX / window.innerWidth ) * 2 - 1;
var mouseY = -(domEvent.clientY / window.innerHeight) * 2 + 1;
return this._onMove(mouseX, mouseY, domEvent);
}
THREEx.DomEvent.prototype._onClick = function(event)
{
// TODO handle touch ?
return this._onMouseEvent('click' , event);
}
THREEx.DomEvent.prototype._onDblClick = function(event)
{
// TODO handle touch ?
return this._onMouseEvent('dblclick' , event);
}
/********************************************************************************/
/* handle touch events */
/********************************************************************************/
// # handle touch events
THREEx.DomEvent.prototype._onTouchStart = function(event){ return this._onTouchEvent('mousedown', event); }
THREEx.DomEvent.prototype._onTouchEnd = function(event){ return this._onTouchEvent('mouseup' , event); }
THREEx.DomEvent.prototype._onTouchMove = function(domEvent)
{
if( domEvent.touches.length != 1 ) return undefined;
domEvent.preventDefault();
var mouseX = +(domEvent.touches[ 0 ].pageX / window.innerWidth ) * 2 - 1;
var mouseY = -(domEvent.touches[ 0 ].pageY / window.innerHeight) * 2 + 1;
return this._onMove('mousemove', mouseX, mouseY, domEvent);
}
THREEx.DomEvent.prototype._onTouchEvent = function(eventName, domEvent)
{
if( domEvent.touches.length != 1 ) return undefined;
domEvent.preventDefault();
var mouseX = +(domEvent.touches[ 0 ].pageX / window.innerWidth ) * 2 - 1;
var mouseY = -(domEvent.touches[ 0 ].pageY / window.innerHeight) * 2 + 1;
return this._onEvent(eventName, mouseX, mouseY, domEvent);
}