import { isDefined } from '../util/util'; /** * @hidden */ export class Animation { constructor(plt, ele, opts) { this._dur = null; this._es = null; this._rvEs = null; this.hasChildren = false; this.isPlaying = false; this.hasCompleted = false; this.plt = plt; this.element(ele); this.opts = opts; } element(ele) { if (ele) { if (typeof ele === 'string') { ele = this.plt.doc().querySelectorAll(ele); for (let i = 0; i < ele.length; i++) { this._addEle(ele[i]); } } else if (ele.length) { for (let i = 0; i < ele.length; i++) { this._addEle(ele[i]); } } else { this._addEle(ele); } } return this; } /** * NO DOM */ _addEle(ele) { if (ele.nativeElement) { ele = ele.nativeElement; } if (ele.nodeType === 1) { this._eL = (this._e = this._e || []).push(ele); } } /** * Add a child animation to this animation. */ add(childAnimation) { childAnimation.parent = this; this.hasChildren = true; this._cL = (this._c = this._c || []).push(childAnimation); return this; } /** * Get the duration of this animation. If this animation does * not have a duration, then it'll get the duration from its parent. */ getDuration(opts) { if (opts && isDefined(opts.duration)) { return opts.duration; } else if (this._dur !== null) { return this._dur; } else if (this.parent) { return this.parent.getDuration(); } return 0; } /** * Returns if the animation is a root one. */ isRoot() { return !this.parent; } /** * Set the duration for this animation. */ duration(milliseconds) { this._dur = milliseconds; return this; } /** * Get the easing of this animation. If this animation does * not have an easing, then it'll get the easing from its parent. */ getEasing() { if (this._rv && this._rvEs) { return this._rvEs; } return this._es !== null ? this._es : (this.parent && this.parent.getEasing()) || null; } /** * Set the easing for this animation. */ easing(name) { this._es = name; return this; } /** * Set the easing for this reversed animation. */ easingReverse(name) { this._rvEs = name; return this; } /** * Add the "from" value for a specific property. */ from(prop, val) { this._addProp('from', prop, val); return this; } /** * Add the "to" value for a specific property. */ to(prop, val, clearProperyAfterTransition) { const fx = this._addProp('to', prop, val); if (clearProperyAfterTransition) { // if this effect is a transform then clear the transform effect // otherwise just clear the actual property this.afterClearStyles([fx.trans ? this.plt.Css.transform : prop]); } return this; } /** * Shortcut to add both the "from" and "to" for the same property. */ fromTo(prop, fromVal, toVal, clearProperyAfterTransition) { return this.from(prop, fromVal).to(prop, toVal, clearProperyAfterTransition); } /** * @hidden * NO DOM */ _getProp(name) { if (this._fx) { return this._fx.find((prop) => prop.name === name); } else { this._fx = []; } return null; } _addProp(state, prop, val) { let fxProp = this._getProp(prop); if (!fxProp) { // first time we've see this EffectProperty const shouldTrans = (ANIMATION_TRANSFORMS[prop] === 1); fxProp = { name: prop, trans: shouldTrans, // add the will-change property for transforms or opacity wc: (shouldTrans ? this.plt.Css.transform : prop) }; this._fx.push(fxProp); } // add from/to EffectState to the EffectProperty let fxState = { val: val, num: null, unit: '', }; fxProp[state] = fxState; if (typeof val === 'string' && val.indexOf(' ') < 0) { let r = val.match(ANIMATION_CSS_VALUE_REGEX); let num = parseFloat(r[1]); if (!isNaN(num)) { fxState.num = num; } fxState.unit = (r[0] !== r[2] ? r[2] : ''); } else if (typeof val === 'number') { fxState.num = val; } return fxProp; } /** * Add CSS class to this animation's elements * before the animation begins. */ beforeAddClass(className) { (this._bfAdd = this._bfAdd || []).push(className); return this; } /** * Remove CSS class from this animation's elements * before the animation begins. */ beforeRemoveClass(className) { (this._bfRm = this._bfRm || []).push(className); return this; } /** * Set CSS inline styles to this animation's elements * before the animation begins. */ beforeStyles(styles) { this._bfSty = styles; return this; } /** * Clear CSS inline styles from this animation's elements * before the animation begins. */ beforeClearStyles(propertyNames) { this._bfSty = this._bfSty || {}; for (let i = 0; i < propertyNames.length; i++) { this._bfSty[propertyNames[i]] = ''; } return this; } /** * Add a function which contains DOM reads, which will run * before the animation begins. */ beforeAddRead(domReadFn) { (this._rdFn = this._rdFn || []).push(domReadFn); return this; } /** * Add a function which contains DOM writes, which will run * before the animation begins. */ beforeAddWrite(domWriteFn) { (this._wrFn = this._wrFn || []).push(domWriteFn); return this; } /** * Add CSS class to this animation's elements * after the animation finishes. */ afterAddClass(className) { (this._afAdd = this._afAdd || []).push(className); return this; } /** * Remove CSS class from this animation's elements * after the animation finishes. */ afterRemoveClass(className) { (this._afRm = this._afRm || []).push(className); return this; } /** * Set CSS inline styles to this animation's elements * after the animation finishes. */ afterStyles(styles) { this._afSty = styles; return this; } /** * Clear CSS inline styles from this animation's elements * after the animation finishes. */ afterClearStyles(propertyNames) { this._afSty = this._afSty || {}; for (let i = 0; i < propertyNames.length; i++) { this._afSty[propertyNames[i]] = ''; } return this; } /** * Play the animation. */ play(opts) { // If the animation was already invalidated (it did finish), do nothing if (!this.plt) { return; } // this is the top level animation and is in full control // of when the async play() should actually kick off // if there is no duration then it'll set the TO property immediately // if there is a duration, then it'll stage all animations at the // FROM property and transition duration, wait a few frames, then // kick off the animation by setting the TO property for each animation this._isAsync = this._hasDuration(opts); // ensure all past transition end events have been cleared this._clearAsync(); // recursively kicks off the correct progress step for each child animation // ******** DOM WRITE **************** this._playInit(opts); // doubling up RAFs since this animation was probably triggered // from an input event, and just having one RAF would have this code // run within the same frame as the triggering input event, and the // input event probably already did way too much work for one frame this.plt.raf(() => { this.plt.raf(this._playDomInspect.bind(this, opts)); }); } syncPlay() { // If the animation was already invalidated (it did finish), do nothing if (!this.plt) { return; } const opts = { duration: 0 }; this._isAsync = false; this._clearAsync(); this._playInit(opts); this._playDomInspect(opts); } /** * @hidden * DOM WRITE * RECURSION */ _playInit(opts) { // always default that an animation does not tween // a tween requires that an Animation class has an element // and that it has at least one FROM/TO effect // and that the FROM/TO effect can tween numeric values this._twn = false; this.isPlaying = true; this.hasCompleted = false; this._hasDur = (this.getDuration(opts) > ANIMATION_DURATION_MIN); const children = this._c; for (let i = 0; i < this._cL; i++) { // ******** DOM WRITE **************** children[i]._playInit(opts); } if (this._hasDur) { // if there is a duration then we want to start at step 0 // ******** DOM WRITE **************** this._progress(0); // add the will-change properties // ******** DOM WRITE **************** this._willChg(true); } } /** * @hidden * DOM WRITE * NO RECURSION * ROOT ANIMATION */ _playDomInspect(opts) { // fire off all the "before" function that have DOM READS in them // elements will be in the DOM, however visibily hidden // so we can read their dimensions if need be // ******** DOM READ **************** // ******** DOM WRITE **************** this._beforeAnimation(); // for the root animation only // set the async TRANSITION END event // and run onFinishes when the transition ends const dur = this.getDuration(opts); if (this._isAsync) { this._asyncEnd(dur, true); } // ******** DOM WRITE **************** this._playProgress(opts); if (this._isAsync && this.plt) { // this animation has a duration so we need another RAF // for the CSS TRANSITION properties to kick in this.plt.raf(this._playToStep.bind(this, 1)); } } /** * @hidden * DOM WRITE * RECURSION */ _playProgress(opts) { const children = this._c; for (let i = 0; i < this._cL; i++) { // ******** DOM WRITE **************** children[i]._playProgress(opts); } if (this._hasDur) { // set the CSS TRANSITION duration/easing // ******** DOM WRITE **************** this._setTrans(this.getDuration(opts), false); } else { // this animation does not have a duration, so it should not animate // just go straight to the TO properties and call it done // ******** DOM WRITE **************** this._progress(1); // since there was no animation, immediately run the after // ******** DOM WRITE **************** this._setAfterStyles(); // this animation has no duration, so it has finished // other animations could still be running this._didFinish(true); } } /** * @hidden * DOM WRITE * RECURSION */ _playToStep(stepValue) { const children = this._c; for (let i = 0; i < this._cL; i++) { // ******** DOM WRITE **************** children[i]._playToStep(stepValue); } if (this._hasDur) { // browser had some time to render everything in place // and the transition duration/easing is set // now set the TO properties which will trigger the transition to begin // ******** DOM WRITE **************** this._progress(stepValue); } } /** * @hidden * DOM WRITE * NO RECURSION * ROOT ANIMATION */ _asyncEnd(dur, shouldComplete) { (void 0) /* assert */; (void 0) /* assert */; (void 0) /* assert */; const self = this; function onTransitionEnd() { // congrats! a successful transition completed! // ensure transition end events and timeouts have been cleared self._clearAsync(); // ******** DOM WRITE **************** self._playEnd(); // transition finished self._didFinishAll(shouldComplete, true, false); } function onTransitionFallback() { (void 0) /* console.debug */; // oh noz! the transition end event didn't fire in time! // instead the fallback timer when first // if all goes well this fallback should never fire // clear the other async end events from firing self._tm = undefined; self._clearAsync(); // set the after styles // ******** DOM WRITE **************** self._playEnd(shouldComplete ? 1 : 0); // transition finished self._didFinishAll(shouldComplete, true, false); } // set the TRANSITION END event on one of the transition elements self._unrgTrns = this.plt.transitionEnd(self._transEl(), onTransitionEnd, false); // set a fallback timeout if the transition end event never fires, or is too slow // transition end fallback: (animation duration + XXms) self._tm = self.plt.timeout(onTransitionFallback, (dur + ANIMATION_TRANSITION_END_FALLBACK_PADDING_MS)); } /** * @hidden * DOM WRITE * RECURSION */ _playEnd(stepValue) { const children = this._c; for (let i = 0; i < this._cL; i++) { // ******** DOM WRITE **************** children[i]._playEnd(stepValue); } if (this._hasDur) { if (isDefined(stepValue)) { // too late to have a smooth animation, just finish it // ******** DOM WRITE **************** this._setTrans(0, true); // ensure the ending progress step gets rendered // ******** DOM WRITE **************** this._progress(stepValue); } // set the after styles // ******** DOM WRITE **************** this._setAfterStyles(); // remove the will-change properties // ******** DOM WRITE **************** this._willChg(false); } } /** * @hidden * NO DOM * RECURSION */ _hasDuration(opts) { if (this.getDuration(opts) > ANIMATION_DURATION_MIN) { return true; } const children = this._c; for (let i = 0; i < this._cL; i++) { if (children[i]._hasDuration(opts)) { return true; } } return false; } /** * @hidden * NO DOM * RECURSION */ _hasDomReads() { if (this._rdFn && this._rdFn.length) { return true; } const children = this._c; for (let i = 0; i < this._cL; i++) { if (children[i]._hasDomReads()) { return true; } } return false; } /** * Immediately stop at the end of the animation. */ stop(stepValue = 1) { // ensure all past transition end events have been cleared this._clearAsync(); this._hasDur = true; this._playEnd(stepValue); } /** * @hidden * NO DOM * NO RECURSION */ _clearAsync() { this._unrgTrns && this._unrgTrns(); this._tm && clearTimeout(this._tm); this._tm = this._unrgTrns = undefined; } /** * @hidden * DOM WRITE * NO RECURSION */ _progress(stepValue) { // bread 'n butter let val; let effects = this._fx; let nuElements = this._eL; if (!effects || !nuElements) { return; } // flip the number if we're going in reverse if (this._rv) { stepValue = ((stepValue * -1) + 1); } let i, j; let finalTransform = ''; const elements = this._e; for (i = 0; i < effects.length; i++) { const fx = effects[i]; if (fx.from && fx.to) { const fromNum = fx.from.num; const toNum = fx.to.num; const tweenEffect = (fromNum !== toNum); (void 0) /* assert */; if (tweenEffect) { this._twn = true; } if (stepValue === 0) { // FROM val = fx.from.val; } else if (stepValue === 1) { // TO val = fx.to.val; } else if (tweenEffect) { // EVERYTHING IN BETWEEN let valNum = (((toNum - fromNum) * stepValue) + fromNum); const unit = fx.to.unit; if (unit === 'px') { valNum = Math.round(valNum); } val = valNum + unit; } if (val !== null) { const prop = fx.name; if (fx.trans) { finalTransform += prop + '(' + val + ') '; } else { for (j = 0; j < nuElements; j++) { // ******** DOM WRITE **************** elements[j].style[prop] = val; } } } } } // place all transforms on the same property if (finalTransform.length) { if (!this._rv && stepValue !== 1 || this._rv && stepValue !== 0) { finalTransform += 'translateZ(0px)'; } const cssTransform = this.plt.Css.transform; for (i = 0; i < elements.length; i++) { // ******** DOM WRITE **************** elements[i].style[cssTransform] = finalTransform; } } } /** * @hidden * DOM WRITE * NO RECURSION */ _setTrans(dur, forcedLinearEasing) { // Transition is not enabled if there are not effects if (!this._fx) { return; } // set the TRANSITION properties inline on the element const elements = this._e; const easing = (forcedLinearEasing ? 'linear' : this.getEasing()); const durString = dur + 'ms'; const Css = this.plt.Css; const cssTransform = Css.transition; const cssTransitionDuration = Css.transitionDuration; const cssTransitionTimingFn = Css.transitionTimingFn; let eleStyle; for (let i = 0; i < this._eL; i++) { eleStyle = elements[i].style; if (dur > 0) { // ******** DOM WRITE **************** eleStyle[cssTransform] = ''; eleStyle[cssTransitionDuration] = durString; // each animation can have a different easing if (easing) { // ******** DOM WRITE **************** eleStyle[cssTransitionTimingFn] = easing; } } else { eleStyle[cssTransform] = 'none'; } } } /** * @hidden * DOM READ * DOM WRITE * RECURSION */ _beforeAnimation() { // fire off all the "before" function that have DOM READS in them // elements will be in the DOM, however visibily hidden // so we can read their dimensions if need be // ******** DOM READ **************** this._fireBeforeReadFunc(); // ******** DOM READS ABOVE / DOM WRITES BELOW **************** // fire off all the "before" function that have DOM WRITES in them // ******** DOM WRITE **************** this._fireBeforeWriteFunc(); // stage all of the before css classes and inline styles // ******** DOM WRITE **************** this._setBeforeStyles(); } /** * @hidden * DOM WRITE * RECURSION */ _setBeforeStyles() { let i, j; const children = this._c; for (i = 0; i < this._cL; i++) { children[i]._setBeforeStyles(); } // before the animations have started // only set before styles if animation is not reversed if (this._rv) { return; } const addClasses = this._bfAdd; const removeClasses = this._bfRm; let ele; let eleClassList; let prop; for (i = 0; i < this._eL; i++) { ele = this._e[i]; eleClassList = ele.classList; // css classes to add before the animation if (addClasses) { for (j = 0; j < addClasses.length; j++) { // ******** DOM WRITE **************** eleClassList.add(addClasses[j]); } } // css classes to remove before the animation if (removeClasses) { for (j = 0; j < removeClasses.length; j++) { // ******** DOM WRITE **************** eleClassList.remove(removeClasses[j]); } } // inline styles to add before the animation if (this._bfSty) { for (prop in this._bfSty) { // ******** DOM WRITE **************** ele.style[prop] = this._bfSty[prop]; } } } } /** * @hidden * DOM READ * RECURSION */ _fireBeforeReadFunc() { const children = this._c; for (let i = 0; i < this._cL; i++) { // ******** DOM READ **************** children[i]._fireBeforeReadFunc(); } const readFunctions = this._rdFn; if (readFunctions) { for (let i = 0; i < readFunctions.length; i++) { // ******** DOM READ **************** readFunctions[i](); } } } /** * @hidden * DOM WRITE * RECURSION */ _fireBeforeWriteFunc() { const children = this._c; for (let i = 0; i < this._cL; i++) { // ******** DOM WRITE **************** children[i]._fireBeforeWriteFunc(); } const writeFunctions = this._wrFn; if (this._wrFn) { for (let i = 0; i < writeFunctions.length; i++) { // ******** DOM WRITE **************** writeFunctions[i](); } } } /** * @hidden * DOM WRITE */ _setAfterStyles() { let i, j; let ele; let eleClassList; let elements = this._e; for (i = 0; i < this._eL; i++) { ele = elements[i]; eleClassList = ele.classList; // remove the transition duration/easing // ******** DOM WRITE **************** ele.style[this.plt.Css.transitionDuration] = ele.style[this.plt.Css.transitionTimingFn] = ''; if (this._rv) { // finished in reverse direction // css classes that were added before the animation should be removed if (this._bfAdd) { for (j = 0; j < this._bfAdd.length; j++) { // ******** DOM WRITE **************** eleClassList.remove(this._bfAdd[j]); } } // css classes that were removed before the animation should be added if (this._bfRm) { for (j = 0; j < this._bfRm.length; j++) { // ******** DOM WRITE **************** eleClassList.add(this._bfRm[j]); } } // inline styles that were added before the animation should be removed if (this._bfSty) { for (const prop in this._bfSty) { // ******** DOM WRITE **************** ele.style[prop] = ''; } } } else { // finished in forward direction // css classes to add after the animation if (this._afAdd) { for (j = 0; j < this._afAdd.length; j++) { // ******** DOM WRITE **************** eleClassList.add(this._afAdd[j]); } } // css classes to remove after the animation if (this._afRm) { for (j = 0; j < this._afRm.length; j++) { // ******** DOM WRITE **************** eleClassList.remove(this._afRm[j]); } } // inline styles to add after the animation if (this._afSty) { for (const prop in this._afSty) { // ******** DOM WRITE **************** ele.style[prop] = this._afSty[prop]; } } } } } /** * @hidden * DOM WRITE * NO RECURSION */ _willChg(addWillChange) { let wc; let effects = this._fx; let willChange; if (addWillChange && effects) { wc = []; for (let i = 0; i < effects.length; i++) { const propWC = effects[i].wc; if (propWC === 'webkitTransform') { wc.push('transform', '-webkit-transform'); } else { wc.push(propWC); } } willChange = wc.join(','); } else { willChange = ''; } for (let i = 0; i < this._eL; i++) { // ******** DOM WRITE **************** this._e[i].style.willChange = willChange; } } /** * Start the animation with a user controlled progress. */ progressStart() { // ensure all past transition end events have been cleared this._clearAsync(); // ******** DOM READ/WRITE **************** this._beforeAnimation(); // ******** DOM WRITE **************** this._progressStart(); } /** * @hidden * DOM WRITE * RECURSION */ _progressStart() { const children = this._c; for (let i = 0; i < this._cL; i++) { // ******** DOM WRITE **************** children[i]._progressStart(); } // force no duration, linear easing // ******** DOM WRITE **************** this._setTrans(0, true); // ******** DOM WRITE **************** this._willChg(true); } /** * Set the progress step for this animation. * progressStep() is not debounced, so it should not be called faster than 60FPS. */ progressStep(stepValue) { // only update if the last update was more than 16ms ago stepValue = Math.min(1, Math.max(0, stepValue)); const children = this._c; for (let i = 0; i < this._cL; i++) { // ******** DOM WRITE **************** children[i].progressStep(stepValue); } if (this._rv) { // if the animation is going in reverse then // flip the step value: 0 becomes 1, 1 becomes 0 stepValue = ((stepValue * -1) + 1); } // ******** DOM WRITE **************** this._progress(stepValue); } /** * End the progress animation. */ progressEnd(shouldComplete, currentStepValue, dur = -1) { (void 0) /* console.debug */; if (this._rv) { // if the animation is going in reverse then // flip the step value: 0 becomes 1, 1 becomes 0 currentStepValue = ((currentStepValue * -1) + 1); } const stepValue = shouldComplete ? 1 : 0; const diff = Math.abs(currentStepValue - stepValue); if (diff < 0.05) { dur = 0; } else if (dur < 0) { dur = this._dur; } this._isAsync = (dur > 30); this._progressEnd(shouldComplete, stepValue, dur, this._isAsync); if (this._isAsync) { // for the root animation only // set the async TRANSITION END event // and run onFinishes when the transition ends // ******** DOM WRITE **************** this._asyncEnd(dur, shouldComplete); // this animation has a duration so we need another RAF // for the CSS TRANSITION properties to kick in this.plt && this.plt.raf(this._playToStep.bind(this, stepValue)); } } /** * @hidden * DOM WRITE * RECURSION */ _progressEnd(shouldComplete, stepValue, dur, isAsync) { const children = this._c; for (let i = 0; i < this._cL; i++) { // ******** DOM WRITE **************** children[i]._progressEnd(shouldComplete, stepValue, dur, isAsync); } if (!isAsync) { // stop immediately // set all the animations to their final position // ******** DOM WRITE **************** this._progress(stepValue); this._willChg(false); this._setAfterStyles(); this._didFinish(shouldComplete); } else { // animate it back to it's ending position this.isPlaying = true; this.hasCompleted = false; this._hasDur = true; // ******** DOM WRITE **************** this._willChg(true); this._setTrans(dur, false); } } /** * Add a callback to fire when the animation has finished. */ onFinish(callback, onceTimeCallback = false, clearOnFinishCallacks = false) { if (clearOnFinishCallacks) { this._fFn = this._fOneFn = undefined; } if (onceTimeCallback) { this._fOneFn = this._fOneFn || []; this._fOneFn.push(callback); } else { this._fFn = this._fFn || []; this._fFn.push(callback); } return this; } /** * @hidden * NO DOM * RECURSION */ _didFinishAll(hasCompleted, finishAsyncAnimations, finishNoDurationAnimations) { const children = this._c; for (let i = 0; i < this._cL; i++) { children[i]._didFinishAll(hasCompleted, finishAsyncAnimations, finishNoDurationAnimations); } if (finishAsyncAnimations && this._isAsync || finishNoDurationAnimations && !this._isAsync) { this._didFinish(hasCompleted); } } /** * @hidden * NO RECURSION */ _didFinish(hasCompleted) { this.isPlaying = false; this.hasCompleted = hasCompleted; if (this._fFn) { // run all finish callbacks for (let i = 0; i < this._fFn.length; i++) { this._fFn[i](this); } } if (this._fOneFn) { // run all "onetime" finish callbacks for (let i = 0; i < this._fOneFn.length; i++) { this._fOneFn[i](this); } this._fOneFn.length = 0; } } /** * Reverse the animation. */ reverse(shouldReverse = true) { const children = this._c; for (let i = 0; i < this._cL; i++) { children[i].reverse(shouldReverse); } this._rv = shouldReverse; return this; } /** * Recursively destroy this animation and all child animations. */ destroy() { const children = this._c; for (let i = 0; i < this._cL; i++) { children[i].destroy(); } this._clearAsync(); this.parent = this.plt = this._e = this._rdFn = this._wrFn = null; if (this._c) { this._c.length = this._cL = 0; } if (this._fFn) { this._fFn.length = 0; } if (this._fOneFn) { this._fOneFn.length = 0; } } /** * @hidden * NO DOM */ _transEl() { // get the lowest level element that has an Animation let targetEl; for (let i = 0; i < this._cL; i++) { targetEl = this._c[i]._transEl(); if (targetEl) { return targetEl; } } return (this._twn && this._hasDur && this._eL ? this._e[0] : null); } } const ANIMATION_TRANSFORMS = { 'translateX': 1, 'translateY': 1, 'translateZ': 1, 'scale': 1, 'scaleX': 1, 'scaleY': 1, 'scaleZ': 1, 'rotate': 1, 'rotateX': 1, 'rotateY': 1, 'rotateZ': 1, 'skewX': 1, 'skewY': 1, 'perspective': 1 }; const ANIMATION_CSS_VALUE_REGEX = /(^-?\d*\.?\d*)(.*)/; const ANIMATION_DURATION_MIN = 32; const ANIMATION_TRANSITION_END_FALLBACK_PADDING_MS = 400; //# sourceMappingURL=animation.js.map