Repository

js/Mobilizing/input/Pointer.js

import Component from '../core/Component';
import Mouse from './Mouse';
import Touch from './Touch';

const TOUCH_TYPE = "touch";
const MOUSE_TYPE = "mouse";

/**
PointerObject is a thin layer that unify mouse and touch inputs to simplify UI interactions.

@private
*/
class PointerObject {
    /**
    * Creates a new PointerObject.
    *
    * @param {Object} params Parameters object, given by the constructor.
    * @param {Number} params.x
    * @param {Number} params.y
    * @param {Number} params.state
    * @param {Number} params.type
    */
    constructor({
        x = undefined,
        y = undefined,
        state = undefined,
        type = undefined
    } = {}) {

        this.x = x;
        this.y = y;
        this.state = state;
        this.type = type;
        this.available = undefined;
        /**
        x delta coordinate, the difference between the previous and the current frame
        @type {Number}
        */
        this.deltaX = 0;

        /**
        x delta coordinate, the difference between the previous and the current frame
        @type {Number}
        */
        this.deltaY = 0;

        /**
        the component this pointer is attached to
        @type {Component}
        */
        this.component = null;
    }
}

/**
* Fired when a pointer is on (pressed or down)
* @event on
*/
const EVT_ON = 'on';

/**
* Fired when a pointer is off (released or erased)
* @event off
*/
const EVT_OFF = 'off';

/**
* Fired when a pointer moved
* @event moved
*/
const EVT_MOVED = 'moved';

/**
* Pointer is an abstraction that enables various input devices to send the same kind of events. It is designed to accumulate various type of inputs in a Map of PointerOject. Each input device is converted in a PointerOject in order to unify its interface. Usefully mainly internally for interactive UI objects like buttons.
*
* @example
*    //TODO
*/
export default class Pointer extends Component {

    /**
    * @param {Object} params Parameters object, given by the constructor.
    * @param {[Component]} params.components an array of input components to add to this pointer (i.e. Mouse)
    */
    constructor({
        components = undefined
    } = {}) {
        super(...arguments);

        this.components = components;

        this.pointers = new Map();

        // special pointer to get the last active pointer
        this.lastActivePointer = new PointerObject();
        //this.lastActivePointer.type = "lastActive";

        if (Array.isArray(this.components)) {
            this.components.forEach((component) => {
                this.add(component);
            });
        }
    }

    /**
    * Remove unused Pointers
    */
    postUpdate() {
        for (const pointerKey of this.pointers.keys()) {
            const pointer = this.pointers.get(pointerKey);

            //why did I ever did this??
            /* if (pointer.state === false && pointer.type === TOUCH_TYPE) {
                console.log("erase",pointer);
                this.pointers.delete(pointerKey);
            } */

            //update ce qui doit l'être mais qui ne peut l'être ailleurs
            if (pointer.type === "touch") {
                pointer.deltaX = pointer.component.getDeltaX();
                pointer.deltaY = pointer.component.getDeltaY();
            }
            else if (pointer.type === "mouse") {
                pointer.deltaX = pointer.component.getDeltaX();
                pointer.deltaY = pointer.component.getDeltaY();
            }

        }
    }

    /**
    * Adds the specified input component as a PointerObject to the pointers list.

    * @param {Component} component
    */
    add(component) {

        if (component instanceof Touch) {
            this.addTouch(component);
        }
        else if (component instanceof Mouse) {
            this.addMouse(component);
        }
    }

    addTouch(component) {

        const pointer = new PointerObject();
        pointer.component = component;
        pointer.type = TOUCH_TYPE;
        pointer.available = true;

        if (!("ontouchstart" in window)) {
            pointer.available = false;
        }

        this.pointers.set(component.id, pointer);

        //register a new Pointer at touch creation
        component.events.on("touchstart", (touch) => {

            //const pointer = this.pointers.get(touch.id);
            pointer.state = true;
            pointer.x = touch.x;
            pointer.y = touch.y;

            this.events.trigger(EVT_ON, {
                pointer,
                "x": pointer.x,
                "y": pointer.y
            });

            this.lastActivePointer.state = true;
            this.lastActivePointer.x = touch.x;
            this.lastActivePointer.y = touch.y;
            this.lastActivePointer.type = pointer.type;
        });

        //updates a Pointer at touch moved
        component.events.on("touchmoved", (touch) => {

            pointer.state = true;
            pointer.x = touch.x;
            pointer.y = touch.y;
            pointer.deltaX = touch.xDelta;
            pointer.deltaY = touch.yDelta;

            this.events.trigger(EVT_MOVED, {
                pointer,
                "x": pointer.x,
                "y": pointer.y
            });

            this.lastActivePointer.state = true;
            this.lastActivePointer.x = touch.x;
            this.lastActivePointer.y = touch.y;
            this.lastActivePointer.deltaX = touch.xDelta;
            this.lastActivePointer.deltaY = touch.yDelta;
        });

        //updates a Pointer at touch end
        component.events.on("touchend", (touch) => {

            pointer.state = false;
            pointer.x = touch.x;
            pointer.y = touch.y;

            this.events.trigger(EVT_OFF, { pointer });

            this.lastActivePointer.state = false;
            this.lastActivePointer.x = touch.x;
            this.lastActivePointer.y = touch.y;
        });
    }

    addMouse(component) {

        const mouseComponent = component;//just to understand better

        const pointer = new PointerObject();
        pointer.component = component;
        pointer.type = MOUSE_TYPE;
        pointer.available = true;

        if (!("onmousedown" in window)) {
            pointer.available = false;
        }

        this.pointers.set(component.id, pointer);

        mouseComponent.events.on("mousepress", () => {
            //const pointer = this.pointers.get(component.id);
            pointer.state = true;
            this.events.trigger(EVT_ON, {
                pointer,
                "x": pointer.x,
                "y": pointer.y
            });

            this.lastActivePointer.state = true;
            //this is needed to keep the current pointer type in lastActivePointer.
            //used in Button, through Clickable, to have specific behaviour depending on input type.
            
            this.lastActivePointer.type = pointer.type;
        });

        mouseComponent.events.on("mouserelease", () => {
            //const pointer = this.pointers.get(component.id);
            pointer.state = false;
            this.events.trigger(EVT_OFF, { pointer });

            this.lastActivePointer.state = false;
        });

        mouseComponent.events.on("mousemove", (coords) => {
            //const pointer = this.pointers.get(component.id);

            pointer.deltaX = mouseComponent.getDeltaX();
            pointer.deltaY = mouseComponent.getDeltaY();

            pointer.x = coords.x;
            pointer.y = coords.y;

            this.events.trigger(EVT_MOVED, {
                pointer,
                "x": pointer.x,
                "y": pointer.y
            });

            this.lastActivePointer.state = true;
            this.lastActivePointer.x = pointer.x;
            this.lastActivePointer.y = pointer.y;
            this.lastActivePointer.deltaX = pointer.deltaX;
            this.lastActivePointer.deltaY = pointer.deltaY;
        });

        mouseComponent.events.on("mouseupdate", () => {
            //const pointer = this.pointers.get(component.id);

            pointer.deltaX = mouseComponent.getDeltaX();
            pointer.deltaY = mouseComponent.getDeltaY();

            /*this.events.trigger(EVT_MOVED, {pointer:pointer, x:pointer.getX(), y:pointer.getY()});*/

            this.lastActivePointer.state = true;
            this.lastActivePointer.deltaX = pointer.deltaX;
            this.lastActivePointer.deltaY = pointer.deltaY;
        });
    }

    /**
    * Returns the specified PointerObject to work with its state

    * @param {Component} component
    * @return {Component} the corresponding input Component
    */
    get(component) {
        return this.pointers.get(component.id);
    }

    /**
    * Returns an Array from the pointers Map object. For debug purpose.
    * @return {Array} Pointers array
    */
    getPointers() {
        return Array.from(this.pointers.values());
    }

    /**
    * Returns true if ANY of the PointerObjects (input Component) state is true, false otherwise. i.e. if you add a mouse and a touch component to the pointer, this will return true if any of these 2 has it's state to true.
    * @return {Array} pointers array
    */
    getState() {
        for (const pointer of this.pointers.values()) {
            //the virtual pointer is on true
            if (pointer.state) {
                return true;
            }
        }
        this.events.trigger("off");
        return false;
    }

    /**
    * Returns x coordinates of the PointerObject (input Component) of the given index. If no index is given, the last active pointer will return.
    * @param {Number} [index] the pointer object we want to get
    * @return {Number} x coordinate
    */
    getX(index) {
        const values = [];

        for (const pointer of this.pointers.values()) {
            values.push(pointer.x);
        }

        if (index) { // we want a specific pointer in the list
            return index === undefined ? values : values[index];
        }

        // we want a value
        return this.lastActivePointer.x;
    }

    /**
    * Returns y coordinates of the PointerObject (input Component) of the given index. If no index is given, the last active pointer will return.
    * @param {Number} [index] the pointer object we want to get
    * @return {Number} y coordinate
    */
    getY(index) {
        const values = [];

        for (const pointer of this.pointers.values()) {
            values.push(pointer.y);
        }

        if (index) { // we want a specific pointer in the list
            return index === undefined ? values : values[index];
        }

        // we want a value
        return this.lastActivePointer.y;
    }

    /**
    * Returns x delta coordinates of the PointerObject (input Component) of the given index. If no index is given, the last active pointer will return.
    * @param {Number} [index] the pointer object we want to get
    * @return {Number} coordinate
    */
    getDeltaX(index) {
        const values = [];

        for (const pointer of this.pointers.values()) {
            values.push(pointer.deltaX);
        }

        if (index) {// we want a specific pointer in the list
            return index === undefined ? values : values[index];
        }

        // we want a value
        return this.lastActivePointer.deltaX;
    }

    /**
    * Returns y delta coordinates of the PointerObject (input Component) of the given index. If no index is given, the last active pointer will return.
    * @param {Number} [index] the pointer object we want to get
    * @return {Number} coordinate
    */
    getDeltaY(index) {
        const values = [];

        for (const pointer of this.pointers.values()) {
            values.push(pointer.deltaY);
        }

        if (index) {// we want a specific pointer in the list
            return index === undefined ? values : values[index];
        }

        // we want a value
        return this.lastActivePointer.deltaY;
    }
}