395 lines
11 KiB
JavaScript
395 lines
11 KiB
JavaScript
var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
|
|
|
|
var SUPPORT_TOUCH = ('ontouchstart' in window);
|
|
var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined;
|
|
var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent);
|
|
|
|
var INPUT_TYPE_TOUCH = 'touch';
|
|
var INPUT_TYPE_PEN = 'pen';
|
|
var INPUT_TYPE_MOUSE = 'mouse';
|
|
var INPUT_TYPE_KINECT = 'kinect';
|
|
|
|
var COMPUTE_INTERVAL = 25;
|
|
|
|
var INPUT_START = 1;
|
|
var INPUT_MOVE = 2;
|
|
var INPUT_END = 4;
|
|
var INPUT_CANCEL = 8;
|
|
|
|
var DIRECTION_NONE = 1;
|
|
var DIRECTION_LEFT = 2;
|
|
var DIRECTION_RIGHT = 4;
|
|
var DIRECTION_UP = 8;
|
|
var DIRECTION_DOWN = 16;
|
|
|
|
var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT;
|
|
var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN;
|
|
var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;
|
|
|
|
var PROPS_XY = ['x', 'y'];
|
|
var PROPS_CLIENT_XY = ['clientX', 'clientY'];
|
|
|
|
/**
|
|
* create new input type manager
|
|
* @param {Manager} manager
|
|
* @param {Function} callback
|
|
* @returns {Input}
|
|
* @constructor
|
|
*/
|
|
function Input(manager, callback) {
|
|
var self = this;
|
|
this.manager = manager;
|
|
this.callback = callback;
|
|
this.element = manager.element;
|
|
this.target = manager.options.inputTarget;
|
|
|
|
// smaller wrapper around the handler, for the scope and the enabled state of the manager,
|
|
// so when disabled the input events are completely bypassed.
|
|
this.domHandler = function(ev) {
|
|
if (boolOrFn(manager.options.enable, [manager])) {
|
|
self.handler(ev);
|
|
}
|
|
};
|
|
|
|
this.init();
|
|
|
|
}
|
|
|
|
Input.prototype = {
|
|
/**
|
|
* should handle the inputEvent data and trigger the callback
|
|
* @virtual
|
|
*/
|
|
handler: function() { },
|
|
|
|
/**
|
|
* bind the events
|
|
*/
|
|
init: function() {
|
|
this.evEl && addEventListeners(this.element, this.evEl, this.domHandler);
|
|
this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler);
|
|
this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
|
|
},
|
|
|
|
/**
|
|
* unbind the events
|
|
*/
|
|
destroy: function() {
|
|
this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler);
|
|
this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler);
|
|
this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* create new input type manager
|
|
* called by the Manager constructor
|
|
* @param {Hammer} manager
|
|
* @returns {Input}
|
|
*/
|
|
function createInputInstance(manager) {
|
|
var Type;
|
|
var inputClass = manager.options.inputClass;
|
|
|
|
if (inputClass) {
|
|
Type = inputClass;
|
|
} else if (SUPPORT_POINTER_EVENTS) {
|
|
Type = PointerEventInput;
|
|
} else if (SUPPORT_ONLY_TOUCH) {
|
|
Type = TouchInput;
|
|
} else if (!SUPPORT_TOUCH) {
|
|
Type = MouseInput;
|
|
} else {
|
|
Type = TouchMouseInput;
|
|
}
|
|
return new (Type)(manager, inputHandler);
|
|
}
|
|
|
|
/**
|
|
* handle input events
|
|
* @param {Manager} manager
|
|
* @param {String} eventType
|
|
* @param {Object} input
|
|
*/
|
|
function inputHandler(manager, eventType, input) {
|
|
var pointersLen = input.pointers.length;
|
|
var changedPointersLen = input.changedPointers.length;
|
|
var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0));
|
|
var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0));
|
|
|
|
input.isFirst = !!isFirst;
|
|
input.isFinal = !!isFinal;
|
|
|
|
if (isFirst) {
|
|
manager.session = {};
|
|
}
|
|
|
|
// source event is the normalized value of the domEvents
|
|
// like 'touchstart, mouseup, pointerdown'
|
|
input.eventType = eventType;
|
|
|
|
// compute scale, rotation etc
|
|
computeInputData(manager, input);
|
|
|
|
// emit secret event
|
|
manager.emit('hammer.input', input);
|
|
|
|
manager.recognize(input);
|
|
manager.session.prevInput = input;
|
|
}
|
|
|
|
/**
|
|
* extend the data with some usable properties like scale, rotate, velocity etc
|
|
* @param {Object} manager
|
|
* @param {Object} input
|
|
*/
|
|
function computeInputData(manager, input) {
|
|
var session = manager.session;
|
|
var pointers = input.pointers;
|
|
var pointersLength = pointers.length;
|
|
|
|
// store the first input to calculate the distance and direction
|
|
if (!session.firstInput) {
|
|
session.firstInput = simpleCloneInputData(input);
|
|
}
|
|
|
|
// to compute scale and rotation we need to store the multiple touches
|
|
if (pointersLength > 1 && !session.firstMultiple) {
|
|
session.firstMultiple = simpleCloneInputData(input);
|
|
} else if (pointersLength === 1) {
|
|
session.firstMultiple = false;
|
|
}
|
|
|
|
var firstInput = session.firstInput;
|
|
var firstMultiple = session.firstMultiple;
|
|
var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center;
|
|
|
|
var center = input.center = getCenter(pointers);
|
|
input.timeStamp = now();
|
|
input.deltaTime = input.timeStamp - firstInput.timeStamp;
|
|
|
|
input.angle = getAngle(offsetCenter, center);
|
|
input.distance = getDistance(offsetCenter, center);
|
|
|
|
computeDeltaXY(session, input);
|
|
input.offsetDirection = getDirection(input.deltaX, input.deltaY);
|
|
|
|
var overallVelocity = getVelocity(input.deltaTime, input.deltaX, input.deltaY);
|
|
input.overallVelocityX = overallVelocity.x;
|
|
input.overallVelocityY = overallVelocity.y;
|
|
input.overallVelocity = (abs(overallVelocity.x) > abs(overallVelocity.y)) ? overallVelocity.x : overallVelocity.y;
|
|
|
|
input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1;
|
|
input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0;
|
|
|
|
input.maxPointers = !session.prevInput ? input.pointers.length : ((input.pointers.length >
|
|
session.prevInput.maxPointers) ? input.pointers.length : session.prevInput.maxPointers);
|
|
|
|
computeIntervalInputData(session, input);
|
|
|
|
// find the correct target
|
|
var target = manager.element;
|
|
if (hasParent(input.srcEvent.target, target)) {
|
|
target = input.srcEvent.target;
|
|
}
|
|
input.target = target;
|
|
}
|
|
|
|
function computeDeltaXY(session, input) {
|
|
var center = input.center;
|
|
var offset = session.offsetDelta || {};
|
|
var prevDelta = session.prevDelta || {};
|
|
var prevInput = session.prevInput || {};
|
|
|
|
if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) {
|
|
prevDelta = session.prevDelta = {
|
|
x: prevInput.deltaX || 0,
|
|
y: prevInput.deltaY || 0
|
|
};
|
|
|
|
offset = session.offsetDelta = {
|
|
x: center.x,
|
|
y: center.y
|
|
};
|
|
}
|
|
|
|
input.deltaX = prevDelta.x + (center.x - offset.x);
|
|
input.deltaY = prevDelta.y + (center.y - offset.y);
|
|
}
|
|
|
|
/**
|
|
* velocity is calculated every x ms
|
|
* @param {Object} session
|
|
* @param {Object} input
|
|
*/
|
|
function computeIntervalInputData(session, input) {
|
|
var last = session.lastInterval || input,
|
|
deltaTime = input.timeStamp - last.timeStamp,
|
|
velocity, velocityX, velocityY, direction;
|
|
|
|
if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) {
|
|
var deltaX = input.deltaX - last.deltaX;
|
|
var deltaY = input.deltaY - last.deltaY;
|
|
|
|
var v = getVelocity(deltaTime, deltaX, deltaY);
|
|
velocityX = v.x;
|
|
velocityY = v.y;
|
|
velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y;
|
|
direction = getDirection(deltaX, deltaY);
|
|
|
|
session.lastInterval = input;
|
|
} else {
|
|
// use latest velocity info if it doesn't overtake a minimum period
|
|
velocity = last.velocity;
|
|
velocityX = last.velocityX;
|
|
velocityY = last.velocityY;
|
|
direction = last.direction;
|
|
}
|
|
|
|
input.velocity = velocity;
|
|
input.velocityX = velocityX;
|
|
input.velocityY = velocityY;
|
|
input.direction = direction;
|
|
}
|
|
|
|
/**
|
|
* create a simple clone from the input used for storage of firstInput and firstMultiple
|
|
* @param {Object} input
|
|
* @returns {Object} clonedInputData
|
|
*/
|
|
function simpleCloneInputData(input) {
|
|
// make a simple copy of the pointers because we will get a reference if we don't
|
|
// we only need clientXY for the calculations
|
|
var pointers = [];
|
|
var i = 0;
|
|
while (i < input.pointers.length) {
|
|
pointers[i] = {
|
|
clientX: round(input.pointers[i].clientX),
|
|
clientY: round(input.pointers[i].clientY)
|
|
};
|
|
i++;
|
|
}
|
|
|
|
return {
|
|
timeStamp: now(),
|
|
pointers: pointers,
|
|
center: getCenter(pointers),
|
|
deltaX: input.deltaX,
|
|
deltaY: input.deltaY
|
|
};
|
|
}
|
|
|
|
/**
|
|
* get the center of all the pointers
|
|
* @param {Array} pointers
|
|
* @return {Object} center contains `x` and `y` properties
|
|
*/
|
|
function getCenter(pointers) {
|
|
var pointersLength = pointers.length;
|
|
|
|
// no need to loop when only one touch
|
|
if (pointersLength === 1) {
|
|
return {
|
|
x: round(pointers[0].clientX),
|
|
y: round(pointers[0].clientY)
|
|
};
|
|
}
|
|
|
|
var x = 0, y = 0, i = 0;
|
|
while (i < pointersLength) {
|
|
x += pointers[i].clientX;
|
|
y += pointers[i].clientY;
|
|
i++;
|
|
}
|
|
|
|
return {
|
|
x: round(x / pointersLength),
|
|
y: round(y / pointersLength)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* calculate the velocity between two points. unit is in px per ms.
|
|
* @param {Number} deltaTime
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @return {Object} velocity `x` and `y`
|
|
*/
|
|
function getVelocity(deltaTime, x, y) {
|
|
return {
|
|
x: x / deltaTime || 0,
|
|
y: y / deltaTime || 0
|
|
};
|
|
}
|
|
|
|
/**
|
|
* get the direction between two points
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @return {Number} direction
|
|
*/
|
|
function getDirection(x, y) {
|
|
if (x === y) {
|
|
return DIRECTION_NONE;
|
|
}
|
|
|
|
if (abs(x) >= abs(y)) {
|
|
return x < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
|
|
}
|
|
return y < 0 ? DIRECTION_UP : DIRECTION_DOWN;
|
|
}
|
|
|
|
/**
|
|
* calculate the absolute distance between two points
|
|
* @param {Object} p1 {x, y}
|
|
* @param {Object} p2 {x, y}
|
|
* @param {Array} [props] containing x and y keys
|
|
* @return {Number} distance
|
|
*/
|
|
function getDistance(p1, p2, props) {
|
|
if (!props) {
|
|
props = PROPS_XY;
|
|
}
|
|
var x = p2[props[0]] - p1[props[0]],
|
|
y = p2[props[1]] - p1[props[1]];
|
|
|
|
return Math.sqrt((x * x) + (y * y));
|
|
}
|
|
|
|
/**
|
|
* calculate the angle between two coordinates
|
|
* @param {Object} p1
|
|
* @param {Object} p2
|
|
* @param {Array} [props] containing x and y keys
|
|
* @return {Number} angle
|
|
*/
|
|
function getAngle(p1, p2, props) {
|
|
if (!props) {
|
|
props = PROPS_XY;
|
|
}
|
|
var x = p2[props[0]] - p1[props[0]],
|
|
y = p2[props[1]] - p1[props[1]];
|
|
return Math.atan2(y, x) * 180 / Math.PI;
|
|
}
|
|
|
|
/**
|
|
* calculate the rotation degrees between two pointersets
|
|
* @param {Array} start array of pointers
|
|
* @param {Array} end array of pointers
|
|
* @return {Number} rotation
|
|
*/
|
|
function getRotation(start, end) {
|
|
return getAngle(end[1], end[0], PROPS_CLIENT_XY) + getAngle(start[1], start[0], PROPS_CLIENT_XY);
|
|
}
|
|
|
|
/**
|
|
* calculate the scale factor between two pointersets
|
|
* no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
|
|
* @param {Array} start array of pointers
|
|
* @param {Array} end array of pointers
|
|
* @return {Number} scale
|
|
*/
|
|
function getScale(start, end) {
|
|
return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY);
|
|
}
|