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; });