/** * @module PIXI UI * @namespace springroll.pixi * @requires Core, PIXI Display */ (function() { var Application, Tween, Point, DragData = include("springroll.pixi.DragData"); /** * Drag manager is responsible for handling the dragging of stage elements * supports click-n-stick and click-n-drag functionality. * * @class DragManager * @constructor * @param {PixiDisplay} display The display that this DragManager is handling objects on. * Optionally, this parameter can be omitted and the Application's * default display will be used. * @param {Function} startCallback The callback when when starting * @param {Function} endCallback The callback when ending */ var DragManager = function(display, startCallback, endCallback) { if (!Application) { Application = include('springroll.Application'); Tween = include('createjs.Tween', false); Point = include('PIXI.Point'); } if (typeof display == "function" && !endCallback) { endCallback = startCallback; startCallback = display; display = Application.instance.display; } /** * The object that's being dragged, or a dictionary of DragData being dragged * by id if multitouch is true. * @public * @readOnly * @property {PIXI.DisplayObject|Dictionary} draggedObj */ this.draggedObj = null; /** * The radius in pixel to allow for dragging, or else does sticky click * @public * @property dragStartThreshold * @default 20 */ this.dragStartThreshold = 20; /** * The position x, y of the mouse down on the stage. This is only used * when multitouch is false - the DragData has it when multitouch is true. * @private * @property {PIXI.Point} mouseDownStagePos */ this.mouseDownStagePos = new Point(0, 0); /** * The position x, y of the object when interaction with it started. If multitouch is * true, then this will only be set during a drag stop callback, for the object that just * stopped getting dragged. * @property {PIXI.Point} mouseDownObjPos */ this.mouseDownObjPos = new Point(0, 0); /** * If sticky click dragging is allowed. * @public * @property {Bool} allowStickyClick * @default true */ this.allowStickyClick = true; /** * Is the move touch based * @public * @readOnly * @property {Bool} isTouchMove * @default false */ this.isTouchMove = false; /** * Is the drag being held on mouse down (not sticky clicking) * @public * @readOnly * @property {Bool} isHeldDrag * @default false */ this.isHeldDrag = false; /** * Is the drag a sticky clicking (click on a item, then mouse the mouse) * @public * @readOnly * @property {Bool} isStickyClick * @default false */ this.isStickyClick = false; /** * Settings for snapping. * * Format for snapping to a list of points: * { * mode:"points", * dist:20,//snap when within 20 pixels/units * points:[ * { x: 20, y:30 }, * { x: 50, y:10 } * ] * } * * @public * @property {Object} snapSettings * @default null */ this.snapSettings = null; /** * Reference to the Pixi InteractionManager. * @private * @property {PIXI.interaction.InteractionManager} _interaction */ this._interaction = display.renderer.plugins.interaction; /** * The offset from the dragged object's position that the initial mouse event * was at. This is only used when multitouch is false - the DragData has * it when multitouch is true. * @private * @property {PIXI.Point} _dragOffset */ this._dragOffset = null; /** * External callback when we start dragging * @private * @property {Function} _dragStartCallback */ this._dragStartCallback = startCallback; /** * External callback when we are done dragging * @private * @property {Function} _dragEndCallback */ this._dragEndCallback = endCallback; this._triggerHeldDrag = this._triggerHeldDrag.bind(this); this._triggerStickyClick = this._triggerStickyClick.bind(this); this._stopDrag = this._stopDrag.bind(this); this._updateObjPosition = this._updateObjPosition.bind(this); /** * The collection of draggable objects * @private * @property {Array} _draggableObjects */ this._draggableObjects = []; /** * If this DragManager is using multitouch for dragging. * @private * @property {Boolean} _multitouch */ this._multitouch = false; /** * If this DragManager has added drag listeners to the InteractionManager * @private * @property {Boolean} _addedDragListeners */ this._addedDragListeners = false; this.helperPoint = new Point(0, 0); }; // Reference to the drag manager var p = extend(DragManager); /** * If the DragManager allows multitouch dragging. Setting this stops any current * drags. * @property {Boolean} multitouch */ Object.defineProperty(p, "multitouch", { get: function() { return this._multitouch; }, set: function(value) { if (this.draggedObj) { if (this._multitouch) { for (var id in this.draggedObj) { this._stopDrag(id, true); } } else this._stopDrag(null, true); } this._multitouch = !!value; this.draggedObj = value ? {} : null; } }); /** * Manually starts dragging an object. If a mouse down event is not supplied * as the second argument, it defaults to a held drag, that ends as soon as * the mouse is released. When using multitouch, passing a interaction data is * required. * @method startDrag * @public * @param {PIXI.DisplayObject} object The object that should be dragged. * @param {PIXI.InteractionData} interactionData The interaction data about * the input event that triggered this. */ p.startDrag = function(object, interactionData) { this._objMouseDown(object, interactionData); }; /** * Mouse down on an object * @method _objMouseDown * @private * @param {PIXI.DisplayObject} object The object that should be dragged. * @param {PIXI.InteractionData} interactionData The interaction data about * the input event that triggered this. */ p._objMouseDown = function(obj, interactionData) { //get the InteractionData we want from the Pixi v3 events if (interactionData.data && interactionData.data.global) interactionData = interactionData.data; // if we are dragging something, then ignore any mouse downs // until we release the currently dragged stuff if ((!this._multitouch && this.draggedObj) || (this._multitouch && !interactionData)) return; var dragData, mouseDownObjPos, mouseDownStagePos, dragOffset; if (this._multitouch) { dragData = new DragData(obj); this.draggedObj[interactionData.identifier] = dragData; mouseDownObjPos = dragData.mouseDownObjPos; mouseDownStagePos = dragData.mouseDownStagePos; dragOffset = dragData.dragOffset; } else { this.draggedObj = obj; mouseDownObjPos = this.mouseDownObjPos; mouseDownStagePos = this.mouseDownStagePos; dragOffset = this._dragOffset = new Point(); } //Stop any tweens on the object (mostly the position) if (Tween) { Tween.removeTweens(obj); Tween.removeTweens(obj.position); } if (obj._dragOffset) { dragOffset.x = obj._dragOffset.x; dragOffset.y = obj._dragOffset.y; } else { //get the mouse position and convert it to object parent space interactionData.getLocalPosition(obj.parent, dragOffset); //move the offset to respect the object's current position dragOffset.x -= obj.position.x; dragOffset.y -= obj.position.y; } mouseDownObjPos.x = obj.position.x; mouseDownObjPos.y = obj.position.y; //if we don't get an event (manual call neglected to pass one) then default to a held drag if (!interactionData) { this.isHeldDrag = true; this._startDrag(); } else { mouseDownStagePos.x = interactionData.global.x; mouseDownStagePos.y = interactionData.global.y; //if it is a touch event, force it to be the held drag type if (!this.allowStickyClick || interactionData.originalEvent.type == "touchstart") { this.isTouchMove = interactionData.originalEvent.type == "touchstart"; this.isHeldDrag = true; this._startDrag(interactionData); } //otherwise, wait for a movement or a mouse up in order to do a //held drag or a sticky click drag else { this._interaction.on("stagemove", this._triggerHeldDrag); this._interaction.on("stageup", this._triggerStickyClick); } } }; /** * Start the sticky click * @method _triggerStickyClick * @param {PIXI.InteractionData} interactionData The interaction data about * the input event that triggered this. * @private */ p._triggerStickyClick = function(interactionData) { //get the InteractionData we want from the Pixi v3 events interactionData = interactionData.data; this.isStickyClick = true; var draggedObj = this._multitouch ? this.draggedObj[interactionData.identifier].obj : this.draggedObj; this._interaction.off("stagemove", this._triggerHeldDrag); this._interaction.off("stageup", this._triggerStickyClick); this._startDrag(interactionData); }; /** * Start hold dragging * @method _triggerHeldDrag * @private * @param {PIXI.InteractionData} interactionData The ineraction data about the moved mouse */ p._triggerHeldDrag = function(interactionData) { //get the InteractionData we want from the Pixi v3 events interactionData = interactionData.data; var mouseDownStagePos, draggedObj; if (this._multitouch) { draggedObj = this.draggedObj[interactionData.identifier].obj; mouseDownStagePos = this.draggedObj[interactionData.identifier].mouseDownStagePos; } else { draggedObj = this.draggedObj; mouseDownStagePos = this.mouseDownStagePos; } var xDiff = interactionData.global.x - mouseDownStagePos.x; var yDiff = interactionData.global.y - mouseDownStagePos.y; if (xDiff * xDiff + yDiff * yDiff >= this.dragStartThreshold * this.dragStartThreshold) { this.isHeldDrag = true; this._interaction.off("stagemove", this._triggerHeldDrag); this._interaction.off("stageup", this._triggerStickyClick); this._startDrag(interactionData); } }; /** * Internal start dragging on the stage * @method _startDrag * @param {PIXI.InteractionData} interactionData The ineraction data about the moved mouse * @private */ p._startDrag = function(interactionData) { var draggedObj; if (this._multitouch) draggedObj = this.draggedObj[interactionData.identifier].obj; else draggedObj = this.draggedObj; this._updateObjPosition( { data: interactionData }); if (!this._addedDragListeners) { this._addedDragListeners = true; this._interaction.on("stagemove", this._updateObjPosition); this._interaction.on("stageup", this._stopDrag); } this._dragStartCallback(draggedObj); }; /** * Stops dragging the currently dragged object. * @public * @method stopDrag * @param {Bool} [doCallback=false] If the drag end callback should be called. * @param {PIXI.DisplayObject} [obj] A specific object to stop dragging, if multitouch * is true. If this is omitted, it stops all drags. */ p.stopDrag = function(doCallback, obj) { var id = null; if (this._multitouch && obj) { for (var key in this.draggedObj) { if (this.draggedObj[key].obj == obj) { id = key; break; } } } //pass true if it was explicitly passed to us, false and undefined -> false this._stopDrag(id, doCallback === true); }; /** * Internal stop dragging on the stage * @method _stopDrag * @private * @param {PIXI.InteractionData} interactionData The ineraction data about the moved mouse * @param {Bool} doCallback If we should do the callback */ p._stopDrag = function(interactionData, doCallback) { var obj, id = null; //if touch id was passed directly if (typeof interactionData == "number") id = interactionData; else if (interactionData) { //get the InteractionData we want from the Pixi v3 events if (interactionData.data && interactionData.data.global) id = interactionData.data.identifier; else if (interactionData instanceof PIXI.interaction.InteractionData) id = interactionData.identifier; } if (this._multitouch) { if (id !== null) { //stop a specific drag var data = this.draggedObj[id]; if (!data) return; obj = data.obj; //save the position that it started at so the callback can make use of it //if they want this.mouseDownObjPos.x = data.mouseDownObjPos.x; this.mouseDownObjPos.y = data.mouseDownObjPos.y; delete this.draggedObj[id]; } else { //stop all drags for (id in this.draggedObj) { this._stopDrag(id, doCallback); } return; } } else { obj = this.draggedObj; this.draggedObj = null; } if (!obj) return; var removeGlobalListeners = !this._multitouch; if (this._multitouch) { //determine if this was the last drag var found = false; for (id in this.draggedObj) { found = true; break; } removeGlobalListeners = !found; } if (removeGlobalListeners && this._addedDragListeners) { this._addedDragListeners = false; this._interaction.off("stagemove", this._updateObjPosition); this._interaction.off("stageup", this._stopDrag); } this.isTouchMove = false; this.isStickyClick = false; this.isHeldMove = false; if (doCallback !== false) // true or undefined this._dragEndCallback(obj); }; /** * Update the object position based on the mouse * @method _updateObjPosition * @private * @param {PIXI.InteractionData} interactionData Mouse move event */ p._updateObjPosition = function(interactionData) { //get the InteractionData we want from the Pixi v3 events interactionData = interactionData.data; //if(!this.isTouchMove && !this._theStage.interactionManager.mouseInStage) return; var draggedObj, dragOffset; if (this._multitouch) { var data = this.draggedObj[interactionData.identifier]; draggedObj = data.obj; dragOffset = data.dragOffset; } else { draggedObj = this.draggedObj; dragOffset = this._dragOffset; } if (!draggedObj || !draggedObj.parent) //not quite sure what chain of events would lead to this, but we'll stop dragging to be safe { this.stopDrag(false, draggedObj); return; } var mousePos = interactionData.getLocalPosition(draggedObj.parent, this.helperPoint); var bounds = draggedObj._dragBounds; if (bounds) { draggedObj.position.x = Math.clamp(mousePos.x - dragOffset.x, bounds.x, bounds.right); draggedObj.position.y = Math.clamp(mousePos.y - dragOffset.y, bounds.y, bounds.bottom); } else { draggedObj.position.x = mousePos.x - dragOffset.x; draggedObj.position.y = mousePos.y - dragOffset.y; } if (this.snapSettings) { switch (this.snapSettings.mode) { case "points": this._handlePointSnap(mousePos, dragOffset, draggedObj); break; case "grid": //not yet implemented break; case "line": //not yet implemented break; } } }; /** * Handles snapping the dragged object to the nearest among a list of points * @method _handlePointSnap * @private * @param {PIXI.Point} localMousePos The mouse position in the same space as the dragged object. * @param {PIXI.Point} dragOffset The drag offset for the dragged object. * @param {PIXI.DisplayObject} obj The object to snap. */ p._handlePointSnap = function(localMousePos, dragOffset, obj) { var snapSettings = this.snapSettings; var minDistSq = snapSettings.dist * snapSettings.dist; var points = snapSettings.points; var objX = localMousePos.x - dragOffset.x; var objY = localMousePos.y - dragOffset.y; var leastDist = -1; var closestPoint = null; for (var i = points.length - 1; i >= 0; --i) { var p = points[i]; var distSq = Math.distSq(objX, objY, p.x, p.y); if (distSq <= minDistSq && (distSq < leastDist || leastDist == -1)) { leastDist = distSq; closestPoint = p; } } if (closestPoint) { draggedObj.position.x = closestPoint.x; draggedObj.position.y = closestPoint.y; } }; //=== Giving functions and properties to draggable objects objects var enableDrag = function() { this.on("touchstart", this._onMouseDownListener); this.on("mousedown", this._onMouseDownListener); this.buttonMode = this.interactive = true; }; var disableDrag = function() { this.off("touchstart", this._onMouseDownListener); this.off("mousedown", this._onMouseDownListener); this.buttonMode = this.interactive = false; }; var _onMouseDown = function(mouseData) { this._dragMan._objMouseDown(this, mouseData); }; /** * Adds properties and functions to the object - use enableDrag() and disableDrag() on * objects to enable/disable them (they start out disabled). Properties added to objects: * _dragBounds (Rectangle), _dragOffset (Point), _onMouseDownListener (Function), * _dragMan (springroll.DragManager) reference to the DragManager * these will override any existing properties of the same name * @method addObject * @public * @param {PIXI.DisplayObject} obj The display object * @param {PIXI.Rectangle} [bounds] The rectangle bounds. 'right' and 'bottom' properties * will be added to this object. * @param {PIXI.Point} [dragOffset] A specific drag offset to use each time, instead of * the mousedown/touchstart position relative to the * object. This is useful if you want something to always * be dragged from a specific position, like the base of * a torch. */ p.addObject = function(obj, bounds, dragOffset) { if (bounds) { bounds.right = bounds.x + bounds.width; bounds.bottom = bounds.y + bounds.height; } obj._dragBounds = bounds; obj._dragOffset = dragOffset || null; if (this._draggableObjects.indexOf(obj) >= 0) { //don't change any of the functions or anything, just quit the function after having updated the bounds return; } obj.enableDrag = enableDrag; obj.disableDrag = disableDrag; obj._onMouseDownListener = _onMouseDown.bind(obj); obj._dragMan = this; this._draggableObjects.push(obj); }; /** * Removes properties and functions added by addObject(). * @public * @method removeObject * @param {PIXI.DisplayObject} obj The display object */ p.removeObject = function(obj) { var index = this._draggableObjects.indexOf(obj); if (index >= 0) { obj.disableDrag(); delete obj.enableDrag; delete obj.disableDrag; delete obj._onMouseDownListener; delete obj._dragMan; delete obj._dragBounds; delete obj._dragOffset; this._draggableObjects.splice(index, 1); } }; /** * Destroy the manager * @public * @method destroy */ p.destroy = function() { //clean up dragged obj this.stopDrag(false); this._updateObjPosition = null; this._dragStartCallback = null; this._dragEndCallback = null; this._triggerHeldDrag = null; this._triggerStickyClick = null; this._stopDrag = null; this._interaction = null; for (var i = this._draggableObjects.length - 1; i >= 0; --i) { var obj = this._draggableObjects[i]; obj.disableDrag(); delete obj.enableDrag; delete obj.disableDrag; delete obj._onMouseDownListener; delete obj._dragMan; delete obj._dragBounds; delete obj._dragOffset; } this._draggableObjects = null; }; // Assign to the global namespace namespace('springroll').DragManager = DragManager; namespace('springroll.pixi').DragManager = DragManager; }());