kopia lustrzana https://github.com/cyoung/stratux
1631 wiersze
56 KiB
JavaScript
Executable File
1631 wiersze
56 KiB
JavaScript
Executable File
/**
|
|
@module mobile-angular-ui.gestures.drag
|
|
@description
|
|
|
|
`mobile-angular-ui.gestures.drag` module exposes the `$drag` service that is used
|
|
to handle drag gestures. `$drag` service wraps [$touch](../module:touch) service adding
|
|
CSS transforms reacting to `touchmove` events.
|
|
|
|
## Usage
|
|
|
|
``` js
|
|
angular.module('myApp', ['mobile-angular-ui.gestures']);
|
|
```
|
|
|
|
Or
|
|
|
|
``` js
|
|
angular.module('myApp', ['mobile-angular-ui.gestures.drag']);
|
|
```
|
|
|
|
``` js
|
|
var dragOptions = {
|
|
transform: $drag.TRANSLATE_BOTH,
|
|
start: function(dragInfo, event){},
|
|
end: function(dragInfo, event){},
|
|
move: function(dragInfo, event){},
|
|
cancel: function(dragInfo, event){}
|
|
};
|
|
|
|
$drag.bind(element, dragOptions, touchOptions);
|
|
```
|
|
|
|
Where:
|
|
|
|
- `transform` is a `function(element, currentTransform, touch) -> newTransform`
|
|
returning taking an `element`, its `currentTransform` and returning the `newTransform`
|
|
for the element in response to `touch`. See [$transform](../module:transform) for more.
|
|
Default to `$drag.TRANSLATE_BOTH`.
|
|
- `start`, `end`, `move`, `cancel` are optional callbacks responding to `drag` movement phases.
|
|
- `dragInfo` is an extended version of `touchInfo` from [$touch](../module:touch),
|
|
extending it with:
|
|
- `originalTransform`: The [$transform](../module:transform) object relative to CSS transform before `$drag` is bound.
|
|
- `originalRect`: The [Bounding Client Rect](https://developer.mozilla.org/en-US/docs/Web/API/Element.getBoundingClientRect) for bound element before any drag action.
|
|
- `startRect`: The [Bounding Client Rect](https://developer.mozilla.org/en-US/docs/Web/API/Element.getBoundingClientRect) for bound element registered at `start` event.
|
|
- `startTransform`: The [$transform](../module:transform) at `start` event.
|
|
- `rect`: The current [Bounding Client Rect](https://developer.mozilla.org/en-US/docs/Web/API/Element.getBoundingClientRect) for bound element.
|
|
- `transform`: The current [$transform](../module:transform).
|
|
- `reset`: A function restoring element to `originalTransform`.
|
|
- `undo`: A function restoring element to `startTransform`.
|
|
- `touchOptions` is an option object to be passed to underlying [`$touch`](../module:touch) service.
|
|
|
|
### Predefined transforms
|
|
|
|
- `$drag.NULL_TRANSFORM`: No transform follow movement
|
|
- `$drag.TRANSLATE_BOTH`: Transform translate following movement on both x and y axis.
|
|
- `$drag.TRANSLATE_HORIZONTAL`: Transform translate following movement on x axis.
|
|
- `$drag.TRANSLATE_UP`: Transform translate following movement on negative y axis.
|
|
- `$drag.TRANSLATE_DOWN`: Transform translate following movement on positive y axis.
|
|
- `$drag.TRANSLATE_LEFT`: Transform translate following movement on negative x axis.
|
|
- `$drag.TRANSLATE_RIGHT`: Transform translate following movement on positive x axis.
|
|
- `$drag.TRANSLATE_VERTICAL`: Transform translate following movement on y axis.
|
|
- `$drag.TRANSLATE_INSIDE`: Is a function and should be used like:
|
|
|
|
``` js
|
|
{
|
|
transform: $drag.TRANSLATE_INSIDE(myElement)
|
|
}
|
|
```
|
|
|
|
It returns a transform function that contains translate movement inside
|
|
the passed element.
|
|
|
|
### `.ui-drag-move` style
|
|
|
|
While moving an `.ui-drag-move` class is attached to element. Style for this class is defined via
|
|
[insertRule](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet.insertRule) and aims to
|
|
fix common problems while dragging, specifically:
|
|
|
|
- Brings the element in front of other elements
|
|
- Disable transitions
|
|
- Makes text unselectable
|
|
|
|
**NOTE** Transitions are disabled cause they may introduce conflicts between `transition: transform`
|
|
and `dragOptions.transform` function.
|
|
|
|
They will be re-enabled after drag, and this can be used to achieve some graceful effects.
|
|
|
|
If you need transition that does not involve transforms during movement you can apply them to an
|
|
inner or wrapping element.
|
|
|
|
### Examples
|
|
|
|
#### Limit movement to an element
|
|
|
|
``` js
|
|
app.directive('dragMe', ['$drag', function($drag){
|
|
return {
|
|
controller: function($scope, $element) {
|
|
$drag.bind($element,
|
|
{
|
|
transform: $drag.TRANSLATE_INSIDE($element.parent()),
|
|
end: function(drag) {
|
|
drag.reset();
|
|
}
|
|
},
|
|
{ // release touch when movement is outside bounduaries
|
|
sensitiveArea: $element.parent()
|
|
}
|
|
);
|
|
}
|
|
};
|
|
}]);
|
|
```
|
|
|
|
<iframe class='embedded-example' src='/examples/drag.html'></iframe>
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
angular.module('mobile-angular-ui.gestures.drag', [
|
|
'mobile-angular-ui.gestures.touch',
|
|
'mobile-angular-ui.gestures.transform'
|
|
])
|
|
|
|
.provider('$drag', function() {
|
|
this.$get = ['$touch', '$transform', function($touch, $transform) {
|
|
|
|
// Add some css rules to be used while moving elements
|
|
var style = document.createElement('style');
|
|
style.appendChild(document.createTextNode(''));
|
|
document.head.appendChild(style);
|
|
var sheet = style.sheet;
|
|
|
|
// Makes z-index 99999
|
|
sheet.insertRule('html .ui-drag-move{z-index: 99999 !important;}', 0);
|
|
// Disable transitions
|
|
sheet.insertRule('html .ui-drag-move{-webkit-transition: none !important;-moz-transition: none !important;-o-transition: none !important;-ms-transition: none !important;transition: none !important;}', 0);
|
|
// Makes text unselectable
|
|
sheet.insertRule('html .ui-drag-move, html .ui-drag-move *{-webkit-touch-callout: none !important;-webkit-user-select: none !important;-khtml-user-select: none !important;-moz-user-select: none !important;-ms-user-select: none !important;user-select: none !important;}', 0);
|
|
|
|
style = sheet = null; // we wont use them anymore so make
|
|
// their memory immediately claimable
|
|
|
|
return {
|
|
|
|
//
|
|
// built-in transforms
|
|
//
|
|
NULL_TRANSFORM: function(element, transform) {
|
|
return transform;
|
|
},
|
|
|
|
TRANSLATE_BOTH: function(element, transform, touch) {
|
|
transform.translateX = touch.distanceX;
|
|
transform.translateY = touch.distanceY;
|
|
return transform;
|
|
},
|
|
|
|
TRANSLATE_HORIZONTAL: function(element, transform, touch) {
|
|
transform.translateX = touch.distanceX;
|
|
transform.translateY = 0;
|
|
return transform;
|
|
},
|
|
|
|
TRANSLATE_UP: function(element, transform, touch) {
|
|
transform.translateY = touch.distanceY <= 0 ? touch.distanceY : 0;
|
|
transform.translateX = 0;
|
|
return transform;
|
|
},
|
|
|
|
TRANSLATE_DOWN: function(element, transform, touch) {
|
|
transform.translateY = touch.distanceY >= 0 ? touch.distanceY : 0;
|
|
transform.translateX = 0;
|
|
return transform;
|
|
},
|
|
|
|
TRANSLATE_LEFT: function(element, transform, touch) {
|
|
transform.translateX = touch.distanceX <= 0 ? touch.distanceX : 0;
|
|
transform.translateY = 0;
|
|
return transform;
|
|
},
|
|
|
|
TRANSLATE_RIGHT: function(element, transform, touch) {
|
|
transform.translateX = touch.distanceX >= 0 ? touch.distanceX : 0;
|
|
transform.translateY = 0;
|
|
return transform;
|
|
},
|
|
|
|
TRANSLATE_VERTICAL: function(element, transform, touch) {
|
|
transform.translateX = 0;
|
|
transform.translateY = touch.distanceY;
|
|
return transform;
|
|
},
|
|
|
|
TRANSLATE_INSIDE: function(wrapperElementOrRectangle) {
|
|
wrapperElementOrRectangle = wrapperElementOrRectangle.length ? wrapperElementOrRectangle[0] : wrapperElementOrRectangle;
|
|
|
|
return function(element, transform, touch) {
|
|
element = element.length ? element[0] : element;
|
|
var re = element.getBoundingClientRect();
|
|
var rw = wrapperElementOrRectangle instanceof Element ? wrapperElementOrRectangle.getBoundingClientRect() : wrapperElementOrRectangle;
|
|
var tx, ty;
|
|
|
|
if (re.width >= rw.width) {
|
|
tx = 0;
|
|
} else {
|
|
// compute translateX so that re.left and re.right will stay between rw.left and rw.right
|
|
if (re.right + touch.stepX > rw.right) {
|
|
tx = rw.right - re.right;
|
|
} else if (re.left + touch.stepX < rw.left) {
|
|
tx = rw.left - re.left;
|
|
} else {
|
|
tx = touch.stepX;
|
|
}
|
|
|
|
}
|
|
|
|
if (re.height >= rw.height) {
|
|
ty = 0;
|
|
} else {
|
|
if (re.bottom + touch.stepY > rw.bottom) {
|
|
ty = rw.bottom - re.bottom;
|
|
} else if (re.top + touch.stepY < rw.top) {
|
|
ty = rw.top - re.top;
|
|
} else {
|
|
ty = touch.stepY;
|
|
}
|
|
}
|
|
|
|
transform.translateX += tx;
|
|
transform.translateY += ty;
|
|
return transform;
|
|
};
|
|
},
|
|
|
|
//
|
|
// bind function
|
|
//
|
|
bind: function($element, dragOptions, touchOptions) {
|
|
$element = angular.element($element);
|
|
dragOptions = dragOptions || {};
|
|
touchOptions = touchOptions || {};
|
|
|
|
var startEventHandler = dragOptions.start,
|
|
endEventHandler = dragOptions.end,
|
|
moveEventHandler = dragOptions.move,
|
|
cancelEventHandler = dragOptions.cancel,
|
|
transformEventHandler = dragOptions.transform || this.TRANSLATE_BOTH;
|
|
|
|
var domElement = $element[0],
|
|
tO = $transform.get($element), // original transform
|
|
rO = domElement.getBoundingClientRect(), // original bounding rect
|
|
tS, // transform at start
|
|
rS;
|
|
|
|
var moving = false;
|
|
|
|
var isMoving = function() {
|
|
return moving;
|
|
};
|
|
|
|
var cleanup = function() {
|
|
moving = false;
|
|
tS = rS = null;
|
|
$element.removeClass('ui-drag-move');
|
|
};
|
|
|
|
var reset = function() {
|
|
$transform.set(domElement, tO);
|
|
};
|
|
|
|
var undo = function() {
|
|
$transform.set(domElement, tS || tO);
|
|
};
|
|
|
|
var setup = function() {
|
|
moving = true;
|
|
rS = domElement.getBoundingClientRect();
|
|
tS = $transform.get(domElement);
|
|
$element.addClass('ui-drag-move');
|
|
};
|
|
|
|
var createDragInfo = function(touch) {
|
|
touch = angular.extend({}, touch);
|
|
touch.originalTransform = tO;
|
|
touch.originalRect = rO;
|
|
touch.startRect = rS;
|
|
touch.rect = domElement.getBoundingClientRect();
|
|
touch.startTransform = tS;
|
|
touch.transform = $transform.get(domElement);
|
|
touch.reset = reset;
|
|
touch.undo = undo;
|
|
return touch;
|
|
};
|
|
|
|
var onTouchMove = function(touch, event) {
|
|
// preventDefault no matter what
|
|
// it is (ie. maybe html5 drag for images or scroll)
|
|
event.preventDefault();
|
|
|
|
// $touch calls start on the first touch
|
|
// to ensure $drag.start is called only while actually
|
|
// dragging and not for touches we will bind $drag.start
|
|
// to the first time move is called
|
|
|
|
if (!isMoving()) { // drag start
|
|
setup();
|
|
if (startEventHandler) {
|
|
startEventHandler(createDragInfo(touch), event);
|
|
}
|
|
} else { // drag move
|
|
touch = createDragInfo(touch);
|
|
|
|
var transform = transformEventHandler($element, angular.extend({}, touch.transform), touch, event);
|
|
|
|
$transform.set(domElement, transform);
|
|
|
|
if (moveEventHandler) {
|
|
moveEventHandler(touch, event);
|
|
}
|
|
}
|
|
};
|
|
|
|
var onTouchEnd = function(touch, event) {
|
|
if (!isMoving()) { return; }
|
|
|
|
// prevents outer swipes
|
|
event.__UiSwipeHandled__ = true;
|
|
|
|
touch = createDragInfo(touch);
|
|
cleanup();
|
|
|
|
if (endEventHandler) {
|
|
endEventHandler(touch, event);
|
|
}
|
|
};
|
|
|
|
var onTouchCancel = function(touch, event) {
|
|
if (!isMoving()) { return; }
|
|
|
|
touch = createDragInfo(touch);
|
|
undo(); // on cancel movement is undoed automatically;
|
|
cleanup();
|
|
|
|
if (cancelEventHandler) {
|
|
cancelEventHandler(touch, event);
|
|
}
|
|
};
|
|
|
|
return $touch.bind($element,
|
|
{move: onTouchMove, end: onTouchEnd, cancel: onTouchCancel},
|
|
touchOptions);
|
|
} // ~ bind
|
|
}; // ~ return $drag
|
|
}]; // ~ $get
|
|
});
|
|
|
|
}());
|
|
/**
|
|
* A module providing swipe gesture services and directives.
|
|
*
|
|
* @module mobile-angular-ui.gestures.swipe
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
|
|
var module = angular.module('mobile-angular-ui.gestures.swipe',
|
|
['mobile-angular-ui.gestures.touch']);
|
|
|
|
/**
|
|
* An adaptation of `ngTouch.$swipe`, it is basically the same despite of:
|
|
*
|
|
* - It is based on [$touch](../module:touch)
|
|
* - Swipes are recognized by touch velocity and direction
|
|
* - It does not require `ngTouch` thus is better compatible with fastclick.js
|
|
* - Swipe directives are nestable
|
|
* - It allows to unbind
|
|
* - It has only one difference in interface, and its about how to pass `pointerTypes`:
|
|
*
|
|
* ``` js
|
|
* // ngTouch.$swipe
|
|
* $swipe.bind(..., ['mouse', ... });
|
|
*
|
|
* // mobile-angular-ui.gestures.swipe.$swipe
|
|
* $swipe.bind(..., pointerTypes: { mouse: { start: 'mousedown', ...} });
|
|
* ```
|
|
* This is due to the fact that the second parameter of `$swipe.bind` is destinated to options for
|
|
* underlying `$touch` service.
|
|
*
|
|
* @service $swipe
|
|
* @as class
|
|
*/
|
|
module.factory('$swipe', ['$touch', function($touch) {
|
|
var VELOCITY_THRESHOLD = 500; // px/sec
|
|
var MOVEMENT_THRESHOLD = 10; // px
|
|
var TURNAROUND_MAX = 10; // px
|
|
var ANGLE_THRESHOLD = 10; // deg
|
|
var abs = Math.abs;
|
|
|
|
var defaultOptions = {
|
|
movementThreshold: MOVEMENT_THRESHOLD, // start to consider only if movement
|
|
// exceeded MOVEMENT_THRESHOLD
|
|
valid: function(t) {
|
|
var absAngle = abs(t.angle);
|
|
absAngle = absAngle >= 90 ? absAngle - 90 : absAngle;
|
|
|
|
var validDistance = t.total - t.distance <= TURNAROUND_MAX,
|
|
validAngle = absAngle <= ANGLE_THRESHOLD || absAngle >= 90 - ANGLE_THRESHOLD,
|
|
validVelocity = t.averageVelocity >= VELOCITY_THRESHOLD;
|
|
|
|
return validDistance && validAngle && validVelocity;
|
|
}
|
|
};
|
|
|
|
return {
|
|
/**
|
|
* Bind swipe gesture handlers for an element.
|
|
*
|
|
* ``` js
|
|
* var unbind = $swipe.bind(elem, {
|
|
* end: function(touch) {
|
|
* console.log('Swiped:', touch.direction);
|
|
* unbind();
|
|
* }
|
|
* });
|
|
* ```
|
|
*
|
|
* **Swipes Detection**
|
|
*
|
|
* Before consider a touch to be a swipe Mobile Angular UI verifies that:
|
|
*
|
|
* 1. Movement is quick. Average touch velocity should exceed a `VELOCITY_THRESHOLD`.
|
|
* 2. Movement is linear.
|
|
* 3. Movement has a clear, non-ambiguous direction. So we can assume without error
|
|
* that underlying `touch.direction` is exactly the swipe direction. For that
|
|
* movement is checked against an `ANGLE_THRESHOLD`.
|
|
*
|
|
* @param {Element|$element} element The element to observe for swipe gestures.
|
|
* @param {object} eventHandlers An object with handlers for specific swipe events.
|
|
* @param {function} [eventHandlers.start] The callback for swipe start event.
|
|
* @param {function} [eventHandlers.end] The callback for swipe end event.
|
|
* @param {function} [eventHandlers.move] The callback for swipe move event.
|
|
* @param {function} [eventHandlers.cancel] The callback for swipe cancel event.
|
|
* @param {object} [options] Options to be passed to underlying [$touch.bind](../module:touch) function.
|
|
*
|
|
* @returns {function} The unbind function.
|
|
*
|
|
* @method bind
|
|
* @memberOf mobile-angular-ui.gestures.swipe~$swipe
|
|
*/
|
|
bind: function(element, eventHandlers, options) {
|
|
options = angular.extend({}, defaultOptions, options || {});
|
|
return $touch.bind(element, eventHandlers, options);
|
|
}
|
|
};
|
|
}]);
|
|
|
|
/**
|
|
* Specify custom behavior when an element is swiped to the left on a touchscreen device.
|
|
* A leftward swipe is a quick, right-to-left slide of the finger.
|
|
*
|
|
* @directive uiSwipeLeft
|
|
* @param {expression} uiSwipeLeft An expression to be evaluated on leftward swipe.
|
|
*/
|
|
/**
|
|
* Specify custom behavior when an element is swiped to the right on a touchscreen device.
|
|
* A rightward swipe is a quick, left-to-right slide of the finger.
|
|
*
|
|
* @directive uiSwipeRight
|
|
* @param {expression} uiSwipeRight An expression to be evaluated on rightward swipe.
|
|
*/
|
|
/**
|
|
* Alias for [uiSwipeLeft](#uiswipeleft).
|
|
*
|
|
* @directive ngSwipeLeft
|
|
* @deprecated
|
|
*/
|
|
/**
|
|
* Alias for [uiSwipeRight](#uiswiperight).
|
|
*
|
|
* @directive ngSwipeRight
|
|
* @deprecated
|
|
*/
|
|
angular.forEach(['ui', 'ng'], function(prefix) {
|
|
angular.forEach(['Left', 'Right'], function(direction) {
|
|
var directiveName = prefix + 'Swipe' + direction;
|
|
module.directive(directiveName, ['$swipe', '$parse', function($swipe, $parse){
|
|
return {
|
|
link: function(scope, elem, attrs) {
|
|
var onSwipe = $parse(attrs[directiveName]);
|
|
$swipe.bind(elem, {
|
|
end: function(swipe, event) {
|
|
if (swipe.direction === direction.toUpperCase()) {
|
|
if (!event.__UiSwipeHandled__) {
|
|
event.__UiSwipeHandled__ = true;
|
|
scope.$apply(function() {
|
|
onSwipe(scope, {$touch: swipe});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}]);
|
|
});
|
|
});
|
|
}());
|
|
/**
|
|
* Device agnostic touch handling.
|
|
*
|
|
* **Usage**
|
|
*
|
|
* Require this module doing either
|
|
*
|
|
* ``` js
|
|
* angular.module('myApp', ['mobile-angular-ui.gestures']);
|
|
* ```
|
|
*
|
|
* Or standalone
|
|
*
|
|
* ``` js
|
|
* angular.module('myApp', ['mobile-angular-ui.gestures.touch']);
|
|
* ```
|
|
*
|
|
* Then you will be able to use the `$touch` service like that:
|
|
*
|
|
* ``` js
|
|
* var unbindFn = $touch.bind(element, {
|
|
* start: function(touchInfo, e);
|
|
* move: function(touchInfo, e);
|
|
* end: function(touchInfo, e);
|
|
* cancel: function(touchInfo, e);
|
|
* }, options);
|
|
* ```
|
|
*
|
|
* @module mobile-angular-ui.gestures.touch
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
var module = angular.module('mobile-angular-ui.gestures.touch', []);
|
|
|
|
/**
|
|
* `$touch` is an abstraction of touch event handling that works with
|
|
* any kind of input devices.
|
|
*
|
|
* It is intended for single touch only and provides
|
|
* extended infos about touch like: movement, direction, velocity, duration, and more.
|
|
* $touch service is intended as base to build any single-touch gesture handlers.
|
|
*
|
|
* **Usage**
|
|
*
|
|
* ``` js
|
|
* var unbindFn = $touch.bind(element, {
|
|
* start: function(touchInfo, e);
|
|
* move: function(touchInfo, e);
|
|
* end: function(touchInfo, e);
|
|
* cancel: function(touchInfo, e);
|
|
* }, options);
|
|
* ```
|
|
*
|
|
* @service $touch
|
|
* @as class
|
|
*/
|
|
|
|
|
|
/**
|
|
* Configurable provider for `$touch` service
|
|
* @class $touchProvider
|
|
* @ngdoc provider
|
|
* @memberOf mobile-angular-ui.gestures.touch~$touch
|
|
*/
|
|
module.provider('$touch', function() {
|
|
|
|
/*=====================================
|
|
= Configuration =
|
|
=====================================*/
|
|
|
|
var VALID = function() {
|
|
return true;
|
|
};
|
|
|
|
var MOVEMENT_THRESHOLD = 1;
|
|
|
|
var POINTER_EVENTS = {
|
|
'mouse': {
|
|
start: 'mousedown',
|
|
move: 'mousemove',
|
|
end: 'mouseup'
|
|
},
|
|
'touch': {
|
|
start: 'touchstart',
|
|
move: 'touchmove',
|
|
end: 'touchend',
|
|
cancel: 'touchcancel'
|
|
}
|
|
};
|
|
|
|
var POINTER_TYPES = ['mouse', 'touch'];
|
|
|
|
// function or element or rect
|
|
var SENSITIVE_AREA = function($element) {
|
|
return $element[0].ownerDocument.documentElement.getBoundingClientRect();
|
|
};
|
|
|
|
/**
|
|
* Set default pointer events option.
|
|
* Pointer Events option specifies a device-by-device map between device specific events and
|
|
* touch events.
|
|
*
|
|
* The default Pointer Events Map is defined as:
|
|
*
|
|
* ``` js
|
|
* var POINTER_EVENTS = {
|
|
* 'mouse': {
|
|
* start: 'mousedown',
|
|
* move: 'mousemove',
|
|
* end: 'mouseup'
|
|
* },
|
|
* 'touch': {
|
|
* start: 'touchstart',
|
|
* move: 'touchmove',
|
|
* end: 'touchend',
|
|
* cancel: 'touchcancel'
|
|
* }
|
|
* };
|
|
* ```
|
|
*
|
|
* Ie.
|
|
*
|
|
* ```
|
|
* app.config(function($touchProvider){
|
|
* $touchProvider.setPointerEvents({ pen: {start: "pendown", end: "penup", move: "penmove" }});
|
|
* });
|
|
* ```
|
|
*
|
|
* @name setPointerEvents
|
|
* @param {object} pointerEvents The pointer events map object
|
|
* @memberOf mobile-angular-ui.gestures.touch~$touch.$touchProvider
|
|
*/
|
|
this.setPointerEvents = function(pointerEvents) {
|
|
POINTER_EVENTS = pointerEvents;
|
|
POINTER_TYPES = Object.keys(POINTER_EVENTS);
|
|
};
|
|
|
|
/**
|
|
* Set default validity function for a touch.
|
|
*
|
|
* The default is defined as always true:
|
|
*
|
|
* ``` js
|
|
* $touchProvider.setValid(function(touch, event) {
|
|
* return true;
|
|
* });
|
|
* ```
|
|
*
|
|
* @param {function} validityFunction The validity function. A function that takes two
|
|
* arguments: `touchInfo` and `event`, and returns
|
|
* a `Boolean` indicating wether the corresponding touch
|
|
* should be considered valid and its handlers triggered,
|
|
* or considered invalid and its handlers be ignored.
|
|
* @method setValid
|
|
* @memberOf mobile-angular-ui.gestures.touch~$touch.$touchProvider
|
|
*/
|
|
this.setValid = function(fn) {
|
|
VALID = fn;
|
|
};
|
|
|
|
/**
|
|
* Set default amount of pixels of movement before
|
|
* start to trigger `touchmove` handlers.
|
|
*
|
|
* Default is `1`.
|
|
*
|
|
* ie.
|
|
*
|
|
* ``` js
|
|
* $touchProvider.setMovementThreshold(120);
|
|
* ```
|
|
*
|
|
* @param {integer} threshold The new treeshold.
|
|
*
|
|
* @method setMovementThreshold
|
|
* @memberOf mobile-angular-ui.gestures.touch~$touch.$touchProvider
|
|
*/
|
|
this.setMovementThreshold = function(v) {
|
|
MOVEMENT_THRESHOLD = v;
|
|
};
|
|
/**
|
|
* Set default sensitive area.
|
|
*
|
|
* The sensitive area of a touch is the area of the screen inside what
|
|
* we consider a touch to be meaningful thus triggering its handlers.
|
|
*
|
|
* **NOTE:** if movement goes out the sensitive area the touch event is not cancelled,
|
|
* instead its handler are just ignored.
|
|
*
|
|
* By default sensitive area is defined as `ownerDocument` bounding rectangle
|
|
* of the bound element.
|
|
*
|
|
* ie.
|
|
*
|
|
* ``` js
|
|
* $touchProvider.setSensitiveArea(function($element) {
|
|
* return $element[0].ownerDocument.documentElement.getBoundingClientRect();
|
|
* });
|
|
* ```
|
|
*
|
|
* @param {function|Element|TextRectangle} sensitiveArea The new default sensitive area,
|
|
* either static or as function
|
|
* taking an element and returning another
|
|
* element or a [rectangle](https://developer.mozilla.org/en-US/docs/Web/API/Element.getBoundingClientRect).
|
|
*
|
|
* @method setSensitiveArea
|
|
* @memberOf mobile-angular-ui.gestures.touch~$touch.$touchProvider
|
|
*/
|
|
this.setSensitiveArea = function(fnOrElementOrRect) {
|
|
SENSITIVE_AREA = fnOrElementOrRect;
|
|
};
|
|
|
|
//
|
|
// Shorthands for minification
|
|
//
|
|
var abs = Math.abs,
|
|
atan2 = Math.atan2,
|
|
sqrt = Math.sqrt;
|
|
|
|
/*===============================
|
|
= Helpers =
|
|
===============================*/
|
|
|
|
var getCoordinates = function(event) {
|
|
var touches = event.touches && event.touches.length ? event.touches : [event];
|
|
var e = (event.changedTouches && event.changedTouches[0]) ||
|
|
(event.originalEvent && event.originalEvent.changedTouches &&
|
|
event.originalEvent.changedTouches[0]) ||
|
|
touches[0].originalEvent || touches[0];
|
|
|
|
return {
|
|
x: e.clientX,
|
|
y: e.clientY
|
|
};
|
|
};
|
|
|
|
var getEvents = function(pointerTypes, eventType) {
|
|
var res = [];
|
|
angular.forEach(pointerTypes, function(pointerType) {
|
|
var eventName = POINTER_EVENTS[pointerType][eventType];
|
|
if (eventName) {
|
|
res.push(eventName);
|
|
}
|
|
});
|
|
return res.join(' ');
|
|
};
|
|
|
|
var now = function() {
|
|
return new Date();
|
|
};
|
|
|
|
var timediff = function(t1, t2) {
|
|
t2 = t2 || now();
|
|
return abs(t2 - t1);
|
|
};
|
|
|
|
var len = function(x, y) {
|
|
return sqrt(x*x + y*y);
|
|
};
|
|
|
|
/**
|
|
* `TouchInfo` is an object containing the following extended informations about any touch
|
|
* event.
|
|
*
|
|
* @property {string} type Normalized event type. Despite of pointer device is always one of `touchstart`, `touchend`, `touchmove`, `touchcancel`.
|
|
* @property {Date} timestamp The time object corresponding to the moment this touch event happened.
|
|
* @property {integer} duration The difference between this touch event and the corresponding `touchstart`.
|
|
* @property {float} startX X coord of related `touchstart`.
|
|
* @property {float} startY Y coord of related `touchstart`.
|
|
* @property {float} prevX X coord of previous `touchstart` or `touchmove`.
|
|
* @property {float} prevY Y coord of previous `touchstart` or `touchmove`.
|
|
* @property {float} x X coord of this touch event.
|
|
* @property {float} y Y coord of this touch event.
|
|
* @property {float} step Distance between `[prevX, prevY]` and `[x, y]` points.
|
|
* @property {float} stepX Distance between `prevX` and `x`.
|
|
* @property {float} stepY Distance between `prevY` and `y`.
|
|
* @property {float} velocity Instantaneous velocity of a touch event in pixels per second.
|
|
* @property {float} averageVelocity Average velocity of a touch event from its corresponding `touchstart` in pixels per second.
|
|
* @property {float} distance Distance between `[startX, startY]` and `[x, y]` points.
|
|
* @property {float} distanceX Distance between `startX` and `x`.
|
|
* @property {float} distanceY Distance between `startY` and `y`.
|
|
* @property {float} total Total number of pixels covered by movement, taking account of direction changes and turnarounds.
|
|
* @property {float} totalX Total number of pixels covered by horizontal movement, taking account of direction changes and turnarounds.
|
|
* @property {float} totalY Total number of pixels covered by vertical, taking account of direction changes and turnarounds.
|
|
* @property {string} direction The current prevalent direction for this touch, one of `LEFT`, `RIGHT`, `TOP`, `BOTTOM`.
|
|
* @property {float} angle Angle in degree between x axis and the vector `[x, y]`, is `null` when no movement happens.
|
|
*
|
|
* @class TouchInfo
|
|
* @ngdoc type
|
|
* @memberOf mobile-angular-ui.gestures.touch~$touch
|
|
*/
|
|
|
|
var buildTouchInfo = function(type, c, t0, tl) {
|
|
// Compute values for new TouchInfo based on coordinates and previus touches.
|
|
// - c is coords of new touch
|
|
// - t0 is first touch: useful to compute duration and distance (how far pointer
|
|
// got from first touch)
|
|
// - tl is last touch: useful to compute velocity and length (total length of the movement)
|
|
|
|
t0 = t0 || {};
|
|
tl = tl || {};
|
|
|
|
var // timestamps
|
|
ts = now(), ts0 = t0.timestamp || ts, tsl = tl.timestamp || ts0,
|
|
// coords
|
|
x = c.x, y = c.y, x0 = t0.x || x, y0 = t0.y || y, xl = tl.x || x0, yl = tl.y || y0,
|
|
// total movement
|
|
totalXl = tl.totalX || 0, totalYl = tl.totalY || 0,
|
|
totalX = totalXl + abs(x - xl), totalY = totalYl + abs(y - yl),
|
|
total = len(totalX, totalY),
|
|
// duration
|
|
duration = timediff(ts, ts0),
|
|
durationl = timediff(ts, tsl),
|
|
// distance
|
|
dxl = x - xl, dyl = y - yl, dl = len(dxl, dyl),
|
|
dx = x - x0, dy = y - y0, d = len(dx, dy),
|
|
// velocity (px per second)
|
|
v = durationl > 0 ? abs(dl / ( durationl / 1000 )) : 0,
|
|
tv = duration > 0 ? abs(total / (duration / 1000)) : 0,
|
|
// main direction: 'LEFT', 'RIGHT', 'TOP', 'BOTTOM'
|
|
dir = abs(dx) > abs(dy) ?
|
|
(dx < 0 ? 'LEFT' : 'RIGHT'):
|
|
(dy < 0 ? 'TOP' : 'BOTTOM'),
|
|
// angle (angle between distance vector and x axis)
|
|
// angle will be:
|
|
// 0 for x > 0 and y = 0
|
|
// 90 for y < 0 and x = 0
|
|
// 180 for x < 0 and y = 0
|
|
// -90 for y > 0 and x = 0
|
|
//
|
|
// -90°
|
|
// |
|
|
// |
|
|
// |
|
|
// 180° --------|-------- 0°
|
|
// |
|
|
// |
|
|
// |
|
|
// 90°
|
|
//
|
|
angle = dx !== 0 || dy !== 0 ? atan2(dy, dx) * (180 / Math.PI) : null;
|
|
angle = angle === -180 ? 180 : angle;
|
|
|
|
return {
|
|
type: type,
|
|
timestamp: ts,
|
|
duration: duration,
|
|
startX: x0,
|
|
startY: y0,
|
|
prevX: xl,
|
|
prevY: yl,
|
|
x: c.x,
|
|
y: c.y,
|
|
|
|
step: dl, // distance from prev
|
|
stepX: dxl,
|
|
stepY: dyl,
|
|
|
|
velocity: v,
|
|
averageVelocity: tv,
|
|
|
|
distance: d, // distance from start
|
|
distanceX: dx,
|
|
distanceY: dy,
|
|
|
|
total: total, // total length of momement,
|
|
// considering turnaround
|
|
totalX: totalX,
|
|
totalY: totalY,
|
|
direction: dir,
|
|
angle: angle
|
|
};
|
|
};
|
|
|
|
/*======================================
|
|
= Factory Method =
|
|
======================================*/
|
|
|
|
this.$get = [function() {
|
|
|
|
return {
|
|
/**
|
|
*
|
|
* Bind touch handlers for an element.
|
|
*
|
|
* ``` js
|
|
* var unbind = $touch.bind(elem, {
|
|
* end: function(touch) {
|
|
* console.log('Avg Speed:', touch.averageVelocity);
|
|
* unbind();
|
|
* }
|
|
* });
|
|
* ```
|
|
*
|
|
* @param {Element|$element} element The element to bound to.
|
|
* @param {object} eventHandlers An object with handlers for specific touch events.
|
|
* @param {function} [eventHandlers.start] The callback for `touchstart` event.
|
|
* @param {function} [eventHandlers.end] The callback for `touchend` event.
|
|
* @param {function} [eventHandlers.move] The callback for `touchmove` event.
|
|
* @param {function} [eventHandlers.cancel] The callback for `touchcancel` event.
|
|
* @param {object} [options] Options.
|
|
* @param {integer} [options.movementThreshold] Amount of pixels of movement before start to trigger `touchmove` handlers.
|
|
* @param {function} [options.valid] Validity function. A `function(TouchInfo, event)⟶boolean` deciding if a touch should be handled or ignored.
|
|
* @param {function|Element|TextRectangle} [options.sensitiveArea] A [Bounding Client Rect](https://developer.mozilla.org/en-US/docs/Web/API/Element.getBoundingClientRect) or an element
|
|
* or a function that takes the bound element and returns one of the previous.
|
|
* Sensitive area define bounduaries to release touch when movement is outside.
|
|
* @param {array} [options.pointerTypes] Pointer types to handle. An array of pointer types that is intended to be
|
|
* a subset of keys from default pointer events map (see `$touchProvider.setPointerEvents`).
|
|
*
|
|
* @returns {function} The unbind function.
|
|
*
|
|
* @memberOf mobile-angular-ui.gestures.touch~$touch
|
|
*/
|
|
bind: function($element, eventHandlers, options) {
|
|
|
|
// ensure element to be an angular element
|
|
$element = angular.element($element);
|
|
|
|
options = options || {};
|
|
// uses default pointer types in case of none passed
|
|
var pointerTypes = options.pointerTypes || POINTER_TYPES,
|
|
isValid = options.valid === undefined ? VALID : options.valid,
|
|
movementThreshold = options.movementThreshold === undefined ? MOVEMENT_THRESHOLD : options.valid,
|
|
sensitiveArea = options.sensitiveArea === undefined ? SENSITIVE_AREA : options.sensitiveArea;
|
|
|
|
var // first and last touch
|
|
t0, tl,
|
|
// events
|
|
startEvents = getEvents(pointerTypes, 'start'),
|
|
endEvents = getEvents(pointerTypes, 'end'),
|
|
moveEvents = getEvents(pointerTypes, 'move'),
|
|
cancelEvents = getEvents(pointerTypes, 'cancel');
|
|
|
|
var startEventHandler = eventHandlers.start,
|
|
endEventHandler = eventHandlers.end,
|
|
moveEventHandler = eventHandlers.move,
|
|
cancelEventHandler = eventHandlers.cancel;
|
|
|
|
var $movementTarget = angular.element($element[0].ownerDocument);
|
|
|
|
var resetTouch = function() {
|
|
t0 = tl = null;
|
|
$movementTarget.off(moveEvents, onTouchMove);
|
|
$movementTarget.off(endEvents, onTouchEnd);
|
|
if (cancelEvents) { $movementTarget.off(cancelEvents, onTouchCancel); }
|
|
};
|
|
|
|
var isActive = function() {
|
|
return !!t0;
|
|
};
|
|
|
|
//
|
|
// Callbacks
|
|
//
|
|
|
|
// on touchstart
|
|
var onTouchStart = function(event) {
|
|
// don't handle multi-touch
|
|
if (event.touches && event.touches.length > 1) { return; }
|
|
tl = t0 = buildTouchInfo('touchstart', getCoordinates(event));
|
|
$movementTarget.on(moveEvents, onTouchMove);
|
|
$movementTarget.on(endEvents, onTouchEnd);
|
|
if (cancelEvents) { $movementTarget.on(cancelEvents, onTouchCancel); }
|
|
if (startEventHandler) {
|
|
startEventHandler(t0, event);
|
|
}
|
|
};
|
|
|
|
// on touchCancel
|
|
var onTouchCancel = function(event) {
|
|
var t = buildTouchInfo('touchcancel', getCoordinates(event), t0, tl);
|
|
resetTouch();
|
|
if (cancelEventHandler) {
|
|
cancelEventHandler(t, event);
|
|
}
|
|
};
|
|
|
|
// on touchMove
|
|
var onTouchMove = function(event) {
|
|
// don't handle multi-touch
|
|
if (event.touches && event.touches.length > 1) { return; }
|
|
|
|
if (!isActive()) { return; }
|
|
|
|
var coords = getCoordinates(event);
|
|
|
|
//
|
|
// wont fire outside sensitive area
|
|
//
|
|
var mva = typeof sensitiveArea === 'function' ? sensitiveArea($element) : sensitiveArea;
|
|
mva = mva.length ? mva[0] : mva;
|
|
|
|
var mvaRect = mva instanceof Element ? mva.getBoundingClientRect() : mva;
|
|
|
|
if (coords.x < mvaRect.left || coords.x > mvaRect.right || coords.y < mvaRect.top || coords.y > mvaRect.bottom){ return; }
|
|
|
|
var t = buildTouchInfo('touchmove', coords, t0, tl),
|
|
totalX = t.totalX,
|
|
totalY = t.totalY;
|
|
|
|
tl = t;
|
|
|
|
if (totalX < movementThreshold && totalY < movementThreshold) {
|
|
return;
|
|
}
|
|
|
|
if (isValid(t, event)) {
|
|
if (event.cancelable === undefined || event.cancelable) {
|
|
event.preventDefault();
|
|
}
|
|
if (moveEventHandler) {
|
|
moveEventHandler(t, event);
|
|
}
|
|
}
|
|
};
|
|
|
|
// on touchEnd
|
|
var onTouchEnd = function(event) {
|
|
// don't handle multi-touch
|
|
if (event.touches && event.touches.length > 1) { return; }
|
|
|
|
if (!isActive()) { return; }
|
|
var t = angular.extend({}, tl, {type: 'touchend'});
|
|
if (isValid(t, event)) {
|
|
if (event.cancelable === undefined || event.cancelable) {
|
|
event.preventDefault();
|
|
}
|
|
if (endEventHandler) {
|
|
setTimeout(function() { // weird workaround to avoid
|
|
// delays with dom manipulations
|
|
// inside the handler
|
|
endEventHandler(t, event);
|
|
}, 0);
|
|
}
|
|
}
|
|
resetTouch();
|
|
};
|
|
|
|
$element.on(startEvents, onTouchStart);
|
|
|
|
return function unbind() {
|
|
if ($element) { // <- wont throw if accidentally called twice
|
|
$element.off(startEvents, onTouchStart);
|
|
if (cancelEvents) { $movementTarget.off(cancelEvents, onTouchCancel); }
|
|
$movementTarget.off(moveEvents, onTouchMove);
|
|
$movementTarget.off(endEvents, onTouchEnd);
|
|
|
|
// Clear all those variables we carried out from `#bind` method scope
|
|
// to local scope and that we don't have to use anymore
|
|
$element = $movementTarget = startEvents = cancelEvents = moveEvents = endEvents = onTouchStart = onTouchCancel = onTouchMove = onTouchEnd = pointerTypes = isValid = movementThreshold = sensitiveArea = null;
|
|
}
|
|
};
|
|
}
|
|
};
|
|
}];
|
|
});
|
|
}());
|
|
/**
|
|
@module mobile-angular-ui.gestures.transform
|
|
@description
|
|
|
|
`mobile-angular-ui.gestures.transform` provides the `$transform` service is designed
|
|
with the specific aim to provide a cross-browser way to interpolate CSS 3d transform
|
|
without having to deal with CSS Matrix, and being able to take into account any previous
|
|
unknown transform already applied to an element.
|
|
|
|
## Usage
|
|
|
|
Require this module doing either
|
|
|
|
``` js
|
|
angular.module('myApp', ['mobile-angular-ui.gestures']);
|
|
```
|
|
|
|
Or standalone
|
|
|
|
``` js
|
|
angular.module('myApp', ['mobile-angular-ui.gestures.transform']);
|
|
```
|
|
|
|
Say we have an element with applyed css:
|
|
|
|
``` html
|
|
<div class='myelem'></div>
|
|
```
|
|
|
|
``` css
|
|
.myelem {
|
|
transform: translate(12px) rotate(20deg);
|
|
}
|
|
```
|
|
|
|
Then you can use `$transform` like this:
|
|
|
|
``` js
|
|
t = $transform.get(e);
|
|
t.rotationZ += 15;
|
|
t.translateX += 1;
|
|
$transform.set(e, t);
|
|
```
|
|
|
|
### `$transform` service API
|
|
|
|
#### `$transform.fromCssMatrix(cssMatrixString) -> transform`
|
|
|
|
Returns a decomposition of the transform matrix `cssMatrixString`.
|
|
NOTE: 2d matrices are translated to 3d matrices before any other operation.
|
|
|
|
#### `$transform.toCss(decomposedTransform)`
|
|
|
|
Recompose a css string from `decomposedTransform`.
|
|
|
|
Transforms are recomposed as a composition of:
|
|
|
|
``` css
|
|
matrix3d(1,0,0,0, 0,1,0,0, 0,0,1,0, perspective[0], perspective[1], perspective[2], perspective[3])
|
|
translate3d(translation[0], translation[1], translation[2])
|
|
rotateX(rotation[0]) rotateY(rotation[1]) rotateZ(rotation[2])
|
|
matrix3d(1,0,0,0, 0,1,0,0, 0,skew[2],1,0, 0,0,0,1)
|
|
matrix3d(1,0,0,0, 0,1,0,0, skew[1],0,1,0, 0,0,0,1)
|
|
matrix3d(1,0,0,0, skew[0],1,0,0, 0,0,1,0, 0,0,0,1)
|
|
scale3d(scale[0], scale[1], scale[2])
|
|
```
|
|
|
|
#### `$transform.get(e) -> transform`
|
|
|
|
Returns a decomposition of the transform matrix applied to `e`.
|
|
|
|
#### `$transform.set(element, transform)`
|
|
|
|
If transform is a string just set it for element `element`. Otherwise is considered as a
|
|
decomposed transform and is recomposed with `$transform.toCss` and then set to element.
|
|
|
|
### The decomposed transform object
|
|
|
|
Result of transform matrix decomposition is an object with the following properties:
|
|
|
|
```
|
|
translateX
|
|
translateY
|
|
translateZ
|
|
perspectiveX
|
|
perspectiveY
|
|
perspectiveZ
|
|
perspectiveW
|
|
scaleX
|
|
scaleY
|
|
scaleZ
|
|
rotateX
|
|
rotateY
|
|
rotateZ
|
|
skewXY
|
|
skewXZ
|
|
skewYZ
|
|
```
|
|
|
|
*/
|
|
(function() {
|
|
'use strict';
|
|
|
|
var module = angular.module('mobile-angular-ui.gestures.transform', []);
|
|
|
|
module.factory('$transform', function(){
|
|
|
|
/*==============================================================
|
|
= Cross-Browser Property Prefix Handling =
|
|
==============================================================*/
|
|
|
|
// Cross-Browser style properties
|
|
var cssPrefix,
|
|
transformProperty,
|
|
styleProperty,
|
|
prefixes = ['', 'webkit', 'Moz', 'O', 'ms'],
|
|
d = document.createElement('div');
|
|
|
|
for (var i = 0; i < prefixes.length; i++) {
|
|
var prefix = prefixes[i];
|
|
if ( (prefix + 'Perspective') in d.style ) {
|
|
cssPrefix = (prefix === '' ? '' : '-' + prefix.toLowerCase() + '-');
|
|
styleProperty = prefix + (prefix === '' ? 'transform' : 'Transform');
|
|
transformProperty = cssPrefix + 'transform';
|
|
break;
|
|
}
|
|
}
|
|
|
|
d = null;
|
|
|
|
// return current element transform matrix in a cross-browser way
|
|
var getElementTransformProperty = function(e) {
|
|
e = e.length ? e[0] : e;
|
|
var tr = window
|
|
.getComputedStyle(e, null)
|
|
.getPropertyValue(transformProperty);
|
|
return tr;
|
|
};
|
|
|
|
// set current element transform matrix in a cross-browser way
|
|
var setElementTransformProperty = function(elem, value) {
|
|
elem = elem.length ? elem[0] : elem;
|
|
elem.style[styleProperty] = value;
|
|
};
|
|
|
|
/*======================================================
|
|
= Transform Matrix Decomposition =
|
|
======================================================*/
|
|
|
|
var SMALL_NUMBER = 1.e-7;
|
|
|
|
var rad2deg = function(angle) {
|
|
return angle * 180 / Math.PI;
|
|
};
|
|
|
|
var sqrt = Math.sqrt,
|
|
asin = Math.asin,
|
|
atan2 = Math.atan2,
|
|
cos = Math.cos,
|
|
abs = Math.abs,
|
|
floor = Math.floor;
|
|
|
|
var cloneMatrix = function(m) {
|
|
var res = [[],[],[],[]];
|
|
for (var i = 0; i < m.length; i++) {
|
|
for (var j = 0; j < m[i].length; j++) {
|
|
res[i][j] = m[i][j];
|
|
}
|
|
}
|
|
return res;
|
|
};
|
|
|
|
var determinant2x2 = function(a, b, c, d) {
|
|
return a * d - b * c;
|
|
};
|
|
|
|
var determinant3x3 = function(a1, a2, a3, b1, b2, b3, c1, c2, c3) {
|
|
return a1 * determinant2x2(b2, b3, c2, c3) - b1 * determinant2x2(a2, a3, c2, c3) + c1 * determinant2x2(a2, a3, b2, b3);
|
|
};
|
|
|
|
var determinant4x4 = function(m) {
|
|
var a1 = m[0][0], b1 = m[0][1], c1 = m[0][2], d1 = m[0][3], a2 = m[1][0], b2 = m[1][1], c2 = m[1][2], d2 = m[1][3], a3 = m[2][0], b3 = m[2][1], c3 = m[2][2], d3 = m[2][3], a4 = m[3][0], b4 = m[3][1], c4 = m[3][2], d4 = m[3][3];
|
|
return a1 * determinant3x3(b2, b3, b4, c2, c3, c4, d2, d3, d4) - b1 * determinant3x3(a2, a3, a4, c2, c3, c4, d2, d3, d4) + c1 * determinant3x3(a2, a3, a4, b2, b3, b4, d2, d3, d4) - d1 * determinant3x3(a2, a3, a4, b2, b3, b4, c2, c3, c4);
|
|
};
|
|
|
|
var adjoint = function(m) {
|
|
var res = [[],[],[],[]], a1 = m[0][0], b1 = m[0][1], c1 = m[0][2], d1 = m[0][3], a2 = m[1][0], b2 = m[1][1], c2 = m[1][2], d2 = m[1][3], a3 = m[2][0], b3 = m[2][1], c3 = m[2][2], d3 = m[2][3], a4 = m[3][0], b4 = m[3][1], c4 = m[3][2], d4 = m[3][3];
|
|
|
|
res[0][0] = determinant3x3(b2, b3, b4, c2, c3, c4, d2, d3, d4);
|
|
res[1][0] = - determinant3x3(a2, a3, a4, c2, c3, c4, d2, d3, d4);
|
|
res[2][0] = determinant3x3(a2, a3, a4, b2, b3, b4, d2, d3, d4);
|
|
res[3][0] = - determinant3x3(a2, a3, a4, b2, b3, b4, c2, c3, c4);
|
|
res[0][1] = - determinant3x3(b1, b3, b4, c1, c3, c4, d1, d3, d4);
|
|
res[1][1] = determinant3x3(a1, a3, a4, c1, c3, c4, d1, d3, d4);
|
|
res[2][1] = - determinant3x3(a1, a3, a4, b1, b3, b4, d1, d3, d4);
|
|
res[3][1] = determinant3x3(a1, a3, a4, b1, b3, b4, c1, c3, c4);
|
|
res[0][2] = determinant3x3(b1, b2, b4, c1, c2, c4, d1, d2, d4);
|
|
res[1][2] = - determinant3x3(a1, a2, a4, c1, c2, c4, d1, d2, d4);
|
|
res[2][2] = determinant3x3(a1, a2, a4, b1, b2, b4, d1, d2, d4);
|
|
res[3][2] = - determinant3x3(a1, a2, a4, b1, b2, b4, c1, c2, c4);
|
|
res[0][3] = - determinant3x3(b1, b2, b3, c1, c2, c3, d1, d2, d3);
|
|
res[1][3] = determinant3x3(a1, a2, a3, c1, c2, c3, d1, d2, d3);
|
|
res[2][3] = - determinant3x3(a1, a2, a3, b1, b2, b3, d1, d2, d3);
|
|
res[3][3] = determinant3x3(a1, a2, a3, b1, b2, b3, c1, c2, c3);
|
|
|
|
return res;
|
|
};
|
|
|
|
var inverse = function(m) {
|
|
var res = adjoint(m),
|
|
det = determinant4x4(m);
|
|
if (abs(det) < SMALL_NUMBER) { return false; }
|
|
|
|
for (var i = 0; i < 4; i++) {
|
|
for (var j = 0; j < 4; j++) {
|
|
res[i][j] = res[i][j] / det;
|
|
}
|
|
}
|
|
return res;
|
|
};
|
|
|
|
var transposeMatrix4 = function(m) {
|
|
var res = [[],[],[],[]];
|
|
for (var i = 0; i < 4; i++) {
|
|
for (var j = 0; j < 4; j++) {
|
|
res[i][j] = m[j][i];
|
|
}
|
|
}
|
|
return res;
|
|
};
|
|
|
|
var v4MulPointByMatrix = function(p, m) {
|
|
var res = [];
|
|
|
|
res[0] = (p[0] * m[0][0]) + (p[1] * m[1][0]) +
|
|
(p[2] * m[2][0]) + (p[3] * m[3][0]);
|
|
res[1] = (p[0] * m[0][1]) + (p[1] * m[1][1]) +
|
|
(p[2] * m[2][1]) + (p[3] * m[3][1]);
|
|
res[2] = (p[0] * m[0][2]) + (p[1] * m[1][2]) +
|
|
(p[2] * m[2][2]) + (p[3] * m[3][2]);
|
|
res[3] = (p[0] * m[0][3]) + (p[1] * m[1][3]) +
|
|
(p[2] * m[2][3]) + (p[3] * m[3][3]);
|
|
|
|
return res;
|
|
};
|
|
|
|
var v3Length = function(a) {
|
|
return sqrt((a[0] * a[0]) + (a[1] * a[1]) + (a[2] * a[2]));
|
|
};
|
|
|
|
var v3Scale = function(v, desiredLength) {
|
|
var res = [], len = v3Length(v);
|
|
if (len !== 0) {
|
|
var l = desiredLength / len;
|
|
res[0] *= l;
|
|
res[1] *= l;
|
|
res[2] *= l;
|
|
}
|
|
return res;
|
|
};
|
|
|
|
var v3Dot = function(a, b){
|
|
return (a[0] * b[0]) + (a[1] * b[1]) + (a[2] * b[2]);
|
|
};
|
|
|
|
var v3Combine = function(a, b, ascl, bscl) {
|
|
var res = [];
|
|
res[0] = (ascl * a[0]) + (bscl * b[0]);
|
|
res[1] = (ascl * a[1]) + (bscl * b[1]);
|
|
res[2] = (ascl * a[2]) + (bscl * b[2]);
|
|
return res;
|
|
};
|
|
|
|
var v3Cross = function(a, b) {
|
|
var res = [];
|
|
res[0] = (a[1] * b[2]) - (a[2] * b[1]);
|
|
res[1] = (a[2] * b[0]) - (a[0] * b[2]);
|
|
res[2] = (a[0] * b[1]) - (a[1] * b[0]);
|
|
return res;
|
|
};
|
|
|
|
var decompose = function(mat) {
|
|
var result = {}, localMatrix = cloneMatrix(mat), i, j;
|
|
|
|
// Normalize the matrix.
|
|
if (localMatrix[3][3] === 0) {
|
|
return false;
|
|
}
|
|
|
|
for (i = 0; i < 4; i++) {
|
|
for (j = 0; j < 4; j++) {
|
|
localMatrix[i][j] /= localMatrix[3][3];
|
|
}
|
|
}
|
|
|
|
var perspectiveMatrix = cloneMatrix(localMatrix);
|
|
for (i = 0; i < 3; i++) {
|
|
perspectiveMatrix[i][3] = 0;
|
|
}
|
|
perspectiveMatrix[3][3] = 1;
|
|
|
|
if (determinant4x4(perspectiveMatrix) === 0) {
|
|
return false;
|
|
}
|
|
|
|
// First, isolate perspective. This is the messiest.
|
|
if (localMatrix[0][3] !== 0 || localMatrix[1][3] !== 0 || localMatrix[2][3] !== 0) {
|
|
// rightHandSide is the right hand side of the equation.
|
|
var rightHandSide = [];
|
|
rightHandSide[0] = localMatrix[0][3];
|
|
rightHandSide[1] = localMatrix[1][3];
|
|
rightHandSide[2] = localMatrix[2][3];
|
|
rightHandSide[3] = localMatrix[3][3];
|
|
|
|
// Solve the equation by inverting perspectiveMatrix and multiplying
|
|
// rightHandSide by the inverse. (This is the easiest way, not
|
|
// necessarily the best.)
|
|
var inversePerspectiveMatrix = inverse(perspectiveMatrix);
|
|
var transposedInversePerspectiveMatrix = transposeMatrix4(inversePerspectiveMatrix);
|
|
var perspectivePoint = v4MulPointByMatrix(rightHandSide, transposedInversePerspectiveMatrix);
|
|
|
|
result.perspectiveX = perspectivePoint[0];
|
|
result.perspectiveY = perspectivePoint[1];
|
|
result.perspectiveZ = perspectivePoint[2];
|
|
result.perspectiveW = perspectivePoint[3];
|
|
|
|
// Clear the perspective partition
|
|
localMatrix[0][3] = localMatrix[1][3] = localMatrix[2][3] = 0;
|
|
localMatrix[3][3] = 1;
|
|
} else {
|
|
// No perspective.
|
|
result.perspectiveX = result.perspectiveY = result.perspectiveZ = 0;
|
|
result.perspectiveW = 1;
|
|
}
|
|
|
|
// Next take care of translation (easy).
|
|
result.translateX = localMatrix[3][0];
|
|
localMatrix[3][0] = 0;
|
|
result.translateY = localMatrix[3][1];
|
|
localMatrix[3][1] = 0;
|
|
result.translateZ = localMatrix[3][2];
|
|
localMatrix[3][2] = 0;
|
|
|
|
// Now get scale and shear.
|
|
var row = [[],[],[]], pdum3;
|
|
|
|
for (i = 0; i < 3; i++) {
|
|
row[i][0] = localMatrix[i][0];
|
|
row[i][1] = localMatrix[i][1];
|
|
row[i][2] = localMatrix[i][2];
|
|
}
|
|
|
|
// Compute X scale factor and normalize first row.
|
|
result.scaleX = v3Length(row[0]);
|
|
v3Scale(row[0], 1.0);
|
|
|
|
// Compute XY shear factor and make 2nd row orthogonal to 1st.
|
|
result.skewXY = v3Dot(row[0], row[1]);
|
|
v3Combine(row[1], row[0], row[1], 1.0, -result.skewXY);
|
|
|
|
// Now, compute Y scale and normalize 2nd row.
|
|
result.scaleY = v3Length(row[1]);
|
|
v3Scale(row[1], 1.0);
|
|
result.skewXY /= result.scaleY;
|
|
|
|
// Compute XZ and YZ shears, orthogonalize 3rd row.
|
|
result.skewXZ = v3Dot(row[0], row[2]);
|
|
v3Combine(row[2], row[0], row[2], 1.0, -result.skewXZ);
|
|
result.skewYZ = v3Dot(row[1], row[2]);
|
|
v3Combine(row[2], row[1], row[2], 1.0, -result.skewYZ);
|
|
|
|
// Next, get Z scale and normalize 3rd row.
|
|
result.scaleZ = v3Length(row[2]);
|
|
v3Scale(row[2], 1.0);
|
|
result.skewXZ /= result.scaleZ;
|
|
result.skewYZ /= result.scaleZ;
|
|
|
|
// At this point, the matrix (in rows[]) is orthonormal.
|
|
// Check for a coordinate system flip. If the determinant
|
|
// is -1, then negate the matrix and the scaling factors.
|
|
pdum3 = v3Cross(row[1], row[2]);
|
|
|
|
if (v3Dot(row[0], pdum3) < 0) {
|
|
for (i = 0; i < 3; i++) {
|
|
result.scaleX *= -1;
|
|
row[i][0] *= -1;
|
|
row[i][1] *= -1;
|
|
row[i][2] *= -1;
|
|
}
|
|
}
|
|
|
|
// Rotation (angles smaller then SMALL_NUMBER are zeroed)
|
|
result.rotateY = rad2deg(asin(-row[0][2])) || 0;
|
|
if (cos(result.rotateY) !== 0) {
|
|
result.rotateX = rad2deg(atan2(row[1][2], row[2][2])) || 0;
|
|
result.rotateZ = rad2deg(atan2(row[0][1], row[0][0])) || 0;
|
|
} else {
|
|
result.rotateX = rad2deg(atan2(-row[2][0], row[1][1])) || 0;
|
|
result.rotateZ = 0;
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
/*=========================================
|
|
= Factory interface =
|
|
=========================================*/
|
|
|
|
var fCom = function(n, def) {
|
|
// avoid scientific notation with toFixed
|
|
var val = (n || def || 0);
|
|
return '' + val.toFixed(20);
|
|
};
|
|
|
|
var fPx = function(n, def) {
|
|
return fCom(n, def) + 'px';
|
|
};
|
|
|
|
var fDeg = function(n, def) {
|
|
return fCom(n, def) + 'deg';
|
|
};
|
|
|
|
return {
|
|
fromCssMatrix: function(tr) {
|
|
var M = [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]];
|
|
|
|
// Just returns identity in case no transform is setup for the element
|
|
if (tr && tr !== 'none') {
|
|
var elems = tr.split('(')[1].split(')')[0].split(',').map(Number);
|
|
|
|
// Is a 2d transform: matrix(a, b, c, d, tx, ty) is a shorthand
|
|
// for matrix3d(a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1)
|
|
if (tr.match(/^matrix\(/)) {
|
|
M[0][0] = elems[0];
|
|
M[1][0] = elems[1];
|
|
M[0][1] = elems[2];
|
|
M[1][1] = elems[3];
|
|
M[3][0] = elems[4];
|
|
M[3][1] = elems[5];
|
|
|
|
// Is a 3d transform, set elements by rows
|
|
} else {
|
|
for (var i = 0; i < 16; i++) {
|
|
var row = floor(i / 4),
|
|
col = i % 4;
|
|
M[row][col] = elems[i];
|
|
}
|
|
}
|
|
}
|
|
return decompose(M);
|
|
},
|
|
|
|
toCss: function(t) {
|
|
//
|
|
// Transforms are recomposed as a composition of:
|
|
//
|
|
// matrix3d(1,0,0,0, 0,1,0,0, 0,0,1,0, perspective[0], perspective[1], perspective[2], perspective[3])
|
|
// translate3d(translation[0], translation[1], translation[2])
|
|
// rotateX(rotation[0]) rotateY(rotation[1]) rotateZ(rotation[2])
|
|
// matrix3d(1,0,0,0, 0,1,0,0, 0,skew[2],1,0, 0,0,0,1)
|
|
// matrix3d(1,0,0,0, 0,1,0,0, skew[1],0,1,0, 0,0,0,1)
|
|
// matrix3d(1,0,0,0, skew[0],1,0,0, 0,0,1,0, 0,0,0,1)
|
|
// scale3d(scale[0], scale[1], scale[2])
|
|
//
|
|
|
|
var perspective = [
|
|
fCom(t.perspectiveX),
|
|
fCom(t.perspectiveY),
|
|
fCom(t.perspectiveZ),
|
|
fCom(t.perspectiveW, 1)
|
|
],
|
|
translate = [
|
|
fPx(t.translateX),
|
|
fPx(t.translateY),
|
|
fPx(t.translateZ)
|
|
],
|
|
scale = [
|
|
fCom(t.scaleX),
|
|
fCom(t.scaleY),
|
|
fCom(t.scaleZ)
|
|
],
|
|
rotation = [
|
|
fDeg(t.rotateX),
|
|
fDeg(t.rotateY),
|
|
fDeg(t.rotateZ)
|
|
],
|
|
skew = [
|
|
fCom(t.skewXY),
|
|
fCom(t.skewXZ),
|
|
fCom(t.skewYZ)
|
|
];
|
|
|
|
return [
|
|
'matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,' + perspective.join(',') + ')',
|
|
'translate3d(' + translate.join(',') + ')',
|
|
'rotateX('+ rotation[0] + ') rotateY(' + rotation[1] + ') rotateZ(' + rotation[2] + ')',
|
|
'matrix3d(1,0,0,0,0,1,0,0,0,' + skew[2] + ',1,0,0,0,0,1)',
|
|
'matrix3d(1,0,0,0,0,1,0,0,' + skew[1] + ',0,1,0,0,0,0,1)',
|
|
'matrix3d(1,0,0,0,' + skew[0] + ',1,0,0,0,0,1,0,0,0,0,1)',
|
|
'scale3d(' + scale.join(',') + ')'
|
|
].join(' ');
|
|
},
|
|
|
|
//
|
|
// Returns a decomposition of the transform matrix applied
|
|
// to `e`;
|
|
//
|
|
// NOTE: 2d matrices are translated to 3d matrices
|
|
// before any other operation.
|
|
//
|
|
get: function(e) {
|
|
return this.fromCssMatrix(getElementTransformProperty(e));
|
|
},
|
|
|
|
// Recompose a transform from decomposition `t` and apply it to element `e`
|
|
set: function(e, t) {
|
|
var str = (typeof t === 'string') ? t : this.toCss(t);
|
|
setElementTransformProperty(e, str);
|
|
}
|
|
};
|
|
});
|
|
}());
|
|
/**
|
|
@module mobile-angular-ui.gestures
|
|
@memberof!
|
|
@position 100
|
|
@description
|
|
|
|
It has directives and services to support `touch`, `swipe` and `drag` gestures.
|
|
|
|
It does not need any `.css` to work.
|
|
|
|
<div class="alert alert-warning">
|
|
<p>
|
|
<i class="fa fa-warning"></i> This module will not work with `ngTouch` cause it is intended, among offering more features, to be a drop-in replacement for it.
|
|
</p>
|
|
<p>
|
|
Be aware that `ngTouch` is still not playing well with `fastclick.js` and its usage with `mobile-angular-ui` is currently discouraged anyway.
|
|
</p>
|
|
</div>
|
|
|
|
## Usage
|
|
|
|
`.gestures` module is not required by `mobile-angular-ui` module. It has no dependency on other modules and is intended to be used alone with any other angular framework.
|
|
|
|
You have to include `mobile-angular-ui.gestures.min.js` to your project in order to use it. Ie.
|
|
|
|
``` html
|
|
<script src="/dist/js/mobile-angular-ui.gestures.min.js"></script>
|
|
```
|
|
|
|
``` js
|
|
angular.module('myApp', ['mobile-angular-ui.gestures']);
|
|
```
|
|
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
angular.module('mobile-angular-ui.gestures', [
|
|
'mobile-angular-ui.gestures.drag',
|
|
'mobile-angular-ui.gestures.swipe',
|
|
'mobile-angular-ui.gestures.transform'
|
|
]);
|
|
|
|
}()); |