mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 02:31:34 +08:00
1137 lines
28 KiB
TypeScript
1137 lines
28 KiB
TypeScript
// TODO: Add more tests. until then, be sure to manually test menu and swipe to go back/routing transitions
|
|
|
|
import { raf } from '../helpers';
|
|
|
|
import { Animation, AnimationDirection, AnimationFill, AnimationOnFinishCallback, AnimationOnFinishOptions } from './animation-interface';
|
|
import { addClassToArray, animationEnd, createKeyframeStylesheet, generateKeyframeName, generateKeyframeRules, removeStyleProperty, setStyleProperty } from './animation-utils';
|
|
|
|
export const createAnimation = () => {
|
|
let _delay: number | undefined;
|
|
let _duration: number | undefined;
|
|
let _easing: string | undefined;
|
|
let _iterations: number | undefined;
|
|
let _fill: AnimationFill | undefined;
|
|
let _direction: AnimationDirection | undefined;
|
|
let _keyframes: any[] = [];
|
|
let beforeAddClasses: string[] = [];
|
|
let beforeRemoveClasses: string[] = [];
|
|
let initialized = false;
|
|
let parentAnimation: Animation | undefined;
|
|
let beforeStylesValue: { [property: string]: any } = {};
|
|
let afterAddClasses: string[] = [];
|
|
let afterRemoveClasses: string[] = [];
|
|
let afterStylesValue: { [property: string]: any } = {};
|
|
let numAnimationsRunning = 0;
|
|
let shouldForceLinearEasing = false;
|
|
let shouldForceSyncPlayback = false;
|
|
let cssAnimationsTimerFallback: any;
|
|
let forceDirectionValue: AnimationDirection | undefined;
|
|
let forceDurationValue: number | undefined;
|
|
let forceDelayValue: number | undefined;
|
|
let willComplete = true;
|
|
let finished = false;
|
|
let shouldCalculateNumAnimations = true;
|
|
let keyframeName: string | undefined;
|
|
let ani: Animation;
|
|
|
|
const onFinishCallbacks: AnimationOnFinishCallback[] = [];
|
|
const onFinishOneTimeCallbacks: AnimationOnFinishCallback[] = [];
|
|
const elements: HTMLElement[] = [];
|
|
const childAnimations: Animation[] = [];
|
|
const stylesheets: HTMLElement[] = [];
|
|
const _beforeAddReadFunctions: any[] = [];
|
|
const _beforeAddWriteFunctions: any[] = [];
|
|
const _afterAddReadFunctions: any[] = [];
|
|
const _afterAddWriteFunctions: any[] = [];
|
|
const webAnimations: any[] = [];
|
|
const supportsAnimationEffect = (typeof (AnimationEffect as any) === 'function' || typeof (window as any).AnimationEffect === 'function');
|
|
const supportsWebAnimations = (typeof (Element as any) === 'function') && (typeof (Element as any).prototype!.animate === 'function') && supportsAnimationEffect;
|
|
const ANIMATION_END_FALLBACK_PADDING_MS = 100;
|
|
|
|
/**
|
|
* Returns the raw Web Animations object
|
|
* for all elements in an Animation.
|
|
* This will return an empty array on
|
|
* browsers that do not support
|
|
* the Web Animations API.
|
|
*/
|
|
const getWebAnimations = () => {
|
|
return webAnimations;
|
|
};
|
|
|
|
/**
|
|
* Destroy the animation and all child animations.
|
|
*/
|
|
const destroy = () => {
|
|
childAnimations.forEach(childAnimation => {
|
|
childAnimation.destroy();
|
|
});
|
|
|
|
cleanUp();
|
|
|
|
elements.length = 0;
|
|
childAnimations.length = 0;
|
|
_keyframes.length = 0;
|
|
|
|
clearOnFinish();
|
|
|
|
initialized = false;
|
|
shouldCalculateNumAnimations = true;
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Cancels any Web Animations, removes
|
|
* any animation properties from the
|
|
* animation's elements, and removes the
|
|
* animation's stylesheets from the DOM.
|
|
*/
|
|
const cleanUp = () => {
|
|
cleanUpElements();
|
|
cleanUpStyleSheets();
|
|
};
|
|
|
|
/**
|
|
* Add a callback to be run
|
|
* upon the animation ending
|
|
*/
|
|
const onFinish = (callback: any, opts?: AnimationOnFinishOptions) => {
|
|
const callbacks = (opts && opts.oneTimeCallback) ? onFinishOneTimeCallbacks : onFinishCallbacks;
|
|
callbacks.push({ callback, opts } as AnimationOnFinishCallback);
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Clears all callbacks
|
|
*/
|
|
const clearOnFinish = () => {
|
|
onFinishCallbacks.length = 0;
|
|
onFinishOneTimeCallbacks.length = 0;
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Cancels any Web Animations and removes
|
|
* any animation properties from the
|
|
* the animation's elements.
|
|
*/
|
|
const cleanUpElements = () => {
|
|
if (supportsWebAnimations) {
|
|
getWebAnimations().forEach(animation => {
|
|
animation.cancel();
|
|
});
|
|
|
|
webAnimations.length = 0;
|
|
} else {
|
|
elements.forEach(element => {
|
|
raf(() => {
|
|
removeStyleProperty(element, 'animation-name');
|
|
removeStyleProperty(element, 'animation-duration');
|
|
removeStyleProperty(element, 'animation-timing-function');
|
|
removeStyleProperty(element, 'animation-iteration-count');
|
|
removeStyleProperty(element, 'animation-delay');
|
|
removeStyleProperty(element, 'animation-play-state');
|
|
removeStyleProperty(element, 'animation-fill-mode');
|
|
removeStyleProperty(element, 'animation-direction');
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Removes the animation's stylesheets
|
|
* from the DOM.
|
|
*/
|
|
const cleanUpStyleSheets = () => {
|
|
stylesheets.forEach(stylesheet => {
|
|
/**
|
|
* When sharing stylesheets, it's possible
|
|
* for another animation to have already
|
|
* cleaned up a particular stylesheet
|
|
*/
|
|
if (stylesheet && stylesheet.parentNode) {
|
|
stylesheet.parentNode.removeChild(stylesheet);
|
|
}
|
|
});
|
|
|
|
stylesheets.length = 0;
|
|
};
|
|
|
|
/**
|
|
* Add a function that performs a
|
|
* DOM read to be run before the
|
|
* animation starts
|
|
*/
|
|
const beforeAddRead = (readFn: () => void) => {
|
|
_beforeAddReadFunctions.push(readFn);
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Add a function that performs a
|
|
* DOM write to be run before the
|
|
* animation starts
|
|
*/
|
|
const beforeAddWrite = (writeFn: () => void) => {
|
|
_beforeAddWriteFunctions.push(writeFn);
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Add a function that performs a
|
|
* DOM read to be run after the
|
|
* animation end
|
|
*/
|
|
const afterAddRead = (readFn: () => void) => {
|
|
_afterAddReadFunctions.push(readFn);
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Add a function that performs a
|
|
* DOM write to be run after the
|
|
* animation end
|
|
*/
|
|
const afterAddWrite = (writeFn: () => void) => {
|
|
_afterAddWriteFunctions.push(writeFn);
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Add a class to the animation's
|
|
* elements before the animation starts
|
|
*/
|
|
const beforeAddClass = (className: string | string[] | undefined) => {
|
|
beforeAddClasses = addClassToArray(beforeAddClasses, className);
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Remove a class from the animation's
|
|
* elements before the animation starts
|
|
*/
|
|
const beforeRemoveClass = (className: string | string[] | undefined) => {
|
|
beforeRemoveClasses = addClassToArray(beforeRemoveClasses, className);
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Set CSS inline styles to the animation's
|
|
* elements before the animation begins.
|
|
*/
|
|
const beforeStyles = (styles: { [property: string]: any } = {}) => {
|
|
beforeStylesValue = styles;
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Clear CSS inline styles from the animation's
|
|
* elements before the animation begins.
|
|
*/
|
|
const beforeClearStyles = (propertyNames: string[] = []) => {
|
|
for (const property of propertyNames) {
|
|
beforeStylesValue[property] = '';
|
|
}
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Add CSS class to the animation's
|
|
* elements after the animation ends.
|
|
*/
|
|
const afterAddClass = (className: string | string[] | undefined) => {
|
|
afterAddClasses = addClassToArray(afterAddClasses, className);
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Remove CSS class from the animation's
|
|
* elements after the animation ends.
|
|
*/
|
|
const afterRemoveClass = (className: string | string[] | undefined) => {
|
|
afterRemoveClasses = addClassToArray(afterRemoveClasses, className);
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Set CSS inline styles to the animation's
|
|
* elements after the animation ends.
|
|
*/
|
|
const afterStyles = (styles: { [property: string]: any } = {}) => {
|
|
afterStylesValue = styles;
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Clear CSS inline styles from the animation's
|
|
* elements after the animation ends.
|
|
*/
|
|
const afterClearStyles = (propertyNames: string[] = []) => {
|
|
for (const property of propertyNames) {
|
|
afterStylesValue[property] = '';
|
|
}
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Returns the animation's fill mode.
|
|
*/
|
|
const getFill = () => {
|
|
if (_fill !== undefined) { return _fill; }
|
|
if (parentAnimation) { return parentAnimation.getFill(); }
|
|
|
|
return 'both';
|
|
};
|
|
|
|
/**
|
|
* Returns the animation's direction.
|
|
*/
|
|
const getDirection = () => {
|
|
if (forceDirectionValue !== undefined) { return forceDirectionValue; }
|
|
if (_direction !== undefined) { return _direction; }
|
|
if (parentAnimation) { return parentAnimation.getDirection(); }
|
|
|
|
return 'normal';
|
|
|
|
};
|
|
|
|
/**
|
|
* Returns the animation's easing.
|
|
*/
|
|
const getEasing = () => {
|
|
if (shouldForceLinearEasing) { return 'linear'; }
|
|
if (_easing !== undefined) { return _easing; }
|
|
if (parentAnimation) { return parentAnimation.getEasing(); }
|
|
|
|
return 'linear';
|
|
};
|
|
|
|
/**
|
|
* Gets the animation's duration in milliseconds.
|
|
*/
|
|
const getDuration = () => {
|
|
if (shouldForceSyncPlayback) { return 0; }
|
|
if (forceDurationValue !== undefined) { return forceDurationValue; }
|
|
if (_duration !== undefined) { return _duration; }
|
|
if (parentAnimation) { return parentAnimation.getDuration(); }
|
|
|
|
return 0;
|
|
};
|
|
|
|
/**
|
|
* Gets the number of iterations the animation will run.
|
|
*/
|
|
const getIterations = () => {
|
|
if (_iterations !== undefined) { return _iterations; }
|
|
if (parentAnimation) { return parentAnimation.getIterations(); }
|
|
|
|
return 1;
|
|
};
|
|
|
|
/**
|
|
* Gets the animation's delay in milliseconds.
|
|
*/
|
|
const getDelay = () => {
|
|
if (forceDelayValue !== undefined) { return forceDelayValue; }
|
|
if (_delay !== undefined) { return _delay; }
|
|
if (parentAnimation) { return parentAnimation.getDelay(); }
|
|
|
|
return 0;
|
|
};
|
|
|
|
/**
|
|
* Get an array of keyframes for the animation.
|
|
*/
|
|
const getKeyframes = () => {
|
|
return _keyframes;
|
|
};
|
|
|
|
/**
|
|
* Sets whether the animation should play forwards,
|
|
* backwards, or alternating back and forth.
|
|
*/
|
|
const direction = (animationDirection: AnimationDirection) => {
|
|
_direction = animationDirection;
|
|
|
|
update(true);
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Sets how the animation applies styles to its
|
|
* elements before and after the animation's execution.
|
|
*/
|
|
const fill = (animationFill: AnimationFill) => {
|
|
_fill = animationFill;
|
|
|
|
update(true);
|
|
|
|
return ani;
|
|
|
|
};
|
|
|
|
/**
|
|
* Sets when an animation starts (in milliseconds).
|
|
*/
|
|
const delay = (animationDelay: number) => {
|
|
_delay = animationDelay;
|
|
|
|
update(true);
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Sets how the animation progresses through the
|
|
* duration of each cycle.
|
|
*/
|
|
const easing = (animationEasing: string) => {
|
|
_easing = animationEasing;
|
|
|
|
update(true);
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Sets the length of time the animation takes
|
|
* to complete one cycle.
|
|
*/
|
|
const duration = (animationDuration: number) => {
|
|
/**
|
|
* CSS Animation Durations of 0ms work fine on Chrome
|
|
* but do not run on Safari, so force it to 1ms to
|
|
* get it to run on both platforms.
|
|
*/
|
|
if (!supportsWebAnimations && animationDuration === 0) {
|
|
animationDuration = 1;
|
|
}
|
|
|
|
_duration = animationDuration;
|
|
|
|
update(true);
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Sets the number of times the animation cycle
|
|
* should be played before stopping.
|
|
*/
|
|
const iterations = (animationIterations: number) => {
|
|
_iterations = animationIterations;
|
|
|
|
update(true);
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Sets the parent animation.
|
|
*/
|
|
const parent = (animation: Animation) => {
|
|
parentAnimation = animation;
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Add one or more elements to the animation
|
|
*/
|
|
const addElement = (el: Element | Element[] | Node | Node[] | NodeList | undefined | null) => {
|
|
if (el != null) {
|
|
|
|
if ((el as Node).nodeType === 1) {
|
|
elements.push(el as any);
|
|
} else if ((el as NodeList).length >= 0) {
|
|
for (let i = 0; i < (el as NodeList).length; i++) {
|
|
elements.push((el as any)[i]);
|
|
}
|
|
} else {
|
|
console.error('Invalid addElement value');
|
|
}
|
|
}
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Group one or more animations together to be controlled by a parent animation.
|
|
*/
|
|
const addAnimation = (animationToAdd: Animation | Animation[] | undefined | null) => {
|
|
if (animationToAdd != null) {
|
|
const parentAnim = ani;
|
|
const animationsToAdd = animationToAdd as Animation[];
|
|
if (animationsToAdd.length >= 0) {
|
|
for (const animation of animationsToAdd) {
|
|
animation.parent(parentAnim);
|
|
childAnimations.push(animation);
|
|
}
|
|
} else {
|
|
(animationToAdd as Animation).parent(parentAnim);
|
|
childAnimations.push(animationToAdd as Animation);
|
|
}
|
|
}
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Set the keyframes for the animation.
|
|
*/
|
|
const keyframes = (keyframeValues: any[]) => {
|
|
_keyframes = keyframeValues;
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Runs all before read callbacks
|
|
*/
|
|
const runBeforeRead = () => {
|
|
_beforeAddReadFunctions.forEach(callback => {
|
|
callback();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Runs all before write callbacks
|
|
*/
|
|
const runBeforeWrite = () => {
|
|
_beforeAddWriteFunctions.forEach(callback => {
|
|
callback();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Updates styles and classes before animation runs
|
|
*/
|
|
const runBeforeStyles = () => {
|
|
const addClasses = beforeAddClasses;
|
|
const removeClasses = beforeRemoveClasses;
|
|
const styles = beforeStylesValue;
|
|
|
|
elements.forEach((el: HTMLElement) => {
|
|
const elementClassList = el.classList;
|
|
|
|
addClasses.forEach(c => elementClassList.add(c));
|
|
removeClasses.forEach(c => elementClassList.remove(c));
|
|
|
|
for (const property in styles) {
|
|
if (styles.hasOwnProperty(property)) {
|
|
setStyleProperty(el, property, styles[property]);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Run all "before" animation hooks.
|
|
*/
|
|
const beforeAnimation = () => {
|
|
runBeforeRead();
|
|
runBeforeWrite();
|
|
runBeforeStyles();
|
|
};
|
|
|
|
/**
|
|
* Runs all after read callbacks
|
|
*/
|
|
const runAfterRead = () => {
|
|
_afterAddReadFunctions.forEach(callback => {
|
|
callback();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Runs all after write callbacks
|
|
*/
|
|
const runAfterWrite = () => {
|
|
_afterAddWriteFunctions.forEach(callback => {
|
|
callback();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Updates styles and classes before animation ends
|
|
*/
|
|
const runAfterStyles = () => {
|
|
const addClasses = afterAddClasses;
|
|
const removeClasses = afterRemoveClasses;
|
|
const styles = afterStylesValue;
|
|
|
|
elements.forEach((el: HTMLElement) => {
|
|
const elementClassList = el.classList;
|
|
|
|
addClasses.forEach(c => elementClassList.add(c));
|
|
removeClasses.forEach(c => elementClassList.remove(c));
|
|
|
|
for (const property in styles) {
|
|
if (styles.hasOwnProperty(property)) {
|
|
setStyleProperty(el, property, styles[property]);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Run all "after" animation hooks.
|
|
*/
|
|
const afterAnimation = () => {
|
|
clearCSSAnimationsTimeout();
|
|
runAfterRead();
|
|
runAfterWrite();
|
|
runAfterStyles();
|
|
|
|
const didComplete = willComplete;
|
|
|
|
onFinishCallbacks.forEach(onFinishCallback => {
|
|
onFinishCallback.callback(didComplete, ani);
|
|
});
|
|
|
|
onFinishOneTimeCallbacks.forEach(onFinishCallback => {
|
|
onFinishCallback.callback(didComplete, ani);
|
|
});
|
|
|
|
onFinishOneTimeCallbacks.length = 0;
|
|
|
|
shouldCalculateNumAnimations = true;
|
|
finished = true;
|
|
};
|
|
|
|
const animationFinish = () => {
|
|
if (numAnimationsRunning === 0) { return; }
|
|
|
|
numAnimationsRunning--;
|
|
|
|
if (numAnimationsRunning === 0) {
|
|
afterAnimation();
|
|
if (parentAnimation) {
|
|
parentAnimation.animationFinish();
|
|
}
|
|
}
|
|
};
|
|
|
|
const initializeCSSAnimation = (toggleAnimationName = true) => {
|
|
cleanUpStyleSheets();
|
|
|
|
elements.forEach(element => {
|
|
if (_keyframes.length > 0) {
|
|
const keyframeRules = generateKeyframeRules(_keyframes);
|
|
keyframeName = generateKeyframeName(keyframeRules);
|
|
const stylesheet = createKeyframeStylesheet(keyframeName, keyframeRules, element);
|
|
stylesheets.push(stylesheet);
|
|
|
|
setStyleProperty(element, 'animation-duration', (getDuration() !== undefined) ? `${getDuration()}ms` : null);
|
|
setStyleProperty(element, 'animation-timing-function', getEasing() || null);
|
|
setStyleProperty(element, 'animation-delay', (getDelay() !== undefined) ? `${getDelay()}ms` : null);
|
|
setStyleProperty(element, 'animation-fill-mode', getFill() || null);
|
|
setStyleProperty(element, 'animation-direction', getDirection() || null);
|
|
|
|
const iterationsCount =
|
|
(getIterations() !== undefined) ?
|
|
(getIterations() === Infinity) ? 'infinite' : getIterations()!.toString()
|
|
: null;
|
|
|
|
setStyleProperty(element, 'animation-iteration-count', iterationsCount);
|
|
setStyleProperty(element, 'animation-play-state', 'paused');
|
|
|
|
if (toggleAnimationName) {
|
|
setStyleProperty(element, 'animation-name', `${stylesheet.id}-alt`);
|
|
}
|
|
|
|
raf(() => {
|
|
setStyleProperty(element, 'animation-name', stylesheet.id || null);
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
const initializeWebAnimation = () => {
|
|
elements.forEach(element => {
|
|
const animation = element.animate(_keyframes, {
|
|
delay: getDelay(),
|
|
duration: getDuration(),
|
|
easing: getEasing(),
|
|
iterations: getIterations(),
|
|
fill: getFill(),
|
|
direction: getDirection()
|
|
});
|
|
|
|
animation.pause();
|
|
|
|
webAnimations.push(animation);
|
|
});
|
|
|
|
if (getWebAnimations().length > 0) {
|
|
webAnimations[0].onfinish = () => {
|
|
animationFinish();
|
|
};
|
|
}
|
|
|
|
};
|
|
|
|
const initializeAnimation = (toggleAnimationName = true) => {
|
|
beforeAnimation();
|
|
|
|
if (_keyframes.length > 0) {
|
|
if (supportsWebAnimations) {
|
|
initializeWebAnimation();
|
|
} else {
|
|
initializeCSSAnimation(toggleAnimationName);
|
|
}
|
|
}
|
|
|
|
initialized = true;
|
|
};
|
|
|
|
const setAnimationStep = (step: number) => {
|
|
step = Math.min(Math.max(step, 0), 0.999);
|
|
if (supportsWebAnimations) {
|
|
getWebAnimations().forEach(animation => {
|
|
animation.currentTime = animation.effect.getComputedTiming().delay + (getDuration()! * step);
|
|
animation.pause();
|
|
});
|
|
|
|
} else {
|
|
const animationDelay = getDelay() || 0;
|
|
const animationDuration = `-${animationDelay + (getDuration()! * step)}ms`;
|
|
|
|
elements.forEach(element => {
|
|
if (_keyframes.length > 0) {
|
|
setStyleProperty(element, 'animation-delay', animationDuration);
|
|
setStyleProperty(element, 'animation-play-state', 'paused');
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const updateWebAnimation = () => {
|
|
getWebAnimations().forEach(animation => {
|
|
animation.effect.updateTiming({
|
|
delay: getDelay(),
|
|
duration: getDuration(),
|
|
easing: getEasing(),
|
|
iterations: getIterations(),
|
|
fill: getFill(),
|
|
direction: getDirection()
|
|
});
|
|
});
|
|
};
|
|
|
|
const updateCSSAnimation = (toggleAnimationName = true) => {
|
|
elements.forEach(element => {
|
|
raf(() => {
|
|
setStyleProperty(element, 'animation-name', keyframeName || null);
|
|
setStyleProperty(element, 'animation-duration', (getDuration() !== undefined) ? `${getDuration()}ms` : null);
|
|
setStyleProperty(element, 'animation-timing-function', getEasing() || null);
|
|
setStyleProperty(element, 'animation-delay', (getDelay() !== undefined) ? `${getDelay()}ms` : null);
|
|
setStyleProperty(element, 'animation-fill-mode', getFill() || null);
|
|
setStyleProperty(element, 'animation-direction', getDirection() || null);
|
|
|
|
const iterationsCount =
|
|
(getIterations() !== undefined) ?
|
|
(getIterations() === Infinity) ? 'infinite' : getIterations()!.toString()
|
|
: null;
|
|
|
|
setStyleProperty(element, 'animation-iteration-count', iterationsCount);
|
|
|
|
if (toggleAnimationName) {
|
|
setStyleProperty(element, 'animation-name', `${keyframeName}-alt`);
|
|
}
|
|
|
|
raf(() => {
|
|
setStyleProperty(element, 'animation-name', keyframeName || null);
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Updates any existing animations.
|
|
*/
|
|
const update = (deep = false, toggleAnimationName = true) => {
|
|
if (deep) {
|
|
childAnimations.forEach(animation => {
|
|
animation.update(deep);
|
|
});
|
|
}
|
|
|
|
if (supportsWebAnimations) {
|
|
updateWebAnimation();
|
|
} else {
|
|
updateCSSAnimation(toggleAnimationName);
|
|
}
|
|
|
|
return ani;
|
|
};
|
|
|
|
const progressStart = (forceLinearEasing = false) => {
|
|
childAnimations.forEach(animation => {
|
|
animation.progressStart(forceLinearEasing);
|
|
});
|
|
|
|
pauseAnimation();
|
|
shouldForceLinearEasing = forceLinearEasing;
|
|
|
|
if (!initialized) {
|
|
initializeAnimation();
|
|
} else {
|
|
update();
|
|
setAnimationStep(0);
|
|
}
|
|
|
|
return ani;
|
|
};
|
|
|
|
const progressStep = (step: number) => {
|
|
childAnimations.forEach(animation => {
|
|
animation.progressStep(step);
|
|
});
|
|
|
|
if (getDuration() !== undefined) {
|
|
setAnimationStep(step);
|
|
}
|
|
|
|
return ani;
|
|
};
|
|
|
|
const progressEnd = (shouldComplete: boolean, step: number, dur: number | undefined) => {
|
|
shouldForceLinearEasing = false;
|
|
|
|
childAnimations.forEach(animation => {
|
|
animation.progressEnd(shouldComplete, step, dur);
|
|
});
|
|
|
|
if (dur !== undefined) {
|
|
forceDurationValue = dur;
|
|
}
|
|
|
|
finished = false;
|
|
|
|
willComplete = shouldComplete;
|
|
|
|
if (!shouldComplete) {
|
|
forceDirectionValue = (getDirection() === 'reverse') ? 'normal' : 'reverse';
|
|
|
|
if (supportsWebAnimations) {
|
|
update();
|
|
setAnimationStep(1 - step);
|
|
} else {
|
|
forceDelayValue = ((1 - step) * getDuration()!) * -1;
|
|
update(false, false);
|
|
}
|
|
} else {
|
|
if (!supportsWebAnimations) {
|
|
forceDelayValue = (step * getDuration()!) * -1;
|
|
update(false, false);
|
|
}
|
|
}
|
|
|
|
onFinish(() => {
|
|
willComplete = true;
|
|
forceDurationValue = undefined;
|
|
forceDirectionValue = undefined;
|
|
forceDelayValue = undefined;
|
|
}, {
|
|
oneTimeCallback: true
|
|
});
|
|
|
|
if (!parentAnimation) {
|
|
play();
|
|
}
|
|
|
|
return ani;
|
|
};
|
|
|
|
const pauseAnimation = () => {
|
|
if (initialized) {
|
|
if (supportsWebAnimations) {
|
|
getWebAnimations().forEach(animation => {
|
|
animation.pause();
|
|
});
|
|
} else {
|
|
elements.forEach(element => {
|
|
setStyleProperty(element, 'animation-play-state', 'paused');
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Pause the animation.
|
|
*/
|
|
const pause = () => {
|
|
childAnimations.forEach(animation => {
|
|
animation.pause();
|
|
});
|
|
|
|
pauseAnimation();
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Play the animation asynchronously.
|
|
* This returns a promise that resolves
|
|
* when the animation has ended.
|
|
*/
|
|
const playAsync = () => {
|
|
return new Promise(resolve => {
|
|
onFinish(resolve, { oneTimeCallback: true });
|
|
play();
|
|
|
|
return ani;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Play the animation synchronously. This
|
|
* is the equivalent of running the animation
|
|
* with a duration of 0ms.
|
|
*/
|
|
const playSync = () => {
|
|
shouldForceSyncPlayback = true;
|
|
|
|
onFinish(() => shouldForceSyncPlayback = false, { oneTimeCallback: true });
|
|
play();
|
|
|
|
return ani;
|
|
};
|
|
|
|
const onAnimationEndFallback = () => {
|
|
cssAnimationsTimerFallback = undefined;
|
|
animationFinish();
|
|
};
|
|
|
|
const clearCSSAnimationsTimeout = () => {
|
|
if (cssAnimationsTimerFallback) {
|
|
clearTimeout(cssAnimationsTimerFallback);
|
|
}
|
|
};
|
|
|
|
const playCSSAnimations = () => {
|
|
clearCSSAnimationsTimeout();
|
|
|
|
elements.forEach(element => {
|
|
if (_keyframes.length > 0) {
|
|
raf(() => {
|
|
setStyleProperty(element, 'animation-play-state', 'running');
|
|
});
|
|
}
|
|
});
|
|
|
|
if (_keyframes.length === 0 || elements.length === 0) {
|
|
animationFinish();
|
|
} else {
|
|
/**
|
|
* This is a catchall in the event that a CSS Animation did not finish.
|
|
* The Web Animations API has mechanisms in place for preventing this.
|
|
* CSS Animations will not fire an `animationend` event
|
|
* for elements with `display: none`. The Web Animations API
|
|
* accounts for this, but using raw CSS Animations requires
|
|
* this workaround.
|
|
*/
|
|
const animationDelay = getDelay() || 0;
|
|
const animationDuration = getDuration() || 0;
|
|
|
|
cssAnimationsTimerFallback = setTimeout(onAnimationEndFallback, animationDelay + animationDuration + ANIMATION_END_FALLBACK_PADDING_MS);
|
|
|
|
animationEnd(elements[0], () => {
|
|
clearCSSAnimationsTimeout();
|
|
|
|
/**
|
|
* Ensure that clean up
|
|
* is always done a frame
|
|
* before the onFinish handlers
|
|
* are fired. Otherwise, there
|
|
* may be flickering if a new
|
|
* animation is started on the same
|
|
* element too quickly
|
|
*
|
|
* TODO: Is there a cleaner way to do this?
|
|
*/
|
|
raf(() => {
|
|
clearCSSAnimationPlayState();
|
|
raf(animationFinish);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
const clearCSSAnimationPlayState = () => {
|
|
elements.forEach(element => {
|
|
removeStyleProperty(element, 'animation-duration');
|
|
removeStyleProperty(element, 'animation-delay');
|
|
removeStyleProperty(element, 'animation-play-state');
|
|
});
|
|
};
|
|
|
|
const playWebAnimations = () => {
|
|
getWebAnimations().forEach(animation => {
|
|
animation.play();
|
|
});
|
|
|
|
if (_keyframes.length === 0 || elements.length === 0) {
|
|
animationFinish();
|
|
}
|
|
};
|
|
|
|
const resetAnimation = () => {
|
|
if (supportsWebAnimations) {
|
|
setAnimationStep(0);
|
|
} else {
|
|
updateCSSAnimation();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Play the animation
|
|
*/
|
|
const play = () => {
|
|
if (!initialized) {
|
|
initializeAnimation();
|
|
}
|
|
|
|
if (finished) {
|
|
resetAnimation();
|
|
finished = false;
|
|
}
|
|
|
|
if (shouldCalculateNumAnimations) {
|
|
numAnimationsRunning = childAnimations.length + 1;
|
|
shouldCalculateNumAnimations = false;
|
|
}
|
|
|
|
childAnimations.forEach(animation => {
|
|
animation.play();
|
|
});
|
|
|
|
if (supportsWebAnimations) {
|
|
playWebAnimations();
|
|
} else {
|
|
playCSSAnimations();
|
|
}
|
|
|
|
return ani;
|
|
};
|
|
|
|
/**
|
|
* Stop the animation and reset
|
|
* all elements to their initial state
|
|
*/
|
|
const stop = () => {
|
|
childAnimations.forEach(animation => {
|
|
animation.stop();
|
|
});
|
|
|
|
if (initialized) {
|
|
cleanUpElements();
|
|
initialized = false;
|
|
}
|
|
|
|
return ani;
|
|
};
|
|
|
|
const from = (property: string, value: any) => {
|
|
const firstFrame = _keyframes[0];
|
|
|
|
if (firstFrame != null && (firstFrame.offset === undefined || firstFrame.offset === 0)) {
|
|
firstFrame[property] = value;
|
|
} else {
|
|
_keyframes = [
|
|
{ offset: 0, [property]: value },
|
|
..._keyframes
|
|
];
|
|
}
|
|
|
|
return ani;
|
|
};
|
|
|
|
const to = (property: string, value: any) => {
|
|
const lastFrame = _keyframes[_keyframes.length - 1];
|
|
|
|
if (lastFrame != null && (lastFrame.offset === undefined || lastFrame.offset === 1)) {
|
|
lastFrame[property] = value;
|
|
} else {
|
|
_keyframes = [
|
|
..._keyframes,
|
|
{ offset: 1, [property]: value }
|
|
];
|
|
}
|
|
return ani;
|
|
};
|
|
|
|
const fromTo = (property: string, fromValue: any, toValue: any) => {
|
|
return from(property, fromValue).to(property, toValue);
|
|
};
|
|
|
|
return ani = {
|
|
parentAnimation,
|
|
elements,
|
|
childAnimations,
|
|
animationFinish,
|
|
from,
|
|
to,
|
|
fromTo,
|
|
parent,
|
|
play,
|
|
playAsync,
|
|
playSync,
|
|
pause,
|
|
stop,
|
|
destroy,
|
|
keyframes,
|
|
addAnimation,
|
|
addElement,
|
|
update,
|
|
fill,
|
|
direction,
|
|
iterations,
|
|
duration,
|
|
easing,
|
|
delay,
|
|
getWebAnimations,
|
|
getKeyframes,
|
|
getFill,
|
|
getDirection,
|
|
getDelay,
|
|
getIterations,
|
|
getEasing,
|
|
getDuration,
|
|
afterAddRead,
|
|
afterAddWrite,
|
|
afterClearStyles,
|
|
afterStyles,
|
|
afterRemoveClass,
|
|
afterAddClass,
|
|
beforeAddRead,
|
|
beforeAddWrite,
|
|
beforeClearStyles,
|
|
beforeStyles,
|
|
beforeRemoveClass,
|
|
beforeAddClass,
|
|
onFinish,
|
|
clearOnFinish,
|
|
|
|
progressStart,
|
|
progressStep,
|
|
progressEnd
|
|
} as Animation;
|
|
};
|