mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 19:57:22 +08:00
909 lines
46 KiB
JavaScript
909 lines
46 KiB
JavaScript
/* Forked from Velocity.js, MIT License. Julian Shapiro http://twitter.com/shapiro */
|
|
|
|
import * as util from 'ionic/util/util'
|
|
import {Collide} from './collide'
|
|
import {CSS} from './css'
|
|
import {getEasing} from './easing'
|
|
|
|
const data = Collide.data;
|
|
|
|
/*********************
|
|
Element Processing
|
|
*********************/
|
|
|
|
/* Element processing consists of three parts -- data processing that cannot go stale and data processing that *can* go stale (i.e. third-party style modifications):
|
|
1) Pre-Queueing: Element-wide variables, including the element's data storage, are instantiated. Call options are prepared. If triggered, the Stop action is executed.
|
|
2) Queueing: The logic that runs once this call has reached its point of execution in the element's Collide.queue() stack. Most logic is placed here to avoid risking it becoming stale.
|
|
3) Pushing: Consolidation of the tween data followed by its push onto the global in-progress calls container.
|
|
*/
|
|
|
|
export function animationProcess(action, elements, elementsIndex, options, propertiesMap, callUnitConversionData, call) {
|
|
var resolve;
|
|
var promise = new Promise(function(res) {
|
|
resolve = res;
|
|
});
|
|
|
|
var element = elements[elementsIndex];
|
|
var elementsLength = elements.length;
|
|
|
|
|
|
/*************************
|
|
Part I: Pre-Queueing
|
|
*************************/
|
|
|
|
/***************************
|
|
Element-Wide Variables
|
|
***************************/
|
|
|
|
/* The runtime opts object is the extension of the current call's options and Collide's page-wide option defaults. */
|
|
var opts = util.extend({}, Collide.defaults, options);
|
|
|
|
/* A container for the processed data associated with each property in the propertyMap.
|
|
(Each property in the map produces its own 'tween'.) */
|
|
var tweensContainer = {};
|
|
var elementUnitConversionData = null;
|
|
|
|
|
|
/******************
|
|
Element Init
|
|
******************/
|
|
|
|
if (data(element) === undefined) {
|
|
Collide.initData(element);
|
|
}
|
|
|
|
|
|
/******************
|
|
Option: Delay
|
|
******************/
|
|
|
|
/* Since queue:false doesn't respect the item's existing queue, we avoid injecting its delay here (it's set later on). */
|
|
if (parseFloat(opts.delay) && opts.queue !== false) {
|
|
Collide.queue(element, opts.queue, function(next) {
|
|
// This is a flag used to indicate to the upcoming completeCall() function that this queue entry was initiated by Collide.
|
|
// See completeCall() for further details.
|
|
Collide.collideQueueEntryFlag = true;
|
|
|
|
// The ensuing queue item (which is assigned to the 'next' argument that Collide.queue() automatically passes in) will be triggered after a setTimeout delay.
|
|
// The setTimeout is stored so that it can be subjected to clearTimeout() if this animation is prematurely stopped via Collide's 'stop' command.
|
|
data(element).delayTimer = {
|
|
setTimeout: setTimeout(next, parseFloat(opts.delay)),
|
|
next: next
|
|
};
|
|
});
|
|
}
|
|
|
|
|
|
/*********************
|
|
Option: Duration
|
|
*********************/
|
|
|
|
opts.duration = parseFloat(opts.duration) || 1;
|
|
|
|
|
|
/*******************
|
|
Option: Easing
|
|
*******************/
|
|
|
|
opts.easing = getEasing(opts.easing, opts.duration);
|
|
|
|
|
|
/**********************
|
|
Option: Callbacks
|
|
**********************/
|
|
|
|
/* Callbacks must functions. Otherwise, default to null. */
|
|
if (opts.begin && typeof opts.begin !== 'function') {
|
|
opts.begin = null;
|
|
}
|
|
|
|
if (opts.progress && typeof opts.progress !== 'function') {
|
|
opts.progress = null;
|
|
}
|
|
|
|
if (opts.complete && typeof opts.complete !== 'function') {
|
|
opts.complete = null;
|
|
}
|
|
|
|
|
|
/*********************************
|
|
Option: Display & Visibility
|
|
*********************************/
|
|
|
|
/* Refer to Collide's documentation (CollideJS.org/#displayAndVisibility) for a description of the display and visibility options' behavior. */
|
|
/* Note: We strictly check for undefined instead of falsiness because display accepts an empty string value. */
|
|
if (opts.display !== undefined && opts.display !== null) {
|
|
opts.display = opts.display.toString().toLowerCase();
|
|
|
|
/* Users can pass in a special 'auto' value to instruct Collide to set the element to its default display value. */
|
|
if (opts.display === 'auto') {
|
|
opts.display = CSS.getDisplayType(element);
|
|
}
|
|
}
|
|
|
|
if (opts.visibility !== undefined && opts.visibility !== null) {
|
|
opts.visibility = opts.visibility.toString().toLowerCase();
|
|
}
|
|
|
|
|
|
/***********************
|
|
Part II: Queueing
|
|
***********************/
|
|
|
|
/* When a set of elements is targeted by a Collide call, the set is broken up and each element has the current Collide call individually queued onto it.
|
|
In this way, each element's existing queue is respected; some elements may already be animating and accordingly should not have this current Collide call triggered immediately. */
|
|
/* In each queue, tween data is processed for each animating property then pushed onto the call-wide calls array. When the last element in the set has had its tweens processed,
|
|
the call array is pushed to Collide.State.calls for live processing by the requestAnimationFrame tick. */
|
|
function buildQueue(next) {
|
|
|
|
/*******************
|
|
Option: Begin
|
|
*******************/
|
|
|
|
/* The begin callback is fired once per call -- not once per elemenet -- and is passed the full raw DOM element set as both its context and its first argument. */
|
|
if (opts.begin && elementsIndex === 0) {
|
|
/* We throw callbacks in a setTimeout so that thrown errors don't halt the execution of Collide itself. */
|
|
try {
|
|
opts.begin.call(elements, elements);
|
|
} catch (error) {
|
|
setTimeout(function() { throw error; });
|
|
}
|
|
}
|
|
|
|
|
|
/*****************************************
|
|
Tween Data Construction (for Scroll)
|
|
*****************************************/
|
|
|
|
/* Note: In order to be subjected to chaining and animation options, scroll's tweening is routed through Collide as if it were a standard CSS property animation. */
|
|
if (action === 'scroll') {
|
|
/* The scroll action uniquely takes an optional 'offset' option -- specified in pixels -- that offsets the targeted scroll position. */
|
|
var scrollDirection = (/^x$/i.test(opts.axis) ? 'Left' : 'Top'),
|
|
scrollOffset = parseFloat(opts.offset) || 0,
|
|
scrollPositionCurrent,
|
|
scrollPositionCurrentAlternate,
|
|
scrollPositionEnd;
|
|
|
|
/* Scroll also uniquely takes an optional 'container' option, which indicates the parent element that should be scrolled --
|
|
as opposed to the browser window itself. This is useful for scrolling toward an element that's inside an overflowing parent element. */
|
|
if (opts.container) {
|
|
/* Ensure that a raw DOM element was passed in. */
|
|
if (opts.container.nodeType) {
|
|
/* Note: Unlike other properties in Collide, the browser's scroll position is never cached since it so frequently changes
|
|
(due to the user's natural interaction with the page). */
|
|
scrollPositionCurrent = opts.container['scroll' + scrollDirection]; /* GET */
|
|
|
|
/* CSS.position(element) values are relative to the container's currently viewable area (without taking into
|
|
account the container's true dimensions, for example, if the container was not overflowing). Thus, the scroll end
|
|
value is the sum of the child element's position *and* the scroll container's current scroll position. */
|
|
scrollPositionEnd = (scrollPositionCurrent + CSS.position(element)[scrollDirection.toLowerCase()]) + scrollOffset; /* GET */
|
|
|
|
} else {
|
|
/* If a value other than a raw DOM element was passed in, default to null so that this option is ignored. */
|
|
opts.container = null;
|
|
}
|
|
|
|
} else {
|
|
/* If the window itself is being scrolled -- not a containing element -- perform a live scroll position lookup using
|
|
the appropriate cached property names (which differ based on browser type). */
|
|
scrollPositionCurrent = Collide.State.scrollAnchor[Collide.State['scrollProperty' + scrollDirection]]; /* GET */
|
|
|
|
/* When scrolling the browser window, cache the alternate axis's current value since window.scrollTo() doesn't let us change only one value at a time. */
|
|
scrollPositionCurrentAlternate = Collide.State.scrollAnchor[Collide.State['scrollProperty' + (scrollDirection === 'Left' ? 'Top' : 'Left')]]; /* GET */
|
|
|
|
/* Unlike CSS.position(element), CSS.offset(element) values are relative to the browser window's true dimensions
|
|
-- not merely its currently viewable area -- and therefore end values do not need to be compounded onto current values. */
|
|
scrollPositionEnd = CSS.offset(element)[scrollDirection.toLowerCase()] + scrollOffset; /* GET */
|
|
}
|
|
|
|
/* Since there's only one format that scroll's associated tweensContainer can take, we create it manually. */
|
|
tweensContainer = {
|
|
scroll: {
|
|
rootPropertyValue: false,
|
|
startValue: scrollPositionCurrent,
|
|
currentValue: scrollPositionCurrent,
|
|
endValue: scrollPositionEnd,
|
|
unitType: '',
|
|
easing: opts.easing,
|
|
scrollData: {
|
|
container: opts.container,
|
|
direction: scrollDirection,
|
|
alternateValue: scrollPositionCurrentAlternate
|
|
}
|
|
},
|
|
element: element
|
|
};
|
|
|
|
if (Collide.debug) console.log("tweensContainer (scroll): ", tweensContainer.scroll, element);
|
|
|
|
|
|
/******************************************
|
|
Tween Data Construction (for Reverse)
|
|
******************************************/
|
|
|
|
/* Reverse acts like a 'start' action in that a property map is animated toward. The only difference is
|
|
that the property map used for reverse is the inverse of the map used in the previous call. Thus, we manipulate
|
|
the previous call to construct our new map: use the previous map's end values as our new map's start values. Copy over all other data. */
|
|
/* Note: Reverse can be directly called via the 'reverse' parameter, or it can be indirectly triggered via the loop option. (Loops are composed of multiple reverses.) */
|
|
/* Note: Reverse calls do not need to be consecutively chained onto a currently-animating element in order to operate on cached values;
|
|
there is no harm to reverse being called on a potentially stale data cache since reverse's behavior is simply defined
|
|
as reverting to the element's values as they were prior to the previous *Collide* call. */
|
|
} else if (action === 'reverse') {
|
|
/* Abort if there is no prior animation data to reverse to. */
|
|
if (!data(element).tweensContainer) {
|
|
/* Dequeue the element so that this queue entry releases itself immediately, allowing subsequent queue entries to run. */
|
|
Collide.dequeue(element, opts.queue);
|
|
return;
|
|
|
|
} else {
|
|
/*********************
|
|
Options Parsing
|
|
*********************/
|
|
|
|
/* If the element was hidden via the display option in the previous call,
|
|
revert display to 'auto' prior to reversal so that the element is visible again. */
|
|
if (data(element).opts.display === 'none') {
|
|
data(element).opts.display = 'auto';
|
|
}
|
|
|
|
if (data(element).opts.visibility === 'hidden') {
|
|
data(element).opts.visibility = 'visible';
|
|
}
|
|
|
|
/* If the loop option was set in the previous call, disable it so that 'reverse' calls aren't recursively generated.
|
|
Further, remove the previous call's callback options; typically, users do not want these to be refired. */
|
|
data(element).opts.loop = false;
|
|
data(element).opts.begin = null;
|
|
data(element).opts.complete = null;
|
|
|
|
/* Since we're extending an opts object that has already been extended with the defaults options object,
|
|
we remove non-explicitly-defined properties that are auto-assigned values. */
|
|
if (!options.easing) {
|
|
delete opts.easing;
|
|
}
|
|
|
|
if (!options.duration) {
|
|
delete opts.duration;
|
|
}
|
|
|
|
/* The opts object used for reversal is an extension of the options object optionally passed into this
|
|
reverse call plus the options used in the previous Collide call. */
|
|
opts = util.extend({}, data(element).opts, opts);
|
|
|
|
/*************************************
|
|
Tweens Container Reconstruction
|
|
*************************************/
|
|
|
|
/* Create a deepy copy (indicated via the true flag) of the previous call's tweensContainer. */
|
|
var lastTweensContainer = util.extend(true, {}, data(element).tweensContainer);
|
|
|
|
/* Manipulate the previous tweensContainer by replacing its end values and currentValues with its start values. */
|
|
for (var lastTween in lastTweensContainer) {
|
|
/* In addition to tween data, tweensContainers contain an element property that we ignore here. */
|
|
if (lastTween !== 'element') {
|
|
var lastStartValue = lastTweensContainer[lastTween].startValue;
|
|
|
|
lastTweensContainer[lastTween].startValue = lastTweensContainer[lastTween].currentValue = lastTweensContainer[lastTween].endValue;
|
|
lastTweensContainer[lastTween].endValue = lastStartValue;
|
|
|
|
/* Easing is the only option that embeds into the individual tween data (since it can be defined on a per-property basis).
|
|
Accordingly, every property's easing value must be updated when an options object is passed in with a reverse call.
|
|
The side effect of this extensibility is that all per-property easing values are forcefully reset to the new value. */
|
|
if (!util.isEmptyObject(options)) {
|
|
lastTweensContainer[lastTween].easing = opts.easing;
|
|
}
|
|
|
|
if (Collide.debug) console.log("reverse tweensContainer (" + lastTween + "): " + JSON.stringify(lastTweensContainer[lastTween]), element);
|
|
}
|
|
}
|
|
|
|
tweensContainer = lastTweensContainer;
|
|
}
|
|
|
|
|
|
/*********************************
|
|
Start: Tween Data Construction
|
|
*********************************/
|
|
|
|
} else if (action === 'start') {
|
|
|
|
/****************************
|
|
Start: Value Transferring
|
|
****************************/
|
|
|
|
/* If this queue entry follows a previous Collide-initiated queue entry *and* if this entry was created
|
|
while the element was in the process of being animated by Collide, then this current call is safe to use
|
|
the end values from the prior call as its start values. Collide attempts to perform this value transfer
|
|
process whenever possible in order to avoid requerying the DOM. */
|
|
/* If values aren't transferred from a prior call and start values were not forcefed by the user (more on this below),
|
|
then the DOM is queried for the element's current values as a last resort. */
|
|
/* Note: Conversely, animation reversal (and looping) *always* perform inter-call value transfers; they never requery the DOM. */
|
|
var lastTweensContainer;
|
|
|
|
/* The per-element isAnimating flag is used to indicate whether it's safe (i.e. the data isn't stale)
|
|
to transfer over end values to use as start values. If it's set to true and there is a previous
|
|
Collide call to pull values from, do so. */
|
|
if (data(element).tweensContainer && data(element).isAnimating === true) {
|
|
lastTweensContainer = data(element).tweensContainer;
|
|
}
|
|
|
|
|
|
/********************************
|
|
Start: Tween Data Calculation
|
|
********************************/
|
|
|
|
/* This function parses property data and defaults endValue, easing, and startValue as appropriate. */
|
|
/* Property map values can either take the form of 1) a single value representing the end value,
|
|
or 2) an array in the form of [ endValue, [, easing] [, startValue] ].
|
|
The optional third parameter is a forcefed startValue to be used instead of querying the DOM for
|
|
the element's current value. */
|
|
function parsePropertyValue(valueData, skipResolvingEasing) {
|
|
var endValue = undefined,
|
|
easing = undefined,
|
|
startValue = undefined;
|
|
|
|
/* Handle the array format, which can be structured as one of three potential overloads:
|
|
A) [ endValue, easing, startValue ], B) [ endValue, easing ], or C) [ endValue, startValue ] */
|
|
if (Array.isArray(valueData)) {
|
|
/* endValue is always the first item in the array. Don't bother validating endValue's value now
|
|
since the ensuing property cycling logic does that. */
|
|
endValue = valueData[0];
|
|
|
|
/* Two-item array format: If the second item is a number, function, or hex string, treat it as a
|
|
start value since easings can only be non-hex strings or arrays. */
|
|
if ((!Array.isArray(valueData[1]) && /^[\d-]/.test(valueData[1])) || typeof valueData[1] === 'function' || CSS.RegEx.isHex.test(valueData[1])) {
|
|
startValue = valueData[1];
|
|
|
|
/* Two or three-item array: If the second item is a non-hex string or an array, treat it as an easing. */
|
|
} else if ((util.isString(valueData[1]) && !CSS.RegEx.isHex.test(valueData[1])) || Array.isArray(valueData[1])) {
|
|
easing = skipResolvingEasing ? valueData[1] : getEasing(valueData[1], opts.duration);
|
|
|
|
/* Don't bother validating startValue's value now since the ensuing property cycling logic inherently does that. */
|
|
if (valueData[2] !== undefined) {
|
|
startValue = valueData[2];
|
|
}
|
|
}
|
|
|
|
} else {
|
|
/* Handle the single-value format. */
|
|
endValue = valueData;
|
|
}
|
|
|
|
/* Default to the call's easing if a per-property easing type was not defined. */
|
|
if (!skipResolvingEasing) {
|
|
easing = easing || opts.easing;
|
|
}
|
|
|
|
/* If functions were passed in as values, pass the function the current element as its context,
|
|
plus the element's index and the element set's size as arguments. Then, assign the returned value. */
|
|
if (typeof endValue === 'function') {
|
|
endValue = endValue.call(element, elementsIndex, elementsLength);
|
|
}
|
|
|
|
if (typeof startValue === 'function') {
|
|
startValue = startValue.call(element, elementsIndex, elementsLength);
|
|
}
|
|
|
|
/* Allow startValue to be left as undefined to indicate to the ensuing code that its value was not forcefed. */
|
|
return [ endValue || 0, easing, startValue ];
|
|
|
|
} // END: parsePropertyValue()
|
|
|
|
|
|
/* Cycle through each property in the map, looking for shorthand color properties (e.g. 'color' as opposed to 'colorRed'). Inject the corresponding
|
|
colorRed, colorGreen, and colorBlue RGB component tweens into the propertiesMap (which Collide understands) and remove the shorthand property. */
|
|
for (var property in propertiesMap) {
|
|
|
|
/* Find shorthand color properties that have been passed a hex string. */
|
|
if (RegExp('^' + CSS.Lists.colors.join('$|^') + '$').test(property)) {
|
|
/* Parse the value data for each shorthand. */
|
|
var valueData = parsePropertyValue(propertiesMap[property], true),
|
|
endValue = valueData[0],
|
|
easing = valueData[1],
|
|
startValue = valueData[2];
|
|
|
|
if (CSS.RegEx.isHex.test(endValue)) {
|
|
/* Convert the hex strings into their RGB component arrays. */
|
|
var colorComponents = [ 'Red', 'Green', 'Blue' ],
|
|
endValueRGB = CSS.Values.hexToRgb(endValue),
|
|
startValueRGB = startValue ? CSS.Values.hexToRgb(startValue) : undefined;
|
|
|
|
/* Inject the RGB component tweens into propertiesMap. */
|
|
for (var i = 0; i < colorComponents.length; i++) {
|
|
var dataArray = [ endValueRGB[i] ];
|
|
|
|
if (easing) {
|
|
dataArray.push(easing);
|
|
}
|
|
|
|
if (startValueRGB !== undefined) {
|
|
dataArray.push(startValueRGB[i]);
|
|
}
|
|
|
|
propertiesMap[property + colorComponents[i]] = dataArray;
|
|
}
|
|
|
|
/* Remove the intermediary shorthand property entry now that we've processed it. */
|
|
delete propertiesMap[property];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Create a tween out of each property, and append its associated data to tweensContainer. */
|
|
for (var property in propertiesMap) {
|
|
|
|
/*********************************************
|
|
parsePropertyValue(), Start Value Sourcing
|
|
*********************************************/
|
|
|
|
/* Parse out endValue, easing, and startValue from the property's data. */
|
|
var valueData = parsePropertyValue(propertiesMap[property]),
|
|
endValue = valueData[0],
|
|
easing = valueData[1],
|
|
startValue = valueData[2];
|
|
|
|
/* Now that the original property name's format has been used for the parsePropertyValue() lookup above,
|
|
we force the property to its camelCase styling to normalize it for manipulation. */
|
|
property = CSS.Names.camelCase(property);
|
|
|
|
/* In case this property is a hook, there are circumstances where we will intend to work on the hook's root property and not the hooked subproperty. */
|
|
var rootProperty = CSS.Hooks.getRoot(property),
|
|
rootPropertyValue = false;
|
|
|
|
/* Other than for the dummy tween property, properties that are not supported by the browser (and do not have an associated normalization) will
|
|
inherently produce no style changes when set, so they are skipped in order to decrease animation tick overhead.
|
|
Property support is determined via prefixCheck(), which returns a false flag when no supported is detected. */
|
|
/* Note: Since SVG elements have some of their properties directly applied as HTML attributes,
|
|
there is no way to check for their explicit browser support, and so we skip skip this check for them. */
|
|
if (!data(element).isSVG && rootProperty !== 'tween' && CSS.Names.prefixCheck(rootProperty)[1] === false && CSS.Normalizations.registered[rootProperty] === undefined) {
|
|
if (Collide.debug) console.log('Skipping [' + rootProperty + '] due to a lack of browser support.');
|
|
continue;
|
|
}
|
|
|
|
/* If the display option is being set to a non-'none' (e.g. 'block') and opacity (filter on IE<=8) is being
|
|
animated to an endValue of non-zero, the user's intention is to fade in from invisible, thus we forcefeed opacity
|
|
a startValue of 0 if its startValue hasn't already been sourced by value transferring or prior forcefeeding. */
|
|
if (((opts.display !== undefined && opts.display !== null && opts.display !== 'none') || (opts.visibility !== undefined && opts.visibility !== 'hidden')) && /opacity|filter/.test(property) && !startValue && endValue !== 0) {
|
|
startValue = 0;
|
|
}
|
|
|
|
/* If values have been transferred from the previous Collide call, extract the endValue and rootPropertyValue
|
|
for all of the current call's properties that were *also* animated in the previous call. */
|
|
/* Note: Value transferring can optionally be disabled by the user via the _cacheValues option. */
|
|
if (opts._cacheValues && lastTweensContainer && lastTweensContainer[property]) {
|
|
if (startValue === undefined) {
|
|
startValue = lastTweensContainer[property].endValue + lastTweensContainer[property].unitType;
|
|
}
|
|
|
|
/* The previous call's rootPropertyValue is extracted from the element's data cache since that's the
|
|
instance of rootPropertyValue that gets freshly updated by the tweening process, whereas the rootPropertyValue
|
|
attached to the incoming lastTweensContainer is equal to the root property's value prior to any tweening. */
|
|
rootPropertyValue = data(element).rootPropertyValueCache[rootProperty];
|
|
|
|
/* If values were not transferred from a previous Collide call, query the DOM as needed. */
|
|
} else {
|
|
/* Handle hooked properties. */
|
|
if (CSS.Hooks.registered[property]) {
|
|
if (startValue === undefined) {
|
|
rootPropertyValue = CSS.getPropertyValue(element, rootProperty); /* GET */
|
|
/* Note: The following getPropertyValue() call does not actually trigger a DOM query;
|
|
getPropertyValue() will extract the hook from rootPropertyValue. */
|
|
startValue = CSS.getPropertyValue(element, property, rootPropertyValue);
|
|
|
|
/* If startValue is already defined via forcefeeding, do not query the DOM for the root property's value;
|
|
just grab rootProperty's zero-value template from CSS.Hooks. This overwrites the element's actual
|
|
root property value (if one is set), but this is acceptable since the primary reason users forcefeed is
|
|
to avoid DOM queries, and thus we likewise avoid querying the DOM for the root property's value. */
|
|
} else {
|
|
/* Grab this hook's zero-value template, e.g. '0px 0px 0px black'. */
|
|
rootPropertyValue = CSS.Hooks.templates[rootProperty][1];
|
|
}
|
|
|
|
/* Handle non-hooked properties that haven't already been defined via forcefeeding. */
|
|
} else if (startValue === undefined) {
|
|
startValue = CSS.getPropertyValue(element, property); /* GET */
|
|
}
|
|
}
|
|
|
|
|
|
/**********************************************
|
|
parsePropertyValue(), Value Data Extraction
|
|
**********************************************/
|
|
|
|
var separatedValue,
|
|
endValueUnitType,
|
|
startValueUnitType,
|
|
operator = false;
|
|
|
|
/* Separates a property value into its numeric value and its unit util. */
|
|
function separateValue (property, value) {
|
|
var unitType,
|
|
numericValue;
|
|
|
|
numericValue = (value || '0')
|
|
.toString()
|
|
.toLowerCase()
|
|
/* Match the unit type at the end of the value. */
|
|
.replace(/[%A-z]+$/, function(match) {
|
|
/* Grab the unit util. */
|
|
unitType = match;
|
|
|
|
/* Strip the unit type off of value. */
|
|
return '';
|
|
});
|
|
|
|
/* If no unit type was supplied, assign one that is appropriate for this property (e.g. 'deg' for rotateZ or 'px' for width). */
|
|
if (!unitType) {
|
|
unitType = CSS.Values.getUnitType(property);
|
|
}
|
|
|
|
return [ numericValue, unitType ];
|
|
}
|
|
|
|
/* Separate startValue. */
|
|
separatedValue = separateValue(property, startValue);
|
|
startValue = separatedValue[0];
|
|
startValueUnitType = separatedValue[1];
|
|
|
|
/* Separate endValue, and extract a value operator (e.g. '+=', '-=') if one exists. */
|
|
separatedValue = separateValue(property, endValue);
|
|
endValue = separatedValue[0].replace(/^([+-\/*])=/, function(match, subMatch) {
|
|
operator = subMatch;
|
|
|
|
/* Strip the operator off of the value. */
|
|
return '';
|
|
});
|
|
endValueUnitType = separatedValue[1];
|
|
|
|
/* Parse float values from endValue and startValue. Default to 0 if NaN is returned. */
|
|
startValue = parseFloat(startValue) || 0;
|
|
endValue = parseFloat(endValue) || 0;
|
|
|
|
|
|
/***************************************
|
|
parsePropertyValue, Property-Specific Value Conversion
|
|
***************************************/
|
|
|
|
/* Custom support for properties that don't actually accept the % unit type, but where pollyfilling is trivial and relatively foolproof. */
|
|
if (endValueUnitType === '%') {
|
|
/* A %-value fontSize/lineHeight is relative to the parent's fontSize (as opposed to the parent's dimensions),
|
|
which is identical to the em unit's behavior, so we piggyback off of that. */
|
|
if (/^(fontSize|lineHeight)$/.test(property)) {
|
|
/* Convert % into an em decimal value. */
|
|
endValue = endValue / 100;
|
|
endValueUnitType = 'em';
|
|
|
|
/* For scaleX and scaleY, convert the value into its decimal format and strip off the unit util. */
|
|
} else if (/^scale/.test(property)) {
|
|
endValue = endValue / 100;
|
|
endValueUnitType = '';
|
|
|
|
/* For RGB components, take the defined percentage of 255 and strip off the unit util. */
|
|
} else if (/(Red|Green|Blue)$/i.test(property)) {
|
|
endValue = (endValue / 100) * 255;
|
|
endValueUnitType = '';
|
|
}
|
|
}
|
|
|
|
|
|
/***************************
|
|
parsePropertyValue(), Unit Ratio Calculation
|
|
***************************/
|
|
|
|
/* When queried, the browser returns (most) CSS property values in pixels. Therefore, if an endValue with a unit type of
|
|
%, em, or rem is animated toward, startValue must be converted from pixels into the same unit type as endValue in order
|
|
for value manipulation logic (increment/decrement) to proceed. Further, if the startValue was forcefed or transferred
|
|
from a previous call, startValue may also not be in pixels. Unit conversion logic therefore consists of two steps:
|
|
1) Calculating the ratio of %/em/rem/vh/vw relative to pixels
|
|
2) Converting startValue into the same unit of measurement as endValue based on these ratios. */
|
|
/* Unit conversion ratios are calculated by inserting a sibling node next to the target node, copying over its position property,
|
|
setting values with the target unit type then comparing the returned pixel value. */
|
|
/* Note: Even if only one of these unit types is being animated, all unit ratios are calculated at once since the overhead
|
|
of batching the SETs and GETs together upfront outweights the potential overhead
|
|
of layout thrashing caused by re-querying for uncalculated ratios for subsequently-processed properties. */
|
|
/* Todo: Shift this logic into the calls' first tick instance so that it's synced with RAF. */
|
|
function calculateUnitRatios() {
|
|
|
|
/**************************************************************
|
|
parsePropertyValue(), calculateUnitRatios(), Same Ratio Checks
|
|
**************************************************************/
|
|
|
|
/* The properties below are used to determine whether the element differs sufficiently from this call's
|
|
previously iterated element to also differ in its unit conversion ratios. If the properties match up with those
|
|
of the prior element, the prior element's conversion ratios are used. Like most optimizations in Collide,
|
|
this is done to minimize DOM querying. */
|
|
var sameRatioIndicators = {
|
|
myParent: element.parentNode || document.body, /* GET */
|
|
position: CSS.getPropertyValue(element, 'position'), /* GET */
|
|
fontSize: CSS.getPropertyValue(element, 'fontSize') /* GET */
|
|
};
|
|
|
|
/* Determine if the same % ratio can be used. % is based on the element's position value and its parent's width and height dimensions. */
|
|
var samePercentRatio = ((sameRatioIndicators.position === callUnitConversionData.lastPosition) && (sameRatioIndicators.myParent === callUnitConversionData.lastParent));
|
|
|
|
/* Determine if the same em ratio can be used. em is relative to the element's fontSize. */
|
|
var sameEmRatio = (sameRatioIndicators.fontSize === callUnitConversionData.lastFontSize);
|
|
|
|
/* Store these ratio indicators call-wide for the next element to compare against. */
|
|
callUnitConversionData.lastParent = sameRatioIndicators.myParent;
|
|
callUnitConversionData.lastPosition = sameRatioIndicators.position;
|
|
callUnitConversionData.lastFontSize = sameRatioIndicators.fontSize;
|
|
|
|
|
|
/**********************************************************************
|
|
parsePropertyValue(), calculateUnitRatios(), Element-Specific Units
|
|
**********************************************************************/
|
|
|
|
var measurement = 100,
|
|
unitRatios = {};
|
|
|
|
if (!sameEmRatio || !samePercentRatio) {
|
|
var dummy = data(element).isSVG ? document.createElementNS('http://www.w3.org/2000/svg', 'rect') : document.createElement('div');
|
|
|
|
Collide.init(dummy);
|
|
sameRatioIndicators.myParent.appendChild(dummy);
|
|
|
|
/* To accurately and consistently calculate conversion ratios, the element's cascaded overflow and box-sizing are stripped.
|
|
Similarly, since width/height can be artificially constrained by their min-/max- equivalents, these are controlled for as well. */
|
|
/* Note: Overflow must be also be controlled for per-axis since the overflow property overwrites its per-axis values. */
|
|
var cssPropNames = [ 'overflow', 'overflowX', 'overflowY' ];
|
|
for (var x = 0; x < overflows.length; x++) {
|
|
Collide.CSS.setPropertyValue(dummy, cssPropNames[x], 'hidden');
|
|
}
|
|
|
|
Collide.CSS.setPropertyValue(dummy, 'position', sameRatioIndicators.position);
|
|
Collide.CSS.setPropertyValue(dummy, 'fontSize', sameRatioIndicators.fontSize);
|
|
Collide.CSS.setPropertyValue(dummy, 'boxSizing', 'content-box');
|
|
|
|
/* width and height act as our proxy properties for measuring the horizontal and vertical % ratios. */
|
|
cssPropNames = [ 'minWidth', 'maxWidth', 'width', 'minHeight', 'maxHeight', 'height' ];
|
|
for (var x = 0; x < overflows.length; x++) {
|
|
Collide.CSS.setPropertyValue(dummy, cssPropNames[x], measurement + '%');
|
|
}
|
|
|
|
/* paddingLeft arbitrarily acts as our proxy property for the em ratio. */
|
|
Collide.CSS.setPropertyValue(dummy, 'paddingLeft', measurement + 'em');
|
|
|
|
/* Divide the returned value by the measurement to get the ratio between 1% and 1px. Default to 1 since working with 0 can produce Infinite. */
|
|
unitRatios.percentToPxWidth = callUnitConversionData.lastPercentToPxWidth = (parseFloat(CSS.getPropertyValue(dummy, 'width', null, true)) || 1) / measurement; /* GET */
|
|
unitRatios.percentToPxHeight = callUnitConversionData.lastPercentToPxHeight = (parseFloat(CSS.getPropertyValue(dummy, 'height', null, true)) || 1) / measurement; /* GET */
|
|
unitRatios.emToPx = callUnitConversionData.lastEmToPx = (parseFloat(CSS.getPropertyValue(dummy, 'paddingLeft')) || 1) / measurement; /* GET */
|
|
|
|
sameRatioIndicators.myParent.removeChild(dummy);
|
|
|
|
} else {
|
|
unitRatios.emToPx = callUnitConversionData.lastEmToPx;
|
|
unitRatios.percentToPxWidth = callUnitConversionData.lastPercentToPxWidth;
|
|
unitRatios.percentToPxHeight = callUnitConversionData.lastPercentToPxHeight;
|
|
}
|
|
|
|
|
|
/**********************************************************************
|
|
parsePropertyValue(), calculateUnitRatios(), Element-Agnostic Units
|
|
***********************************************************************/
|
|
|
|
/* Whereas % and em ratios are determined on a per-element basis, the rem unit only needs to be checked
|
|
once per call since it's exclusively dependant upon document.body's fontSize. If this is the first time
|
|
that calculateUnitRatios() is being run during this call, remToPx will still be set to its default value of null,
|
|
so we calculate it now. */
|
|
if (callUnitConversionData.remToPx === null) {
|
|
/* Default to browsers' default fontSize of 16px in the case of 0. */
|
|
callUnitConversionData.remToPx = parseFloat(CSS.getPropertyValue(document.body, 'fontSize')) || 16; /* GET */
|
|
}
|
|
|
|
/* Similarly, viewport units are %-relative to the window's inner dimensions. */
|
|
if (callUnitConversionData.vwToPx === null) {
|
|
callUnitConversionData.vwToPx = parseFloat(window.innerWidth) / 100; /* GET */
|
|
callUnitConversionData.vhToPx = parseFloat(window.innerHeight) / 100; /* GET */
|
|
}
|
|
|
|
unitRatios.remToPx = callUnitConversionData.remToPx;
|
|
unitRatios.vwToPx = callUnitConversionData.vwToPx;
|
|
unitRatios.vhToPx = callUnitConversionData.vhToPx;
|
|
|
|
if (Collide.debug >= 1) console.log('Unit ratios: ' + JSON.stringify(unitRatios), element);
|
|
|
|
return unitRatios;
|
|
|
|
} // END: calculateUnitRatios()
|
|
|
|
|
|
/****************************************
|
|
parsePropertyValue(), Unit Conversion
|
|
*****************************************/
|
|
|
|
/* The * and / operators, which are not passed in with an associated unit, inherently use startValue's unit. Skip value and unit conversion. */
|
|
if (/[\/*]/.test(operator)) {
|
|
endValueUnitType = startValueUnitType;
|
|
|
|
/* If startValue and endValue differ in unit type, convert startValue into the same unit type as endValue so that if endValueUnitType
|
|
is a relative unit (%, em, rem), the values set during tweening will continue to be accurately relative even if the metrics they depend
|
|
on are dynamically changing during the course of the animation. Conversely, if we always normalized into px and used px for setting values, the px ratio
|
|
would become stale if the original unit being animated toward was relative and the underlying metrics change during the animation. */
|
|
/* Since 0 is 0 in any unit type, no conversion is necessary when startValue is 0 -- we just start at 0 with endValueUnitutil. */
|
|
} else if ((startValueUnitType !== endValueUnitType) && startValue !== 0) {
|
|
/* Unit conversion is also skipped when endValue is 0, but *startValueUnitType* must be used for tween values to remain accurate. */
|
|
/* Note: Skipping unit conversion here means that if endValueUnitType was originally a relative unit, the animation won't relatively
|
|
match the underlying metrics if they change, but this is acceptable since we're animating toward invisibility instead of toward visibility,
|
|
which remains past the point of the animation's completion. */
|
|
if (endValue === 0) {
|
|
endValueUnitType = startValueUnitType;
|
|
|
|
} else {
|
|
/* By this point, we cannot avoid unit conversion (it's undesirable since it causes layout thrashing).
|
|
If we haven't already, we trigger calculateUnitRatios(), which runs once per element per call. */
|
|
elementUnitConversionData = elementUnitConversionData || calculateUnitRatios();
|
|
|
|
/* The following RegEx matches CSS properties that have their % values measured relative to the x-axis. */
|
|
/* Note: W3C spec mandates that all of margin and padding's properties (even top and bottom) are %-relative to the *width* of the parent element. */
|
|
var axis = (/margin|padding|left|right|width|text|word|letter/i.test(property) || /X$/.test(property) || property === 'x') ? 'x' : 'y';
|
|
|
|
/* In order to avoid generating n^2 bespoke conversion functions, unit conversion is a two-step process:
|
|
1) Convert startValue into pixels. 2) Convert this new pixel value into endValue's unit util. */
|
|
switch (startValueUnitType) {
|
|
case '%':
|
|
/* Note: translateX and translateY are the only properties that are %-relative to an element's own dimensions -- not its parent's dimensions.
|
|
Collide does not include a special conversion process to account for this behavior. Therefore, animating translateX/Y from a % value
|
|
to a non-% value will produce an incorrect start value. Fortunately, this sort of cross-unit conversion is rarely done by users in practice. */
|
|
startValue *= (axis === 'x' ? elementUnitConversionData.percentToPxWidth : elementUnitConversionData.percentToPxHeight);
|
|
break;
|
|
|
|
case 'px':
|
|
/* px acts as our midpoint in the unit conversion process; do nothing. */
|
|
break;
|
|
|
|
default:
|
|
startValue *= elementUnitConversionData[startValueUnitType + 'ToPx'];
|
|
}
|
|
|
|
/* Invert the px ratios to convert into to the target unit. */
|
|
switch (endValueUnitType) {
|
|
case '%':
|
|
startValue *= 1 / (axis === 'x' ? elementUnitConversionData.percentToPxWidth : elementUnitConversionData.percentToPxHeight);
|
|
break;
|
|
|
|
case 'px':
|
|
/* startValue is already in px, do nothing; we're done. */
|
|
break;
|
|
|
|
default:
|
|
startValue *= 1 / elementUnitConversionData[endValueUnitType + 'ToPx'];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/****************************************
|
|
parsePropertyValue(), Relative Values
|
|
****************************************/
|
|
|
|
/* Operator logic must be performed last since it requires unit-normalized start and end values. */
|
|
/* Note: Relative *percent values* do not behave how most people think; while one would expect '+=50%'
|
|
to increase the property 1.5x its current value, it in fact increases the percent units in absolute terms:
|
|
50 points is added on top of the current % value. */
|
|
switch (operator) {
|
|
case '+':
|
|
endValue = startValue + endValue;
|
|
break;
|
|
|
|
case '-':
|
|
endValue = startValue - endValue;
|
|
break;
|
|
|
|
case '*':
|
|
endValue = startValue * endValue;
|
|
break;
|
|
|
|
case '/':
|
|
endValue = startValue / endValue;
|
|
break;
|
|
}
|
|
|
|
|
|
/*********************************************
|
|
parsePropertyValue(), tweensContainer Push
|
|
*********************************************/
|
|
|
|
/* Construct the per-property tween object, and push it to the element's tweensContainer. */
|
|
tweensContainer[property] = {
|
|
rootPropertyValue: rootPropertyValue,
|
|
startValue: startValue,
|
|
currentValue: startValue,
|
|
endValue: endValue,
|
|
unitType: endValueUnitType,
|
|
easing: easing
|
|
};
|
|
|
|
if (Collide.debug) console.log('tweensContainer (' + property + '): ' + JSON.stringify(tweensContainer[property]), element);
|
|
}
|
|
|
|
/* Along with its property data, store a reference to the element itself onto tweensContainer. */
|
|
tweensContainer.element = element;
|
|
|
|
} // END: parsePropertyValue()
|
|
|
|
|
|
/*****************
|
|
Call Push
|
|
*****************/
|
|
|
|
/* Note: tweensContainer can be empty if all of the properties in this call's property map were skipped due to not
|
|
being supported by the browser. The element property is used for checking that the tweensContainer has been appended to. */
|
|
if (tweensContainer.element) {
|
|
|
|
/* The call array houses the tweensContainers for each element being animated in the current call. */
|
|
call.push(tweensContainer);
|
|
|
|
/* Store the tweensContainer and options if we're working on the default effects queue, so that they can be used by the reverse command. */
|
|
if (opts.queue === '') {
|
|
data(element).tweensContainer = tweensContainer;
|
|
data(element).opts = opts;
|
|
}
|
|
|
|
/* Switch on the element's animating flag. */
|
|
data(element).isAnimating = true;
|
|
|
|
/* Once the final element in this call's element set has been processed, push the call array onto
|
|
Collide.State.calls for the animation tick to immediately begin processing. */
|
|
if (elementsIndex === elementsLength - 1) {
|
|
/* Add the current call plus its associated metadata (the element set and the call's options) onto the global call container.
|
|
Anything on this call container is subjected to tick() processing. */
|
|
Collide.State.calls.push([ call, elements, opts, null, resolve ]);
|
|
|
|
} else {
|
|
elementsIndex++;
|
|
}
|
|
}
|
|
|
|
} // END: buildQueue
|
|
|
|
|
|
/* When the queue option is set to false, the call skips the element's queue and fires immediately. */
|
|
if (opts.queue === false) {
|
|
/* Since this buildQueue call doesn't respect the element's existing queue (which is where a delay option would have been appended),
|
|
we manually inject the delay property here with an explicit setTimeout. */
|
|
if (opts.delay) {
|
|
setTimeout(buildQueue, opts.delay);
|
|
} else {
|
|
buildQueue();
|
|
}
|
|
|
|
/* Otherwise, the call undergoes element queueing as normal. */
|
|
} else {
|
|
Collide.queue(element, opts.queue, function(next, clearQueue) {
|
|
/* If the clearQueue flag was passed in by the stop command, resolve this call's promise. (Promises can only be resolved once,
|
|
so it's fine if this is repeatedly triggered for each element in the associated call.) */
|
|
if (clearQueue === true) {
|
|
/* Do not continue with animation queueing. */
|
|
resolve(elements);
|
|
return true;
|
|
}
|
|
|
|
/* This flag indicates to the upcoming completeCall() function that this queue entry was initiated by Collide.
|
|
See completeCall() for further details. */
|
|
Collide.collideQueueEntryFlag = true;
|
|
|
|
buildQueue(next);
|
|
});
|
|
}
|
|
|
|
|
|
/*********************
|
|
Auto-Dequeuing
|
|
*********************/
|
|
|
|
/* To fire the first non-custom-queue entry on an element, the element
|
|
must be dequeued if its queue stack consists *solely* of the current call. (This can be determined by checking
|
|
for the 'inprogress' item that is prepended to active queue stack arrays.) Regardless, whenever the element's
|
|
queue is further appended with additional items -- including delay()'s calls, the queue's
|
|
first entry is automatically fired. This behavior contrasts that of custom queues, which never auto-fire. */
|
|
/* Note: When an element set is being subjected to a non-parallel Collide call, the animation will not begin until
|
|
each one of the elements in the set has reached the end of its individually pre-existing queue chain. */
|
|
if ((opts.queue === '' || opts.queue === 'fx') && Collide.queue(element)[0] !== 'inprogress') {
|
|
Collide.dequeue(element);
|
|
}
|
|
|
|
return promise;
|
|
}
|