/**
	Gesture events: emulates iOS gesture events on non iOS devices
	Event Contents
		- pageX / pageY: center point between fingers
		- rotation: degrees of rotation from beginning of gesture
		- scale: % change of distance between fingers
*/
//* @protected
(function() {
	if (!enyo.platform.gesture && enyo.platform.touch) {
		enyo.dispatcher.features.push(function(e) {
			if (handlers[e.type]) {
				touchGestures[e.type](e);
			}
		});
	}
	var handlers = {
		touchstart: true,
		touchmove: true,
		touchend: true
	};
	var touchGestures = {
		orderedTouches: [],
		gesture: null,
		touchstart: function(inEvent) {
			// some devices can send multiple changed touches on start and end
			enyo.forEach(inEvent.changedTouches, function(t) {
				var id = t.identifier;
				// some devices can send multiple touchstarts
				if (enyo.indexOf(id, this.orderedTouches) < 0) {
					this.orderedTouches.push(id);
				}
			}, this);
			if (inEvent.touches.length >= 2 && !this.gesture) {
				var p = this.gesturePositions(inEvent);
				this.gesture = this.gestureVector(p);
				this.gesture.angle = this.gestureAngle(p);
				this.gesture.scale = 1;
				this.gesture.rotation = 0;
				var g = this.makeGesture("gesturestart", inEvent, {vector: this.gesture, scale: 1, rotation: 0});
				enyo.dispatch(g);
			}
		},
		touchend: function(inEvent) {
			// some devices can send multiple changed touches on start and end
			enyo.forEach(inEvent.changedTouches, function(t) {
				enyo.remove(t.identifier, this.orderedTouches);
			}, this);
			if (inEvent.touches.length <= 1 && this.gesture) {
				var t = inEvent.touches[0] || inEvent.changedTouches[inEvent.changedTouches.length - 1];
				// gesture end sends last rotation and scale, with the x/y of the last finger
				enyo.dispatch(this.makeGesture("gestureend", inEvent, {vector: {xcenter: t.pageX, ycenter: t.pageY}, scale: this.gesture.scale, rotation: this.gesture.rotation}));
				this.gesture = null;
			}
		},
		touchmove: function(inEvent) {
			if (this.gesture) {
				var g = this.makeGesture("gesturechange", inEvent);
				this.gesture.scale = g.scale;
				this.gesture.rotation = g.rotation;
				enyo.dispatch(g);
			}
		},
		findIdentifiedTouch: function(inTouches, inId) {
			for (var i = 0, t; (t = inTouches[i]); i++) {
				if (t.identifier === inId) {
					return t;
				}
			}
		},
		gesturePositions: function(inEvent) {
			var first = this.findIdentifiedTouch(inEvent.touches, this.orderedTouches[0]);
			var last = this.findIdentifiedTouch(inEvent.touches, this.orderedTouches[this.orderedTouches.length - 1]);
			var fx = first.pageX, lx = last.pageX, fy = first.pageY, ly = last.pageY;
			// center the first touch as 0,0
			var x = lx - fx, y = ly - fy;
			var h = Math.sqrt(x*x + y*y);
			return {x: x, y: y, h: h, fx: fx, lx: lx, fy: fy, ly: ly};
		},
		// find rotation angle
		gestureAngle: function(inPositions) {
			var p = inPositions;
			// yay math!, rad -> deg
			var a = Math.asin(p.y / p.h) * (180 / Math.PI);
			// fix for range limits of asin (-90 to 90)
			// Quadrants II and III
			if (p.x < 0) {
				a = 180 - a;
			}
			// Quadrant IV
			if (p.x > 0 && p.y < 0) {
				a += 360;
			}
			return a;
		},
		// find bounding box
		gestureVector: function(inPositions) {
			// the least recent touch and the most recent touch determine the bounding box of the gesture event
			var p = inPositions;
			// center the first touch as 0,0
			return {
				magnitude: p.h,
				xcenter: Math.abs(Math.round(p.fx + (p.x / 2))),
				ycenter: Math.abs(Math.round(p.fy + (p.y / 2)))
			};
		},
		makeGesture: function(inType, inEvent, inCache) {
			var vector, scale, rotation;
			if (inCache) {
				vector = inCache.vector;
				scale = inCache.scale;
				rotation = inCache.rotation;
			} else {
				var p = this.gesturePositions(inEvent);
				vector = this.gestureVector(p);
				scale = vector.magnitude / this.gesture.magnitude;
				// gestureEvent.rotation is difference from the starting angle, clockwise
				rotation = (360 + this.gestureAngle(p) - this.gesture.angle) % 360;
			}
			var e = enyo.clone(inEvent);
			return enyo.mixin(e, {
				type: inType,
				scale: scale,
				pageX: vector.xcenter,
				pageY: vector.ycenter,
				rotation: rotation
			});
		}
	};
})();

@@ -687,73 +687,158 @@
 %0A%09%09%09
-enyo.forEach(inEvent.changedTouches, function(t) %7B%0A%09%09%09%09var id = t
+var i,%0A%09%09%09changedTouches = inEvent.changedTouches,%0A%09%09%09length = changedTouches.length;%0A%09%09%09for (i = 0; i %3C length; i++) %7B%0A%09%09%09%09var id = changedTouches%5Bi%5D
 .ide
@@ -990,32 +990,24 @@
 ;%0A%09%09%09%09%7D%0A%09%09%09%7D
-, this);
 %0A%09%09%09if (inEv
@@ -1485,55 +1485,124 @@
 %0A%09%09%09
-enyo.forEach(inEvent.changedTouches, function(t
+var i,%0A%09%09%09changedTouches = inEvent.changedTouches,%0A%09%09%09length = changedTouches.length;%0A%09%09%09for (i = 0; i %3C length; i++
 ) %7B%0A
@@ -1617,17 +1617,33 @@
 .remove(
-t
+changedTouches%5Bi%5D
 .identif
@@ -1677,16 +1677,8 @@
 %09%09%09%7D
-, this);
 %0A%09%09%09

Fix uncaught TypeError on mobile