Source: common/models/multi_touch.js

define(["lib/jquery",               "lib/underscore",        "lib/sylvester",
        "common/util/log",          "common/util/dom_utils", "common/util/id_generator",
        "common/util/binding_list", "ARCore/touches",        "require",
        "common/util/check"],
function($,                          _,                       Sylvester,
         Log,                        DOMUtils,                IDGenerator,
         BindingList,                Touches,                 require,
         check) {

    "use strict";

    var $V = Sylvester.$V;

    /**
     * @privconstructor
     * @class
     * See {@tutorial touch} for a description of the Lens multitouch system. A
     * MultiTouch is an array representing multiple touches. It is a subclass
     * of Array; that is, you can treat it as an array containing Touches, and
     * you can also use a number of additional properties and methods. Note: X
     * and Y coordinates of MultiTouches are calculated as the average X/Y
     * coordinates of their constituent touches.
     *
     * @property {String}  id        Unique ID for this MultiTouch.
     * @property {Element} el        Element this multitouch started on.
     * @property {Number}  origX     Original X coordinate of the multitouch.
     * @property {Number}  origY     Original Y coordinate of the multitouch.
     * @property {Number}  pageX     Current X coordinate of the multitouch.
     * @property {Number}  pageY     Current Y coordinate of the multitouch.
     * @property {Number}  elX       Current X coordinate of the multitouch,
                                     relative to the parent element
     * @property {Number}  elY       Current Y coordinate of the multitouch,
                                     relative to the parent element.
     * @property {Number}  dx        X displacement: pageX-origX
     * @property {Number}  dy        Y displacement: pageY-origY
     * @property {Number}  size      Current size of the multitouch: the average
     *                               distance, in pixels, between the center and
     *                               a constituent touch.
     * @property {Number}  origSize  Original size of the multitouch: the
     *                               average distance, in pixels, between the
     *                               center and a constituent touch.
     * @property {Number}  dSize     Difference between current size and
     *                               original size. Equivalent to
     *                               <tt>size - origSize</tt>.
     * @property {Number}  rotation  For two-element multitouches: the amount
     *                               the touch has rotated. Measured in
     *                               counterclockwise radians.
     * @property {Number}  scale     The amount this multitouch has scaled by.
     *                               a value of 1 indicates it is the same size,
     *                               a value of 2 indicates it is twice as
     *                               large, etc. Equivalent to
     *                               <tt>size / origSize</tt>
     * @name MultiTouch
     */
    var MultiTouch = function(touches, el) {
        /** @alias MultiTouch.prototype */
        var self = _.clone(touches);

        var pageX, pageY, elX, elY, dx, dy, size, dSize, rotation, scale, origAngle;

        // updates this MultiTouch's pageX, pageY, dx, dy, rotation, size, and
        // scale based on its constituent touches' pageX and pageY.
        var update = function() {
            // pageX and pageY: sum constituent pageX/pageY and divide by length.
            pageX = _.reduce(self, function(memo, touch) {
                return memo + touch.pageX;
            }, 0) / self.length;

            pageY = _.reduce(self, function(memo, touch) {
                return memo + touch.pageY;
            }, 0) / self.length;

            // elX and elY
            var offsets = $(el).offset();
            if(offsets) {
                elX = pageX - offsets.left;
                elY = pageY - offsets.top;
            }
            else {
                // document doens't have offsets; use 0,0
                elX = pageX;
                elY = pageY;
            }

            // dx and dy
            if(!self.origX) {
                // first run, set origX and origY
                self.origX = pageX;
                self.origY = pageY;
            }
            dx = pageX - self.origX;
            dy = pageY - self.origY;

            // size: average distance between center of multitouch and the
            //       individual touches.
            var centerVector = $V([pageX, pageY]);
            size = _.reduce(self, function(memo, touch) {
                return memo + centerVector.distanceFrom(touch._vector());
            }, 0) / self.length;
            if (size === 0) {
                // make sure size > 0 to avoid divide-by-zero when calculating scale
                size = 1;
            }

            // scale: current size / original size
            if(!self.origSize) {
                // first run, set origSize
                self.origSize = size;
            }
            scale = size / self.origSize;
            dSize = size - self.origSize;

            // rotation: angle between original line and current line
            if(self.length === 2) {
                var curAngle = Math.atan2(self[0].pageY - self[1].pageY,
                                          self[0].pageX - self[1].pageX);

                if(!origAngle) {
                    // if we don't already have an origAngle, we assume this is the
                    // first update(), and set original to current.
                    origAngle = curAngle;
                }

                rotation = curAngle - origAngle;
            }
        };

        /* A list of movement subscribers. Each element is an object with four
         * properties: fn, the subscriber function; delta, the amount a finger
         * must move before calling the subscriber; and lastX and lastY, the x
         * and y position at which the subscriber was called.
         */
        var moveSubscribers = BindingList();

        // A list of end subscribers.
        var endSubscribers = BindingList();

        /**
         * Registers a function to be called whenever this multitouch moves.
         * @param  {Function} fn      The function to call. Gets this multitouch
         *                            as an argument.
         * @param  {Number}   [delta] If given, the function will only be called
         *                            when the multitouch has moved this many
         *                            pixels.
         * @return {Binding}          A {@link Binding} that allows this handler
         *                            to be cleared
         */
        self.moved = function(fn, delta) {
            check(fn, Function);
            check(delta, check.Match.Optional(Number));

            if(!delta) {
                delta = 0;
            }

            require("ARCore/touches").registerMultiTouch(self);

            return moveSubscribers.add({
                fn: fn,
                delta: delta,
                lastX: pageX,
                lastY: pageY
            });
        };

        /**
         * Registers a function to be called when this multitouch ends.
         * @param  {Function} fn The function to call. Gets this multitouch as
         *                       an argument.
         * @return {Binding}     A {@link Binding} that allows this handler to
         *                       be cleared
         */
        self.ended = function(fn) {
            check(fn, Function);

            require("ARCore/touches").registerMultiTouch(self);

            return endSubscribers.add(fn);
        };

        /**
         * Updates this multitouch to reflect the current state of its
         * constituent touches, informing movement subscribers.
         * @private
         */
        self._update = function() {
            update();

            moveSubscribers.each(function(sub) {
                if($V([pageX, pageY]).distanceFrom($V([sub.lastX, sub.lastY])) > sub.delta) {
                    // touch has moved enough
                    sub.fn(self);
                    sub.lastX = self.pageX;
                    sub.lastY = self.pageY;
                }
            });
        };

        /**
         * Ends this multitouch, informing end subscribers.
         * @private
         */
        self._markDone = function() {
            endSubscribers.callAll(self);

            require("ARCore/touches").deregisterMultiTouch(self);
        };

        Object.defineProperty(self, "pageX",    { get: function() { return pageX;    } });
        Object.defineProperty(self, "pageY",    { get: function() { return pageY;    } });
        Object.defineProperty(self, "elX",      { get: function() { return elX;      } });
        Object.defineProperty(self, "elY",      { get: function() { return elY;      } });
        Object.defineProperty(self, "dx",       { get: function() { return dx;       } });
        Object.defineProperty(self, "dy",       { get: function() { return dy;       } });
        Object.defineProperty(self, "size",     { get: function() { return size;     } });
        Object.defineProperty(self, "dSize",    { get: function() { return dSize;    } });
        Object.defineProperty(self, "rotation", { get: function() { return rotation; } });
        Object.defineProperty(self, "scale",    { get: function() { return scale;    } });

        self.el = el;

        // generate a unique id
        self.id = IDGenerator.uuid();

        // run update() to set pageX, pageY, rotation, size, and scale
        //  we run it twice because we need to set dx based on origX based on
        //  pageX
        update();

        Object.freeze(self);
        return self;
    };

    return MultiTouch;
});