|
// 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.DomEvents.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.DomEvents = 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._$onContextmenu = function(){ _this._onContextmenu.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 ); |
|
this._domElement.addEventListener( 'contextmenu', this._$onContextmenu , false ); |
|
|
|
} |
|
|
|
// # Destructor |
|
THREEx.DomEvents.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 ); |
|
this._domElement.removeEventListener( 'contextmenu' , this._$onContextmenu , false ); |
|
} |
|
|
|
THREEx.DomEvents.eventNames = [ |
|
"click", |
|
"dblclick", |
|
"mouseover", |
|
"mouseout", |
|
"mousemove", |
|
"mousedown", |
|
"mouseup", |
|
"contextmenu" |
|
]; |
|
|
|
THREEx.DomEvents.prototype._getRelativeMouseXY = function(domEvent){ |
|
var element = domEvent.target || domEvent.srcElement; |
|
if (element.nodeType === 3) { |
|
element = element.parentNode; // Safari fix -- see http://www.quirksmode.org/js/events_properties.html |
|
} |
|
|
|
//get the real position of an element relative to the page starting point (0, 0) |
|
//credits go to brainjam on answering http://stackoverflow.com/questions/5755312/getting-mouse-position-relative-to-content-area-of-an-element |
|
var elPosition = { x : 0 , y : 0}; |
|
var tmpElement = element; |
|
//store padding |
|
var style = getComputedStyle(tmpElement, null); |
|
elPosition.y += parseInt(style.getPropertyValue("padding-top"), 10); |
|
elPosition.x += parseInt(style.getPropertyValue("padding-left"), 10); |
|
//add positions |
|
do { |
|
elPosition.x += tmpElement.offsetLeft; |
|
elPosition.y += tmpElement.offsetTop; |
|
style = getComputedStyle(tmpElement, null); |
|
|
|
elPosition.x += parseInt(style.getPropertyValue("border-left-width"), 10); |
|
elPosition.y += parseInt(style.getPropertyValue("border-top-width"), 10); |
|
} while(tmpElement = tmpElement.offsetParent); |
|
|
|
var elDimension = { |
|
width : (element === window) ? window.innerWidth : element.offsetWidth, |
|
height : (element === window) ? window.innerHeight : element.offsetHeight |
|
}; |
|
|
|
return { |
|
x : +((domEvent.pageX - elPosition.x) / elDimension.width ) * 2 - 1, |
|
y : -((domEvent.pageY - elPosition.y) / elDimension.height) * 2 + 1 |
|
}; |
|
}; |
|
|
|
|
|
/********************************************************************************/ |
|
/* domevent context */ |
|
/********************************************************************************/ |
|
|
|
// handle domevent context in object3d instance |
|
|
|
THREEx.DomEvents.prototype._objectCtxInit = function(object3d){ |
|
object3d._3xDomEvent = {}; |
|
} |
|
THREEx.DomEvents.prototype._objectCtxDeinit = function(object3d){ |
|
delete object3d._3xDomEvent; |
|
} |
|
THREEx.DomEvents.prototype._objectCtxIsInit = function(object3d){ |
|
return object3d._3xDomEvent ? true : false; |
|
} |
|
THREEx.DomEvents.prototype._objectCtxGet = function(object3d){ |
|
return object3d._3xDomEvent; |
|
} |
|
|
|
/********************************************************************************/ |
|
/* */ |
|
/********************************************************************************/ |
|
|
|
/** |
|
* Getter/Setter for camera |
|
*/ |
|
THREEx.DomEvents.prototype.camera = function(value) |
|
{ |
|
if( value ) this._camera = value; |
|
return this._camera; |
|
} |
|
|
|
THREEx.DomEvents.prototype.bind = function(object3d, eventName, callback, useCapture) |
|
{ |
|
console.assert( THREEx.DomEvents.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.DomEvents.prototype.addEventListener = THREEx.DomEvents.prototype.bind |
|
|
|
THREEx.DomEvents.prototype.unbind = function(object3d, eventName, callback, useCapture) |
|
{ |
|
console.assert( THREEx.DomEvents.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.DomEvents.prototype.removeEventListener = THREEx.DomEvents.prototype.unbind |
|
|
|
THREEx.DomEvents.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.DomEvents.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(); |
|
var raycaster = new THREE.Raycaster(); |
|
var dir = new THREE.Vector3(); |
|
|
|
if ( this._camera instanceof THREE.OrthographicCamera ) { |
|
|
|
vector.set( mouseX, mouseY, -1 ); |
|
vector.unproject( this._camera ); |
|
dir.set( 0, 0, - 1 ).transformDirection( this._camera.matrixWorld ); |
|
raycaster.set( vector, dir ); |
|
|
|
} else if ( this._camera instanceof THREE.PerspectiveCamera ) { |
|
|
|
vector.set( mouseX, mouseY, 0.5 ); |
|
vector.unproject( this._camera ); |
|
raycaster.set( this._camera.position, vector.sub( this._camera.position ).normalize() ); |
|
} |
|
|
|
var intersects = raycaster.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.DomEvents.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(); |
|
var raycaster = new THREE.Raycaster(); |
|
var dir = new THREE.Vector3(); |
|
|
|
if ( this._camera instanceof THREE.OrthographicCamera ) { |
|
|
|
vector.set( mouseX, mouseY, -1 ); |
|
vector.unproject( this._camera ); |
|
dir.set( 0, 0, - 1 ).transformDirection( this._camera.matrixWorld ); |
|
raycaster.set( vector, dir ); |
|
|
|
} else if ( this._camera instanceof THREE.PerspectiveCamera ) { |
|
|
|
vector.set( mouseX, mouseY, 0.5 ); |
|
vector.unproject( this._camera ); |
|
raycaster.set( this._camera.position, vector.sub( this._camera.position ).normalize() ); |
|
} |
|
|
|
var intersects = raycaster.intersectObjects( boundObjs, true); |
|
// if there are no intersections, return now |
|
if( intersects.length === 0 ) return; |
|
|
|
// init some variables |
|
var intersect = intersects[0]; |
|
var object3d = intersect.object; |
|
var objectCtx = this._objectCtxGet(object3d); |
|
var objectParent = object3d.parent; |
|
|
|
while ( typeof(objectCtx) == 'undefined' && objectParent ) |
|
{ |
|
objectCtx = this._objectCtxGet(objectParent); |
|
objectParent = objectParent.parent; |
|
} |
|
if( !objectCtx ) return; |
|
|
|
// notify handlers |
|
this._notify(eventName, object3d, origDomEvent, intersect); |
|
} |
|
|
|
THREEx.DomEvents.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.DomEvents.prototype._onMouseDown = function(event){ return this._onMouseEvent('mousedown', event); } |
|
THREEx.DomEvents.prototype._onMouseUp = function(event){ return this._onMouseEvent('mouseup' , event); } |
|
|
|
|
|
THREEx.DomEvents.prototype._onMouseEvent = function(eventName, domEvent) |
|
{ |
|
var mouseCoords = this._getRelativeMouseXY(domEvent); |
|
this._onEvent(eventName, mouseCoords.x, mouseCoords.y, domEvent); |
|
} |
|
|
|
THREEx.DomEvents.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.DomEvents.prototype._onClick = function(event) |
|
{ |
|
// TODO handle touch ? |
|
this._onMouseEvent('click' , event); |
|
} |
|
THREEx.DomEvents.prototype._onDblClick = function(event) |
|
{ |
|
// TODO handle touch ? |
|
this._onMouseEvent('dblclick' , event); |
|
} |
|
|
|
THREEx.DomEvents.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.DomEvents.prototype._onTouchStart = function(event){ return this._onTouchEvent('mousedown', event); } |
|
THREEx.DomEvents.prototype._onTouchEnd = function(event){ return this._onTouchEvent('mouseup' , event); } |
|
|
|
THREEx.DomEvents.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.DomEvents.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); |
|
} |