src/components/GestureListener.js
import Script from './Script';
import Camera2D from './Camera2D';
import Shape from './Shape';
import { vec2 } from '../utils/gl-matrix';
import { propsEnumStringify } from '../utils';
const cachedGlobalVec = vec2.create();
const ActionFlags = {
NONE: 0,
CLICK: 1 << 0,
CLICK_RELEASE: 1 << 1,
MOUSE_ENTER_LEAVE: 1 << 2,
DRAG_DROP: 1 << 3,
ALL: 0xF,
};
/**
* Entity listener for mouse input (good for buttons logic).
*
* @example
* const component = new GestureListener();
* component.deserialize({ camera: '/ui' });
*/
export default class GestureListener extends Script {
/**
* Component factory.
*
* @return {GestureListener} Component factory.
*/
static factory() {
return new GestureListener();
}
/** @type {*} */
static get propsTypes() {
return {
...Script.propsTypes,
camera: 'string_null',
layer: 'string_null',
actions: `flags(${propsEnumStringify(ActionFlags)})`,
};
}
static get ActionFlags() {
return ActionFlags;
}
/** @type {string|null} */
get camera() {
return this._camera;
}
/** @type {string|null} */
set camera(value) {
if (!value) {
this._camera = null;
this._cameraEntity = null;
this._cameraComponent = null;
return;
}
if (typeof value !== 'string') {
throw new Error('`value` is not type of String!');
}
const { entity } = this;
this._camera = value;
if (!!entity) {
this._cameraEntity = entity.findEntity(value);
if (!this._cameraEntity) {
throw new Error(`Cannot find entity: ${value}`);
}
this._cameraComponent = this._cameraEntity.getComponent('Camera2D');
if (!this._cameraComponent) {
throw new Error(
`Entity doe not have Camera2D component: ${this._cameraEntity.path}`
);
}
}
}
/** @type {string|null} */
get layer() {
return this._layer;
}
/** @type {string|null} */
set layer(value) {
if (!value) {
this._layer = null;
return;
}
if (typeof value !== 'string') {
throw new Error('`value` is not type of String!');
}
this._layer = value;
}
get actions() {
return this._actions;
}
set actions(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._actions = value | 0;
}
/**
* Constructor.
*/
constructor() {
super();
this._camera = null;
this._cameraEntity = null;
this._cameraComponent = null;
this._layer = null;
this._actions = ActionFlags.NONE;
this._lastOver = false;
this._shape = null;
this._dragging = false;
}
dispose() {
super.dispose();
this._camera = null;
this._cameraEntity = null;
this._cameraComponent = null;
this._layer = null;
this._shape = null;
}
/**
* @override
*/
onAttach() {
super.onAttach();
this.camera = this.camera;
this._shape = this.entity.getComponent(Shape);
if (!this._shape) {
throw new Error(
`There is no Shape component in entity: ${this.entity.path}`
);
}
}
/**
* @override
*/
onPropertySetup(name, value) {
if (name === 'actions') {
if (!(value instanceof Array)) {
throw new Error('`value` is not type of Array!');
}
let flags = ActionFlags.NONE;
for (let i = 0, c = value.length; i < c; ++i) {
const flag = value[i];
if (flag === 'click') {
flags |= ActionFlags.CLICK;
} else if (flag === 'click-release') {
flags |= ActionFlags.CLICK_RELEASE;
} else if (flag === 'mouse-enter-leave') {
flags |= ActionFlags.MOUSE_ENTER_LEAVE;
} else if (flag === 'drag-drop') {
flags |= ActionFlags.DRAG_DROP;
} else if (flag === 'all') {
flags |= ActionFlags.ALL;
}
}
this._actions = flags;
} else {
super.onPropertySetup(name, value);
}
}
/**
* @override
*/
onPropertySerialize(name, value) {
if (name === 'actions') {
if ((value & ActionFlags.ALL) === ActionFlags.ALL) {
return [ 'all' ];
}
const result = [];
if ((value & ActionFlags.CLICK) !== 0) {
result.push('click');
}
if ((value & ActionFlags.CLICK_RELEASE) !== 0) {
result.push('click-release');
}
if ((value & ActionFlags.MOUSE_ENTER_LEAVE) !== 0) {
result.push('mouse-enter-leave');
}
if ((value & ActionFlags.DRAG_DROP) !== 0) {
result.push('drag-drop');
}
return result;
} else {
return super.onPropertySerialize(name, value);
}
}
/**
* @override
*/
onMouseDown(unitVec, screenVec) {
if ((this._actions & ActionFlags.CLICK) === 0 &&
(this._actions & ActionFlags.DRAG_DROP) === 0
) {
return;
}
this._convertUnitToGlobalCoords(cachedGlobalVec, unitVec);
if (this._containsPoint(cachedGlobalVec)) {
if ((this._actions & ActionFlags.CLICK) !== 0) {
this.entity.performAction('click', cachedGlobalVec);
}
if (!this._dragging && (this._actions & ActionFlags.DRAG_DROP) !== 0) {
this._dragging = true;
this.entity.performAction('drag', cachedGlobalVec);
}
}
}
/**
* @override
*/
onMouseUp(unitVec, screenVec) {
if ((this._actions & ActionFlags.CLICK_RELEASE) === 0 &&
(this._actions & ActionFlags.DRAG_DROP) === 0
) {
return;
}
this._convertUnitToGlobalCoords(cachedGlobalVec, unitVec);
if (this._containsPoint(cachedGlobalVec)) {
if ((this._actions & ActionFlags.CLICK_RELEASE) !== 0) {
this.entity.performAction('click-release', cachedGlobalVec);
}
if (!!this._dragging && (this._actions & ActionFlags.DRAG_DROP) !== 0) {
this.entity.performAction('drop', cachedGlobalVec);
this._dragging = false;
}
}
}
/**
* @override
*/
onMouseMove(unitVec, screenVec) {
if ((this._actions & ActionFlags.MOUSE_ENTER_LEAVE) === 0 &&
(this._actions & ActionFlags.DRAG_DROP) === 0
) {
return;
}
const { _lastOver } = this;
this._convertUnitToGlobalCoords(cachedGlobalVec, unitVec);
if ((this._actions & ActionFlags.MOUSE_ENTER_LEAVE) !== 0) {
const over = this._containsPoint(cachedGlobalVec);
if (over && !_lastOver) {
this.entity.performAction('mouse-enter', cachedGlobalVec);
} else if (!over && _lastOver) {
this.entity.performAction('mouse-leave', cachedGlobalVec);
}
this._lastOver = over;
}
if (!!this._dragging && (this._actions & ActionFlags.DRAG_DROP) !== 0) {
this.entity.performAction('drag-move', cachedGlobalVec);
}
}
/**
* @override
*/
onTouchDown(unitVec, screenVec) {
if ((this._actions & ActionFlags.CLICK) === 0 &&
(this._actions & ActionFlags.DRAG_DROP) === 0
) {
return;
}
this._convertUnitToGlobalCoords(cachedGlobalVec, unitVec);
if (this._containsPoint(cachedGlobalVec)) {
if ((this._actions & ActionFlags.CLICK) !== 0) {
this.entity.performAction('click', cachedGlobalVec);
}
if (!this._dragging && (this._actions & ActionFlags.DRAG_DROP) !== 0) {
this._dragging = true;
this.entity.performAction('drag', cachedGlobalVec);
}
}
}
/**
* @override
*/
onTouchUp(unitVec, screenVec) {
if ((this._actions & ActionFlags.CLICK_RELEASE) === 0 &&
(this._actions & ActionFlags.DRAG_DROP) === 0
) {
return;
}
this._convertUnitToGlobalCoords(cachedGlobalVec, unitVec);
if (this._containsPoint(cachedGlobalVec)) {
if ((this._actions & ActionFlags.CLICK_RELEASE) !== 0) {
this.entity.performAction('click-release', cachedGlobalVec);
}
if (!!this._dragging && (this._actions & ActionFlags.DRAG_DROP) !== 0) {
this.entity.performAction('drop', cachedGlobalVec);
this._dragging = false;
}
}
}
/**
* @override
*/
onTouchMove(unitVec, screenVec) {
if ((this._actions & ActionFlags.DRAG_DROP) === 0 || !this._dragging) {
return;
}
this._convertUnitToGlobalCoords(cachedGlobalVec, unitVec);
if (this._containsPoint(cachedGlobalVec)) {
this.entity.performAction('drag-move', cachedGlobalVec);
}
}
_containsPoint(globalVec) {
const { _shape } = this;
return !!_shape ? _shape.containsPoint(globalVec, this._layer) : false;
}
_convertUnitToGlobalCoords(out, unitVec) {
const { _cameraComponent } = this;
if (!!_cameraComponent) {
vec2.transformMat4(
out,
unitVec,
_cameraComponent.inverseProjectionMatrix,
);
}
}
}