diff --git a/ionic/collide/collide.js b/ionic/collide/collide.js new file mode 100644 index 0000000000..19ff8f1b7e --- /dev/null +++ b/ionic/collide/collide.js @@ -0,0 +1,163 @@ +import {dom} from 'ionic/util' + + +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: '', + duration: 400, + easing: 'swing', + begin: undefined, + complete: undefined, + progress: undefined, + display: undefined, + visibility: undefined, + loop: false, + delay: false, + /* Advanced: Set to false to prevent property values from being cached between consecutive Collide-initiated chain calls. */ + _cacheValues: true + }, + + /* Used for getting/setting Collide's hooked CSS properties. */ + hook: null, /* Defined below. */ + + /* Collide-wide animation time remapping for testing purposes. */ + mock: false, + + /* 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. */ + isSVG: dom.isSVG(element), + + /* Keep track of whether the element is currently being animated by Collide. + This is used to ensure that property values are not transferred between non-consecutive (stale) calls. */ + isAnimating: false, + + /* A reference to the element's live computedStyle object. Learn more here: https://developer.mozilla.org/en/docs/Web/API/window.getComputedStyle */ + computedStyle: null, + + /* Tween data is cached for each animation on the element so that data can be passed across calls -- + in particular, end values are used as subsequent start values in consecutive Collide calls. */ + tweensContainer: null, + + /* The full root property values of each CSS hook being animated on this element are cached so that: + 1) Concurrently-animating hooks sharing the same root can have their root values' merged into one while tweening. + 2) Post-hook-injection root values can be transferred over to consecutively chained Collide calls as starting root values. */ + rootPropertyValueCache: {}, + + /* A cache for transform updates, which must be manually flushed via CSS.flushTransformCache(). */ + transformCache: {} + }; + }, + + /* 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 || []; + + if (arr != null) { + [].push.call(ret, arr); + } + + return ret; + } + + if (!element) { + return; + } + + type = (type || 'collide') + 'queue'; + + var q = this.data(element, type); + + if (!data) { + return q || []; + } + + if (!q || Array.isArray(data)) { + q = this.data(element, type, $makeArray(data)); + + } else { + q.push(data); + } + + return q; + }, + + /* dequeue element */ + dequeue: function (element, type) { + type = type || 'collide'; + + let queue = this.queue(element, type); + let fn = queue.shift(); + + if (fn === 'inprogress') { + fn = queue.shift(); + } + + if (fn) { + if (type === 'collide') { + queue.unshift('inprogress'); + } + + fn.call(element, () => { + this.dequeue(element, type); + }); + } + } + +}; diff --git a/ionic/collide/complete-call.js b/ionic/collide/complete-call.js new file mode 100644 index 0000000000..15f37dd0d7 --- /dev/null +++ b/ionic/collide/complete-call.js @@ -0,0 +1,172 @@ +/* Ported from Velocity.js, MIT License. Julian Shapiro http://twitter.com/shapiro */ + +import {Collide} from 'ionic/collide/collide' +import {CSS} from 'ionic/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/ionic/collide/css.js b/ionic/collide/css.js new file mode 100644 index 0000000000..3d2cb4283d --- /dev/null +++ b/ionic/collide/css.js @@ -0,0 +1,896 @@ +/* Ported from Velocity.js, MIT License. Julian Shapiro http://twitter.com/shapiro */ + +import {Collide} from 'ionic/collide/collide' + +const data = Collide.data; + + +/***************** + CSS Stack +*****************/ + +/* The CSS object is a highly condensed and performant CSS stack that fully replaces jQuery's. + It handles the validation, getting, and setting of both standard CSS properties and CSS property hooks. */ +/* Note: A 'CSS' shorthand is aliased so that our code is easier to read. */ +export var CSS = { + + + /************* + CSS RegEx + *************/ + + RegEx: { + isHex: /^#([A-f\d]{3}){1,2}$/i, + + /* Unwrap a property value's surrounding text, e.g. 'rgba(4, 3, 2, 1)' ==> '4, 3, 2, 1' and 'rect(4px 3px 2px 1px)' ==> '4px 3px 2px 1px'. */ + valueUnwrap: /^[A-z]+\((.*)\)$/i, + + wrappedValueAlreadyExtracted: /[0-9.]+ [0-9.]+ [0-9.]+( [0-9.]+)?/, + + /* Split a multi-value property into an array of subvalues, e.g. 'rgba(4, 3, 2, 1) 4px 3px 2px 1px' ==> [ 'rgba(4, 3, 2, 1)', '4px', '3px', '2px', '1px' ]. */ + valueSplit: /([A-z]+\(.+\))|(([A-z0-9#-.]+?)(?=\s|$))/ig + }, + + + /************ + CSS Lists + ************/ + + Lists: { + colors: [ 'fill', 'stroke', 'stopColor', 'color', 'backgroundColor', 'borderColor', 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor', 'outlineColor' ], + transformsBase: [ 'translateX', 'translateY', 'scale', 'scaleX', 'scaleY', 'skewX', 'skewY', 'rotateZ' ], + transforms3D: [ 'transformPerspective', 'translateZ', 'scaleZ', 'rotateX', 'rotateY' ] + }, + + + /************ + CSS Hooks + ************/ + + /* Hooks allow a subproperty (e.g. 'boxShadowBlur') of a compound-value CSS property + (e.g. 'boxShadow: X Y Blur Spread Color') to be animated as if it were a discrete property. */ + /* Note: Beyond enabling fine-grained property animation, hooking is necessary since Collide only + tweens properties with single numeric values; unlike CSS transitions, Collide does not interpolate compound-values. */ + Hooks: { + + /******************** + CSS Hook Registration + ********************/ + + /* Templates are a concise way of indicating which subproperties must be individually registered for each compound-value CSS property. */ + /* Each template consists of the compound-value's base name, its constituent subproperty names, and those subproperties' default values. */ + templates: { + textShadow: [ 'Color X Y Blur', 'black 0px 0px 0px' ], + boxShadow: [ 'Color X Y Blur Spread', 'black 0px 0px 0px 0px' ], + clip: [ 'Top Right Bottom Left', '0px 0px 0px 0px' ], + backgroundPosition: [ 'X Y', '0% 0%' ], + transformOrigin: [ 'X Y Z', '50% 50% 0px' ], + perspectiveOrigin: [ 'X Y', '50% 50%' ] + }, + + /* A 'registered' hook is one that has been converted from its template form into a live, + tweenable property. It contains data to associate it with its root property. */ + registered: { + /* Note: A registered hook looks like this ==> textShadowBlur: [ 'textShadow', 3 ], + which consists of the subproperty's name, the associated root property's name, + and the subproperty's position in the root's value. */ + }, + + /* Convert the templates into individual hooks then append them to the registered object above. */ + register: function() { + /* Color hooks registration: Colors are defaulted to white -- as opposed to black -- since colors that are + currently set to 'transparent' default to their respective template below when color-animated, + and white is typically a closer match to transparent than black is. An exception is made for text ('color'), + which is almost always set closer to black than white. */ + for (var i = 0; i < CSS.Lists.colors.length; i++) { + var rgbComponents = (CSS.Lists.colors[i] === 'color') ? '0 0 0 1' : '255 255 255 1'; + CSS.Hooks.templates[CSS.Lists.colors[i]] = [ 'Red Green Blue Alpha', rgbComponents ]; + } + + var rootProperty, + hookTemplate, + hookNames; + + /* In IE, color values inside compound-value properties are positioned at the end the value instead of at the beginning. + Thus, we re-arrange the templates accordingly. */ + // if (IE) { + // for (rootProperty in CSS.Hooks.templates) { + // hookTemplate = CSS.Hooks.templates[rootProperty]; + // hookNames = hookTemplate[0].split(' '); + + // var defaultValues = hookTemplate[1].match(CSS.RegEx.valueSplit); + + // if (hookNames[0] === 'Color') { + // // Reposition both the hook's name and its default value to the end of their respective strings. + // hookNames.push(hookNames.shift()); + // defaultValues.push(defaultValues.shift()); + + // // Replace the existing template for the hook's root property. + // CSS.Hooks.templates[rootProperty] = [ hookNames.join(' '), defaultValues.join(' ') ]; + // } + // } + // } + + /* Hook registration. */ + for (rootProperty in CSS.Hooks.templates) { + hookTemplate = CSS.Hooks.templates[rootProperty]; + hookNames = hookTemplate[0].split(' '); + + for (var i in hookNames) { + var fullHookName = rootProperty + hookNames[i], + hookPosition = i; + + /* For each hook, register its full name (e.g. textShadowBlur) with its root property (e.g. textShadow) + and the hook's position in its template's default value string. */ + CSS.Hooks.registered[fullHookName] = [ rootProperty, hookPosition ]; + } + } + }, + + + /***************************** + CSS Hook Injection and Extraction + *****************************/ + + /* Look up the root property associated with the hook (e.g. return 'textShadow' for 'textShadowBlur'). */ + /* Since a hook cannot be set directly (the browser won't recognize it), style updating for hooks is routed through the hook's root property. */ + getRoot: function(property) { + var hookData = CSS.Hooks.registered[property]; + + if (hookData) { + return hookData[0]; + } + + /* If there was no hook match, return the property name untouched. */ + return property; + }, + + /* Convert any rootPropertyValue, null or otherwise, into a space-delimited list of hook values so that + the targeted hook can be injected or extracted at its standard position. */ + cleanRootPropertyValue: function(rootProperty, rootPropertyValue) { + /* If the rootPropertyValue is wrapped with 'rgb()', 'clip()', etc., remove the wrapping to normalize the value before manipulation. */ + if (CSS.RegEx.valueUnwrap.test(rootPropertyValue)) { + rootPropertyValue = rootPropertyValue.match(CSS.RegEx.valueUnwrap)[1]; + } + + /* If rootPropertyValue is a CSS null-value (from which there's inherently no hook value to extract), + default to the root's default value as defined in CSS.Hooks.templates. */ + /* Note: CSS null-values include 'none', 'auto', and 'transparent'. They must be converted into their + zero-values (e.g. textShadow: 'none' ==> textShadow: '0px 0px 0px black') for hook manipulation to proceed. */ + if (CSS.Values.isCSSNullValue(rootPropertyValue)) { + rootPropertyValue = CSS.Hooks.templates[rootProperty][1]; + } + + return rootPropertyValue; + }, + + /* Extracted the hook's value from its root property's value. This is used to get the starting value of an animating hook. */ + extractValue: function(fullHookName, rootPropertyValue) { + var hookData = CSS.Hooks.registered[fullHookName]; + + if (hookData) { + var hookRoot = hookData[0], + hookPosition = hookData[1]; + + rootPropertyValue = CSS.Hooks.cleanRootPropertyValue(hookRoot, rootPropertyValue); + + /* Split rootPropertyValue into its constituent hook values then grab the desired hook at its standard position. */ + return rootPropertyValue.toString().match(CSS.RegEx.valueSplit)[hookPosition]; + } + + /* If the provided fullHookName isn't a registered hook, return the rootPropertyValue that was passed in. */ + return rootPropertyValue; + }, + + /* Inject the hook's value into its root property's value. This is used to piece back together the root property + once Collide has updated one of its individually hooked values through tweening. */ + injectValue: function(fullHookName, hookValue, rootPropertyValue) { + var hookData = CSS.Hooks.registered[fullHookName]; + + if (hookData) { + var hookRoot = hookData[0], + hookPosition = hookData[1], + rootPropertyValueParts, + rootPropertyValueUpdated; + + rootPropertyValue = CSS.Hooks.cleanRootPropertyValue(hookRoot, rootPropertyValue); + + /* Split rootPropertyValue into its individual hook values, replace the targeted value with hookValue, + then reconstruct the rootPropertyValue string. */ + rootPropertyValueParts = rootPropertyValue.toString().match(CSS.RegEx.valueSplit); + rootPropertyValueParts[hookPosition] = hookValue; + rootPropertyValueUpdated = rootPropertyValueParts.join(' '); + + return rootPropertyValueUpdated; + } + + /* If the provided fullHookName isn't a registered hook, return the rootPropertyValue that was passed in. */ + return rootPropertyValue; + } + }, + + + /******************* + CSS Normalizations + *******************/ + + /* Normalizations standardize CSS property manipulation by pollyfilling browser-specific implementations (e.g. opacity) + and reformatting special properties (e.g. clip, rgba) to look like standard ones. */ + Normalizations: { + + /* Normalizations are passed a normalization target (either the property's name, its extracted value, or its injected value), + the targeted element (which may need to be queried), and the targeted property value. */ + registered: { + clip: function(type, element, propertyValue) { + switch (type) { + + case 'name': + return 'clip'; + + /* Clip needs to be unwrapped and stripped of its commas during extraction. */ + case 'extract': + var extracted; + + /* If Collide also extracted this value, skip extraction. */ + if (CSS.RegEx.wrappedValueAlreadyExtracted.test(propertyValue)) { + extracted = propertyValue; + + } else { + /* Remove the 'rect()' wrapper. */ + extracted = propertyValue.toString().match(CSS.RegEx.valueUnwrap); + + /* Strip off commas. */ + extracted = extracted ? extracted[1].replace(/,(\s+)?/g, ' ') : propertyValue; + } + + return extracted; + + /* Clip needs to be re-wrapped during injection. */ + case 'inject': + return 'rect(' + propertyValue + ')'; + } + }, + + blur: function(type, element, propertyValue) { + switch (type) { + + case 'name': + return '-webkit-filter'; + + case 'extract': + var extracted = parseFloat(propertyValue); + + /* If extracted is NaN, meaning the value isn't already extracted. */ + if (!(extracted || extracted === 0)) { + var blurComponent = propertyValue.toString().match(/blur\(([0-9]+[A-z]+)\)/i); + + /* If the filter string had a blur component, return just the blur value and unit type. */ + if (blurComponent) { + extracted = blurComponent[1]; + + /* If the component doesn't exist, default blur to 0. */ + } else { + extracted = 0; + } + } + + return extracted; + + /* Blur needs to be re-wrapped during injection. */ + case 'inject': + /* For the blur effect to be fully de-applied, it needs to be set to 'none' instead of 0. */ + if (!parseFloat(propertyValue)) { + return 'none'; + } + + return 'blur(' + propertyValue + ')'; + } + }, + + opacity: function(type, element, propertyValue) { + switch (type) { + case 'name': + return 'opacity'; + + case 'extract': + return propertyValue; + + case 'inject': + return propertyValue; + } + } + }, + + + /***************************** + CSS Batched Registrations + *****************************/ + + /* Note: Batched normalizations extend the CSS.Normalizations.registered object. */ + register: function() { + + /***************** + CSS Batched Registration Transforms + *****************/ + + /* Transforms are the subproperties contained by the CSS 'transform' property. Transforms must undergo normalization + so that they can be referenced in a properties map by their individual names. */ + /* Note: When transforms are 'set', they are actually assigned to a per-element transformCache. When all transform + setting is complete complete, CSS.flushTransformCache() must be manually called to flush the values to the DOM. + Transform setting is batched in this way to improve performance: the transform style only needs to be updated + once when multiple transform subproperties are being animated simultaneously. */ + + for (var i = 0; i < CSS.Lists.transformsBase.length; i++) { + /* Wrap the dynamically generated normalization function in a new scope so that transformName's value is + paired with its respective function. (Otherwise, all functions would take the final for loop's transformName.) */ + (function() { + var transformName = CSS.Lists.transformsBase[i]; + + CSS.Normalizations.registered[transformName] = function(type, element, propertyValue) { + var eleData = data(element); + + switch (type) { + + /* The normalized property name is the parent 'transform' property -- the property that is actually set in CSS. */ + case 'name': + return 'transform'; + + /* Transform values are cached onto a per-element transformCache object. */ + case 'extract': + /* If this transform has yet to be assigned a value, return its null value. */ + if (eleData === undefined || eleData.transformCache[transformName] === undefined) { + /* Scale CSS.Lists.transformsBase default to 1 whereas all other transform properties default to 0. */ + return /^scale/i.test(transformName) ? 1 : 0; + } + /* When transform values are set, they are wrapped in parentheses as per the CSS spec. + Thus, when extracting their values (for tween calculations), we strip off the parentheses. */ + return eleData.transformCache[transformName].replace(/[()]/g, ''); + + case 'inject': + var invalid = false; + + /* If an individual transform property contains an unsupported unit type, the browser ignores the *entire* transform property. + Thus, protect users from themselves by skipping setting for transform values supplied with invalid unit types. */ + /* Switch on the base transform type; ignore the axis by removing the last letter from the transform's name. */ + switch (transformName.substr(0, transformName.length - 1)) { + /* Whitelist unit types for each transform. */ + case 'translate': + invalid = !/(%|px|em|rem|vw|vh|\d)$/i.test(propertyValue); + break; + + /* Since an axis-free 'scale' property is supported as well, a little hack is used here to detect it by chopping off its last letter. */ + case 'scal': + case 'scale': + invalid = !/(\d)$/i.test(propertyValue); + break; + + case 'skew': + invalid = !/(deg|\d)$/i.test(propertyValue); + break; + + case 'rotate': + invalid = !/(deg|\d)$/i.test(propertyValue); + break; + } + + if (!invalid) { + /* As per the CSS spec, wrap the value in parentheses. */ + eleData.transformCache[transformName] = '(' + propertyValue + ')'; + } + + /* Although the value is set on the transformCache object, return the newly-updated value for the calling code to process as normal. */ + return eleData.transformCache[transformName]; + } + }; + })(); + } + + /************* + CSS Batched Registration Colors + *************/ + + /* Since Collide only animates a single numeric value per property, color animation is achieved by hooking the individual RGBA components of CSS color properties. + Accordingly, color values must be normalized (e.g. '#ff0000', 'red', and 'rgb(255, 0, 0)' ==> '255 0 0 1') so that their components can be injected/extracted by CSS.Hooks logic. */ + for (var i = 0; i < CSS.Lists.colors.length; i++) { + /* Wrap the dynamically generated normalization function in a new scope so that colorName's value is paired with its respective function. + (Otherwise, all functions would take the final for loop's colorName.) */ + (function() { + var colorName = CSS.Lists.colors[i]; + + /* Note: In IE<=8, which support rgb but not rgba, color properties are reverted to rgb by stripping off the alpha component. */ + CSS.Normalizations.registered[colorName] = function(type, element, propertyValue) { + switch (type) { + + case 'name': + return colorName; + + /* Convert all color values into the rgb format. (Old IE can return hex values and color names instead of rgb/rgba.) */ + case 'extract': + var extracted; + + /* If the color is already in its hookable form (e.g. '255 255 255 1') due to having been previously extracted, skip extraction. */ + if (CSS.RegEx.wrappedValueAlreadyExtracted.test(propertyValue)) { + extracted = propertyValue; + + } else { + var converted, + colorNames = { + black: 'rgb(0, 0, 0)', + blue: 'rgb(0, 0, 255)', + gray: 'rgb(128, 128, 128)', + green: 'rgb(0, 128, 0)', + red: 'rgb(255, 0, 0)', + white: 'rgb(255, 255, 255)' + }; + + /* Convert color names to rgb. */ + if (/^[A-z]+$/i.test(propertyValue)) { + if (colorNames[propertyValue] !== undefined) { + converted = colorNames[propertyValue] + + } else { + /* If an unmatched color name is provided, default to black. */ + converted = colorNames.black; + } + + /* Convert hex values to rgb. */ + } else if (CSS.RegEx.isHex.test(propertyValue)) { + converted = 'rgb(' + CSS.Values.hexToRgb(propertyValue).join(' ') + ')'; + + /* If the provided color doesn't match any of the accepted color formats, default to black. */ + } else if (!(/^rgba?\(/i.test(propertyValue))) { + converted = colorNames.black; + } + + /* Remove the surrounding 'rgb/rgba()' string then replace commas with spaces and strip + repeated spaces (in case the value included spaces to begin with). */ + extracted = (converted || propertyValue).toString().match(CSS.RegEx.valueUnwrap)[1].replace(/,(\s+)?/g, ' '); + } + + /* add a fourth (alpha) component if it's missing and default it to 1 (visible). */ + if (extracted.split(' ').length === 3) { + extracted += ' 1'; + } + + return extracted; + + case 'inject': + /* add a fourth (alpha) component if it's missing and default it to 1 (visible). */ + if (propertyValue.split(' ').length === 3) { + propertyValue += ' 1'; + } + + /* Re-insert the browser-appropriate wrapper('rgb/rgba()'), insert commas, and strip off decimal units + on all values but the fourth (R, G, and B only accept whole numbers). */ + return 'rgba(' + propertyValue.replace(/\s+/g, ',').replace(/\.(\d)+(?=,)/g, '') + ')'; + } + }; + })(); + } + } + }, + + + /************************ + CSS Property Names + ************************/ + + Names: { + /* Camelcase a property name into its JavaScript notation (e.g. 'background-color' ==> 'backgroundColor'). + Camelcasing is used to normalize property names between and across calls. */ + camelCase: function(property) { + return property.replace(/-(\w)/g, function(match, subMatch) { + return subMatch.toUpperCase(); + }); + }, + + /* For SVG elements, some properties (namely, dimensional ones) are GET/SET via the element's HTML attributes (instead of via CSS styles). */ + SVGAttribute: function(property) { + return new RegExp('^(width|height|x|y|cx|cy|r|rx|ry|x1|x2|y1|y2)$', 'i').test(property); + }, + + /* Determine whether a property should be set with a vendor prefix. */ + /* If a prefixed version of the property exists, return it. Otherwise, return the original property name. + If the property is not at all supported by the browser, return a false flag. */ + prefixCheck: function(property) { + /* If this property has already been checked, return the cached value. */ + if (CSS.Names.prefixMatches[property]) { + return [ CSS.Names.prefixMatches[property], true ]; + + } else { + for (var i = 0, vendorsLength = vendorPrefixes.length; i < vendorsLength; i++) { + var propertyPrefixed; + + if (i === 0) { + propertyPrefixed = property; + + } else { + /* Capitalize the first letter of the property to conform to JavaScript vendor prefix notation (e.g. webkitFilter). */ + propertyPrefixed = vendorPrefixes[i] + property.replace(/^\w/, function(match) { return match.toUpperCase(); }); + } + + /* Check if the browser supports this property as prefixed. */ + if (typeof Collide.State.prefixElement.style[propertyPrefixed] === 'string') { + /* Cache the match. */ + CSS.Names.prefixMatches[property] = propertyPrefixed; + + return [ propertyPrefixed, true ]; + } + } + + /* If the browser doesn't support this property in any form, include a false flag so that the caller can decide how to proceed. */ + return [ property, false ]; + } + }, + + /* cached property name prefixes */ + prefixMatches: {} + }, + + + /************************ + CSS Property Values + ************************/ + + Values: { + /* Hex to RGB conversion. Copyright Tim Down: http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb */ + hexToRgb: function(hex) { + var shortformRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i, + longformRegex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i, + rgbParts; + + hex = hex.replace(shortformRegex, function(m, r, g, b) { + return r + r + g + g + b + b; + }); + + rgbParts = longformRegex.exec(hex); + + return rgbParts ? [ parseInt(rgbParts[1], 16), parseInt(rgbParts[2], 16), parseInt(rgbParts[3], 16) ] : [ 0, 0, 0 ]; + }, + + isCSSNullValue: function(value) { + /* The browser defaults CSS values that have not been set to either 0 or one of several possible null-value strings. + Thus, we check for both falsiness and these special strings. */ + /* Null-value checking is performed to default the special strings to 0 (for the sake of tweening) or their hook + templates as defined as CSS.Hooks (for the sake of hook injection/extraction). */ + /* Note: Chrome returns 'rgba(0, 0, 0, 0)' for an undefined color whereas IE returns 'transparent'. */ + return (value == 0 || /^(none|auto|transparent|(rgba\(0, ?0, ?0, ?0\)))$/i.test(value)); + }, + + /* Retrieve a property's default unit type. Used for assigning a unit type when one is not supplied by the user. */ + getUnitType: function(property) { + if (/^(rotate|skew)/i.test(property)) { + return 'deg'; + + } else if (/(^(scale|scaleX|scaleY|scaleZ|alpha|flexGrow|flexHeight|zIndex|fontWeight)$)|((opacity|red|green|blue|alpha)$)/i.test(property)) { + /* The above properties are unitless. */ + return ''; + } + + /* Default to px for all other properties. */ + return 'px'; + }, + + /* HTML elements default to an associated display type when they're not set to display:none. */ + /* Note: This function is used for correctly setting the non-'none' display value in certain Collide redirects, such as fadeIn/Out. */ + getDisplayType: function(element) { + var tagName = element && element.tagName && element.tagName.toString().toLowerCase(); + + if (/^(b|big|i|small|tt|abbr|acronym|cite|code|dfn|em|kbd|strong|samp|var|a|bdo|br|img|map|object|q|script|span|sub|sup|button|input|label|select|textarea)$/.test(tagName)) { + return 'inline'; + + } else if (/^(li)$/.test(tagName)) { + return 'list-item'; + + } else if (/^(tr)$/.test(tagName)) { + return 'table-row'; + + } else if (/^(table)$/.test(tagName)) { + return 'table'; + + } else if (/^(tbody)$/.test(tagName)) { + return 'table-row-group'; + } + + /* Default to 'block' when no match is found. */ + return 'block'; + } + + }, + + + /**************************** + CSS Style Getting & Setting + ****************************/ + + /* The singular getPropertyValue, which routes the logic for all normalizations, hooks, and standard CSS properties. */ + getPropertyValue: function(element, property, rootPropertyValue, forceStyleLookup) { + + /* Get an element's computed property value. */ + /* Note: Retrieving the value of a CSS property cannot simply be performed by checking an element's + style attribute (which only reflects user-defined values). Instead, the browser must be queried for a property's + *computed* value. You can read more about getComputedStyle here: https://developer.mozilla.org/en/docs/Web/API/window.getComputedStyle */ + function computePropertyValue (element, property) { + /* When box-sizing isn't set to border-box, height and width style values are incorrectly computed when an + element's scrollbars are visible (which expands the element's dimensions). Thus, we defer to the more accurate + offsetHeight/Width property, which includes the total dimensions for interior, border, padding, and scrollbar. + We subtract border and padding to get the sum of interior + scrollbar. */ + var computedValue = 0; + + /* Browsers do not return height and width values for elements that are set to display:'none'. Thus, we temporarily + toggle display to the element type's default value. */ + var toggleDisplay = false; + + if (/^(width|height)$/.test(property) && CSS.getPropertyValue(element, 'display') === 0) { + toggleDisplay = true; + CSS.setPropertyValue(element, 'display', CSS.Values.getDisplayType(element)); + } + + function revertDisplay () { + if (toggleDisplay) { + CSS.setPropertyValue(element, 'display', 'none'); + } + } + + if (!forceStyleLookup) { + if (property === 'height' && CSS.getPropertyValue(element, 'boxSizing').toString().toLowerCase() !== 'border-box') { + var contentBoxHeight = element.offsetHeight - (parseFloat(CSS.getPropertyValue(element, 'borderTopWidth')) || 0) - (parseFloat(CSS.getPropertyValue(element, 'borderBottomWidth')) || 0) - (parseFloat(CSS.getPropertyValue(element, 'paddingTop')) || 0) - (parseFloat(CSS.getPropertyValue(element, 'paddingBottom')) || 0); + revertDisplay(); + + return contentBoxHeight; + + } else if (property === 'width' && CSS.getPropertyValue(element, 'boxSizing').toString().toLowerCase() !== 'border-box') { + var contentBoxWidth = element.offsetWidth - (parseFloat(CSS.getPropertyValue(element, 'borderLeftWidth')) || 0) - (parseFloat(CSS.getPropertyValue(element, 'borderRightWidth')) || 0) - (parseFloat(CSS.getPropertyValue(element, 'paddingLeft')) || 0) - (parseFloat(CSS.getPropertyValue(element, 'paddingRight')) || 0); + revertDisplay(); + + return contentBoxWidth; + } + } + + var computedStyle; + var eleData = data(element); + + /* For elements that Collide hasn't been called on directly (e.g. when Collide queries the DOM on behalf + of a parent of an element its animating), perform a direct getComputedStyle lookup since the object isn't cached. */ + if (eleData === undefined) { + computedStyle = window.getComputedStyle(element, null); /* GET */ + + /* If the computedStyle object has yet to be cached, do so now. */ + } else if (!eleData.computedStyle) { + computedStyle = eleData.computedStyle = window.getComputedStyle(element, null); /* GET */ + + /* If computedStyle is cached, use it. */ + } else { + computedStyle = eleData.computedStyle; + } + + /* IE and Firefox do not return a value for the generic borderColor -- they only return individual values for each border side's color. + Also, in all browsers, when border colors aren't all the same, a compound value is returned that isn't setup to parse. + So, as a polyfill for querying individual border side colors, we just return the top border's color and animate all borders from that value. */ + if (property === 'borderColor') { + property = 'borderTopColor'; + } + + computedValue = computedStyle[property]; + + /* Fall back to the property's style value (if defined) when computedValue returns nothing, + which can happen when the element hasn't been painted. */ + if (computedValue === '' || computedValue === null) { + computedValue = element.style[property]; + } + + revertDisplay(); + + /* For top, right, bottom, and left (TRBL) values that are set to 'auto' on elements of 'fixed' or 'absolute' position, + defer to jQuery for converting 'auto' to a numeric value. (For elements with a 'static' or 'relative' position, 'auto' has the same + effect as being set to 0, so no conversion is necessary.) */ + /* An example of why numeric conversion is necessary: When an element with 'position:absolute' has an untouched 'left' + property, which reverts to 'auto', left's value is 0 relative to its parent element, but is often non-zero relative + to its *containing* (not parent) element, which is the nearest 'position:relative' ancestor or the viewport (and always the viewport in the case of 'position:fixed'). */ + if (computedValue === 'auto' && /^(top|right|bottom|left)$/i.test(property)) { + var position = computePropertyValue(element, 'position'); /* GET */ + + /* For absolute positioning, jQuery's $.position() only returns values for top and left; + right and bottom will have their 'auto' value reverted to 0. */ + /* Note: A jQuery object must be created here since jQuery doesn't have a low-level alias for $.position(). + Not a big deal since we're currently in a GET batch anyway. */ + if (position === 'fixed' || (position === 'absolute' && /top|left/i.test(property))) { + /* Note: jQuery strips the pixel unit from its returned values; we re-add it here to conform with computePropertyValue's behavior. */ + // TODO!!!! + computedValue = $(element).position()[property] + 'px'; /* GET */ + } + } + + return computedValue; + } + + var propertyValue; + + /* If this is a hooked property (e.g. 'clipLeft' instead of the root property of 'clip'), + extract the hook's value from a normalized rootPropertyValue using CSS.Hooks.extractValue(). */ + if (CSS.Hooks.registered[property]) { + var hook = property, + hookRoot = CSS.Hooks.getRoot(hook); + + /* If a cached rootPropertyValue wasn't passed in (which Collide always attempts to do in order to avoid requerying the DOM), + query the DOM for the root property's value. */ + if (rootPropertyValue === undefined) { + /* Since the browser is now being directly queried, use the official post-prefixing property name for this lookup. */ + rootPropertyValue = CSS.getPropertyValue(element, CSS.Names.prefixCheck(hookRoot)[0]); /* GET */ + } + + /* If this root has a normalization registered, peform the associated normalization extraction. */ + if (CSS.Normalizations.registered[hookRoot]) { + rootPropertyValue = CSS.Normalizations.registered[hookRoot]('extract', element, rootPropertyValue); + } + + /* Extract the hook's value. */ + propertyValue = CSS.Hooks.extractValue(hook, rootPropertyValue); + + /* If this is a normalized property (e.g. 'opacity' becomes 'filter' in <=IE8) or 'translateX' becomes 'transform'), + normalize the property's name and value, and handle the special case of transforms. */ + /* Note: Normalizing a property is mutually exclusive from hooking a property since hook-extracted values are strictly + numerical and therefore do not require normalization extraction. */ + } else if (CSS.Normalizations.registered[property]) { + var normalizedPropertyName, + normalizedPropertyValue; + + normalizedPropertyName = CSS.Normalizations.registered[property]('name', element); + + /* Transform values are calculated via normalization extraction (see below), which checks against the element's transformCache. + At no point do transform GETs ever actually query the DOM; initial stylesheet values are never processed. + This is because parsing 3D transform matrices is not always accurate and would bloat our codebase; + thus, normalization extraction defaults initial transform values to their zero-values (e.g. 1 for scaleX and 0 for translateX). */ + if (normalizedPropertyName !== 'transform') { + normalizedPropertyValue = computePropertyValue(element, CSS.Names.prefixCheck(normalizedPropertyName)[0]); /* GET */ + + /* If the value is a CSS null-value and this property has a hook template, use that zero-value template so that hooks can be extracted from it. */ + if (CSS.Values.isCSSNullValue(normalizedPropertyValue) && CSS.Hooks.templates[property]) { + normalizedPropertyValue = CSS.Hooks.templates[property][1]; + } + } + + propertyValue = CSS.Normalizations.registered[property]('extract', element, normalizedPropertyValue); + } + + /* If a (numeric) value wasn't produced via hook extraction or normalization, query the DOM. */ + if (!/^[\d-]/.test(propertyValue)) { + /* For SVG elements, dimensional properties (which SVGAttribute() detects) are tweened via + their HTML attribute values instead of their CSS style values. */ + var eleData = data(element); + if (eleData && eleData.isSVG && CSS.Names.SVGAttribute(property)) { + /* Since the height/width attribute values must be set manually, they don't reflect computed values. + Thus, we use use getBBox() to ensure we always get values for elements with undefined height/width attributes. */ + if (/^(height|width)$/i.test(property)) { + /* Firefox throws an error if .getBBox() is called on an SVG that isn't attached to the DOM. */ + try { + propertyValue = element.getBBox()[property]; + } catch (error) { + propertyValue = 0; + } + + /* Otherwise, access the attribute value directly. */ + } else { + propertyValue = element.getAttribute(property); + } + + } else { + propertyValue = computePropertyValue(element, CSS.Names.prefixCheck(property)[0]); /* GET */ + } + } + + /* Since property lookups are for animation purposes (which entails computing the numeric delta between start and end values), + convert CSS null-values to an integer of value 0. */ + if (CSS.Values.isCSSNullValue(propertyValue)) { + propertyValue = 0; + } + + return propertyValue; + }, + + /* The singular setPropertyValue, which routes the logic for all normalizations, hooks, and standard CSS properties. */ + setPropertyValue: function(element, property, propertyValue, rootPropertyValue, scrollData) { + var propertyName = property; + + /* In order to be subjected to call options and element queueing, scroll animation is routed through Collie 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, Collide defaults to scrolling the browser window. */ + } else { + if (scrollData.direction === 'Left') { + window.scrollTo(propertyValue, scrollData.alternateValue); + } else { + window.scrollTo(scrollData.alternateValue, propertyValue); + } + } + + } else { + var eleData = data(element); + + /* 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 = eleData.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]; + + /* SVG elements have their dimensional properties (width, height, x, y, cx, etc.) applied directly as attributes instead of as styles. */ + + if (eleData && eleData.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 (Collide.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 ]; + }, + + /* To increase performance by batching transform updates into a single SET, transforms are not directly applied to an element until flushTransformCache() is called. */ + /* 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 = data(element).transformCache; + + var transformValue, + perspective; + + /* Transform properties are stored as members of the transformCache object. Concatenate all the members into a string. */ + for (var transformName in transformCache) { + transformValue = transformCache[transformName]; + + /* Transform's perspective subproperty must be set first in order to take effect. Store it temporarily. */ + if (transformName === 'transformPerspective') { + perspective = transformValue; + return true; + } + + transformString += transformName + transformValue + ' '; + } + + /* If present, set the perspective subproperty first. */ + if (perspective) { + transformString = 'perspective' + perspective + ' ' + transformString; + } + + CSS.setPropertyValue(element, 'transform', transformString); + } + +}; + +const vendorPrefixes = [ '', 'Webkit', 'ms' ]; diff --git a/ionic/collide/easing.js b/ionic/collide/easing.js new file mode 100644 index 0000000000..58ca1bb0c8 --- /dev/null +++ b/ionic/collide/easing.js @@ -0,0 +1,312 @@ +/* Ported from Velocity.js, MIT License. Julian Shapiro http://twitter.com/shapiro */ + +import * as util from 'ionic/util/util' +import {Collide} from 'ionic/collide/collide' + + +/************** + Easing +**************/ + +/* Step easing generator. */ +function generateStep (steps) { + return function (p) { + return Math.round(p * steps) * (1 / steps); + }; +} + +/* Bezier curve function generator. Copyright Gaetan Renaudeau. MIT License: http://en.wikipedia.org/wiki/MIT_License */ +function generateBezier (mX1, mY1, mX2, mY2) { + var NEWTON_ITERATIONS = 4, + NEWTON_MIN_SLOPE = 0.001, + SUBDIVISION_PRECISION = 0.0000001, + SUBDIVISION_MAX_ITERATIONS = 10, + kSplineTableSize = 11, + kSampleStepSize = 1.0 / (kSplineTableSize - 1.0), + float32ArraySupported = "Float32Array" in window; + + /* Must contain four arguments. */ + if (arguments.length !== 4) { + return false; + } + + /* Arguments must be numbers. */ + for (var i = 0; i < 4; ++i) { + if (typeof arguments[i] !== "number" || isNaN(arguments[i]) || !isFinite(arguments[i])) { + return false; + } + } + + /* X values must be in the [0, 1] range. */ + mX1 = Math.min(mX1, 1); + mX2 = Math.min(mX2, 1); + mX1 = Math.max(mX1, 0); + mX2 = Math.max(mX2, 0); + + var mSampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize); + + function A (aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; } + function B (aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; } + function C (aA1) { return 3.0 * aA1; } + + function calcBezier (aT, aA1, aA2) { + return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT; + } + + function getSlope (aT, aA1, aA2) { + return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1); + } + + function newtonRaphsonIterate (aX, aGuessT) { + for (var i = 0; i < NEWTON_ITERATIONS; ++i) { + var currentSlope = getSlope(aGuessT, mX1, mX2); + + if (currentSlope === 0.0) return aGuessT; + + var currentX = calcBezier(aGuessT, mX1, mX2) - aX; + aGuessT -= currentX / currentSlope; + } + + return aGuessT; + } + + function calcSampleValues () { + for (var i = 0; i < kSplineTableSize; ++i) { + mSampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2); + } + } + + function binarySubdivide (aX, aA, aB) { + var currentX, currentT, i = 0; + + do { + currentT = aA + (aB - aA) / 2.0; + currentX = calcBezier(currentT, mX1, mX2) - aX; + if (currentX > 0.0) { + aB = currentT; + } else { + aA = currentT; + } + } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS); + + return currentT; + } + + function getTForX (aX) { + var intervalStart = 0.0, + currentSample = 1, + lastSample = kSplineTableSize - 1; + + for (; currentSample != lastSample && mSampleValues[currentSample] <= aX; ++currentSample) { + intervalStart += kSampleStepSize; + } + + --currentSample; + + var dist = (aX - mSampleValues[currentSample]) / (mSampleValues[currentSample+1] - mSampleValues[currentSample]), + guessForT = intervalStart + dist * kSampleStepSize, + initialSlope = getSlope(guessForT, mX1, mX2); + + if (initialSlope >= NEWTON_MIN_SLOPE) { + return newtonRaphsonIterate(aX, guessForT); + } else if (initialSlope == 0.0) { + return guessForT; + } else { + return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize); + } + } + + var _precomputed = false; + + function precompute() { + _precomputed = true; + if (mX1 != mY1 || mX2 != mY2) calcSampleValues(); + } + + var f = function (aX) { + if (!_precomputed) precompute(); + if (mX1 === mY1 && mX2 === mY2) return aX; + if (aX === 0) return 0; + if (aX === 1) return 1; + + return calcBezier(getTForX(aX), mY1, mY2); + }; + + f.getControlPoints = function() { return [{ x: mX1, y: mY1 }, { x: mX2, y: mY2 }]; }; + + var str = "generateBezier(" + [mX1, mY1, mX2, mY2] + ")"; + f.toString = function () { return str; }; + + return f; +} + +/* Runge-Kutta spring physics function generator. Adapted from Framer.js, copyright Koen Bok. MIT License: http://en.wikipedia.org/wiki/MIT_License */ +/* Given a tension, friction, and duration, a simulation at 60FPS will first run without a defined duration in order to calculate the full path. A second pass + then adjusts the time delta -- using the relation between actual time and duration -- to calculate the path for the duration-constrained animation. */ +var generateSpringRK4 = (function () { + function springAccelerationForState (state) { + return (-state.tension * state.x) - (state.friction * state.v); + } + + function springEvaluateStateWithDerivative (initialState, dt, derivative) { + var state = { + x: initialState.x + derivative.dx * dt, + v: initialState.v + derivative.dv * dt, + tension: initialState.tension, + friction: initialState.friction + }; + + return { dx: state.v, dv: springAccelerationForState(state) }; + } + + function springIntegrateState (state, dt) { + var a = { + dx: state.v, + dv: springAccelerationForState(state) + }, + b = springEvaluateStateWithDerivative(state, dt * 0.5, a), + c = springEvaluateStateWithDerivative(state, dt * 0.5, b), + d = springEvaluateStateWithDerivative(state, dt, c), + dxdt = 1.0 / 6.0 * (a.dx + 2.0 * (b.dx + c.dx) + d.dx), + dvdt = 1.0 / 6.0 * (a.dv + 2.0 * (b.dv + c.dv) + d.dv); + + state.x = state.x + dxdt * dt; + state.v = state.v + dvdt * dt; + + return state; + } + + return function springRK4Factory (tension, friction, duration) { + + var initState = { + x: -1, + v: 0, + tension: null, + friction: null + }, + path = [0], + time_lapsed = 0, + tolerance = 1 / 10000, + DT = 16 / 1000, + have_duration, dt, last_state; + + tension = parseFloat(tension) || 500; + friction = parseFloat(friction) || 20; + duration = duration || null; + + initState.tension = tension; + initState.friction = friction; + + have_duration = duration !== null; + + /* Calculate the actual time it takes for this animation to complete with the provided conditions. */ + if (have_duration) { + /* Run the simulation without a duration. */ + time_lapsed = springRK4Factory(tension, friction); + /* Compute the adjusted time delta. */ + dt = time_lapsed / duration * DT; + } else { + dt = DT; + } + + while (true) { + /* Next/step function .*/ + last_state = springIntegrateState(last_state || initState, dt); + /* Store the position. */ + path.push(1 + last_state.x); + time_lapsed += 16; + /* If the change threshold is reached, break. */ + if (!(Math.abs(last_state.x) > tolerance && Math.abs(last_state.v) > tolerance)) { + break; + } + } + + /* If duration is not defined, return the actual time required for completing this animation. Otherwise, return a closure that holds the + computed path and returns a snapshot of the position according to a given percentComplete. */ + return !have_duration ? time_lapsed : function(percentComplete) { return path[ (percentComplete * (path.length - 1)) | 0 ]; }; + }; +}()); + + +/* default 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)); } +}; + + +/* CSS3 and Robert Penner easings. */ +(function() { + + let penner = [ + [ "ease", [ 0.25, 0.1, 0.25, 1.0 ] ], + [ "ease-in", [ 0.42, 0.0, 1.00, 1.0 ] ], + [ "ease-out", [ 0.00, 0.0, 0.58, 1.0 ] ], + [ "ease-in-out", [ 0.42, 0.0, 0.58, 1.0 ] ], + [ "easeInSine", [ 0.47, 0, 0.745, 0.715 ] ], + [ "easeOutSine", [ 0.39, 0.575, 0.565, 1 ] ], + [ "easeInOutSine", [ 0.445, 0.05, 0.55, 0.95 ] ], + [ "easeInQuad", [ 0.55, 0.085, 0.68, 0.53 ] ], + [ "easeOutQuad", [ 0.25, 0.46, 0.45, 0.94 ] ], + [ "easeInOutQuad", [ 0.455, 0.03, 0.515, 0.955 ] ], + [ "easeInCubic", [ 0.55, 0.055, 0.675, 0.19 ] ], + [ "easeOutCubic", [ 0.215, 0.61, 0.355, 1 ] ], + [ "easeInOutCubic", [ 0.645, 0.045, 0.355, 1 ] ], + [ "easeInQuart", [ 0.895, 0.03, 0.685, 0.22 ] ], + [ "easeOutQuart", [ 0.165, 0.84, 0.44, 1 ] ], + [ "easeInOutQuart", [ 0.77, 0, 0.175, 1 ] ], + [ "easeInQuint", [ 0.755, 0.05, 0.855, 0.06 ] ], + [ "easeOutQuint", [ 0.23, 1, 0.32, 1 ] ], + [ "easeInOutQuint", [ 0.86, 0, 0.07, 1 ] ], + [ "easeInExpo", [ 0.95, 0.05, 0.795, 0.035 ] ], + [ "easeOutExpo", [ 0.19, 1, 0.22, 1 ] ], + [ "easeInOutExpo", [ 1, 0, 0, 1 ] ], + [ "easeInCirc", [ 0.6, 0.04, 0.98, 0.335 ] ], + [ "easeOutCirc", [ 0.075, 0.82, 0.165, 1 ] ], + [ "easeInOutCirc", [ 0.785, 0.135, 0.15, 0.86 ] ] + ]; + + for (let x = 0; x < penner.length; x++) { + Collide.Easings[ penner[x][0] ] = generateBezier.apply(null, penner[x][1]); + } + +})(); + + +/* Determine the appropriate easing type given an easing input. */ +export function getEasing(value, duration) { + let easing = value; + + /* The easing option can either be a string that references a pre-registered easing, + 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 (!Collide.Easings[value]) { + easing = false; + } + + } else if (util.isArray(value) && value.length === 1) { + easing = generateStep.apply(null, value); + + } else if (util.isArray(value) && value.length === 2) { + /* springRK4 must be passed the animation's duration. */ + /* Note: If the springRK4 array contains non-numbers, generateSpringRK4() returns an easing + function generated with default tension and friction values. */ + easing = generateSpringRK4.apply(null, value.concat([ duration ])); + + } else if (util.isArray(value) && value.length === 4) { + /* Note: If the bezier array contains non-numbers, generateBezier() returns false. */ + easing = generateBezier.apply(null, value); + + } else { + easing = false; + } + + /* Revert to the fall back to "swing" */ + if (easing === false) { + easing = 'swing'; + } + + return easing; +} diff --git a/ionic/collide/element-process.js b/ionic/collide/element-process.js new file mode 100644 index 0000000000..a1702fede3 --- /dev/null +++ b/ionic/collide/element-process.js @@ -0,0 +1,945 @@ +/* Ported from Velocity.js, MIT License. Julian Shapiro http://twitter.com/shapiro */ + +import * as util from 'ionic/util/util' +import {Collide} from 'ionic/collide/collide' +import {CSS} from 'ionic/collide/css' +import {getEasing} from 'ionic/collide/easing' +import {tick} from 'ionic/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/ionic/collide/tick.js b/ionic/collide/tick.js new file mode 100644 index 0000000000..162efd7368 --- /dev/null +++ b/ionic/collide/tick.js @@ -0,0 +1,265 @@ +/* Ported from Velocity.js, MIT License. Julian Shapiro http://twitter.com/shapiro */ + +import {Collide} from 'ionic/collide/collide' +import {CSS} from 'ionic/collide/css' +import {completeCall} from 'ionic/collide/complete-call' +import {dom} from 'ionic/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/ionic/collide/transition-action.js b/ionic/collide/transition-action.js new file mode 100644 index 0000000000..780f1bdaef --- /dev/null +++ b/ionic/collide/transition-action.js @@ -0,0 +1,155 @@ +/* Ported from Collide.js, MIT License. Julian Shapiro http://twitter.com/shapiro */ + +import {Collide} from 'ionic/collide/collide' +import {elementProcess} from 'ionic/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/ionic/collide/transition.js b/ionic/collide/transition.js new file mode 100644 index 0000000000..d70b11f041 --- /dev/null +++ b/ionic/collide/transition.js @@ -0,0 +1,66 @@ +import {transitionAction} from 'ionic/collide/transition-action' + + +export class Transition { + constructor(ele) { + console.log('Transition', ele) + + if (!ele || ele.length === 0) return + + this.elements = !ele.length ? [ele] : ele + ele = null + + // Animations that happen to this element + this.animations = [] + + // Sub transitions that happen to sub elements + this.transitions = [] + + this.options = {} + this.propertiesMap = {} + + this.isRunning = false + } + + start() { + var p = transitionAction('start', this.elements, this.options, this.propertiesMap) + + p.then(() => { + console.log('start success done') + }).catch(() => { + console.log('start error done') + }) + } + + stop() { + transitionAction('stop', this.elements, this.options, this.propertiesMap) + } + + properties(val) { + this.propertiesMap = val || {} + } + + property(key, val) { + this.propertiesMap[key] = val + } + + removeProperty(key) { + delete this.propertiesMap[key] + } + + duration(val) { + this.options.duration = val + } + + easing(val) { + this.options.easing = val + } + +} + + +export class IOSTransition extends Transition { + constructor(ele) { + super(ele) + } +} diff --git a/ionic/components.js b/ionic/components.js index afa5b55394..a70d4da65d 100644 --- a/ionic/components.js +++ b/ionic/components.js @@ -7,6 +7,8 @@ export * from 'ionic/components/checkbox/checkbox' export * from 'ionic/components/content/content' export * from 'ionic/components/icon/icon' export * from 'ionic/components/item/item' +export * from 'ionic/components/form/form' +export * from 'ionic/components/input/input' export * from 'ionic/components/layout/layout' export * from 'ionic/components/list/list' export * from 'ionic/components/nav-pane/nav-pane' diff --git a/ionic/components/action-menu/action-menu.js b/ionic/components/action-menu/action-menu.js index 396380cc22..7ae23f4803 100644 --- a/ionic/components/action-menu/action-menu.js +++ b/ionic/components/action-menu/action-menu.js @@ -1,44 +1,49 @@ -import {NgElement, Component, View, Parent} from 'angular2/angular2' -import {ComponentConfig} from 'ionic/config/component-config' +import {NgElement, Component, View as NgView, Parent} from 'angular2/angular2' +import {IonicComponent} from 'ionic/config/component' +import {Icon} from 'ionic/components/icon/icon' +import {Item} from 'ionic/components/item/item' -export let ActionMenuConfig = new ComponentConfig('action-menu') @Component({ - selector: 'ion-action-menu', - injectables: [ActionMenuConfig] + selector: 'ion-action-menu' }) -@View({ +@NgView({ template: `