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