Repository

js/Mobilizing/input/Touch.js

import Component from '../core/Component';
import Time from '../time/Time';
import * as debug from '../core/util/Debug';
import * as _Math from '../core/util/Math';
import * as _DOM from '../core/util/Dom';

/**
Object that represent what defines a Touch. Touch Class returns this type of objets when events are fired.
This class is used internally and is documented for consultation purpose. Users should not make new instances from it.
*/
class TouchObject {
    /**
    * @param {Object} params The parameters object
    * @param {Number} [params.x = 0] the x coordinate of the touch
    * @param {Number} [params.y = 0] the y coordinate of the touch
    * @param {Number} [params.index = 0] the index of the touch
    */
    constructor({
        x = 0,
        y = 0,
        index = 0,
    } = {}) {
        this.y = y;
        this.x = x;
        this.index = index;

        /**
        pX pevious x coordinate of the touch
        @property {Number} pX
        */
        this.pX = this.x;

        /**
        pY previous y coordinate of the touch
        @property {Number} pY
        */
        this.pY = this.y;

        /**
        id used internally to manage the touch list in Touch Class
        @property {Object} id
        */
        this.id = undefined;

        /**
        xDelta delta on x coordinate (difference between pevious and current x)
        @property {Number} xDelta
        */
        this.xDelta = 0;

        /**
        yDelta delta on y coordinate (difference between pevious and current y)
        @property {Number} yDelta
        */
        this.yDelta = 0;

        /**
        startX x coordinate where the touch began
        @property {Number} startX
        */
        this.startX = this.x;

        /**
        startY x coordinate where the touch began
        @property {Number} startY
        */
        this.startY = this.y;

        /**
        offset on the X coordinate (difference between startX and current x)
        @property {Number} offsetX
        */
        this.offsetX = 0;

        /**
        offset on the Y coordinate (difference between startY and current y)
        @property {Number} offsetY
        */
        this.offsetY = 0;
    }

    /**
    Set a new x coordinate and compute the new xDelta and offsetX
    *
    @param {Object} x
    */
    setX(x) {
        this.x = x;
        this.xDelta = this.x - this.pX;
        this.offsetX = this.x - this.startX;
    }

    /**
    Set a new y coordinate and compute the new yDelta and offsetY
    *
    @param {Object} y
    */
    setY(y) {
        this.y = y;
        this.yDelta = this.y - this.pY;

        this.offsetY = this.y - this.startY;
    }

    setPX() {
        this.pX = this.x;
    }

    setPY() {
        this.pY = this.y;
    }
}

/**
Fired when a touch starts
@event touchstart
*/
const EVT_TOUCH_START = 'touchstart';

/**
Fired when a touch ends
@event touchend
*/
const EVT_TOUCH_END = 'touchend';

/**
Fired when a touch moved
@event touchmoved
*/
const EVT_TOUCH_MOVED = 'touchmoved';

/**
Fired when the tap count changed
@event tapchanged
*/
const EVT_TAP_CHANGED = 'tapchanged';

/**
Fired when the pressed state changed (ie a touch is on screen)
@event pressedchanged
*/
const EVT_PRESSED_CHANGED = 'pressedchanged';

const EVT_SWIPE_UP = "swipeup";
const EVT_SWIPE_DOWN = "swipedown";
const EVT_SWIPE_LEFT = "swipeleft";
const EVT_SWIPE_RIGHT = "swiperight";

/**
Touch give an interface to access the multitouch events of the device. It holds a list of currently active touches and various ways to access their coordinates.
@extends Component
*/
export default class Touch extends Component {
    /**
    * @param {Object} params Parameters object, given by the constructor.
    * @param {DOMElement} params.target The DOM element that will be used to attach touch events on
    */
    constructor({
        target = window,
    } = {}) {

        super(...arguments);

        this.target = target;
        this._time = new Time();
        this.chain(this._time);
    }

    /**
    Initialization method
    */
    setup() {
        if (!this._setupDone) {
            this._time.setup();
            this._time.on();

            /**
            Gives the number of touch currently active
            @property {Number} count
            */
            this.count = 0;
            /**
            true if the active touch on screen > 1, false otherwise
            @property {Boolean} pressed
            */
            this.pressed = false;

            this.touches = {};

            this.taps = 0;
            this.tapsMaxInterval = 500; //millsec but depends on timeScale (!?)
            this.oldTapTime = 0;

            this.pinchTouches = [];
            this.pinchStart = 0;
            this.pinch = 0;
            this.pinchActive = false;

            this.swipeMaxTime = 250;
            this.swipeMinDist = 100;

            // this.touchState;
            // this.touchDown;
            // this.touchUp;

            super.setup();
        }
    }

    on() {
        super.on();

        this.target.addEventListener("touchstart", (event) => this.onTouchStart(event), { passive: false });

        this.target.addEventListener("touchend", (event) => this.onTouchEnd(event), { passive: false });
        this.target.addEventListener("touchcancel", (event) => this.onTouchEnd(event), { passive: false });
        this.target.addEventListener("touchleave", (event) => this.onTouchEnd(event), { passive: false });

        this.target.addEventListener("touchmove", (event) => this.onTouchMove(event), { passive: false });
    }

    off() {
        super.off();

        this.target.removeEventListener("touchstart", (event) => this.onTouchStart(event), { passive: false });

        this.target.removeEventListener("touchend", (event) => this.onTouchEnd(event), { passive: false });
        this.target.removeEventListener("touchcancel", (event) => this.onTouchEnd(event), { passive: false });
        this.target.removeEventListener("touchleave", (event) => this.onTouchEnd(event), { passive: false });

        this.target.removeEventListener("touchmove", (event) => this.onTouchMove(event), { passive: false });
    }

    update() {

        if (this.pressed && !this.touchState) {
            this.touchDown = true;
        }
        else {
            this.touchDown = false;
        }
        if (!this.pressed && this.touchState) {
            this.touchUp = true;
        }
        else {
            this.touchUp = false;
        }
        this.touchState = this.pressed;

        Object.values(this.touches).forEach((touch) => {
            touch.setPX();
            touch.setPY();
        });
    }

    /**
    onTouchStart listener
    Manage a new touch and organize it in the main touch list Input.touches

    @private
    */
    onTouchStart(event) {
        let position;

        if (this.target !== window) {
            position = _DOM.getElementPosition(this.target);
        }
        else {
            position = { "x": 0, "y": 0 };
        }

        //console.log(_DOM, position);

        let newTouch = null;

        if (event.changedTouches !== null && event.changedTouches !== undefined) {//touch

            for (let i = 0; i < event.changedTouches.length; i++) {
                const changed = event.changedTouches[i];

                if (!(changed.identifier in this.touches)) {
                    //no touch in memory, build it
                    const x = changed.pageX - position.x;
                    const y = changed.pageY - position.x;

                    newTouch = new TouchObject({ x, y, "index": this.count });
                    debug.log("newTouch", newTouch);

                    newTouch.setX(x);
                    newTouch.setY(y);
                    newTouch.id = changed.identifier;

                    //à laisser ici!
                    this.touches[changed.identifier] = newTouch;

                    this.count += 1;
                }
                else {
                    //touch is already there, update
                    const touch = this.touches[changed.identifier];

                    const x = touch.pageX - position.x;
                    const y = touch.pageY - position.x;

                    touch.setX(x);
                    touch.setY(y);
                }

                //Events
                this.events.trigger(EVT_TOUCH_START, newTouch);
            }
        }

        //tap management, add a tap after a time interval
        if (this._time.currentTime - this.oldTapTime < this.tapsMaxInterval) {
            this.taps += 1;
            //Events
            this.events.trigger(EVT_TAP_CHANGED, this.taps);
        }
        else {
            this.taps = 1;
            //Events
            this.events.trigger(EVT_TAP_CHANGED, this.taps);
        }
        //current time memory for next tap
        this.oldTapTime = this._time.currentTime;

        //pressed state
        this.pressed = true;
        this.events.trigger(EVT_PRESSED_CHANGED, this.pressed);

        //swipe management
        this.swipeStartTime = this._time.currentTime;

        //avoid the browser's defaults interactions
        event.preventDefault();
    }

    /**
    onTouchEnd listener

    Manage a touch removal and organize it in the main touch list Input.touches
    @private
    */
    onTouchEnd(event) {
        if (event.changedTouches !== null && event.changedTouches !== undefined) {
            //touch
            for (let i = 0; i < event.changedTouches.length; i++) {

                const touch = event.changedTouches[i];

                if (touch.identifier in this.touches) {
                    //touch is there!
                    //Events
                    this.events.trigger(EVT_TOUCH_END, this.touches[touch.identifier]);

                    //swipe event generator (for the 1st touche only!)
                    if (i === 0) {
                        this.swipeCurrentTime = this._time.currentTime;
                        this.swipeTime = this.swipeCurrentTime - this.swipeStartTime;

                        if (this.swipeTime < this.swipeMaxTime) {

                            let xAxisValidated = false;

                            //x + axis validation
                            if (this.getOffsetX(0) > this.getOffsetY(0)) {
                                if (this.getOffsetX(0) > this.swipeMinDist) {
                                    //console.log("swipeX+");
                                    this.events.trigger(EVT_SWIPE_RIGHT);
                                    xAxisValidated = true;
                                }
                            }
                            //x - axis validated
                            else if (this.getOffsetX(0) < this.getOffsetY(0)) {
                                if (this.getOffsetX(0) < -this.swipeMinDist) {
                                    //console.log("swipeX-");
                                    this.events.trigger(EVT_SWIPE_LEFT);
                                    xAxisValidated = true;
                                }
                            }

                            //avoid double axis swipe
                            if (!xAxisValidated) {
                                //y + axis validation
                                if (this.getOffsetY(0) > this.getOffsetX(0)) {
                                    if (this.getOffsetY(0) > this.swipeMinDist) {
                                        console.log("swipeY+");
                                        this.events.trigger(EVT_SWIPE_DOWN);
                                    }
                                }
                                //y - axis validated
                                else if (this.getOffsetY(0) < -this.getOffsetX(0)) {
                                    if (this.getOffsetY(0) < -this.swipeMinDist) {
                                        console.log("swipeY-");
                                        this.events.trigger(EVT_SWIPE_UP);
                                    }
                                }
                            }
                        }

                    }

                    //erase
                    delete this.touches[touch.identifier];
                    this.count -= 1;
                }
            }

            //reset pinch value when no fingers
            if (this.count <= 1) {
                this.pinchTouches = [];
                this.pinchStart = 0;
                this.pinch = 0;
                this.pinchActive = false;
            }

        }

        if (this.count === 0) {
            this.pressed = false;
            this.events.trigger(EVT_PRESSED_CHANGED, this.pressed);
        }

        event.preventDefault();
    }

    /**
    onTouchMove listener
    *
    Manage a touch move and organize it in the main touch list Input.touches
    *
    @private
    */
    onTouchMove(event) {
        let position;

        if (this.target !== window) {
            position = _DOM.getElementPosition(this.target);
        }
        else {
            position = { "x": 0, "y": 0 };
        }

        if (event.changedTouches !== null && event.changedTouches !== undefined) {
            //touch
            for (let i = 0; i < event.changedTouches.length; i++) {
                const touch = event.changedTouches[i];

                if (touch.identifier in this.touches) {
                    //touch is there!
                    const myTouch = this.touches[touch.identifier];

                    const x = touch.pageX - position.x;
                    const y = touch.pageY - position.y;

                    myTouch.setX(x);
                    myTouch.setY(y);

                    this.events.trigger(EVT_TOUCH_MOVED, myTouch);
                }
            }
        }

        this.taps = 0;

        event.preventDefault();
    }

    /**
    returns the x coordinate of the touch given as paramater
    
    @param {Number:Int} index the index of the touch to get x coordinate from
    @return {Number:Int} x coordinate of the touch if active, -1 if not active
    */
    getX(index) {
        let val;

        if (typeof index === "number") {
            for (const obj in this.touches) {
                if (this.touches[obj].index === index) {
                    val = this.touches[obj].x;
                }
            }
        }
        return val;
    }

    /**
    returns the y coordinate of the touch given as paramater
    *
    @param {Number:Int} index the index of the touch to get y coordinate from
    @return {Number:Int} y coordinate of the touch if active, -1 if not active
    */
    getY(index) {
        let val;

        if (typeof index === "number") {
            for (const obj in this.touches) {
                if (this.touches[obj].index === index) {
                    val = this.touches[obj].y;
                }
            }
        }
        return val;
    }

    /**
    returns a TouchObject
    *
    @param {Number:Int} index the index of the touchObject to get
    @return {TouchObject}
    */
    get(index) {
        let val;

        if (typeof index === "number") {
            for (const obj in this.touches) {
                if (this.touches[obj].index === index) {
                    val = this.touches[obj];
                }
            }
        }
        return val;
    }

    /**
    Returns a Number that represents the coordinate of the touch delta,
    that is the numerical difference between the previous state in time and the actual one.
    
    @param {Number:Int|Object:Touch} index the index of the touch to get x coordinate from, or the touch object
    @return {Number:Int}, the x coordinates of the touch delta;
    */
    getDeltaX(index) {
        let val;

        if (typeof index === "number") {
            Object.keys(this.touches).forEach((identifier) => {
                const touch = this.touches[identifier];

                if (touch.index === index) {
                    val = touch.xDelta;
                }
            });
        }
        return val;
    }

    /**
    Returns a Number that represents the coordinate of the touch delta,
    that is the numerical difference between the previous state in time and the actual one.
    *
    @param {Number:Int|Object:Touch} index the index of the touch to get y coordinate from, or the touch object
    @return {Number:Int}, the y coordinates of the touch delta;
    */
    getDeltaY(index) {
        let val;

        if (typeof index === "number") {
            Object.keys(this.touches).forEach((identifier) => {
                const touch = this.touches[identifier];

                if (touch.index === index) {
                    val = touch.yDelta;
                }
            });
        }
        return val;
    }

    /**
    Returns an object {x:Number, y:Number} that represents the coordinate of the touch delta,
    that is the numerical difference between the previous state in time and the actual one.
    *
    @param {Number:Int|Object:Touch} index the index of the touch to get coordinates from, or the touch object
    @return {Object} {x:Number, y:Number}, the x & y coordinates of the touch delta;
    */
    getDelta(index) {
        let val;

        if (typeof index === "number") {
            Object.keys(this.touches).forEach((identifier) => {
                const touch = this.touches[identifier];

                if (touch.index === index) {
                    val = { "x": touch.xDelta, "y": touch.yDelta };
                }
            });
        }
        return val;
    }

    /**
    Returns a Number that represents the x coordinate of the touch offset,
    that is the numerical difference between the start point of the touch and the actual one.
    *
    @param {Number:Int|Object:Touch} index the index of the touch to get x offset coordinate from, or the touch object
    @return {Number} the x coordinates of the touch offset;
    */
    getOffsetX(index) {
        let val;

        Object.keys(this.touches).forEach((identifier) => {
            const touch = this.touches[identifier];

            if (touch.index === index) {
                val = touch.offsetX;
            }
        });

        return val;
    }

    /**
    Returns a Number that represents the y coordinate of the touch offset,
    that is the numerical difference between the start point of the touch and the actual one.
    *
    @param {Number:Int|Object:Touch} index the index of the touch to get y offset coordinate from, or the touch object
    @return {Number} the y coordinates of the touch offset;
    */
    getOffsetY(index) {
        let val;

        Object.keys(this.touches).forEach((identifier) => {
            const touch = this.touches[identifier];

            if (touch.index === index) {
                val = touch.offsetY;
            }
        });

        return val;
    }

    /**
    Returns an object {x:Number, y:Number} that represents the coordinate of the touch offset,
    that is the numerical difference between the start point of the touch and the actual one.
    *
    @param {Number:Int|Object:Touch} index the index of the touch to get offset coordinate from, or the touch object
    @return {Object} {x:Number, y:Number}, the x & y coordinates of the touch offset;
    */
    getOffset(index) {
        let val;

        Object.keys(this.touches).forEach((identifier) => {
            const touch = this.touches[identifier];

            if (touch.index === index) {
                val = { "x": touch.offsetX, "y": touch.offsetY };
            }
        });

        return val;
    }
    /**
    *Returns a Number that represents the pinch touch move,
    *that is the numerical difference between the start point of 2 touches and the actual one.
    *
    @return {Number} the pinch delta value;
    */
    getPinch() {
        //We must use a min of 2 touches
        if (this.count >= 2) {
            //if don't have the touches, find and save them, else compute start point
            if (typeof this.pinchTouches[1] === "undefined") {
                let touchNb = 0;

                //loop through touches to find at least 2
                Object.values(this.touches).some((touch) => {
                    if (touchNb < 2) {
                        //gather 2 touch if not already in memory
                        this.pinchTouches[touchNb] = touch;
                        touchNb++;
                    }
                    if (touchNb === 2) {

                        this.pinchActive = true;
                        //we have 2 touches in memory, compute startpoint
                        this.pinchStart = _Math.dist(this.pinchTouches[0].x, this.pinchTouches[0].y, this.pinchTouches[1].x, this.pinchTouches[1].y);

                        //debug.log("pinchStart at break",this.pinchStart);
                        return true; //2 touches found, break loop;
                    }

                    return false;
                });
            }

            //pinch computation's ready to be done
            this.pinch =
                _Math.dist(this.pinchTouches[0].x, this.pinchTouches[0].y, this.pinchTouches[1].x, this.pinchTouches[1].y)
                - this.pinchStart;

            //we have 2 touches in memory, compute startpoint
            this.pinchStart = _Math.dist(this.pinchTouches[0].x, this.pinchTouches[0].y, this.pinchTouches[1].x, this.pinchTouches[1].y);
        }
        else {
            this.pinchStart = 0;
            this.pinch = 0;
            this.pinchTouches = [];
        }

        return this.pinch;
    }
}