topical media & game development
mobile-query-three-plugins-domevent-threex.domevent.js / js
// 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._onDblClick = function(){ _this._onDblClick.apply(_this, arguments); };
this._onMouseDown = function(){ _this._onMouseDown.apply(_this, arguments); };
this._onTouchMove = function(){ _this._onTouchMove.apply(_this, arguments); };
this._onTouchEnd = function(){ _this._onTouchEnd.apply(_this, arguments); };
this._onClick , false );
this._domElement.addEventListener( 'dblclick' , this._onMouseMove , false );
this._domElement.addEventListener( 'mousedown' , this._onMouseUp , false );
this._domElement.addEventListener( 'touchmove' , this._onTouchStart , false );
this._domElement.addEventListener( 'touchend' , this._onContextmenu , false );
}
// # Destructor
THREEx.DomEvent.prototype.destroy = function()
{
// unBind dom event for mouse and touch
this._domElement.removeEventListener( 'click' , this._onDblClick , false );
this._domElement.removeEventListener( 'mousemove' , this._onMouseDown , false );
this._domElement.removeEventListener( 'mouseup' , this._onTouchMove , false );
this._domElement.removeEventListener( 'touchstart' , this._onTouchEnd , false );
this._domElement.removeEventListener( 'contextmenu' , this._
*****************************************************************************
/* 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
if( this._boundObjs[eventName] === undefined ){
this._boundObjs[eventName] = [];
}
this._boundObjs[eventName].push(object3d);
}
THREEx.DomEvent.prototype.unbind = 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'] = [];
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[eventName].indexOf(object3d);
console.assert( index !== -1 );
this._boundObjs[eventName].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(eventName, mouseX, mouseY, origDomEvent)
{
//console.log('eventName', eventName, 'boundObjs', this._boundObjs[eventName])
// get objects bound to this event
var boundObjs = this._boundObjs[eventName];
if( boundObjs === undefined || boundObjs.length === 0 ) return;
// compute the intersection
var vector = new THREE.Vector3( mouseX, mouseY, 0.5 );
var ray = this._projector.pickingRay( vector, this._camera );
var intersects = ray.intersectObjects( boundObjs );
var oldSelected = this._selected;
if( intersects.length > 0 ){
var notifyOver, notifyOut, notifyMove;
var intersect = intersects[ 0 ];
var newSelected = intersect.object;
this._selected = newSelected;
// if newSelected bound mousemove, notify it
notifyMove = this._bound('mousemove', newSelected);
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 mouseMove - done at the end with a copy of the list to allow callback to remove handlers
notifyMove && this._notify('mousemove', newSelected, origDomEvent, intersect);
// notify mouseEnter - done at the end with a copy of the list to allow callback to remove handlers
notifyOver && this._notify('mouseover', newSelected, origDomEvent, intersect);
// notify mouseLeave - done at the end with a copy of the list to allow callback to remove handlers
notifyOut && this._notify('mouseout' , oldSelected, origDomEvent, intersect);
}
*****************************************************************************
/* onEvent */
*****************************************************************************
// # handle click kind of events
THREEx.DomEvent.prototype._onEvent = function(eventName, mouseX, mouseY, origDomEvent)
{
//console.log('eventName', eventName, 'boundObjs', this._boundObjs[eventName])
// get objects bound to this event
var boundObjs = this._boundObjs[eventName];
if( boundObjs === undefined || boundObjs.length === 0 ) return;
// compute the intersection
var vector = new THREE.Vector3( mouseX, mouseY, 0.5 );
var ray = this._projector.pickingRay( vector, this._camera );
var intersects = ray.intersectObjects( 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, intersect);
}
THREEx.DomEvent.prototype._notify = function(eventName, object3d, origDomEvent, intersect)
{
var objectCtx = this._objectCtxGet(object3d);
var handlers = objectCtx ? objectCtx[eventName+'Handlers'] : null;
// parameter check
console.assert(arguments.length === 4)
// do bubbling
if( !objectCtx || !handlers || handlers.length === 0 ){
object3d.parent && this._notify(eventName, object3d.parent, origDomEvent, intersect);
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,
intersect : intersect,
stopPropagation : function(){
toPropagate = false;
}
});
if( !toPropagate ) continue;
// do bubbling
if( handler.useCapture === false ){
object3d.parent && this._notify(eventName, object3d.parent, origDomEvent, intersect);
}
}
}
*****************************************************************************
/* 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 mouseCoords = this._getRelativeMouseXY(domEvent);
this._onEvent(eventName, mouseCoords.x, mouseCoords.y, domEvent);
}
THREEx.DomEvent.prototype._onMouseMove = function(domEvent)
{
var mouseCoords = this._getRelativeMouseXY(domEvent);
this._onMove('mousemove', mouseCoords.x, mouseCoords.y, domEvent);
this._onMove('mouseover', mouseCoords.x, mouseCoords.y, domEvent);
this._onMove('mouseout' , mouseCoords.x, mouseCoords.y, domEvent);
}
THREEx.DomEvent.prototype._onClick = function(event)
{
// TODO handle touch ?
this._onMouseEvent('click' , event);
}
THREEx.DomEvent.prototype._onDblClick = function(event)
{
// TODO handle touch ?
this._onMouseEvent('dblclick' , event);
}
THREEx.DomEvent.prototype._onContextmenu = function(event)
{
//TODO don't have a clue about how this should work with touch..
this._onMouseEvent('contextmenu' , 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;
this._onMove('mousemove', mouseX, mouseY, domEvent);
this._onMove('mouseover', mouseX, mouseY, domEvent);
this._onMove('mouseout' , 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;
this._onEvent(eventName, mouseX, mouseY, domEvent);
}
(C) Æliens
04/09/2009
You may not copy or print any of this material without explicit permission of the author or the publisher.
In case of other copyright issues, contact the author.