import Data from '../Data.js';
import createComponentClass from '../factory.js';
import {arrayCache} from '../utils/array.js';
import {shapeFromObject, shapeToPixiShape} from '../shapes/index.js';
const
savedHitAreas = {}; //So generated hitAreas are reused across identical entities.
export default createComponentClass(/** @lends platypus.components.Interactive.prototype */{
id: 'Interactive',
properties: {
/**
* Sets the container that represents the interactive area.
*
* @property container
* @type PIXI.Container
* @default null
*/
"container": null,
/**
* Sets the hit area for interactive responses by describing the dimensions of a clickable rectangle:
*
* "hitArea": {
* "x": 10,
* "y": 10,
* "width": 40,
* "height": 40
* }
*
* Or a circle:
*
* "hitArea": {
* "x": 10,
* "y": 10,
* "radius": 40
* }
*
* Or use an array of numbers to define a polygon: [x1, y1, x2, y2, ...]
*
* "hitArea": [-10, -10, 30, -10, 30, 30, -5, 30]
*
* Defaults to the container if not specified.
*
* @property hitArea
* @type Object
* @default null
*/
"hitArea": null,
/**
* Sets whether the entity should respond to mouse hovering.
*
* @property hover
* @type Boolean
* @default false
*/
"hover": false,
/**
* Used when returning world coordinates. Typically coordinates are relative to the parent, but when this component is added to the layer level, coordinates must be relative to self.
*
* @property relativeToSelf
* @type String
* @default false
*/
"relativeToSelf": false,
/**
* Whether camera updates should trigger "pointermove" and "pressmove" events.
*
* @property trackCameraUpdates
* @type Boolean
* @default false
*/
"trackCameraUpdates": false
},
publicProperties: {
/**
* Determines whether hovering over the sprite should alter the cursor.
*
* @property buttonMode
* @type Boolean
* @default false
* @deprecated
*/
buttonMode: false
},
/**
* This component accepts touches and clicks on the entity. It is typically automatically added by a render component that requires interactive functionality.
*
* @memberof platypus.components
* @uses platypus.Component
* @constructs
* @listens platypus.Entity#dispatch-event
* @listens platypus.Entity#handle-render
* @listens platypus.Entity#input-off
* @listens platypus.Entity#input-on
* @listens platypus.Entity#set-hit-area
* @fires platypus.Entity#pressmove
* @fires platypus.Entity#pressup
* @fires platypus.Entity#pointerdown
* @fires platypus.Entity#pointermove
* @fires platypus.Entity#pointertap
* @fires platypus.Entity#pointerout
* @fires platypus.Entity#pointerover
* @fires platypus.Entity#pointerup
* @fires platypus.Entity#pointerupoutside
* @fires platypus.Entity#pointercancel
*/
initialize: function () {
this.pressed = null;
if (this.hitArea) {
this.container.hitArea = this.setHitArea(this.hitArea);
}
if (this.buttonMode) {
platypus.debug.warn('Interactive: "buttonMode" is deprecated. Set "cursor" to "pointer" on the entity instead.');
}
this.dragOver = false;
},
events: {
/**
* This event dispatches a PIXI.Event on this component's PIXI.Sprite. Useful for rerouting mouse/keyboard events.
*
* @event platypus.Entity#dispatch-event
* @param event {Object | PIXI.Event} The event to dispatch.
*/
"dispatch-event": function (event) {
this.sprite.dispatchEvent(this.sprite, event.event, event.data);
},
'camera-update': function () {
if (this.dragOver) {
this.dragOver();
}
},
"input-on": function () {
if (!this.removeInputListeners) {
this.addInputs();
}
},
"input-off": function () {
if (this.removeInputListeners) {
// Make sure currently-pressed is released.
if (this.pressed) {
this.handlePressUp(this.pressed);
}
this.removeInputListeners();
}
},
"pointerdown": function (event) {
if (this.pressed === null) { // so extra multi-touch presses won't trigger 'pressmove' / 'pressup'
this.pressed = Data.setUp(event);
}
},
"pointermove": function (event) {
if (this.pressed?.pixiEvent?.pointerId === event.pixiEvent.pointerId) {
/**
* This event is triggered on press move (drag).
*
* @event platypus.Entity#pressmove
* @param event {DOMEvent} The original DOM pointer event.
* @param pixiEvent {PIXI.interaction.InteractionEvent} The Pixi pointer event.
* @param x {Number} The x coordinate in world units.
* @param y {Number} The y coordinate in world units.
* @param entity {platypus.Entity} The entity receiving this event.
*/
this.owner.triggerEvent('pressmove', event);
this.pressed.recycle();
this.pressed = Data.setUp(event);
}
},
"pointerup": function (event) {
this.handlePressUp(event);
},
"pointerupoutside": function (event) {
this.handlePressUp(event);
},
"pointercancel": function (event) {
this.handlePressUp(event);
},
/**
* Sets the hit area for interactive responses by describing the dimensions of a clickable rectangle:
*
* "hitArea": {
* "x": 10,
* "y": 10,
* "width": 40,
* "height": 40
* }
*
* Or a circle:
*
* "hitArea": {
* "x": 10,
* "y": 10,
* "radius": 40
* }
*
* Or use an array of numbers to define a polygon: [x1, y1, x2, y2, ...]
*
* "hitArea": [-10, -10, 30, -10, 30, 30, -5, 30]
*
* Defaults to the container if set to `null`.
*
* @event platypus.Entity#set-hit-area
* @param {Object} shape
*/
"set-hit-area": function (shape) {
this.container.hitArea = this.setHitArea(shape);
}
},
methods: {
addInputs () {
const
sprite = this.container,
removals = arrayCache.setUp(),
addListener = (event, handler) => {
sprite.addListener(event, handler);
removals.push(() => {
sprite.removeListener(event, handler);
});
},
trigger = (target, eventName, event) => {
const
{container} = target;
if (
container && //TML - This is in case we do a scene change using an event and the container is destroyed.
event.data.originalEvent // This is a workaround for a bug in Pixi 3 where phantom hover events are triggered. - DDD 7/20/16
) {
container.getBounds(); //TML 1/19/24 - Temporary solution for when container transform scale values were unset for a frame. getBounds() calls _recursivePostUpdateTransform() internally which ensures that the transform is up-to-date for this call.
const
{owner, relativeToSelf} = target,
camera = owner.worldCamera?.viewport ?? owner.parent.worldCamera.viewport,
matrix = relativeToSelf ? container.worldTransform : container.parent.worldTransform,
msg = Data.setUp(
"event", event.data.originalEvent,
"pixiEvent", event,
"x", event.data.global.x / matrix.a + camera.left,
"y", event.data.global.y / matrix.d + camera.top,
"entity", owner
);
owner.trigger(eventName, msg);
msg.recycle();
}
};
// The following appends necessary information to displayed objects to allow them to receive touches and clicks
sprite.eventMode = 'static';
addListener('pointerdown', (event) => {
/**
* This event is triggered on pointer down.
*
* @event platypus.Entity#pointerdown
* @param event {DOMEvent} The original DOM pointer event.
* @param pixiEvent {PIXI.interaction.InteractionEvent} The Pixi pointer event.
* @param x {Number} The x coordinate in world units.
* @param y {Number} The y coordinate in world units.
* @param entity {platypus.Entity} The entity receiving this event.
*/
trigger(this, 'pointerdown', event);
event.currentTarget.mouseTarget = true;
});
addListener('pointerup', (event) => {
/**
* This event is triggered on pointer up.
*
* @event platypus.Entity#pointerup
* @param event {DOMEvent} The original DOM pointer event.
* @param pixiEvent {PIXI.interaction.InteractionEvent} The Pixi pointer event.
* @param x {Number} The x coordinate in world units.
* @param y {Number} The y coordinate in world units.
* @param entity {platypus.Entity} The entity receiving this event.
*/
trigger(this, 'pointerup', event);
event.currentTarget.mouseTarget = false;
if (event.currentTarget.removeDisplayObject) {
event.currentTarget.removeDisplayObject();
}
});
addListener('pointerupoutside', (event) => {
/**
* This event is triggered on pointer up outside.
*
* @event platypus.Entity#pointerupoutside
* @param event {DOMEvent} The original DOM pointer event.
* @param pixiEvent {PIXI.interaction.InteractionEvent} The Pixi pointer event.
* @param x {Number} The x coordinate in world units.
* @param y {Number} The y coordinate in world units.
* @param entity {platypus.Entity} The entity receiving this event.
*/
trigger(this, 'pointerupoutside', event);
event.currentTarget.mouseTarget = false;
if (event.currentTarget.removeDisplayObject) {
event.currentTarget.removeDisplayObject();
}
});
addListener('pointercancel', (event) => {
/**
* This event is triggered on pointer cancel.
*
* @event platypus.Entity#pointercancel
* @param event {DOMEvent} The original DOM pointer event.
* @param pixiEvent {PIXI.interaction.InteractionEvent} The Pixi pointer event.
* @param x {Number} The x coordinate in world units.
* @param y {Number} The y coordinate in world units.
* @param entity {platypus.Entity} The entity receiving this event.
*/
trigger(this, 'pointercancel', event);
event.currentTarget.mouseTarget = false;
if (event.currentTarget.removeDisplayObject) {
event.currentTarget.removeDisplayObject();
}
});
addListener('pointermove', (event) => {
/**
* This event is triggered on pointer move.
*
* @event platypus.Entity#pointermove
* @param event {DOMEvent} The original DOM pointer event.
* @param pixiEvent {PIXI.interaction.InteractionEvent} The Pixi pointer event.
* @param x {Number} The x coordinate in world units.
* @param y {Number} The y coordinate in world units.
* @param entity {platypus.Entity} The entity receiving this event.
*/
trigger(this, 'pointermove', event);
event.currentTarget.mouseTarget = true;
});
addListener('pointertap', (event) => {
/**
* This event is triggered on pointer tap.
*
* @event platypus.Entity#pointertap
* @param event {DOMEvent} The original DOM pointer event.
* @param pixiEvent {PIXI.interaction.InteractionEvent} The Pixi pointer event.
* @param x {Number} The x coordinate in world units.
* @param y {Number} The y coordinate in world units.
* @param entity {platypus.Entity} The entity receiving this event.
*/
trigger(this, 'pointertap', event);
});
if (this.hover) {
addListener('pointerover', (event) => {
/**
* This event is triggered on pointer over.
*
* @event platypus.Entity#pointerover
* @param event {DOMEvent} The original DOM pointer event.
* @param pixiEvent {PIXI.interaction.InteractionEvent} The Pixi pointer event.
* @param x {Number} The x coordinate in world units.
* @param y {Number} The y coordinate in world units.
* @param entity {platypus.Entity} The entity receiving this event.
*/
trigger(this, 'pointerover', event);
});
addListener('pointerout', (event) => {
/**
* This event is triggered on pointer out.
*
* @event platypus.Entity#pointerout
* @param event {DOMEvent} The original DOM pointer event.
* @param pixiEvent {PIXI.interaction.InteractionEvent} The Pixi pointer event.
* @param x {Number} The x coordinate in world units.
* @param y {Number} The y coordinate in world units.
* @param entity {platypus.Entity} The entity receiving this event.
*/
trigger(this, 'pointerout', event);
});
}
if (this.trackCameraUpdates) {
const
handlePositionUpdate = (event) => {
if (over) {
over = event;
}
};
let over = null;
this.dragOver = () => {
if (over) {
trigger(this, 'pointermove', over);
}
};
addListener('pointerover', (event) => over = event);
addListener('pointerdown', handlePositionUpdate);
addListener('pointermove', handlePositionUpdate);
addListener('pointerout', () => over = null);
removals.push(() => {
over = null;
this.dragOver = null;
});
}
this.removeInputListeners = () => {
for (let i = 0; i < removals.length; i++) {
removals[i]();
}
arrayCache.recycle(removals);
sprite.eventMode = 'auto';
this.removeInputListeners = null;
};
},
setHitArea (shape) {
if (shape) {
let sav = '',
ha = null;
try {
sav = JSON.stringify(shape);
ha = savedHitAreas[sav];
} catch (e) {}
if (!ha) {
const
definition = Array.isArray(shape) ? {
type: 'polygon',
points: shape
} : shape;
ha = shapeToPixiShape(shapeFromObject(definition));
if (sav) { // can't save if we don't have a valid id
savedHitAreas[sav] = ha;
}
}
return ha;
} else {
return null;
}
},
handlePressUp (event) {
if (this.pressed?.pixiEvent?.pointerId === event.pixiEvent.pointerId) {
/**
* This event is triggered on press up.
*
* @event platypus.Entity#pressup
* @param event {DOMEvent} The original DOM pointer event.
* @param pixiEvent {PIXI.interaction.InteractionEvent} The Pixi pointer event.
* @param x {Number} The x coordinate in world units.
* @param y {Number} The y coordinate in world units.
* @param entity {platypus.Entity} The entity receiving this event.
*/
this.owner.triggerEvent('pressup', event);
this.pressed.recycle();
this.pressed = null;
}
},
toJSON () { // This component is added by another component, so it shouldn't be returned for reconstruction.
return null;
},
destroy () {
const
{container, removeInputListeners} = this;
if (removeInputListeners) {
removeInputListeners();
}
// This handles removal after the mouseup event to prevent weird input behaviors. If it's not currently a mouse target, we let the render component handle its removal from the parent container.
if (container.mouseTarget && container.parent) {
container.visible = false;
container.removeDisplayObject = () => {
if (container.parent) {
container.parent.removeChild(container);
}
this.container = null;
};
}
}
}
});