From e67ff15e08e1005297fe585cb30d0edcefbeb801 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Sat, 25 Apr 2015 10:14:21 -0500 Subject: [PATCH] collide updates --- src/collide/animator.js | 370 ------- src/collide/collide.js | 89 +- src/collide/complete-call.js | 172 ++++ src/collide/css.js | 6 +- src/collide/easing.js | 7 +- src/collide/element-process.js | 945 ++++++++++++++++++ src/collide/init-transition.js | 233 ----- src/collide/process-transition.js | 920 ----------------- src/collide/tick.js | 265 +++++ src/collide/transition-action.js | 155 +++ src/collide/transition.js | 15 +- src/components/app/test/transitions/main.html | 5 +- 12 files changed, 1619 insertions(+), 1563 deletions(-) delete mode 100644 src/collide/animator.js create mode 100644 src/collide/complete-call.js create mode 100644 src/collide/element-process.js delete mode 100644 src/collide/init-transition.js delete mode 100644 src/collide/process-transition.js create mode 100644 src/collide/tick.js create mode 100644 src/collide/transition-action.js diff --git a/src/collide/animator.js b/src/collide/animator.js deleted file mode 100644 index 3669c0206c..0000000000 --- a/src/collide/animator.js +++ /dev/null @@ -1,370 +0,0 @@ -import {raf} from 'ionic2/util/dom' - - - -class GlobalAnimator { - - constructor() { - console.log('Animator instance', Math.random()) - - this.isTicking = false - this.calls = [] - } - - run(animation) { - animation.timeStart = null; - this.calls.push(animation); - - if (!this.isTicking) { - this.tick() - } - } - - /* Note: All calls to Animator are pushed to the this.calls array, which is fully iterated through upon each tick. */ - tick (timestamp) { - console.log('tick', timestamp) - - /* An empty timestamp argument indicates that this is the first tick occurence since ticking was turned on. - We leverage this metadata to fully ignore the first tick pass since RAF's initial pass is fired whenever - the browser's next tick sync time occurs, which results in the first elements subjected to Animator - calls being animated out of sync with any elements animated immediately thereafter. In short, we ignore - the first RAF tick pass so that elements being immediately consecutively animated -- instead of simultaneously animated - by the same Animator call -- are properly batched into the same initial RAF tick and consequently remain in sync thereafter. */ - if (timestamp) { - /* We ignore RAF's high resolution timestamp since it can be significantly offset when the browser is - under high stress; we opt for choppiness over allowing the browser to drop huge chunks of frames. */ - var timeCurrent = (new Date).getTime(); - - /******************** - Call Iteration - ********************/ - - var callsLength = this.calls.length; - - /* To speed up iterating over this array, it is compacted (falsey items -- calls that have completed -- are removed) - when its length has ballooned to a point that can impact tick performance. This only becomes necessary when animation - has been continuous with many elements over a long period of time; whenever all active calls are completed, completeCall() clears this.calls. */ - if (callsLength > 10000) { - this.calls = compactSparseArray(this.calls); - } - - /* Iterate through each active call. */ - for (var i = 0; i < callsLength; i++) { - /* When a Animator call is completed, its this.calls entry is set to false. Continue on to the next call. */ - if (!this.calls[i]) { - continue; - } - - /************************ - Call-Wide Variables - ************************/ - - var animation = this.calls[i]; - var timeStart = animation.timeStart; - var firstTick = !!timeStart; - var opts = animation.options(); - - /* If timeStart is undefined, then this is the first time that this call has been processed by tick(). - We assign timeStart now so that its value is as close to the real animation start time as possible. - (Conversely, had timeStart been defined when this call was added to this.calls, the delay - between that time and now would cause the first few frames of the tween to be skipped since - percentComplete is calculated relative to timeStart.) */ - /* Further, subtract 16ms (the approximate resolution of RAF) from the current time value so that the - first tick iteration isn't wasted by animating at 0% tween completion, which would produce the - same style value as the element's current value. */ - if (!timeStart) { - timeStart = timeCurrent - 16; - } - - /* The tween's completion percentage is relative to the tween's start time, not the tween's start value - (which would result in unpredictable tween durations since JavaScript's timers are not particularly accurate). - Accordingly, we ensure that percentComplete does not exceed 1. */ - var percentComplete = Math.min((timeCurrent - timeStart) / opts.duration, 1); - - - /********************** - Element Iteration - **********************/ - - for (var j = 0, elementCount = animation.elements.length; j < elementCount; j++) { - var element = animation.elements[j]; - - /* Check to see if this element has been deleted midway through the animation */ - if (!element.parent) { - continue; - } - - var transformPropertyExists = false; - - - /********************************** - Display & Visibility Toggling - **********************************/ - - /* If the display option is set to non-"none", set it upfront so that the element can become visible before tweening begins. - (Otherwise, display's "none" value is set in completeCall() once the animation has completed.) */ - if (opts.display !== undefined && opts.display !== null && opts.display !== "none") { - if (opts.display === "flex") { - - for (var x = 0; x < flexValues.length; x++) { - setPropertyValue(element, "display", flexValues[x]); - } - } - - setPropertyValue(element, "display", opts.display); - } - - /* Same goes with the visibility option, but its "none" equivalent is "hidden". */ - if (opts.visibility !== undefined && opts.visibility !== "hidden") { - setPropertyValue(element, "visibility", opts.visibility); - } - - - /************************ - Property Iteration - ************************/ - - /* For every element, iterate through each property. */ - for (var property in tweensContainer) { - /* Note: In addition to property tween data, tweensContainer contains a reference to its associated element. */ - if (property !== "element") { - var tween = tweensContainer[property], - currentValue, - /* Easing can either be a pre-genereated function or a string that references a pre-registered easing - on the Animator.Easings object. In either case, return the appropriate easing *function*. */ - easing = Type.isString(tween.easing) ? Animator.Easings[tween.easing] : tween.easing; - - /****************************** - Current Value Calculation - ******************************/ - - /* If this is the last tick pass (if we've reached 100% completion for this tween), - ensure that currentValue is explicitly set to its target endValue so that it's not subjected to any rounding. */ - if (percentComplete === 1) { - currentValue = tween.endValue; - /* Otherwise, calculate currentValue based on the current delta from startValue. */ - } else { - var tweenDelta = tween.endValue - tween.startValue; - currentValue = tween.startValue + (tweenDelta * easing(percentComplete, opts, tweenDelta)); - - /* If no value change is occurring, don't proceed with DOM updating. */ - if (!firstTick && (currentValue === tween.currentValue)) { - continue; - } - } - - tween.currentValue = currentValue; - - /* If we're tweening a fake 'tween' property in order to log transition values, update the one-per-call variable so that - it can be passed into the progress callback. */ - if (property === "tween") { - tweenDummyValue = currentValue; - } else { - /****************** - Hooks: Part I - ******************/ - - /* For hooked properties, the newly-updated rootPropertyValueCache is cached onto the element so that it can be used - for subsequent hooks in this call that are associated with the same root property. If we didn't cache the updated - rootPropertyValue, each subsequent update to the root property in this tick pass would reset the previous hook's - updates to rootPropertyValue prior to injection. A nice performance byproduct of rootPropertyValue caching is that - subsequently chained animations using the same hookRoot but a different hook can use this cached rootPropertyValue. */ - if (CSS.Hooks.registered[property]) { - var hookRoot = CSS.Hooks.getRoot(property), - rootPropertyValueCache = Data(element).rootPropertyValueCache[hookRoot]; - - if (rootPropertyValueCache) { - tween.rootPropertyValue = rootPropertyValueCache; - } - } - - /***************** - DOM Update - *****************/ - - /* setPropertyValue() returns an array of the property name and property value post any normalization that may have been performed. */ - /* Note: To solve an IE<=8 positioning bug, the unit type is dropped when setting a property value of 0. */ - var adjustedSetData = setPropertyValue(element, /* SET */ - property, - tween.currentValue + (parseFloat(currentValue) === 0 ? "" : tween.unitType), - tween.rootPropertyValue, - tween.scrollData); - - /******************* - Hooks: Part II - *******************/ - - /* Now that we have the hook's updated rootPropertyValue (the post-processed value provided by adjustedSetData), cache it onto the element. */ - if (CSS.Hooks.registered[property]) { - /* Since adjustedSetData contains normalized data ready for DOM updating, the rootPropertyValue needs to be re-extracted from its normalized form. ?? */ - if (CSS.Normalizations.registered[hookRoot]) { - Data(element).rootPropertyValueCache[hookRoot] = CSS.Normalizations.registered[hookRoot]("extract", null, adjustedSetData[1]); - } else { - Data(element).rootPropertyValueCache[hookRoot] = adjustedSetData[1]; - } - } - - /*************** - Transforms - ***************/ - - /* Flag whether a transform property is being animated so that flushTransformCache() can be triggered once this tick pass is complete. */ - if (adjustedSetData[0] === "transform") { - transformPropertyExists = true; - } - - } - } - } - - /**************** - mobileHA - ****************/ - - /* If mobileHA is enabled, set the translate3d transform to null to force hardware acceleration. - It's safe to override this property since Animator doesn't actually support its animation (hooks are used in its place). */ - if (opts.mobileHA) { - /* Don't set the null transform hack if we've already done so. */ - if (Data(element).transformCache.translate3d === undefined) { - /* All entries on the transformCache object are later concatenated into a single transform string via flushTransformCache(). */ - Data(element).transformCache.translate3d = "(0px, 0px, 0px)"; - - transformPropertyExists = true; - } - } - - if (transformPropertyExists) { - CSS.flushTransformCache(element); - } - } - - /* The non-"none" display value is only applied to an element once -- when its associated call is first ticked through. - Accordingly, it's set to false so that it isn't re-processed by this call in the next tick. */ - if (opts.display !== undefined && opts.display !== "none") { - this.calls[i][2].display = false; - } - if (opts.visibility !== undefined && opts.visibility !== "hidden") { - this.calls[i][2].visibility = false; - } - - /* Pass the elements and the timing data (percentComplete, msRemaining, timeStart, tweenDummyValue) into the progress callback. */ - if (opts.progress) { - opts.progress.call(callContainer[1], - callContainer[1], - percentComplete, - Math.max(0, (timeStart + opts.duration) - timeCurrent), - timeStart, - tweenDummyValue); - } - - /* If this call has finished tweening, pass its index to completeCall() to handle call cleanup. */ - if (percentComplete === 1) { - completeCall(i); - } - } - } - - /* Note: completeCall() sets the isTicking flag to false when the last call on this.calls has completed. */ - if (this.isTicking) { - raf(tick); - } - - } - -} - - -const flexValues = [ "-webkit-box", "-moz-box", "-ms-flexbox", "-webkit-flex" ]; - - -/* Array compacting. Copyright Lo-Dash. MIT License: https://github.com/lodash/lodash/blob/master/LICENSE.txt */ -function compactSparseArray (array) { - var index = -1, - length = array ? array.length : 0, - result = []; - - while (++index < length) { - var value = array[index]; - - if (value) { - result.push(value); - } - } - - return result; -} - -function setPropertyValue(element, property, propertyValue, rootPropertyValue, scrollData) { - var propertyName = property; - - /* In order to be subjected to call options and element queueing, scroll animation is routed through Velocity as if it were a standard CSS property. */ - if (property === "scroll") { - /* If a container option is present, scroll the container instead of the browser window. */ - if (scrollData.container) { - scrollData.container["scroll" + scrollData.direction] = propertyValue; - /* Otherwise, Velocity defaults to scrolling the browser window. */ - } else { - if (scrollData.direction === "Left") { - window.scrollTo(propertyValue, scrollData.alternateValue); - } else { - window.scrollTo(scrollData.alternateValue, propertyValue); - } - } - } else { - /* Transforms (translateX, rotateZ, etc.) are applied to a per-element transformCache object, which is manually flushed via flushTransformCache(). - Thus, for now, we merely cache transforms being SET. */ - if (CSS.Normalizations.registered[property] && CSS.Normalizations.registered[property]("name", element) === "transform") { - /* Perform a normalization injection. */ - /* Note: The normalization logic handles the transformCache updating. */ - CSS.Normalizations.registered[property]("inject", element, propertyValue); - - propertyName = "transform"; - propertyValue = Data(element).transformCache[property]; - } else { - /* Inject hooks. */ - if (CSS.Hooks.registered[property]) { - var hookName = property, - hookRoot = CSS.Hooks.getRoot(property); - - /* If a cached rootPropertyValue was not provided, query the DOM for the hookRoot's current value. */ - rootPropertyValue = rootPropertyValue || CSS.getPropertyValue(element, hookRoot); /* GET */ - - propertyValue = CSS.Hooks.injectValue(hookName, propertyValue, rootPropertyValue); - property = hookRoot; - } - - /* Normalize names and values. */ - if (CSS.Normalizations.registered[property]) { - propertyValue = CSS.Normalizations.registered[property]("inject", element, propertyValue); - property = CSS.Normalizations.registered[property]("name", element); - } - - /* Assign the appropriate vendor prefix before performing an official style update. */ - propertyName = CSS.Names.prefixCheck(property)[0]; - - /* A try/catch is used for IE<=8, which throws an error when "invalid" CSS values are set, e.g. a negative width. - Try/catch is avoided for other browsers since it incurs a performance overhead. */ - if (IE <= 8) { - try { - element.style[propertyName] = propertyValue; - } catch (error) { if (Velocity.debug) console.log("Browser does not support [" + propertyValue + "] for [" + propertyName + "]"); } - /* SVG elements have their dimensional properties (width, height, x, y, cx, etc.) applied directly as attributes instead of as styles. */ - /* Note: IE8 does not support SVG elements, so it's okay that we skip it for SVG animation. */ - } else if (Data(element) && Data(element).isSVG && CSS.Names.SVGAttribute(property)) { - /* Note: For SVG attributes, vendor-prefixed property names are never used. */ - /* Note: Not all CSS properties can be animated via attributes, but the browser won't throw an error for unsupported properties. */ - element.setAttribute(property, propertyValue); - } else { - element.style[propertyName] = propertyValue; - } - - if (Velocity.debug >= 2) console.log("Set " + property + " (" + propertyName + "): " + propertyValue); - } - } - - /* Return the normalized property name and value in case the caller wants to know how these values were modified before being applied to the DOM. */ - return [ propertyName, propertyValue ]; -} - - -export let Animator = new GlobalAnimator() diff --git a/src/collide/collide.js b/src/collide/collide.js index 37dcfb04b4..69ffc02157 100644 --- a/src/collide/collide.js +++ b/src/collide/collide.js @@ -1,10 +1,36 @@ import {dom} from 'ionic2/util' -export const Collide = { +export let Collide = { + /* Container for page-wide Collide state data. */ + State: { + /* Create a cached element for re-use when checking for CSS property prefixes. */ + prefixElement: document.createElement('div'), + + /* Cache every prefix match to avoid repeating lookups. */ + prefixMatches: {}, + + /* Cache the anchor used for animating window scrolling. */ + scrollAnchor: null, + + /* Cache the browser-specific property names associated with the scroll anchor. */ + scrollPropertyLeft: null, + scrollPropertyTop: null, + + /* Keep track of whether our RAF tick is running. */ + isTicking: false, + + /* Container for every in-progress call to Collide. */ + calls: [] + }, + + CSS: {}, + Easings: {}, + + /* Collide option defaults, which can be overriden by the user. */ defaults: { - queue: "", + queue: '', duration: 400, easing: 'swing', begin: undefined, @@ -14,32 +40,20 @@ export const Collide = { visibility: undefined, loop: false, delay: false, - cacheValues: true + /* Advanced: Set to false to prevent property values from being cached between consecutive Collide-initiated chain calls. */ + _cacheValues: true }, - data: function(element, key, value) { - if (value === undefined) { + /* Used for getting/setting Collide's hooked CSS properties. */ + hook: null, /* Defined below. */ - if (key === undefined) { - // get data object: Data(element) - return element.$collide; - } + /* Collide-wide animation time remapping for testing purposes. */ + mock: false, - if (element.$collide) { - // get data by key: Data(element, key) - return element.$collide[key]; - } - - } else if (key !== undefined) { - // set data: Data(element, key, value) - if (!element.$collide) { - element.$collide = {}; - } - element.$collide[key] = value; - } - - }, + /* Set to 1 or 2 (most verbose) to output debug info to console. */ + debug: false, + /* initialize element data */ initData: function(element) { element.$collide = { /* Store whether this is an SVG element, since its properties are retrieved and updated differently than standard HTML elements. */ @@ -66,6 +80,31 @@ export const Collide = { }; }, + /* get/set element data */ + data: function(element, key, value) { + if (value === undefined) { + + if (key === undefined) { + // get data object: Data(element) + return element.$collide; + } + + if (element.$collide) { + // get data by key: Data(element, key) + return element.$collide[key]; + } + + } else if (key !== undefined) { + // set data: Data(element, key, value) + if (!element.$collide) { + element.$collide = {}; + } + element.$collide[key] = value; + } + + }, + + /* get/set element queue data */ queue: function (element, type, data) { function $makeArray (arr, results) { let ret = results || []; @@ -99,6 +138,7 @@ export const Collide = { return q; }, + /* dequeue element */ dequeue: function (element, type) { type = type || 'collide'; @@ -120,5 +160,4 @@ export const Collide = { } } -} - +}; diff --git a/src/collide/complete-call.js b/src/collide/complete-call.js new file mode 100644 index 0000000000..68e89ed44d --- /dev/null +++ b/src/collide/complete-call.js @@ -0,0 +1,172 @@ +/* Ported from Velocity.js, MIT License. Julian Shapiro http://twitter.com/shapiro */ + +import {Collide} from 'ionic2/collide/collide' +import {CSS} from 'ionic2/collide/css' + + +/*************************** + Call Completion +***************************/ + +/* Note: Unlike tick(), which processes all active calls at once, call completion is handled on a per-call basis. */ +export function completeCall (callIndex, isStopped) { + + /* Ensure the call exists. */ + if (!Collide.State.calls[callIndex]) { + return false; + } + + /* Pull the metadata from the call. */ + var call = Collide.State.calls[callIndex][0], + elements = Collide.State.calls[callIndex][1], + opts = Collide.State.calls[callIndex][2], + resolve = Collide.State.calls[callIndex][4]; + + var remainingCallsExist = false; + + + /************************* + Element Finalization + *************************/ + + for (var i = 0, callLength = call.length; i < callLength; i++) { + var element = call[i].element; + var eleData = Collide.data(element); + + /* If the user set display to 'none' (intending to hide the element), set it now that the animation has completed. */ + /* Note: display:none isn't set when calls are manually stopped (via Collide('stop'). */ + /* Note: Display gets ignored with 'reverse' calls and infinite loops, since this behavior would be undesirable. */ + if (!isStopped && !opts.loop) { + if (opts.display === 'none') { + CSS.setPropertyValue(element, 'display', opts.display); + } + + if (opts.visibility === 'hidden') { + CSS.setPropertyValue(element, 'visibility', opts.visibility); + } + } + + /* If the element's queue is empty (if only the 'inprogress' item is left at position 0) or if its queue is about to run + a non-Collide-initiated entry, turn off the isAnimating flag. A non-Collide-initiatied queue entry's logic might alter + an element's CSS values and thereby cause Collide's cached value data to go stale. To detect if a queue entry was initiated by Collide, + we check for the existence of our special Collide.queueEntryFlag declaration, which minifiers won't rename since the flag + is assigned to's global object and thus exists out of Collide's own scope. */ + if (opts.loop !== true && (Collide.queue(element)[1] === undefined || !/\.collideQueueEntryFlag/i.test(Collide.queue(element)[1]))) { + /* The element may have been deleted. Ensure that its data cache still exists before acting on it. */ + + if (eleData) { + eleData.isAnimating = false; + + /* Clear the element's rootPropertyValueCache, which will become stale. */ + eleData.rootPropertyValueCache = {}; + + /* If any 3D transform subproperty is at its default value (regardless of unit type), remove it. */ + for (var x = 0, transforms3DLength = CSS.Lists.transforms3D.length; i < transforms3DLength; i++) { + var transformName = CSS.Lists.transforms3D[x]; + var defaultValue = /^scale/.test(transformName) ? 1 : 0; + var currentValue = eleData.transformCache[transformName]; + + if (currentValue !== undefined && new RegExp('^\\(' + defaultValue + '[^.]').test(currentValue)) { + delete eleData.transformCache[transformName]; + } + } + + /* Flush the subproperty removals to the DOM. */ + CSS.flushTransformCache(element); + } + } + + + /********************* + Option: Complete + *********************/ + + /* Complete is fired once per call (not once per element) and is passed the full raw DOM element set as both its context and its first argument. */ + /* Note: Callbacks aren't fired when calls are manually stopped (via Collide('stop'). */ + if (!isStopped && opts.complete && !opts.loop && (i === callLength - 1)) { + /* We throw callbacks in a setTimeout so that thrown errors don't halt the execution of Collide itself. */ + try { + opts.complete.call(elements, elements); + } catch (error) { + setTimeout(function() { throw error; }, 1); + } + } + + + /********************** + Promise Resolving + **********************/ + + /* Note: Infinite loops don't return promises. */ + if (resolve && opts.loop !== true) { + resolve(elements); + } + + + /**************************** + Option: Loop (Infinite) + ****************************/ + + if (eleData && opts.loop === true && !isStopped) { + /* If a rotateX/Y/Z property is being animated to 360 deg with loop:true, swap tween start/end values to enable + continuous iterative rotation looping. (Otherise, the element would just rotate back and forth.) */ + + for (var propertyName in eleData.tweensContainer) { + var tweenContainer = eleData.tweensContainer[propertyName] + + if (/^rotate/.test(propertyName) && parseFloat(tweenContainer.endValue) === 360) { + tweenContainer.endValue = 0; + tweenContainer.startValue = 360; + } + + if (/^backgroundPosition/.test(propertyName) && parseFloat(tweenContainer.endValue) === 100 && tweenContainer.unitType === '%') { + tweenContainer.endValue = 0; + tweenContainer.startValue = 100; + } + } + + //TODO!!! FIXME!!! + //Collide(element, 'reverse', { loop: true, delay: opts.delay }); + } + + + /*************** + Dequeueing + ***************/ + + /* Fire the next call in the queue so long as this call's queue wasn't set to false (to trigger a parallel animation), + which would have already caused the next call to fire. Note: Even if the end of the animation queue has been reached, + Collide.dequeue() must still be called in order to completely clear jQuery's animation queue. */ + if (opts.queue !== false) { + Collide.dequeue(element, opts.queue); + } + + } // END: for (var i = 0, callLength = call.length; i < callLength; i++) + + + /*********************** + Calls Array Cleanup + ************************/ + + /* Since this call is complete, set it to false so that the rAF tick skips it. This array is later compacted via compactSparseArray(). + (For performance reasons, the call is set to false instead of being deleted from the array: http://www.html5rocks.com/en/tutorials/speed/v8/) */ + Collide.State.calls[callIndex] = false; + + /* Iterate through the calls array to determine if this was the final in-progress animation. + If so, set a flag to end ticking and clear the calls array. */ + for (var j = 0, callsLength = Collide.State.calls.length; j < callsLength; j++) { + if (Collide.State.calls[j] !== false) { + remainingCallsExist = true; + break; + } + } + + if (remainingCallsExist === false) { + /* tick() will detect this flag upon its next iteration and subsequently turn itself off. */ + Collide.State.isTicking = false; + + /* Clear the calls array so that its length is reset. */ + delete Collide.State.calls; + Collide.State.calls = []; + } +} diff --git a/src/collide/css.js b/src/collide/css.js index ec9f1de7cb..1e3e363bc0 100644 --- a/src/collide/css.js +++ b/src/collide/css.js @@ -510,7 +510,7 @@ export var CSS = { } /* Check if the browser supports this property as prefixed. */ - if (typeof prefixElement.style[propertyPrefixed] === 'string') { + if (typeof Collide.State.prefixElement.style[propertyPrefixed] === 'string') { /* Cache the match. */ CSS.Names.prefixMatches[property] = propertyPrefixed; @@ -865,7 +865,7 @@ export var CSS = { /* Note: Collide applies transform properties in the same order that they are chronogically introduced to the element's CSS styles. */ flushTransformCache: function(element) { var transformString = ''; - var transformCache = eleData(element).transformCache; + var transformCache = data(element).transformCache; var transformValue, perspective; @@ -894,5 +894,3 @@ export var CSS = { }; const vendorPrefixes = [ '', 'Webkit', 'ms' ]; - -var prefixElement = document.createElement("div"); diff --git a/src/collide/easing.js b/src/collide/easing.js index bfece8799f..2a876c4205 100644 --- a/src/collide/easing.js +++ b/src/collide/easing.js @@ -1,6 +1,7 @@ /* Ported from Velocity.js, MIT License. Julian Shapiro http://twitter.com/shapiro */ import * as util from 'ionic2/util/util' +import {Collide} from 'ionic2/collide/collide' /************** @@ -228,7 +229,7 @@ var generateSpringRK4 = (function () { /* default easings. */ -let easings = { +Collide.Easings = { linear: function(p) { return p; }, swing: function(p) { return 0.5 - Math.cos( p * Math.PI ) / 2 }, spring: function(p) { return 1 - (Math.cos(p * 4.5 * Math.PI) * Math.exp(-p * 6)); } @@ -267,7 +268,7 @@ let easings = { ]; for (let x = 0; x < penner.length; x++) { - easings[ penner[x][0] ] = generateBezier.apply(null, penner[x][1]); + Collide.Easings[ penner[x][0] ] = generateBezier.apply(null, penner[x][1]); } })(); @@ -281,7 +282,7 @@ export function getEasing(value, duration) { or it can be a two-/four-item array of integers to be converted into a bezier/spring function. */ if (util.isString(value)) { /* Ensure that the easing has been assigned to standard easings object. */ - if (!easings[value]) { + if (!Collide.Easings[value]) { easing = false; } diff --git a/src/collide/element-process.js b/src/collide/element-process.js new file mode 100644 index 0000000000..91b3ff6aaa --- /dev/null +++ b/src/collide/element-process.js @@ -0,0 +1,945 @@ +/* Ported from Velocity.js, MIT License. Julian Shapiro http://twitter.com/shapiro */ + +import * as util from 'ionic2/util/util' +import {Collide} from 'ionic2/collide/collide' +import {CSS} from 'ionic2/collide/css' +import {getEasing} from 'ionic2/collide/easing' +import {tick} from 'ionic2/collide/tick' + +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 elementProcess(action, elements, elementsIndex, options, propertiesMap) { + var resolve; + var promise = new Promise(function(res) { + resolve = res; + }); + + var element = elements[elementsIndex]; + var elementsLength = elements.length; + + + /************************** + Call-Wide Variables + **************************/ + + /* A container for CSS unit conversion ratios (e.g. %, rem, and em ==> px) that is used to cache ratios across all elements + being animated in a single Collide call. Calculating unit ratios necessitates DOM querying and updating, and is therefore + avoided (via caching) wherever possible. This container is call-wide instead of page-wide to avoid the risk of using stale + conversion metrics across Collide animations that are not immediately consecutively chained. */ + var callUnitConversionData = { + lastParent: null, + lastPosition: null, + lastFontSize: null, + lastPercentToPxWidth: null, + lastPercentToPxHeight: null, + lastEmToPx: null, + remToPx: null, + vwToPx: null, + vhToPx: null + }; + + /* A container for all the ensuing tween data and metadata associated with this call. This container gets pushed to the page-wide + Collide.State.calls array that is processed during animation ticking. */ + var call = []; + + + /************************* + 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). */ + /* Note: Collide rolls its own delay function since jQuery doesn't have a utility alias for $.fn.delay() + (and thus requires jQuery element creation, which we avoid since its overhead includes DOM querying). */ + 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 either a jQuery object or a raw DOM element was passed in. */ + if (util.isWrapped(opts.container) || util.isNode(opts.container)) { + /* Extract the raw DOM element from the jQuery wrapper. */ + opts.container = opts.container[0] || opts.container; + /* 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 */ + + /* $.position() values are relative to the container's currently viewable area (without taking into account the container's true dimensions + -- say, 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 + $(element).position()[scrollDirection.toLowerCase()]) + scrollOffset; /* GET */ + + } else { + /* If a value other than a jQuery object or 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 $.position(), $.offset() 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 = $(element).offset()[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 + }; + + + /****************************************** + 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; + } + } + } + + 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. Read Velocity's docmentation to learn more about forcefeeding: VelocityJS.org/#forcefeeding */ + 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 + **********************************************************************/ + + /* Note: IE8 rounds to the nearest pixel when returning CSS values, thus we perform conversions using a measurement + of 100 (instead of 1) to give our ratios a precision of at least 2 decimal values. */ + 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 ]); + + /* If the animation tick isn't running, start it. (Collide shuts it off when there are no active calls to process.) */ + if (Collide.State.isTicking === false) { + Collide.State.isTicking = true; + + /* Start the tick loop. */ + tick(); + } + + } 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 jQuery prepends to active queue stack arrays.) Regardless, whenever the element's + queue is further appended with additional items -- including $.delay()'s or even $.animate() 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. */ + /* Note: Unfortunately, most people don't fully grasp jQuery's powerful, yet quirky, Collide.queue() function. + Lean more here: http://stackoverflow.com/questions/1058158/can-somebody-explain-jquery-queue-to-me */ + if ((opts.queue === '' || opts.queue === 'fx') && Collide.queue(element)[0] !== 'inprogress') { + Collide.dequeue(element); + } + + return promise; +} diff --git a/src/collide/init-transition.js b/src/collide/init-transition.js deleted file mode 100644 index b565511f46..0000000000 --- a/src/collide/init-transition.js +++ /dev/null @@ -1,233 +0,0 @@ -/* Ported from Collide.js, MIT License. Julian Shapiro http://twitter.com/shapiro */ - -import {Collide} from 'ionic2/collide/collide' -import {Animator} from 'ionic2/collide/animator' - - -export function initTransition(action, elements, options, propertiesMap) { - - elements = elements && !elements.length ? [elements] : elements - - if (!elements) { - return; - } - - /* The length of the element set (in the form of a nodeList or an array of elements) is defaulted to 1 in case a - single raw DOM element is passed in (which doesn't contain a length property). */ - var elementsLength = elements.length; - var elementsIndex = 0; - var eleData; - - - /********************* - Action Detection - *********************/ - - switch (action) { - - case 'stop': - /****************** - Action: Stop - *******************/ - - /* Clear the currently-active delay on each targeted element. */ - for (var x = 0; x < elements.length; x++) { - eleData = Collide.data(elements[x]); - - if (eleData && eleData.delayTimer) { - /* Stop the timer from triggering its cached next() function. */ - clearTimeout(eleData.delayTimer.setTimeout); - - /* Manually call the next() function so that the subsequent queue items can progress. */ - if (eleData.delayTimer.next) { - eleData.delayTimer.next(); - } - - delete eleData.delayTimer; - } - } - - - var callsToStop = []; - var activeCall; - - /* When the stop action is triggered, the elements' currently active call is immediately stopped. The active call might have - been applied to multiple elements, in which case all of the call's elements will be stopped. When an element - is stopped, the next item in its animation queue is immediately triggered. */ - /* An additional argument may be passed in to clear an element's remaining queued calls. Either true (which defaults to the 'fx' queue) - or a custom queue string can be passed in. */ - /* Note: The stop command runs prior to Collide's Queueing phase since its behavior is intended to take effect *immediately*, - regardless of the element's current queue state. */ - - /* Iterate through every active call. */ - for (var x = 0, callsLength = Animator.calls.length; x < callsLength; x++) { - - /* Inactive calls are set to false by the logic inside completeCall(). Skip them. */ - activeCall = Animator.calls[x]; - if (activeCall) { - - /* Iterate through the active call's targeted elements. */ - - $.each(activeCall[1], function(k, activeElement) { - /* If true was passed in as a secondary argument, clear absolutely all calls on this element. Otherwise, only - clear calls associated with the relevant queue. */ - /* Call stopping logic works as follows: - - options === true --> stop current default queue calls (and queue:false calls), including remaining queued ones. - - options === undefined --> stop current queue:'' call and all queue:false calls. - - options === false --> stop only queue:false calls. - - options === 'custom' --> stop current queue:'custom' call, including remaining queued ones (there is no functionality to only clear the currently-running queue:'custom' call). */ - var queueName = (options === undefined) ? '' : options; - - if (queueName !== true && (activeCall[2].queue !== queueName) && !(options === undefined && activeCall[2].queue === false)) { - return true; - } - - /* Iterate through the calls targeted by the stop command. */ - $.each(elements, function(l, element) { - /* Check that this call was applied to the target element. */ - if (element === activeElement) { - /* Optionally clear the remaining queued calls. */ - if (options === true || Type.isString(options)) { - /* Iterate through the items in the element's queue. */ - $.each($.queue(element, Type.isString(options) ? options : ''), function(_, item) { - /* The queue array can contain an 'inprogress' string, which we skip. */ - if (Type.isFunction(item)) { - /* Pass the item's callback a flag indicating that we want to abort from the queue call. - (Specifically, the queue will resolve the call's associated promise then abort.) */ - item(null, true); - } - }); - - /* Clearing the $.queue() array is achieved by resetting it to []. */ - $.queue(element, Type.isString(options) ? options : '', []); - } - - if (propertiesMap === 'stop') { - /* Since 'reverse' uses cached start values (the previous call's endValues), these values must be - changed to reflect the final value that the elements were actually tweened to. */ - /* Note: If only queue:false animations are currently running on an element, it won't have a tweensContainer - object. Also, queue:false animations can't be reversed. */ - if (Data(element) && Data(element).tweensContainer && queueName !== false) { - $.each(Data(element).tweensContainer, function(m, activeTween) { - activeTween.endValue = activeTween.currentValue; - }); - } - - callsToStop.push(i); - } else if (propertiesMap === 'finish') { - /* To get active tweens to finish immediately, we forcefully shorten their durations to 1ms so that - they finish upon the next rAf tick then proceed with normal call completion logic. */ - activeCall[2].duration = 1; - } - } - }); - }); - - } - - } // END: for (var x = 0, l = Animator.calls.length; x < l; x++) { - - /* Prematurely call completeCall() on each matched active call. Pass an additional flag for 'stop' to indicate - that the complete callback and display:none setting should be skipped since we're completing prematurely. */ - if (propertiesMap === 'stop') { - $.each(callsToStop, function(i, j) { - completeCall(j, true); - }); - - if (promiseData.promise) { - /* Immediately resolve the promise associated with this stop call since stop runs synchronously. */ - promiseData.resolver(elements); - } - } - - /* Since we're stopping, and not proceeding with queueing, exit out of Collide. */ - return getChain(); - - case 'start': - /* Treat a non-empty plain object as a literal properties map. */ - if ($.isPlainObject(propertiesMap) && !Type.isEmptyObject(propertiesMap)) { - action = 'start'; - - /**************** - Redirects - ****************/ - - /* Check if a string matches a registered redirect (see Redirects above). */ - } else if (Type.isString(propertiesMap) && Collide.Redirects[propertiesMap]) { - var opts = $.extend({}, options), - durationOriginal = opts.duration, - delayOriginal = opts.delay || 0; - - /* If the backwards option was passed in, reverse the element set so that elements animate from the last to the first. */ - if (opts.backwards === true) { - elements = $.extend(true, [], elements).reverse(); - } - - /* Individually trigger the redirect for each element in the set to prevent users from having to handle iteration logic in their redirect. */ - $.each(elements, function(elementIndex, element) { - /* If the stagger option was passed in, successively delay each element by the stagger value (in ms). Retain the original delay value. */ - if (parseFloat(opts.stagger)) { - opts.delay = delayOriginal + (parseFloat(opts.stagger) * elementIndex); - } else if (Type.isFunction(opts.stagger)) { - opts.delay = delayOriginal + opts.stagger.call(element, elementIndex, elementsLength); - } - - /* If the drag option was passed in, successively increase/decrease (depending on the presense of opts.backwards) - the duration of each element's animation, using floors to prevent producing very short durations. */ - if (opts.drag) { - /* Default the duration of UI pack effects (callouts and transitions) to 1000ms instead of the usual default duration of 400ms. */ - opts.duration = parseFloat(durationOriginal) || (/^(callout|transition)/.test(propertiesMap) ? 1000 : DURATION_DEFAULT); - - /* For each element, take the greater duration of: A) animation completion percentage relative to the original duration, - B) 75% of the original duration, or C) a 200ms fallback (in case duration is already set to a low value). - The end result is a baseline of 75% of the redirect's duration that increases/decreases as the end of the element set is approached. */ - opts.duration = Math.max(opts.duration * (opts.backwards ? 1 - elementIndex/elementsLength : (elementIndex + 1) / elementsLength), opts.duration * 0.75, 200); - } - - /* Pass in the call's opts object so that the redirect can optionally extend it. It defaults to an empty object instead of null to - reduce the opts checking logic required inside the redirect. */ - Collide.Redirects[propertiesMap].call(element, element, opts || {}, elementIndex, elementsLength, elements, promiseData.promise ? promiseData : undefined); - }); - - /* Since the animation logic resides within the redirect's own code, abort the remainder of this call. - (The performance overhead up to this point is virtually non-existant.) */ - /* Note: The jQuery call chain is kept intact by returning the complete element set. */ - return getChain(); - } else { - var abortError = 'Collide: First argument (' + propertiesMap + ') was not a property map, a known action, or a registered redirect. Aborting.'; - - if (promiseData.promise) { - promiseData.rejecter(new Error(abortError)); - } else { - console.log(abortError); - } - - return getChain(); - } - } - - /************************** - Call-Wide Variables - **************************/ - - /* A container for CSS unit conversion ratios (e.g. %, rem, and em ==> px) that is used to cache ratios across all elements - being animated in a single Collide call. Calculating unit ratios necessitates DOM querying and updating, and is therefore - avoided (via caching) wherever possible. This container is call-wide instead of page-wide to avoid the risk of using stale - conversion metrics across Collide animations that are not immediately consecutively chained. */ - var callUnitConversionData = { - lastParent: null, - lastPosition: null, - lastFontSize: null, - lastPercentToPxWidth: null, - lastPercentToPxHeight: null, - lastEmToPx: null, - remToPx: null, - vwToPx: null, - vhToPx: null - }; - - /* A container for all the ensuing tween data and metadata associated with this call. This container gets pushed to the page-wide - Collide.State.calls array that is processed during animation ticking. */ - var call = []; - -}; diff --git a/src/collide/process-transition.js b/src/collide/process-transition.js deleted file mode 100644 index fea7de0af0..0000000000 --- a/src/collide/process-transition.js +++ /dev/null @@ -1,920 +0,0 @@ -/* Ported from Velocity.js, MIT License. Julian Shapiro http://twitter.com/shapiro */ - -import * as util from 'ionic2/util/util' -import {Collide} from 'ionic2/collide/collide' -import {CSS} from 'ionic2/collide/css' -import {getEasing} from 'ionic2/collide/easing' - - -/********************* - 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 processTransition(action, elements, options, propertiesMap) { - var data = Collide.data; - - for (var elementsIndex = 0; elementsIndex < elements.length; elementsIndex++) { - var element = elements[elementsIndex]; - - /************************* - 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). */ - /* Note: Collide rolls its own delay function since jQuery doesn't have a utility alias for $.fn.delay() - (and thus requires jQuery element creation, which we avoid since its overhead includes DOM querying). */ - 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 either a jQuery object or a raw DOM element was passed in. */ - if (util.isWrapped(opts.container) || util.isNode(opts.container)) { - /* Extract the raw DOM element from the jQuery wrapper. */ - opts.container = opts.container[0] || opts.container; - /* 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 */ - - /* $.position() values are relative to the container's currently viewable area (without taking into account the container's true dimensions - -- say, 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 + $(element).position()[scrollDirection.toLowerCase()]) + scrollOffset; /* GET */ - - } else { - /* If a value other than a jQuery object or 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 $.position(), $.offset() 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 = $(element).offset()[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 - }; - - - /****************************************** - 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; - } - } - } - - 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. Read Velocity's docmentation to learn more about forcefeeding: VelocityJS.org/#forcefeeding */ - 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 - **********************************************************************/ - - /* Note: IE8 rounds to the nearest pixel when returning CSS values, thus we perform conversions using a measurement - of 100 (instead of 1) to give our ratios a precision of at least 2 decimal values. */ - 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, promiseData.resolver ]); - - /* If the animation tick isn't running, start it. (Collide shuts it off when there are no active calls to process.) */ - if (Collide.State.isTicking === false) { - Collide.State.isTicking = true; - - /* Start the tick loop. */ - tick(); - } - - } else { - elementsIndex++; - } - } - - } // END: for (var elementsIndex = 0; elementsIndex < transition.elements.length; x++) { - - - /* 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) { - if (promiseData.promise) { - promiseData.resolver(elements); - } - - /* Do not continue with animation queueing. */ - 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 jQuery prepends to active queue stack arrays.) Regardless, whenever the element's - queue is further appended with additional items -- including $.delay()'s or even $.animate() 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. */ - /* Note: Unfortunately, most people don't fully grasp jQuery's powerful, yet quirky, Collide.queue() function. - Lean more here: http://stackoverflow.com/questions/1058158/can-somebody-explain-jquery-queue-to-me */ - if ((opts.queue === '' || opts.queue === 'fx') && Collide.queue(element)[0] !== 'inprogress') { - Collide.dequeue(element); - } - - } - -} diff --git a/src/collide/tick.js b/src/collide/tick.js new file mode 100644 index 0000000000..db51817b66 --- /dev/null +++ b/src/collide/tick.js @@ -0,0 +1,265 @@ +/* Ported from Velocity.js, MIT License. Julian Shapiro http://twitter.com/shapiro */ + +import {Collide} from 'ionic2/collide/collide' +import {CSS} from 'ionic2/collide/css' +import {completeCall} from 'ionic2/collide/complete-call' +import {dom} from 'ionic2/util' + + +/************ + Tick +************/ + +/* Note: All calls to Collide are pushed to the Collide.State.calls array, which is fully iterated through upon each tick. */ +export function tick(timestamp) { + /* An empty timestamp argument indicates that this is the first tick occurence since ticking was turned on. + We leverage this metadata to fully ignore the first tick pass since RAF's initial pass is fired whenever + the browser's next tick sync time occurs, which results in the first elements subjected to Collide + calls being animated out of sync with any elements animated immediately thereafter. In short, we ignore + the first RAF tick pass so that elements being immediately consecutively animated -- instead of simultaneously animated + by the same Collide call -- are properly batched into the same initial RAF tick and consequently remain in sync thereafter. */ + if (timestamp) { + /* We ignore RAF's high resolution timestamp since it can be significantly offset when the browser is + under high stress; we opt for choppiness over allowing the browser to drop huge chunks of frames. */ + var timeCurrent = (new Date).getTime(); + + + /******************** + Call Iteration + ********************/ + + var callsLength = Collide.State.calls.length; + + /* To speed up iterating over this array, it is compacted (falsey items -- calls that have completed -- are removed) + when its length has ballooned to a point that can impact tick performance. This only becomes necessary when animation + has been continuous with many elements over a long period of time; whenever all active calls are completed, completeCall() clears Collide.State.calls. */ + if (callsLength > 10000) { + Collide.State.calls = compactSparseArray(Collide.State.calls); + } + + /* Iterate through each active call. */ + for (var i = 0; i < callsLength; i++) { + + /* When a Collide call is completed, its Collide.State.calls entry is set to false. Continue on to the next call. */ + if (!Collide.State.calls[i]) { + continue; + } + + + /************************ + Call-Wide Variables + ************************/ + + var callContainer = Collide.State.calls[i], + call = callContainer[0], + opts = callContainer[2], + timeStart = callContainer[3], + firstTick = !!timeStart, + tweenDummyValue = null; + + /* If timeStart is undefined, then this is the first time that this call has been processed by tick(). + We assign timeStart now so that its value is as close to the real animation start time as possible. + (Conversely, had timeStart been defined when this call was added to Collide.State.calls, the delay + between that time and now would cause the first few frames of the tween to be skipped since + percentComplete is calculated relative to timeStart.) */ + /* Further, subtract 16ms (the approximate resolution of RAF) from the current time value so that the + first tick iteration isn't wasted by animating at 0% tween completion, which would produce the + same style value as the element's current value. */ + if (!timeStart) { + timeStart = Collide.State.calls[i][3] = timeCurrent - 16; + } + + /* The tween's completion percentage is relative to the tween's start time, not the tween's start value + (which would result in unpredictable tween durations since JavaScript's timers are not particularly accurate). + Accordingly, we ensure that percentComplete does not exceed 1. */ + var percentComplete = Math.min((timeCurrent - timeStart) / opts.duration, 1); + + + /********************** + Element Iteration + **********************/ + + /* For every call, iterate through each of the elements in its set. */ + for (var j = 0, callLength = call.length; j < callLength; j++) { + var tweensContainer = call[j], + element = tweensContainer.element; + + /* Check to see if this element has been deleted midway through the animation by checking for the + continued existence of its data cache. If it's gone, skip animating this element. */ + if (!Collide.data(element)) { + continue; + } + + var transformPropertyExists = false; + + + /********************************** + Display & Visibility Toggling + **********************************/ + + /* If the display option is set to non-'none', set it upfront so that the element can become visible before tweening begins. + (Otherwise, display's 'none' value is set in completeCall() once the animation has completed.) */ + if (opts.display !== undefined && opts.display !== null && opts.display !== 'none') { + if (opts.display === 'flex') { + var flexValues = [ '-webkit-box', '-moz-box', '-ms-flexbox', '-webkit-flex' ]; + + for (var f = 0; f < flexValues.length; f++) { + CSS.setPropertyValue(element, 'display', flexValues[f]); + } + } + + CSS.setPropertyValue(element, 'display', opts.display); + } + + /* Same goes with the visibility option, but its 'none' equivalent is 'hidden'. */ + if (opts.visibility !== undefined && opts.visibility !== 'hidden') { + CSS.setPropertyValue(element, 'visibility', opts.visibility); + } + + + /************************ + Property Iteration + ************************/ + + /* For every element, iterate through each property. */ + for (var property in tweensContainer) { + + /* Note: In addition to property tween data, tweensContainer contains a reference to its associated element. */ + if (property !== 'element') { + var tween = tweensContainer[property], + currentValue, + /* Easing can either be a pre-genereated function or a string that references a pre-registered easing + on the Collide.Easings object. In either case, return the appropriate easing *function*. */ + easing = typeof tween.easing === 'string' ? Collide.Easings[tween.easing] : tween.easing; + + /****************************** + Current Value Calculation + ******************************/ + + /* If this is the last tick pass (if we've reached 100% completion for this tween), + ensure that currentValue is explicitly set to its target endValue so that it's not subjected to any rounding. */ + if (percentComplete === 1) { + currentValue = tween.endValue; + + /* Otherwise, calculate currentValue based on the current delta from startValue. */ + } else { + var tweenDelta = tween.endValue - tween.startValue; + currentValue = tween.startValue + (tweenDelta * easing(percentComplete, opts, tweenDelta)); + + /* If no value change is occurring, don't proceed with DOM updating. */ + if (!firstTick && (currentValue === tween.currentValue)) { + continue; + } + } + + tween.currentValue = currentValue; + + /* If we're tweening a fake 'tween' property in order to log transition values, update the one-per-call variable so that + it can be passed into the progress callback. */ + if (property === 'tween') { + tweenDummyValue = currentValue; + + } else { + + /****************** + Hooks: Part I + ******************/ + + /* For hooked properties, the newly-updated rootPropertyValueCache is cached onto the element so that it can be used + for subsequent hooks in this call that are associated with the same root property. If we didn't cache the updated + rootPropertyValue, each subsequent update to the root property in this tick pass would reset the previous hook's + updates to rootPropertyValue prior to injection. A nice performance byproduct of rootPropertyValue caching is that + subsequently chained animations using the same hookRoot but a different hook can use this cached rootPropertyValue. */ + if (CSS.Hooks.registered[property]) { + var hookRoot = CSS.Hooks.getRoot(property), + rootPropertyValueCache = Collide.data(element).rootPropertyValueCache[hookRoot]; + + if (rootPropertyValueCache) { + tween.rootPropertyValue = rootPropertyValueCache; + } + } + + + /***************** + DOM Update + *****************/ + + /* setPropertyValue() returns an array of the property name and property value post any normalization that may have been performed. */ + /* Note: To solve an IE<=8 positioning bug, the unit type is dropped when setting a property value of 0. */ + var adjustedSetData = CSS.setPropertyValue(element, /* SET */ + property, + tween.currentValue + (parseFloat(currentValue) === 0 ? '' : tween.unitType), + tween.rootPropertyValue, + tween.scrollData); + + + /******************* + Hooks: Part II + *******************/ + + /* Now that we have the hook's updated rootPropertyValue (the post-processed value provided by adjustedSetData), cache it onto the element. */ + if (CSS.Hooks.registered[property]) { + /* Since adjustedSetData contains normalized data ready for DOM updating, the rootPropertyValue needs to be re-extracted from its normalized form. ?? */ + if (CSS.Normalizations.registered[hookRoot]) { + Collide.data(element).rootPropertyValueCache[hookRoot] = CSS.Normalizations.registered[hookRoot]('extract', null, adjustedSetData[1]); + } else { + Collide.data(element).rootPropertyValueCache[hookRoot] = adjustedSetData[1]; + } + } + + /*************** + Transforms + ***************/ + + /* Flag whether a transform property is being animated so that flushTransformCache() can be triggered once this tick pass is complete. */ + if (adjustedSetData[0] === 'transform') { + transformPropertyExists = true; + } + + } + + } // END: if (property !== 'element') + + } // END: for (var property in tweensContainer) + + if (transformPropertyExists) { + CSS.flushTransformCache(element); + } + + } // END: for (var j = 0, callLength = call.length; j < callLength; j++) + + + /* The non-'none' display value is only applied to an element once -- when its associated call is first ticked through. + Accordingly, it's set to false so that it isn't re-processed by this call in the next tick. */ + if (opts.display !== undefined && opts.display !== 'none') { + Collide.State.calls[i][2].display = false; + } + if (opts.visibility !== undefined && opts.visibility !== 'hidden') { + Collide.State.calls[i][2].visibility = false; + } + + /* Pass the elements and the timing data (percentComplete, msRemaining, timeStart, tweenDummyValue) into the progress callback. */ + if (opts.progress) { + opts.progress.call(callContainer[1], + callContainer[1], + percentComplete, + Math.max(0, (timeStart + opts.duration) - timeCurrent), + timeStart, + tweenDummyValue); + } + + /* If this call has finished tweening, pass its index to completeCall() to handle call cleanup. */ + if (percentComplete === 1) { + completeCall(i); + } + + } // END: for (var i = 0; i < callsLength; i++) + + } // END: if (timestamp) + + /* Note: completeCall() sets the isTicking flag to false when the last call on Collide.State.calls has completed. */ + if (Collide.State.isTicking) { + dom.raf(tick); + } + +}; diff --git a/src/collide/transition-action.js b/src/collide/transition-action.js new file mode 100644 index 0000000000..3b33424327 --- /dev/null +++ b/src/collide/transition-action.js @@ -0,0 +1,155 @@ +/* Ported from Collide.js, MIT License. Julian Shapiro http://twitter.com/shapiro */ + +import {Collide} from 'ionic2/collide/collide' +import {elementProcess} from 'ionic2/collide/element-process' + + +export function transitionAction(action, elements, options, propertiesMap) { + + elements = elements && !elements.length ? [elements] : elements + + if (!elements) { + return Promise.reject(); + } + + /* The length of the element set (in the form of a nodeList or an array of elements) is defaulted to 1 in case a + single raw DOM element is passed in (which doesn't contain a length property). */ + var elementsLength = elements.length; + var elementsIndex = 0; + var eleData; + + + if (action === 'stop') { + /****************** + Action: Stop + *******************/ + + /* Clear the currently-active delay on each targeted element. */ + for (var x = 0; x < elements.length; x++) { + eleData = Collide.data(elements[x]); + + if (eleData && eleData.delayTimer) { + /* Stop the timer from triggering its cached next() function. */ + clearTimeout(eleData.delayTimer.setTimeout); + + /* Manually call the next() function so that the subsequent queue items can progress. */ + if (eleData.delayTimer.next) { + eleData.delayTimer.next(); + } + + delete eleData.delayTimer; + } + } + + + var callsToStop = []; + var activeCall; + + /* When the stop action is triggered, the elements' currently active call is immediately stopped. The active call might have + been applied to multiple elements, in which case all of the call's elements will be stopped. When an element + is stopped, the next item in its animation queue is immediately triggered. */ + /* An additional argument may be passed in to clear an element's remaining queued calls. Either true (which defaults to the 'fx' queue) + or a custom queue string can be passed in. */ + /* Note: The stop command runs prior to Collide's Queueing phase since its behavior is intended to take effect *immediately*, + regardless of the element's current queue state. */ + + /* Iterate through every active call. */ + for (var x = 0, callsLength = Collide.State.calls.length; x < callsLength; x++) { + + /* Inactive calls are set to false by the logic inside completeCall(). Skip them. */ + activeCall = Collide.State.calls[x]; + if (activeCall) { + + /* Iterate through the active call's targeted elements. */ + + $.each(activeCall[1], function(k, activeElement) { + /* If true was passed in as a secondary argument, clear absolutely all calls on this element. Otherwise, only + clear calls associated with the relevant queue. */ + /* Call stopping logic works as follows: + - options === true --> stop current default queue calls (and queue:false calls), including remaining queued ones. + - options === undefined --> stop current queue:'' call and all queue:false calls. + - options === false --> stop only queue:false calls. + - options === 'custom' --> stop current queue:'custom' call, including remaining queued ones (there is no functionality to only clear the currently-running queue:'custom' call). */ + var queueName = (options === undefined) ? '' : options; + + if (queueName !== true && (activeCall[2].queue !== queueName) && !(options === undefined && activeCall[2].queue === false)) { + return true; + } + + /* Iterate through the calls targeted by the stop command. */ + $.each(elements, function(l, element) { + /* Check that this call was applied to the target element. */ + if (element === activeElement) { + /* Optionally clear the remaining queued calls. */ + if (options === true || Type.isString(options)) { + /* Iterate through the items in the element's queue. */ + $.each($.queue(element, Type.isString(options) ? options : ''), function(_, item) { + /* The queue array can contain an 'inprogress' string, which we skip. */ + if (Type.isFunction(item)) { + /* Pass the item's callback a flag indicating that we want to abort from the queue call. + (Specifically, the queue will resolve the call's associated promise then abort.) */ + item(null, true); + } + }); + + /* Clearing the $.queue() array is achieved by resetting it to []. */ + $.queue(element, Type.isString(options) ? options : '', []); + } + + if (propertiesMap === 'stop') { + /* Since 'reverse' uses cached start values (the previous call's endValues), these values must be + changed to reflect the final value that the elements were actually tweened to. */ + /* Note: If only queue:false animations are currently running on an element, it won't have a tweensContainer + object. Also, queue:false animations can't be reversed. */ + if (Data(element) && Data(element).tweensContainer && queueName !== false) { + $.each(Data(element).tweensContainer, function(m, activeTween) { + activeTween.endValue = activeTween.currentValue; + }); + } + + callsToStop.push(i); + } else if (propertiesMap === 'finish') { + /* To get active tweens to finish immediately, we forcefully shorten their durations to 1ms so that + they finish upon the next rAf tick then proceed with normal call completion logic. */ + activeCall[2].duration = 1; + } + } + }); + }); + + } + + } // END: for (var x = 0, l = Collide.State.calls.length; x < l; x++) { + + /* Prematurely call completeCall() on each matched active call. Pass an additional flag for 'stop' to indicate + that the complete callback and display:none setting should be skipped since we're completing prematurely. */ + if (propertiesMap === 'stop') { + $.each(callsToStop, function(i, j) { + completeCall(j, true); + }); + + return Promise.reject(); + } + } + + + /************************** + Element Set Iteration + **************************/ + + var promises = []; + + if (elements && elements.length) { + for (var i = 0, l = elements.length; i < l; i++) { + if (elements[i] && elements[i].parentElement) { + + promises.push( + elementProcess(action, elements, i, options, propertiesMap) + ); + + } + } + } + + return Promise.all(promises); +}; diff --git a/src/collide/transition.js b/src/collide/transition.js index 084d841581..14a7270a16 100644 --- a/src/collide/transition.js +++ b/src/collide/transition.js @@ -1,6 +1,4 @@ -import {initTransition} from 'ionic2/collide/init-transition' -import {processTransition} from 'ionic2/collide/process-transition' - +import {transitionAction} from 'ionic2/collide/transition-action' export class Transition { @@ -25,12 +23,17 @@ export class Transition { } start() { - console.log('start') - initTransition('start', this.elements, this.options, this.propertiesMap) + var p = transitionAction('start', this.elements, this.options, this.propertiesMap) + + p.then(() => { + console.log('start success done') + }).catch(() => { + console.log('start error done') + }) } stop() { - console.log('stop') + transitionAction('stop', this.elements, this.options, this.propertiesMap) } properties(val) { diff --git a/src/components/app/test/transitions/main.html b/src/components/app/test/transitions/main.html index 24cdc28bcf..0d1ae0a16a 100644 --- a/src/components/app/test/transitions/main.html +++ b/src/components/app/test/transitions/main.html @@ -1,8 +1,9 @@
-
-
+
+
+