Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
684
gaia/shared/js/gesture_detector.js
Executable file
684
gaia/shared/js/gesture_detector.js
Executable file
|
|
@ -0,0 +1,684 @@
|
|||
/* exported GestureDetector */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* GestureDetector.js: generate events for one and two finger gestures.
|
||||
*
|
||||
* A GestureDetector object listens for touch events on a specified
|
||||
* element and generates higher-level events that describe one and two finger
|
||||
* gestures on the element.
|
||||
*
|
||||
* Supported events:
|
||||
*
|
||||
* tap like a click event
|
||||
* dbltap like dblclick
|
||||
* pan one finger motion
|
||||
* swipe when a finger is released following pan events
|
||||
* holdstart touch and hold. Must set an option to get these.
|
||||
* holdmove motion after a holdstart event
|
||||
* holdend when the finger goes up after holdstart/holdmove
|
||||
* transform 2-finger pinch and twist gestures for scaling and rotation
|
||||
*
|
||||
* Each of these events is a bubbling CustomEvent with important details in the
|
||||
* event.detail field. The event details are not yet stable and are not yet
|
||||
* documented. See the calls to emitEvent() for details.
|
||||
*
|
||||
* To use this library, create a GestureDetector object by passing an element to
|
||||
* the GestureDetector() constructor and then calling startDetecting() on it.
|
||||
* The element will be the target of all the emitted gesture events. You can
|
||||
* also pass an optional object as the second constructor argument. If you're
|
||||
* interested in holdstart/holdmove/holdend events, pass {holdEvents:true} as
|
||||
* this second argument. Otherwise they will not be generated.
|
||||
* If you want to customize the pan threshold, pass {panThreshold:X}
|
||||
* (X and Y in pixels) in the options argument.
|
||||
*
|
||||
* Implementation note: event processing is done with a simple finite-state
|
||||
* machine. This means that in general, the various kinds of gestures are
|
||||
* mutually exclusive. You won't get pan events until your finger has
|
||||
* moved more than a minimum threshold, for example, but it does, the FSM enters
|
||||
* a new state in which it can emit pan and swipe events and cannot emit hold
|
||||
* events. Similarly, if you've started a 1 finger pan/swipe gesture and
|
||||
* accidentally touch with a second finger, you'll continue to get pan events,
|
||||
* and won't suddenly start getting 2-finger transform events.
|
||||
*
|
||||
* This library never calls preventDefault() or stopPropagation on any of the
|
||||
* events it processes, so the raw touch events should still be
|
||||
* available for other code to process. It is not clear to me whether this is a
|
||||
* feature or a bug.
|
||||
*/
|
||||
|
||||
var GestureDetector = (function() {
|
||||
|
||||
//
|
||||
// Constructor
|
||||
//
|
||||
function GD(e, options) {
|
||||
this.element = e;
|
||||
this.options = options || {};
|
||||
this.options.panThreshold = this.options.panThreshold || GD.PAN_THRESHOLD;
|
||||
this.state = initialState;
|
||||
this.timers = {};
|
||||
}
|
||||
|
||||
//
|
||||
// Public methods
|
||||
//
|
||||
|
||||
GD.prototype.startDetecting = function() {
|
||||
var self = this;
|
||||
eventtypes.forEach(function(t) {
|
||||
self.element.addEventListener(t, self);
|
||||
});
|
||||
};
|
||||
|
||||
GD.prototype.stopDetecting = function() {
|
||||
var self = this;
|
||||
eventtypes.forEach(function(t) {
|
||||
self.element.removeEventListener(t, self);
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Internal methods
|
||||
//
|
||||
|
||||
GD.prototype.handleEvent = function(e) {
|
||||
var handler = this.state[e.type];
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
// If this is a touch event handle each changed touch separately
|
||||
if (e.changedTouches) {
|
||||
for (var i = 0; i < e.changedTouches.length; i++) {
|
||||
handler(this, e, e.changedTouches[i]);
|
||||
// The first changed touch might have changed the state of the FSM.
|
||||
handler = this.state[e.type];
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else { // Otherwise, just dispatch the event to the handler
|
||||
handler(this, e);
|
||||
}
|
||||
};
|
||||
|
||||
GD.prototype.startTimer = function(type, time) {
|
||||
this.clearTimer(type);
|
||||
var self = this;
|
||||
this.timers[type] = setTimeout(function() {
|
||||
self.timers[type] = null;
|
||||
var handler = self.state[type];
|
||||
if (handler) {
|
||||
handler(self, type);
|
||||
}
|
||||
}, time);
|
||||
};
|
||||
|
||||
GD.prototype.clearTimer = function(type) {
|
||||
if (this.timers[type]) {
|
||||
clearTimeout(this.timers[type]);
|
||||
this.timers[type] = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Switch to a new FSM state, and call the init() function of that
|
||||
// state, if it has one. The event and touch arguments are optional
|
||||
// and are just passed through to the state init function.
|
||||
GD.prototype.switchTo = function(state, event, touch) {
|
||||
this.state = state;
|
||||
if (state.init) {
|
||||
state.init(this, event, touch);
|
||||
}
|
||||
};
|
||||
|
||||
GD.prototype.emitEvent = function(type, detail) {
|
||||
if (!this.target) {
|
||||
console.error('Attempt to emit event with no target');
|
||||
return;
|
||||
}
|
||||
|
||||
var event = this.element.ownerDocument.createEvent('CustomEvent');
|
||||
event.initCustomEvent(type, true, true, detail);
|
||||
this.target.dispatchEvent(event);
|
||||
};
|
||||
|
||||
//
|
||||
// Tuneable parameters
|
||||
//
|
||||
GD.HOLD_INTERVAL = 1000; // Hold events after 1000 ms
|
||||
GD.PAN_THRESHOLD = 20; // 20 pixels movement before touch panning
|
||||
GD.DOUBLE_TAP_DISTANCE = 50;
|
||||
GD.DOUBLE_TAP_TIME = 500;
|
||||
GD.VELOCITY_SMOOTHING = 0.5;
|
||||
|
||||
// Don't start sending transform events until the gesture exceeds a threshold
|
||||
GD.SCALE_THRESHOLD = 20; // pixels
|
||||
GD.ROTATE_THRESHOLD = 22.5; // degrees
|
||||
|
||||
// For pans and zooms, we compute new starting coordinates that are part way
|
||||
// between the initial event and the event that crossed the threshold so that
|
||||
// the first event we send doesn't cause a big lurch. This constant must be
|
||||
// between 0 and 1 and says how far along the line between the initial value
|
||||
// and the new value we pick
|
||||
GD.THRESHOLD_SMOOTHING = 0.9;
|
||||
|
||||
//
|
||||
// Helpful shortcuts and utility functions
|
||||
//
|
||||
|
||||
var abs = Math.abs, floor = Math.floor, sqrt = Math.sqrt, atan2 = Math.atan2;
|
||||
var PI = Math.PI;
|
||||
|
||||
// The names of events that we need to register handlers for
|
||||
var eventtypes = [
|
||||
'touchstart',
|
||||
'touchmove',
|
||||
'touchend'
|
||||
];
|
||||
|
||||
// Return the event's timestamp in ms
|
||||
function eventTime(e) {
|
||||
// In gecko, synthetic events seem to be in microseconds rather than ms.
|
||||
// So if the timestamp is much larger than the current time, assue it is
|
||||
// in microseconds and divide by 1000
|
||||
var ts = e.timeStamp;
|
||||
if (ts > 2 * Date.now()) {
|
||||
return Math.floor(ts / 1000);
|
||||
} else {
|
||||
return ts;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Return an object containg the space and time coordinates of
|
||||
// and event and touch. We freeze the object to make it immutable so
|
||||
// we can pass it in events and not worry about values being changed.
|
||||
function coordinates(e, t) {
|
||||
return Object.freeze({
|
||||
screenX: t.screenX,
|
||||
screenY: t.screenY,
|
||||
clientX: t.clientX,
|
||||
clientY: t.clientY,
|
||||
timeStamp: eventTime(e)
|
||||
});
|
||||
}
|
||||
|
||||
// Like coordinates(), but return the midpoint between two touches
|
||||
function midpoints(e, t1, t2) {
|
||||
return Object.freeze({
|
||||
screenX: floor((t1.screenX + t2.screenX) / 2),
|
||||
screenY: floor((t1.screenY + t2.screenY) / 2),
|
||||
clientX: floor((t1.clientX + t2.clientX) / 2),
|
||||
clientY: floor((t1.clientY + t2.clientY) / 2),
|
||||
timeStamp: eventTime(e)
|
||||
});
|
||||
}
|
||||
|
||||
// Given coordinates objects c1 and c2, return a new coordinates object
|
||||
// representing a point and time along the line between those points.
|
||||
// The position of the point is controlled by the THRESHOLD_SMOOTHING constant
|
||||
function between(c1, c2) {
|
||||
var r = GD.THRESHOLD_SMOOTHING;
|
||||
return Object.freeze({
|
||||
screenX: floor(c1.screenX + r * (c2.screenX - c1.screenX)),
|
||||
screenY: floor(c1.screenY + r * (c2.screenY - c1.screenY)),
|
||||
clientX: floor(c1.clientX + r * (c2.clientX - c1.clientX)),
|
||||
clientY: floor(c1.clientY + r * (c2.clientY - c1.clientY)),
|
||||
timeStamp: floor(c1.timeStamp + r * (c2.timeStamp - c1.timeStamp))
|
||||
});
|
||||
}
|
||||
|
||||
// Compute the distance between two touches
|
||||
function touchDistance(t1, t2) {
|
||||
var dx = t2.screenX - t1.screenX;
|
||||
var dy = t2.screenY - t1.screenY;
|
||||
return sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
// Compute the direction (as an angle) of the line between two touches
|
||||
// Returns a number d, -180 < d <= 180
|
||||
function touchDirection(t1, t2) {
|
||||
return atan2(t2.screenY - t1.screenY,
|
||||
t2.screenX - t1.screenX) * 180 / PI;
|
||||
}
|
||||
|
||||
// Compute the clockwise angle between direction d1 and direction d2.
|
||||
// Returns an angle a -180 < a <= 180.
|
||||
function touchRotation(d1, d2) {
|
||||
var angle = d2 - d1;
|
||||
if (angle > 180) {
|
||||
angle -= 360;
|
||||
} else if (angle <= -180) {
|
||||
angle += 360;
|
||||
}
|
||||
return angle;
|
||||
}
|
||||
|
||||
// Determine if two taps are close enough in time and space to
|
||||
// trigger a dbltap event. The arguments are objects returned
|
||||
// by the coordinates() function.
|
||||
function isDoubleTap(lastTap, thisTap) {
|
||||
var dx = abs(thisTap.screenX - lastTap.screenX);
|
||||
var dy = abs(thisTap.screenY - lastTap.screenY);
|
||||
var dt = thisTap.timeStamp - lastTap.timeStamp;
|
||||
return (dx < GD.DOUBLE_TAP_DISTANCE &&
|
||||
dy < GD.DOUBLE_TAP_DISTANCE &&
|
||||
dt < GD.DOUBLE_TAP_TIME);
|
||||
}
|
||||
|
||||
//
|
||||
// The following objects are the states of our Finite State Machine
|
||||
//
|
||||
|
||||
// In this state we're not processing any gestures, just waiting
|
||||
// for an event to start a gesture and ignoring others
|
||||
var initialState = {
|
||||
name: 'initialState',
|
||||
init: function(d, e, t) {
|
||||
// When we enter or return to the initial state, clear
|
||||
// the detector properties that were tracking gestures
|
||||
// Don't clear d.lastTap here, though. We need it for dbltap events
|
||||
d.target = null;
|
||||
d.start = d.last = null;
|
||||
d.touch1 = d.touch2 = null;
|
||||
d.vx = d.vy = null;
|
||||
d.startDistance = d.lastDistance = null;
|
||||
d.startDirection = d.lastDirection = null;
|
||||
d.lastMidpoint = null;
|
||||
d.scaled = d.rotated = null;
|
||||
|
||||
// If we were invoked with an event and touch, then process it.
|
||||
if (e && t && e.type === 'touchstart') {
|
||||
initialState.touchstart(d, e, t);
|
||||
}
|
||||
},
|
||||
|
||||
// Switch to the touchstarted state and process the touch event there
|
||||
touchstart: function(d, e, t) {
|
||||
d.switchTo(touchStartedState, e, t);
|
||||
}
|
||||
};
|
||||
|
||||
// One finger is down but we haven't generated any event yet. We're
|
||||
// waiting to see... If the finger goes up soon, its a tap. If the finger
|
||||
// stays down and still, its a hold. If the finger moves its a pan/swipe.
|
||||
// And if a second finger goes down, its a transform
|
||||
var touchStartedState = {
|
||||
name: 'touchStartedState',
|
||||
init: function(d, e, t) {
|
||||
// Remember the target of the event
|
||||
d.target = e.target;
|
||||
// Remember the id of the touch that started
|
||||
d.touch1 = t.identifier;
|
||||
// Get the coordinates of the touch
|
||||
d.start = d.last = coordinates(e, t);
|
||||
// Start a timer for a hold
|
||||
// If we're doing hold events, start a timer for them
|
||||
if (d.options.holdEvents) {
|
||||
d.startTimer('holdtimeout', GD.HOLD_INTERVAL);
|
||||
}
|
||||
},
|
||||
|
||||
touchstart: function(d, e, t) {
|
||||
// If another finger goes down in this state, then
|
||||
// go to transform state to start 2-finger gestures.
|
||||
d.clearTimer('holdtimeout');
|
||||
|
||||
if (e.touches.length > 1) { // verify that we have 2 fingers
|
||||
d.switchTo(transformState, e, t);
|
||||
}
|
||||
else {
|
||||
// If Gecko fails to deliver a touchend event to us (bug 1162771, e.g.)
|
||||
// then we might get two touchstart events in a row and end up here
|
||||
// when there is actually only one finger currently on the screen.
|
||||
// In that case, we switch back to the initial state and process
|
||||
// this touch from that state. (We don't switch directly to
|
||||
// touchStartedState because we need to reset things in the
|
||||
// initialState init() function.)
|
||||
console.warn('Ignoring missing touchend event. See bug 1162771.');
|
||||
d.switchTo(initialState, e, t);
|
||||
}
|
||||
},
|
||||
touchmove: function(d, e, t) {
|
||||
// Ignore any touches but the initial one
|
||||
// This could happen if there was still a finger down after
|
||||
// the end of a previous 2-finger gesture, e.g.
|
||||
if (t.identifier !== d.touch1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (abs(t.screenX - d.start.screenX) > d.options.panThreshold ||
|
||||
abs(t.screenY - d.start.screenY) > d.options.panThreshold) {
|
||||
d.clearTimer('holdtimeout');
|
||||
d.switchTo(panStartedState, e, t);
|
||||
}
|
||||
},
|
||||
touchend: function(d, e, t) {
|
||||
// Ignore any touches but the initial one
|
||||
if (t.identifier !== d.touch1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there was a previous tap that was close enough in time
|
||||
// and space, then emit a 'dbltap' event
|
||||
if (d.lastTap && isDoubleTap(d.lastTap, d.start)) {
|
||||
d.emitEvent('tap', d.start);
|
||||
d.emitEvent('dbltap', d.start);
|
||||
// clear the lastTap property, so we don't get another one
|
||||
d.lastTap = null;
|
||||
}
|
||||
else {
|
||||
// Emit a 'tap' event using the starting coordinates
|
||||
// as the event details
|
||||
d.emitEvent('tap', d.start);
|
||||
|
||||
// Remember the coordinates of this tap so we can detect double taps
|
||||
d.lastTap = coordinates(e, t);
|
||||
}
|
||||
|
||||
// In either case clear the timer and go back to the initial state
|
||||
d.clearTimer('holdtimeout');
|
||||
d.switchTo(initialState);
|
||||
},
|
||||
|
||||
holdtimeout: function(d) {
|
||||
d.switchTo(holdState);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// A single touch has moved enough to exceed the pan threshold and now
|
||||
// we're going to generate pan events after each move and a swipe event
|
||||
// when the touch ends. We ignore any other touches that occur while this
|
||||
// pan/swipe gesture is in progress.
|
||||
var panStartedState = {
|
||||
name: 'panStartedState',
|
||||
init: function(d, e, t) {
|
||||
// Panning doesn't start until the touch has moved more than a
|
||||
// certain threshold. But we don't want the pan to have a jerky
|
||||
// start where the first event is a big distance. So proceed as
|
||||
// pan actually started at a point along the path between the
|
||||
// first touch and this current touch.
|
||||
d.start = d.last = between(d.start, coordinates(e, t));
|
||||
|
||||
// If we transition into this state with a touchmove event,
|
||||
// then process it with that handler. If we don't do this then
|
||||
// we can end up with swipe events that don't know their velocity
|
||||
if (e.type === 'touchmove') {
|
||||
panStartedState.touchmove(d, e, t);
|
||||
}
|
||||
},
|
||||
|
||||
touchmove: function(d, e, t) {
|
||||
// Ignore any fingers other than the one we're tracking
|
||||
if (t.identifier !== d.touch1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Each time the touch moves, emit a pan event but stay in this state
|
||||
var current = coordinates(e, t);
|
||||
d.emitEvent('pan', {
|
||||
absolute: {
|
||||
dx: current.screenX - d.start.screenX,
|
||||
dy: current.screenY - d.start.screenY
|
||||
},
|
||||
relative: {
|
||||
dx: current.screenX - d.last.screenX,
|
||||
dy: current.screenY - d.last.screenY
|
||||
},
|
||||
position: current
|
||||
});
|
||||
|
||||
// Track the pan velocity so we can report this with the swipe
|
||||
// Use a exponential moving average for a bit of smoothing
|
||||
// on the velocity
|
||||
var dt = current.timeStamp - d.last.timeStamp;
|
||||
var vx = (current.screenX - d.last.screenX) / dt;
|
||||
var vy = (current.screenY - d.last.screenY) / dt;
|
||||
|
||||
if (d.vx == null) { // first time; no average
|
||||
d.vx = vx;
|
||||
d.vy = vy;
|
||||
}
|
||||
else {
|
||||
d.vx = d.vx * GD.VELOCITY_SMOOTHING +
|
||||
vx * (1 - GD.VELOCITY_SMOOTHING);
|
||||
d.vy = d.vy * GD.VELOCITY_SMOOTHING +
|
||||
vy * (1 - GD.VELOCITY_SMOOTHING);
|
||||
}
|
||||
|
||||
d.last = current;
|
||||
},
|
||||
touchend: function(d, e, t) {
|
||||
// Ignore any fingers other than the one we're tracking
|
||||
if (t.identifier !== d.touch1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit a swipe event when the finger goes up.
|
||||
// Report start and end point, dx, dy, dt, velocity and direction
|
||||
var current = coordinates(e, t);
|
||||
var dx = current.screenX - d.start.screenX;
|
||||
var dy = current.screenY - d.start.screenY;
|
||||
// angle is a positive number of degrees, starting at 0 on the
|
||||
// positive x axis and increasing clockwise.
|
||||
var angle = atan2(dy, dx) * 180 / PI;
|
||||
if (angle < 0) {
|
||||
angle += 360;
|
||||
}
|
||||
|
||||
// Direction is 'right', 'down', 'left' or 'up'
|
||||
var direction;
|
||||
if (angle >= 315 || angle < 45) {
|
||||
direction = 'right';
|
||||
} else if (angle >= 45 && angle < 135) {
|
||||
direction = 'down';
|
||||
} else if (angle >= 135 && angle < 225) {
|
||||
direction = 'left';
|
||||
} else if (angle >= 225 && angle < 315) {
|
||||
direction = 'up';
|
||||
}
|
||||
|
||||
d.emitEvent('swipe', {
|
||||
start: d.start,
|
||||
end: current,
|
||||
dx: dx,
|
||||
dy: dy,
|
||||
dt: e.timeStamp - d.start.timeStamp,
|
||||
vx: d.vx,
|
||||
vy: d.vy,
|
||||
direction: direction,
|
||||
angle: angle
|
||||
});
|
||||
|
||||
// Go back to the initial state
|
||||
d.switchTo(initialState);
|
||||
}
|
||||
};
|
||||
|
||||
// We enter this state if the user touches and holds for long enough
|
||||
// without moving much. When we enter we emit a holdstart event. Motion
|
||||
// after the holdstart generates holdmove events. And when the touch ends
|
||||
// we generate a holdend event. holdmove and holdend events can be used
|
||||
// kind of like drag and drop events in a mouse-based UI. Currently,
|
||||
// these events just report the coordinates of the touch. Do we need
|
||||
// other details?
|
||||
var holdState = {
|
||||
name: 'holdState',
|
||||
init: function(d) {
|
||||
d.emitEvent('holdstart', d.start);
|
||||
},
|
||||
|
||||
touchmove: function(d, e, t) {
|
||||
var current = coordinates(e, t);
|
||||
d.emitEvent('holdmove', {
|
||||
absolute: {
|
||||
dx: current.screenX - d.start.screenX,
|
||||
dy: current.screenY - d.start.screenY
|
||||
},
|
||||
relative: {
|
||||
dx: current.screenX - d.last.screenX,
|
||||
dy: current.screenY - d.last.screenY
|
||||
},
|
||||
position: current
|
||||
});
|
||||
|
||||
d.last = current;
|
||||
},
|
||||
|
||||
touchend: function(d, e, t) {
|
||||
var current = coordinates(e, t);
|
||||
d.emitEvent('holdend', {
|
||||
start: d.start,
|
||||
end: current,
|
||||
dx: current.screenX - d.start.screenX,
|
||||
dy: current.screenY - d.start.screenY
|
||||
});
|
||||
d.switchTo(initialState);
|
||||
}
|
||||
};
|
||||
|
||||
// We enter this state if a second touch starts before we start
|
||||
// recoginzing any other gesture. As the touches move we track the
|
||||
// distance and angle between them to report scale and rotation values
|
||||
// in transform events.
|
||||
var transformState = {
|
||||
name: 'transformState',
|
||||
init: function(d, e, t) {
|
||||
// Remember the id of the second touch
|
||||
d.touch2 = t.identifier;
|
||||
|
||||
// Get the two Touch objects
|
||||
var t1 = e.touches.identifiedTouch(d.touch1);
|
||||
var t2 = e.touches.identifiedTouch(d.touch2);
|
||||
|
||||
// Compute and remember the initial distance and angle
|
||||
d.startDistance = d.lastDistance = touchDistance(t1, t2);
|
||||
d.startDirection = d.lastDirection = touchDirection(t1, t2);
|
||||
|
||||
// Don't start emitting events until we're past a threshold
|
||||
d.scaled = d.rotated = false;
|
||||
},
|
||||
|
||||
touchmove: function(d, e, t) {
|
||||
// Ignore touches we're not tracking
|
||||
if (t.identifier !== d.touch1 && t.identifier !== d.touch2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the two Touch objects
|
||||
var t1 = e.touches.identifiedTouch(d.touch1);
|
||||
var t2 = e.touches.identifiedTouch(d.touch2);
|
||||
|
||||
// Compute the new midpoints, distance and direction
|
||||
var midpoint = midpoints(e, t1, t2);
|
||||
var distance = touchDistance(t1, t2);
|
||||
var direction = touchDirection(t1, t2);
|
||||
var rotation = touchRotation(d.startDirection, direction);
|
||||
|
||||
// Check all of these numbers against the thresholds. Otherwise
|
||||
// the transforms are too jittery even when you try to hold your
|
||||
// fingers still.
|
||||
if (!d.scaled) {
|
||||
if (abs(distance - d.startDistance) > GD.SCALE_THRESHOLD) {
|
||||
d.scaled = true;
|
||||
d.startDistance = d.lastDistance =
|
||||
floor(d.startDistance +
|
||||
GD.THRESHOLD_SMOOTHING * (distance - d.startDistance));
|
||||
} else {
|
||||
distance = d.startDistance;
|
||||
}
|
||||
}
|
||||
if (!d.rotated) {
|
||||
if (abs(rotation) > GD.ROTATE_THRESHOLD) {
|
||||
d.rotated = true;
|
||||
} else {
|
||||
direction = d.startDirection;
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing has exceeded the threshold yet, then we
|
||||
// don't even have to fire an event.
|
||||
if (d.scaled || d.rotated) {
|
||||
// The detail field for the transform gesture event includes
|
||||
// 'absolute' transformations against the initial values and
|
||||
// 'relative' transformations against the values from the last
|
||||
// transformgesture event.
|
||||
d.emitEvent('transform', {
|
||||
absolute: { // transform details since gesture start
|
||||
scale: distance / d.startDistance,
|
||||
rotate: touchRotation(d.startDirection, direction)
|
||||
},
|
||||
relative: { // transform since last gesture change
|
||||
scale: distance / d.lastDistance,
|
||||
rotate: touchRotation(d.lastDirection, direction)
|
||||
},
|
||||
midpoint: midpoint
|
||||
});
|
||||
|
||||
d.lastDistance = distance;
|
||||
d.lastDirection = direction;
|
||||
d.lastMidpoint = midpoint;
|
||||
}
|
||||
},
|
||||
|
||||
touchend: function(d, e, t) {
|
||||
// If either finger goes up, we're done with the gesture.
|
||||
// The user might move that finger and put it right back down
|
||||
// again to begin another 2-finger gesture, so we can't go
|
||||
// back to the initial state while one of the fingers remains up.
|
||||
// On the other hand, we can't go back to touchStartedState because
|
||||
// that would mean that the finger left down could cause a tap or
|
||||
// pan event. So we need an afterTransform state that waits for
|
||||
// a finger to come back down or the other finger to go up.
|
||||
if (t.identifier === d.touch2) {
|
||||
d.touch2 = null;
|
||||
} else if (t.identifier === d.touch1) {
|
||||
d.touch1 = d.touch2;
|
||||
d.touch2 = null;
|
||||
} else {
|
||||
return; // It was a touch we weren't tracking
|
||||
}
|
||||
|
||||
// If we emitted any transform events, now we need to emit
|
||||
// a transformend event to end the series. The details of this
|
||||
// event use the values from the last touchmove, and the
|
||||
// relative amounts will 1 and 0, but they are included for
|
||||
// completeness even though they are not useful.
|
||||
if (d.scaled || d.rotated) {
|
||||
d.emitEvent('transformend', {
|
||||
absolute: { // transform details since gesture start
|
||||
scale: d.lastDistance / d.startDistance,
|
||||
rotate: touchRotation(d.startDirection, d.lastDirection)
|
||||
},
|
||||
relative: { // nothing has changed relative to the last touchmove
|
||||
scale: 1,
|
||||
rotate: 0
|
||||
},
|
||||
midpoint: d.lastMidpoint
|
||||
});
|
||||
}
|
||||
|
||||
d.switchTo(afterTransformState);
|
||||
}
|
||||
};
|
||||
|
||||
// We did a tranform and one finger went up. Wait for that finger to
|
||||
// come back down or the other finger to go up too.
|
||||
var afterTransformState = {
|
||||
name: 'afterTransformState',
|
||||
touchstart: function(d, e, t) {
|
||||
d.switchTo(transformState, e, t);
|
||||
},
|
||||
|
||||
touchend: function(d, e, t) {
|
||||
if (t.identifier === d.touch1) {
|
||||
d.switchTo(initialState);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return GD;
|
||||
}());
|
||||
Loading…
Reference in a new issue