UNPKG

three.interaction

Version:

a interaction manager, serve the three.js, help you build a interactivity 3d-scene

1,680 lines (1,423 loc) 147 kB
import { EventDispatcher, Object3D, Raycaster, Vector2 } from 'three'; /** * get variable type * @param {*} val a variable which you want to get the type * @return {String} variable-type */ function _rt(val) { return Object.prototype.toString.call(val); } /** * Utils tool box * * @namespace Utils */ var Utils = { /** * determine whether it is a `Function` * * @static * @method * @memberof Utils * @param {*} variable a variable which you want to determine * @return {Boolean} type result */ isFunction: function () { var ks = _rt(function () {}); return function (variable) { return _rt(variable) === ks; }; }(), /** * determine whether it is a `undefined` * * @static * @method * @memberof Utils * @param {*} variable a variable which you want to determine * @return {Boolean} type result */ isUndefined: function isUndefined(variable) { return typeof variable === 'undefined'; } }; /** * proxy `addEventListener` function * * @param {String} type event type, evnet name * @param {Function} fn callback * @return {this} this */ EventDispatcher.prototype.on = function (type, fn) { if (!Utils.isFunction(fn)) return; if (this instanceof Object3D) this.interactive = true; this.addEventListener(type, fn); return this; }; /** * proxy `removeEventListener` function * * @param {String} type event type, evnet name * @param {Function} fn callback, which you had bind before * @return {this} this */ EventDispatcher.prototype.off = function (type, fn) { this.removeEventListener(type, fn); return this; }; /** * binding a once event, just emit once time * * @param {String} type event type, evnet name * @param {Function} fn callback * @return {this} this */ EventDispatcher.prototype.once = function (type, fn) { var _this = this; if (!Utils.isFunction(fn)) return; var cb = function cb(ev) { fn(ev); _this.off(type, cb); }; this.on(type, cb); return this; }; /** * emit a event * * @param {String} type event type, evnet name * @return {this} this */ EventDispatcher.prototype.emit = function (type) { if (this._listeners === undefined || Utils.isUndefined(this._listeners[type])) return; var cbs = this._listeners[type] || []; var cache = cbs.slice(0); for (var _len = arguments.length, argument = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { argument[_key - 1] = arguments[_key]; } for (var i = 0; i < cache.length; i++) { cache[i].apply(this, argument); } return this; }; /** * whether displayObject is interactively */ Object3D.prototype.interactive = false; /** * whether displayObject's children is interactively */ Object3D.prototype.interactiveChildren = true; /** * whether displayObject had touchstart * @private */ Object3D.prototype.started = false; /** * tracked event cache, like: touchend、mouseout、pointerout which decided by primary-event */ Object.defineProperty(Object3D.prototype, 'trackedPointers', { get: function get() { if (!this._trackedPointers) this._trackedPointers = {}; return this._trackedPointers; } }); /** * dispatch a raycast * * @param {Raycaster} raycaster Raycaster object, get from THREE.Raycaster * @return {Object|Boolean} had pass hit-test */ Object3D.prototype.raycastTest = function (raycaster) { var result = []; this.raycast(raycaster, result); if (result.length > 0) { return result[0]; } return false; }; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }; var possibleConstructorReturn = function (self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }; /** * Holds all information related to an Interaction event * * @class */ var InteractionData = function () { /** * InteractionData constructor */ function InteractionData() { classCallCheck(this, InteractionData); /** * This point stores the global coords of where the touch/mouse event happened * * @member {Vector2} */ this.global = new Vector2(); /** * The target DisplayObject that was interacted with * * @member {Object3D} */ this.target = null; /** * When passed to an event handler, this will be the original DOM Event that was captured * * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent * @see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent * @member {MouseEvent|TouchEvent|PointerEvent} */ this.originalEvent = null; /** * Unique identifier for this interaction * * @member {number} */ this.identifier = null; /** * Indicates whether or not the pointer device that created the event is the primary pointer. * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/isPrimary * @type {Boolean} */ this.isPrimary = false; /** * Indicates which button was pressed on the mouse or pointer device to trigger the event. * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button * @type {number} */ this.button = 0; /** * Indicates which buttons are pressed on the mouse or pointer device when the event is triggered. * @see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons * @type {number} */ this.buttons = 0; /** * The width of the pointer's contact along the x-axis, measured in CSS pixels. * radiusX of TouchEvents will be represented by this value. * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/width * @type {number} */ this.width = 0; /** * The height of the pointer's contact along the y-axis, measured in CSS pixels. * radiusY of TouchEvents will be represented by this value. * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/height * @type {number} */ this.height = 0; /** * The angle, in degrees, between the pointer device and the screen. * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/tiltX * @type {number} */ this.tiltX = 0; /** * The angle, in degrees, between the pointer device and the screen. * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/tiltY * @type {number} */ this.tiltY = 0; /** * The type of pointer that triggered the event. * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pointerType * @type {string} */ this.pointerType = null; /** * Pressure applied by the pointing device during the event. A Touch's force property * will be represented by this value. * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pressure * @type {number} */ this.pressure = 0; /** * From TouchEvents (not PointerEvents triggered by touches), the rotationAngle of the Touch. * @see https://developer.mozilla.org/en-US/docs/Web/API/Touch/rotationAngle * @type {number} */ this.rotationAngle = 0; /** * Twist of a stylus pointer. * @see https://w3c.github.io/pointerevents/#pointerevent-interface * @type {number} */ this.twist = 0; /** * Barrel pressure on a stylus pointer. * @see https://w3c.github.io/pointerevents/#pointerevent-interface * @type {number} */ this.tangentialPressure = 0; } /** * The unique identifier of the pointer. It will be the same as `identifier`. * @readonly * @member {number} * @see https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pointerId */ createClass(InteractionData, [{ key: '_copyEvent', /** * Copies properties from normalized event data. * * @param {Touch|MouseEvent|PointerEvent} event The normalized event data * @private */ value: function _copyEvent(event) { // isPrimary should only change on touchstart/pointerdown, so we don't want to overwrite // it with "false" on later events when our shim for it on touch events might not be // accurate if (event.isPrimary) { this.isPrimary = true; } this.button = event.button; this.buttons = event.buttons; this.width = event.width; this.height = event.height; this.tiltX = event.tiltX; this.tiltY = event.tiltY; this.pointerType = event.pointerType; this.pressure = event.pressure; this.rotationAngle = event.rotationAngle; this.twist = event.twist || 0; this.tangentialPressure = event.tangentialPressure || 0; } /** * Resets the data for pooling. * * @private */ }, { key: '_reset', value: function _reset() { // isPrimary is the only property that we really need to reset - everything else is // guaranteed to be overwritten this.isPrimary = false; } }, { key: 'pointerId', get: function get$$1() { return this.identifier; } }]); return InteractionData; }(); /** * Event class that mimics native DOM events. * * @class */ var InteractionEvent = function () { /** * InteractionEvent constructor */ function InteractionEvent() { classCallCheck(this, InteractionEvent); /** * Whether this event will continue propagating in the tree * * @member {boolean} */ this.stopped = false; /** * The object which caused this event to be dispatched. * * @member {Object3D} */ this.target = null; /** * The object whose event listener’s callback is currently being invoked. * * @member {Object3D} */ this.currentTarget = null; /** * Type of the event * * @member {string} */ this.type = null; /** * InteractionData related to this event * * @member {InteractionData} */ this.data = null; /** * ray caster detial from 3d-mesh * * @member {Intersects} */ this.intersects = []; } /** * Prevents event from reaching any objects other than the current object. * */ createClass(InteractionEvent, [{ key: "stopPropagation", value: function stopPropagation() { this.stopped = true; } /** * Resets the event. * * @private */ }, { key: "_reset", value: function _reset() { this.stopped = false; this.currentTarget = null; this.target = null; this.intersects = []; } }]); return InteractionEvent; }(); /** * DisplayObjects with the `trackedPointers` property use this class to track interactions * * @class * @private */ var InteractionTrackingData = function () { /** * @param {number} pointerId - Unique pointer id of the event */ function InteractionTrackingData(pointerId) { classCallCheck(this, InteractionTrackingData); this._pointerId = pointerId; this._flags = InteractionTrackingData.FLAGS.NONE; } /** * * @private * @param {number} flag - The interaction flag to set * @param {boolean} yn - Should the flag be set or unset */ createClass(InteractionTrackingData, [{ key: "_doSet", value: function _doSet(flag, yn) { if (yn) { this._flags = this._flags | flag; } else { this._flags = this._flags & ~flag; } } /** * Unique pointer id of the event * * @readonly * @member {number} */ }, { key: "pointerId", get: function get$$1() { return this._pointerId; } /** * State of the tracking data, expressed as bit flags * * @member {number} */ }, { key: "flags", get: function get$$1() { return this._flags; } /** * Set the flags for the tracking data * * @param {number} flags - Flags to set */ , set: function set$$1(flags) { this._flags = flags; } /** * Is the tracked event inactive (not over or down)? * * @member {number} */ }, { key: "none", get: function get$$1() { return this._flags === this.constructor.FLAGS.NONE; } /** * Is the tracked event over the DisplayObject? * * @member {boolean} */ }, { key: "over", get: function get$$1() { return (this._flags & this.constructor.FLAGS.OVER) !== 0; } /** * Set the over flag * * @param {boolean} yn - Is the event over? */ , set: function set$$1(yn) { this._doSet(this.constructor.FLAGS.OVER, yn); } /** * Did the right mouse button come down in the DisplayObject? * * @member {boolean} */ }, { key: "rightDown", get: function get$$1() { return (this._flags & this.constructor.FLAGS.RIGHT_DOWN) !== 0; } /** * Set the right down flag * * @param {boolean} yn - Is the right mouse button down? */ , set: function set$$1(yn) { this._doSet(this.constructor.FLAGS.RIGHT_DOWN, yn); } /** * Did the left mouse button come down in the DisplayObject? * * @member {boolean} */ }, { key: "leftDown", get: function get$$1() { return (this._flags & this.constructor.FLAGS.LEFT_DOWN) !== 0; } /** * Set the left down flag * * @param {boolean} yn - Is the left mouse button down? */ , set: function set$$1(yn) { this._doSet(this.constructor.FLAGS.LEFT_DOWN, yn); } }]); return InteractionTrackingData; }(); InteractionTrackingData.FLAGS = Object.freeze({ NONE: 0, OVER: 1 << 0, LEFT_DOWN: 1 << 1, RIGHT_DOWN: 1 << 2 }); var MOUSE_POINTER_ID = 'MOUSE'; // helpers for hitTest() - only used inside hitTest() var hitTestEvent = { target: null, data: { global: null } }; /** * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true * This manager also supports multitouch. * * reference to [pixi.js](http://www.pixijs.com/) impl * * @private * @class * @extends EventDispatcher */ var InteractionManager = function (_EventDispatcher) { inherits(InteractionManager, _EventDispatcher); /** * @param {WebGLRenderer} renderer - A reference to the current renderer * @param {Scene} scene - A reference to the current scene * @param {Camera} camera - A reference to the current camera * @param {Object} [options] - The options for the manager. * @param {Boolean} [options.autoPreventDefault=false] - Should the manager automatically prevent default browser actions. * @param {Boolean} [options.autoAttach=true] - Should the manager automatically attach target element. * @param {Number} [options.interactionFrequency=10] - Frequency increases the interaction events will be checked. */ function InteractionManager(renderer, scene, camera, options) { classCallCheck(this, InteractionManager); var _this = possibleConstructorReturn(this, (InteractionManager.__proto__ || Object.getPrototypeOf(InteractionManager)).call(this)); options = options || {}; /** * The renderer this interaction manager works for. * * @member {WebGLRenderer} */ _this.renderer = renderer; /** * The renderer this interaction manager works for. * * @member {Scene} */ _this.scene = scene; /** * The renderer this interaction manager works for. * * @member {Camera} */ _this.camera = camera; /** * Should default browser actions automatically be prevented. * Does not apply to pointer events for backwards compatibility * preventDefault on pointer events stops mouse events from firing * Thus, for every pointer event, there will always be either a mouse of touch event alongside it. * * @member {boolean} * @default false */ _this.autoPreventDefault = options.autoPreventDefault || false; /** * Frequency in milliseconds that the mousemove, moveover & mouseout interaction events will be checked. * * @member {number} * @default 10 */ _this.interactionFrequency = options.interactionFrequency || 10; /** * The mouse data * * @member {InteractionData} */ _this.mouse = new InteractionData(); _this.mouse.identifier = MOUSE_POINTER_ID; // setting the mouse to start off far off screen will mean that mouse over does // not get called before we even move the mouse. _this.mouse.global.set(-999999); /** * Actively tracked InteractionData * * @private * @member {Object.<number,InteractionData>} */ _this.activeInteractionData = {}; _this.activeInteractionData[MOUSE_POINTER_ID] = _this.mouse; /** * Pool of unused InteractionData * * @private * @member {InteractionData[]} */ _this.interactionDataPool = []; /** * An event data object to handle all the event tracking/dispatching * * @member {object} */ _this.eventData = new InteractionEvent(); /** * The DOM element to bind to. * * @private * @member {HTMLElement} */ _this.interactionDOMElement = null; /** * This property determines if mousemove and touchmove events are fired only when the cursor * is over the object. * Setting to true will make things work more in line with how the DOM verison works. * Setting to false can make things easier for things like dragging * It is currently set to false as this is how three.js used to work. * * @member {boolean} * @default true */ _this.moveWhenInside = true; /** * Have events been attached to the dom element? * * @private * @member {boolean} */ _this.eventsAdded = false; /** * Is the mouse hovering over the renderer? * * @private * @member {boolean} */ _this.mouseOverRenderer = false; /** * Does the device support touch events * https://www.w3.org/TR/touch-events/ * * @readonly * @member {boolean} */ _this.supportsTouchEvents = 'ontouchstart' in window; /** * Does the device support pointer events * https://www.w3.org/Submission/pointer-events/ * * @readonly * @member {boolean} */ _this.supportsPointerEvents = !!window.PointerEvent; // this will make it so that you don't have to call bind all the time /** * @private * @member {Function} */ _this.onClick = _this.onClick.bind(_this); _this.processClick = _this.processClick.bind(_this); /** * @private * @member {Function} */ _this.onPointerUp = _this.onPointerUp.bind(_this); _this.processPointerUp = _this.processPointerUp.bind(_this); /** * @private * @member {Function} */ _this.onPointerCancel = _this.onPointerCancel.bind(_this); _this.processPointerCancel = _this.processPointerCancel.bind(_this); /** * @private * @member {Function} */ _this.onPointerDown = _this.onPointerDown.bind(_this); _this.processPointerDown = _this.processPointerDown.bind(_this); /** * @private * @member {Function} */ _this.onPointerMove = _this.onPointerMove.bind(_this); _this.processPointerMove = _this.processPointerMove.bind(_this); /** * @private * @member {Function} */ _this.onPointerOut = _this.onPointerOut.bind(_this); _this.processPointerOverOut = _this.processPointerOverOut.bind(_this); /** * @private * @member {Function} */ _this.onPointerOver = _this.onPointerOver.bind(_this); /** * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor * values, objects are handled as dictionaries of CSS values for interactionDOMElement, * and functions are called instead of changing the CSS. * Default CSS cursor values are provided for 'default' and 'pointer' modes. * @member {Object.<string, (string|Function|Object.<string, string>)>} */ _this.cursorStyles = { default: 'inherit', pointer: 'pointer' }; /** * The mode of the cursor that is being used. * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ _this.currentCursorMode = null; /** * Internal cached let. * * @private * @member {string} */ _this.cursor = null; /** * ray caster, for survey intersects from 3d-scene * * @private * @member {Raycaster} */ _this.raycaster = new Raycaster(); /** * snippet time * * @private * @member {Number} */ _this._deltaTime = 0; _this.setTargetElement(_this.renderer.domElement); /** * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * * @event InteractionManager#mousedown * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. * * @event InteractionManager#rightdown * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. * * @event InteractionManager#mouseup * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. * * @event InteractionManager#rightup * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. * * @event InteractionManager#click * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. * * @event InteractionManager#rightclick * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link InteractionManager#event:mousedown}. * * @event InteractionManager#mouseupoutside * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * outside the display object that initially registered a * [rightdown]{@link InteractionManager#event:rightdown}. * * @event InteractionManager#rightupoutside * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved while over the display object * * @event InteractionManager#mousemove * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object * * @event InteractionManager#mouseover * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object * * @event InteractionManager#mouseout * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * * @event InteractionManager#pointerdown * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * * @event InteractionManager#pointerup * @param {InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event * * @event InteractionManager#pointercancel * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * * @event InteractionManager#pointertap * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link InteractionManager#event:pointerdown}. * * @event InteractionManager#pointerupoutside * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object * * @event InteractionManager#pointermove * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object * * @event InteractionManager#pointerover * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object * * @event InteractionManager#pointerout * @param {InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * * @event InteractionManager#touchstart * @param {InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * * @event InteractionManager#touchend * @param {InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch * * @event InteractionManager#touchcancel * @param {InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * * @event InteractionManager#tap * @param {InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link InteractionManager#event:touchstart}. * * @event InteractionManager#touchendoutside * @param {InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * * @event InteractionManager#touchmove * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button (usually a mouse left-button) is pressed on the display. * object. DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#mousedown * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#rightdown * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button (usually a mouse left-button) is released over the display * object. DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#mouseup * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * over the display object. DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#rightup * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button (usually a mouse left-button) is pressed and released on * the display object. DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#click * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is pressed * and released on the display object. DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#rightclick * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button (usually a mouse left-button) is released outside the * display object that initially registered a * [mousedown]{@link Object3D#event:mousedown}. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#mouseupoutside * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device secondary button (usually a mouse right-button) is released * outside the display object that initially registered a * [rightdown]{@link Object3D#event:rightdown}. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#rightupoutside * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved while over the display object. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#mousemove * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved onto the display object. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#mouseover * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device (usually a mouse) is moved off the display object. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#mouseout * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed on the display object. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#pointerdown * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released over the display object. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#pointerup * @param {InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a pointer event. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#pointercancel * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is pressed and released on the display object. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#pointertap * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device button is released outside the display object that initially * registered a [pointerdown]{@link Object3D#event:pointerdown}. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#pointerupoutside * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved while over the display object. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#pointermove * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved onto the display object. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#pointerover * @param {InteractionEvent} event - Interaction event */ /** * Fired when a pointer device is moved off the display object. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#pointerout * @param {InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed on the display object. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#touchstart * @param {InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed from the display object. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#touchend * @param {InteractionEvent} event - Interaction event */ /** * Fired when the operating system cancels a touch. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#touchcancel * @param {InteractionEvent} event - Interaction event */ /** * Fired when a touch point is placed and removed from the display object. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#tap * @param {InteractionEvent} event - Interaction event */ /** * Fired when a touch point is removed outside of the display object that initially * registered a [touchstart]{@link Object3D#event:touchstart}. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#touchendoutside * @param {InteractionEvent} event - Interaction event */ /** * Fired when a touch point is moved along the display object. * DisplayObject's `interactive` property must be set to `true` to fire event. * * @event Object3D#touchmove * @param {InteractionEvent} event - Interaction event */ return _this; } /** * Hit tests a point against the display tree, returning the first interactive object that is hit. * * @param {Point} globalPoint - A point to hit test with, in global space. * @param {Object3D} [root] - The root display object to start from. If omitted, defaults * to the last rendered root of the associated renderer. * @return {Object3D} The hit display object, if any. */ createClass(InteractionManager, [{ key: 'hitTest', value: function hitTest(globalPoint, root) { // clear the target for our hit test hitTestEvent.target = null; // assign the global point hitTestEvent.data.global = globalPoint; // ensure safety of the root if (!root) { root = this.scene; } // run the hit test this.processInteractive(hitTestEvent, root, null, true); // return our found object - it'll be null if we didn't hit anything return hitTestEvent.target; } /** * Sets the DOM element which will receive mouse/touch events. This is useful for when you have * other DOM elements on top of the renderers Canvas element. With this you'll be bale to deletegate * another DOM element to receive those events. * * @param {HTMLCanvasElement} element - the DOM element which will receive mouse and touch events. */ }, { key: 'setTargetElement', value: function setTargetElement(element) { this.removeEvents(); this.interactionDOMElement = element; this.addEvents(); } /** * Registers all the DOM events * * @private */ }, { key: 'addEvents', value: function addEvents() { if (!this.interactionDOMElement || this.eventsAdded) { return; } this.emit('addevents'); this.interactionDOMElement.addEventListener('click', this.onClick, true); if (window.navigator.msPointerEnabled) { this.interactionDOMElement.style['-ms-content-zooming'] = 'none'; this.interactionDOMElement.style['-ms-touch-action'] = 'none'; } else if (this.supportsPointerEvents) { this.interactionDOMElement.style['touch-action'] = 'none'; } /** * These events are added first, so that if pointer events are normalised, they are fired * in the same order as non-normalised events. ie. pointer event 1st, mouse / touch 2nd */ if (this.supportsPointerEvents) { window.document.addEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); // pointerout is fired in addition to pointerup (for touch events) and pointercancel // we already handle those, so for the purposes of what we do in onPointerOut, we only // care about the pointerleave event this.interactionDOMElement.addEventListener('pointerleave', this.onPointerOut, true); this.interactionDOMElement.addEventListener('pointerover', this.onPointerOver, true); window.addEventListener('pointercancel', this.onPointerCancel, true); window.addEventListener('pointerup', this.onPointerUp, true); } else { window.document.addEventListener('mousemove', this.onPointerMove, true); this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); window.addEventListener('mouseup', this.onPointerUp, true); } // always look directly for touch events so that we can provide original data // In a future version we should change this to being just a fallback and rely solely on // PointerEvents whenever available if (this.supportsTouchEvents) { this.interactionDOMElement.addEventListener('touchstart', this.onPointerDown, true); this.interactionDOMElement.addEventListener('touchcancel', this.onPointerCancel, true); this.interactionDOMElement.addEventListener('touchend', this.onPointerUp, true); this.interactionDOMElement.addEventListener('touchmove', this.onPointerMove, true); } this.eventsAdded = true; } /** * Removes all the DOM events that were previously registered * * @private */ }, { key: 'removeEvents', value: function removeEvents() { if (!this.interactionDOMElement) { return; } this.emit('removeevents'); this.interactionDOMElement.removeEventListener('click', this.onClick, true); if (window.navigator.msPointerEnabled) { this.interactionDOMElement.style['-ms-content-zooming'] = ''; this.interactionDOMElement.style['-ms-touch-action'] = ''; } else if (this.supportsPointerEvents) { this.interactionDOMElement.style['touch-action'] = ''; } if (this.supportsPointerEvents) { window.document.removeEventListener('pointermove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.removeEventListener('pointerleave', this.onPointerOut, true); this.interactionDOMElement.removeEventListener('pointerover', this.onPointerOver, true); window.removeEventListener('pointercancel', this.onPointerCancel, true); window.removeEventListener('pointerup', this.onPointerUp, true); } else { window.document.removeEventListener('mousemove', this.onPointerMove, true); this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); window.removeEventListener('mouseup', this.onPointerUp, true); } if (this.supportsTouchEvents) { this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, true); this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); } this.interactionDOMElement = null; this.eventsAdded = false; } /** * Updates the state of interactive objects. * Invoked by a throttled ticker. * * @param {number} deltaTime - time delta since last tick */ }, { key: 'update', value: function update(_ref) { var snippet = _ref.snippet; this._deltaTime += snippet; if (this._deltaTime < this.interactionFrequency) { return; } this._deltaTime = 0; if (!this.interactionDOMElement) { return; } // if the user move the mouse this check has already been done using the mouse move! if (this.didMove) { this.didMove = false; return; } this.cursor = null; // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. // In this case, mouseover and mouseevents would not pass the flag test in triggerEvent function for (var k in this.activeInteractionData) { // eslint-disable-next-line no-prototype-builtins if (this.activeInteractionData.hasOwnProperty(k)) { var interactionData = this.activeInteractionData[k]; if (interactionData.originalEvent && interactionData.pointerType !== 'touch') { var interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, interactionData.originalEvent, interactionData); this.processInteractive(interactionEvent, this.scene, this.processPointerOverOut, true); } } } this.setCursorMode(this.cursor); // TODO } /** * Sets the current cursor mode, handling any callbacks or CSS style changes. * * @param {string} mode - cursor mode, a key from the cursorStyles dictionary */ }, { key: 'setCursorMode', value: function setCursorMode(mode) { mode = mode || 'default'; // if the mode didn't actually change, bail early if (this.currentCursorMode === mode) { return; } this.currentCursorMode = mode; var style = this.cursorStyles[mode]; // only do things if there is a cursor style for it if (style) { switch (typeof style === 'undefined' ? 'undefined' : _typeof(style)) { case 'string': // string styles are handled as cursor CSS this.interactionDOMElement.style.cursor = style; break; case 'function': // functions are just called, and passed the cursor mode style(mode); break; case 'object': // if it is an object, assume that it is a dictionary of CSS styles, // apply it to the interactionDOMElement Object.assign(this.interactionDOMElement.style, style); break; default: break; } } else if (typeof mode === 'string' && !Object.prototype.hasOwnProperty.call(this.cursorStyles, mode)) { // if it mode is a string (not a Symbol) and cursorStyles doesn't have any entry // for the mode, then assume that the dev wants it to be CSS for the cursor. this.interactionDOMElement.style.cursor = mode; } } /** * Dispatches an event on the display object that was interacted with * * @param {Object3D} displayObject - the display object in question * @param {string} eventString - the name of the event (e.g, mousedown) * @param {object} eventData - the event data object * @private */ }, { key: 'triggerEvent', value: function triggerEvent(displayObject, eventString, eventData) { if (!eventData.stopped) { eventData.currentTarget = displayObject; eventData.type = eventString; displayObject.emit(eventString, eventData); if (displayObject[eventString]) { displayObject[eventString](eventData); } } } /** * This function is provides a neat way of crawling through the scene graph and running a * specified function on all interactive objects it finds. It will also take care of hit * testing the interactive objects and passes the hit across in the function. * * @private * @param {InteractionEvent} interactionEvent - event containing the point that * is tested for collision * @param {Object3D} displayObject - the displayObject * that will be hit test (recursively crawls its children) * @param {Function} [func] - the function that will be called on each interactive object. The * interactionEvent, displayObject and hit will be passed to the function * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point * @param {boolean} [interactive] - Whether the displayObject is interactive * @return {boolean} returns true if the displayObject hit the point */ }, { key: 'processInteractive', value: function processInteractive(interactionEvent, displayObject, func, hitTest, interactive) { if (!displayObject || !displayObject.visible) { return false; } // Took a little while to rework this function correctly! But now it is done and nice and optimised. ^_^ // // This function will now loop through all objects and then only hit test the objects it HAS // to, not all of them. MUCH faster.. // An object will be hit test if the following is true: // // 1: It is interactive. // 2: It belongs to a parent that is interactive AND one of the parents children have not already been hit. // // As another little optimisation once an interactive object has been hit we can carry on // through the scenegraph, but we know that there will be no more hits! So we can avoid extra hit tests // A final optimisation is that an object is not hit test directly if a child has already been hit. interactive = displayObject.interactive || interactive; var hit = false; var interactiveParent = interactive; if (displayObject.interactiveChildren && displayObject.children) { var children = displayObject.children; for (var i = children.length - 1; i >= 0; i--) { var child = children[i]; // time to get recursive.. if this function will return if something is hit.. var childHit = this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent); if (childHit) { // its a good idea to check if a child has lost its parent. // this means it has been removed whilst looping so its best if (!child.parent) { continue; } // we no longer need to hit test any more objects in this container as we we // now know the parent has been hit interactiveParent = false; // If the child is interactive , that means that the object hit was actually // interactive and not just the child of an interactive object. // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. if (chil