Controls/ControlsBase.js

/** 
 * An abstract class representing the base for controls implementations. 
 * 
 * @property {Lore.Renderer} renderer A Lore.Renderer instance.
 * @property {Lore.CameraBase} camera A Lore.CameraBase extending object.
 * @property {HTMLElement} canvas A canvas HTMLElement.
 * @property {Number} lowFps The FPS limit when throttling FPS.
 * @property {Number} highFps The FPS limit when not throttling FPS.
 * @property {String} touchMode The current touch mode.
 * @property {Lore.Vector3f} lookAt The current lookat associated with these controls.
 */
Lore.ControlsBase = class ControlsBase {

    /**
     * Creates an instance of ControlsBase.
     * @param {Lore.Renderer} renderer An instance of a Lore renderer.
     * @param {Boolean} [lookAt=new Lore.Vector3f()] The look at vector of the controls.
     * @param {Boolean} [enableVR=false] Whether or not to track phone spatial information using the WebVR API.
     */
    constructor(renderer, lookAt = new Lore.Vector3f(), enableVR = false) {
        this.renderer = renderer;
        this.camera = renderer.camera;
        this.canvas = renderer.canvas;
        this.lowFps = 15;
        this.highFps = 30;
        this._eventListeners = {};
        this.renderer.setMaxFps(this.lowFps);
        this.touchMode = 'drag';
        this.lookAt = lookAt;

        this.mouse = {
            previousPosition: {
                x: null,
                y: null
            },
            delta: {
                x: 0.0,
                y: 0.0
            },
            position: {
                x: 0.0,
                y: 0.0
            },
            state: {
                left: false,
                middle: false,
                right: false
            },
            normalizedPosition: {
                x: 0.0,
                y: 0.0
            },
            touches: 0
        };

        this.keyboard = {
            alt: false,
            ctrl: false,
            shift: false
        };

        this.VR = {};

        let that = this;
        this.canvas.addEventListener('mousemove', function (e) {
            if (that.mouse.previousPosition.x !== null && that.mouse.state.left ||
                that.mouse.state.middle ||
                that.mouse.state.right) {
                that.mouse.delta.x = e.pageX - that.mouse.previousPosition.x;
                that.mouse.delta.y = e.pageY - that.mouse.previousPosition.y;

                that.mouse.position.x += 0.01 * that.mouse.delta.x;
                that.mouse.position.y += 0.01 * that.mouse.delta.y;

                // Give priority to left, then middle, then right
                if (that.mouse.state.left) {
                    that.raiseEvent('mousedrag', {
                        e: that.mouse.delta,
                        source: 'left'
                    });
                } else if (that.mouse.state.middle) {
                    that.raiseEvent('mousedrag', {
                        e: that.mouse.delta,
                        source: 'middle'
                    });
                } else if (that.mouse.state.right) {
                    that.raiseEvent('mousedrag', {
                        e: that.mouse.delta,
                        source: 'right'
                    });
                }
            }

            // Set normalized mouse position
            let rect = that.canvas.getBoundingClientRect();
            that.mouse.normalizedPosition.x = ((e.clientX - rect.left) / that.canvas.width) * 2 - 1;
            that.mouse.normalizedPosition.y = -((e.clientY - rect.top) / that.canvas.height) * 2 + 1;

            that.raiseEvent('mousemove', {
                e: that
            });

            that.mouse.previousPosition.x = e.pageX;
            that.mouse.previousPosition.y = e.pageY;
        });

        this.canvas.addEventListener('touchstart', function (e) {
            that.mouse.touches++;
            let touch = e.touches[0];
            e.preventDefault();

            that.mouse.touched = true;

            that.renderer.setMaxFps(that.highFps);

            // This is for selecting stuff when touching but not moving

            // Set normalized mouse position
            let rect = that.canvas.getBoundingClientRect();
            that.mouse.normalizedPosition.x = ((touch.clientX - rect.left) / that.canvas.width) * 2 - 1;
            that.mouse.normalizedPosition.y = -((touch.clientY - rect.top) / that.canvas.height) * 2 + 1;

            if (that.touchMode !== 'drag') {
                that.raiseEvent('mousemove', {
                    e: that
                });
            }

            that.raiseEvent('mousedown', {
                e: that,
                source: 'touch'
            });
        });

        this.canvas.addEventListener('touchend', function (e) {
            that.mouse.touches--;
            e.preventDefault();

            that.mouse.touched = false;

            // Reset the previous position and delta of the mouse
            that.mouse.previousPosition.x = null;
            that.mouse.previousPosition.y = null;

            that.renderer.setMaxFps(that.lowFps);

            that.raiseEvent('mouseup', {
                e: that,
                source: 'touch'
            });
        });

        this.canvas.addEventListener('touchmove', function (e) {
            let touch = e.touches[0];
            let source = 'left';

            if (that.mouse.touches == 2) source = 'right';

            e.preventDefault();
            
            if (that.mouse.previousPosition.x !== null && that.mouse.touched) {
                that.mouse.delta.x = touch.pageX - that.mouse.previousPosition.x;
                that.mouse.delta.y = touch.pageY - that.mouse.previousPosition.y;

                that.mouse.position.x += 0.01 * that.mouse.delta.x;
                that.mouse.position.y += 0.01 * that.mouse.delta.y;

                if (that.touchMode === 'drag')
                    that.raiseEvent('mousedrag', {
                        e: that.mouse.delta,
                        source: source
                    });
            }

            // Set normalized mouse position
            let rect = that.canvas.getBoundingClientRect();
            that.mouse.normalizedPosition.x = ((touch.clientX - rect.left) / that.canvas.width) * 2 - 1;
            that.mouse.normalizedPosition.y = -((touch.clientY - rect.top) / that.canvas.height) * 2 + 1;

            if (that.touchMode !== 'drag')
                that.raiseEvent('mousemove', {
                    e: that
                });

            that.mouse.previousPosition.x = touch.pageX;
            that.mouse.previousPosition.y = touch.pageY;
        });

        let wheelevent = 'mousewheel';
        if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) wheelevent = 'DOMMouseScroll';

        this.canvas.addEventListener(wheelevent, function (e) {
            e.preventDefault();

            let delta = 'wheelDelta' in e ? e.wheelDelta : -40 * e.detail;
            that.raiseEvent('mousewheel', {
                e: delta
            });
        });

        this.canvas.addEventListener('keydown', function (e) {
            if (e.which == 16) {
                that.keyboard.shift = true;
            } else if (e.which == 17) {
                that.keyboard.ctrl = true;
            } else if (e.which == 18) {
                that.keyboard.alt = true;
            }

            that.raiseEvent('keydown', {
                e: e.which
            })
        });

        this.canvas.addEventListener('keyup', function (e) {
            if (e.which == 16) {
                that.keyboard.shift = false;
            } else if (e.which == 17) {
                that.keyboard.ctrl = false;
            } else if (e.which == 18) {
                that.keyboard.alt = false;
            }

            that.raiseEvent('keyup', {
                e: e.which
            });
        });

        this.canvas.addEventListener('mousedown', function (e) {
            let btn = e.button;
            let source = 'left';

            // Only handle single button events
            if (btn == 0) {
                that.mouse.state.left = true;
            } else if (btn == 1) {
                that.mouse.state.middle = true;
                source = 'middle';
            } else if (btn == 2) {
                that.mouse.state.right = true;
                source = 'right';
            }

            that.renderer.setMaxFps(that.highFps);

            that.raiseEvent('mousedown', {
                e: that,
                source: source
            });
        });

        this.canvas.addEventListener('click', function (e) {
            let btn = e.button;
            let source = 'left';

            that.raiseEvent('click', {
                e: that,
                source: source
            });
        });

        this.canvas.addEventListener('dblclick', function (e) {
            let btn = e.button;
            let source = 'left';

            that.raiseEvent('dblclick', {
                e: that,
                source: source
            });
        });

        this.canvas.addEventListener('mouseup', function (e) {
            let btn = e.button;
            let source = 'left';

            // Only handle single button events
            if (btn == 0) {
                that.mouse.state.left = false;
            } else if (btn == 1) {
                that.mouse.state.middle = false;
                source = 'middle';
            } else if (btn == 2) {
                that.mouse.state.right = false;
                source = 'right';
            }

            // Reset the previous position and delta of the mouse
            that.mouse.previousPosition.x = null;
            that.mouse.previousPosition.y = null;

            that.renderer.setMaxFps(that.lowFps);

            that.raiseEvent('mouseup', {
                e: that,
                source: source
            });
        });

        this.canvas.addEventListener('mouseleave', function(e) {
            that.mouse.state.left = false;
            that.mouse.state.middle = false;
            that.mouse.state.right = false;

            that.mouse.previousPosition.x = null;
            that.mouse.previousPosition.y = null;

            that.renderer.setMaxFps(that.lowFps);

            that.raiseEvent('mouseleave', {
                e: that,
                source: that.canvas
            });
        });


    }

    /**
     * Initialiizes WebVR, if the API is available and the device suppports it.
     */
    initWebVR() {
        if (navigator.getVRDevices) {
            navigator.getVRDisplays().then(function (displays) {
                if (displays.length === 0) {
                    return;
                }
                
                for (var i = 0; i < displays.length; ++i) {
                    
                }
            });
        }
    }

    /**
     * Adds an event listener to this controls instance.
     * 
     * @param {String} eventName The name of the event that is to be listened for.
     * @param {Function} callback A callback function to be called on the event being fired.
     */
    addEventListener(eventName, callback) {
        if (!this._eventListeners[eventName]) {
            this._eventListeners[eventName] = [];
        }

        this._eventListeners[eventName].push(callback);
    }

    /**
     * Remove an event listener from this controls instance.
     * 
     * @param {String} eventName The name of the event that is to be listened for.
     * @param {Function} callback A callback function to be called on the event being fired.
     */
    removeEventListener(eventName, callback) {
        if (!this._eventListeners.hasOwnProperty(eventName)) {
            return;
        }

        let index = this._eventListeners[eventName].indexOf(callback);
 
        if (index > -1) {
            this._eventListeners[eventName].splice(index, 1);
        }
    }

    /**
     * Raises an event.
     * 
     * @param {String} eventName The name of the event to be raised.
     * @param {*} data The data to be supplied to the callback function.
     */
    raiseEvent(eventName, data) {
        if (this._eventListeners[eventName]) {
            for (let i = 0; i < this._eventListeners[eventName].length; i++) {
                this._eventListeners[eventName][i](data);
            }
        }
    }

    /**
     * Returns the current look at vector associated with this controls.
     * 
     * @returns {Lore.Vector3f} The current look at vector.
     */
    getLookAt() {
        return this.lookAt;
    }

    /**
     * Sets the lookat vector, which is the center of the orbital camera sphere.
     * 
     * @param {Lore.Vector3f} lookAt The lookat vector.
     * @returns {Lore.OrbitalControls} Returns itself.
     */
    setLookAt(lookAt) {
        //this.camera.position = new Lore.Vector3f(this.radius, this.radius, this.radius);
        this.lookAt = lookAt.clone();
        this.update();
        
        return this;
    }

    /**
     * Update the camera (on mouse move, touch drag, mousewheel scroll, ...).
     * 
     * @param {*} e A mouse or touch events data.
     * @param {String} source The source of the input ('left', 'middle', 'right', 'wheel', ...).
     * @returns {Lore.ControlsBase} Returns itself.
     */
    update(e, source) {
        return this;
    }
}