Merge branch 'feat-animation' of https://github.com/ionic-team/ionic into feat-animation

merget.
This commit is contained in:
Liam DeBeasi
2019-07-16 13:27:54 -04:00
10 changed files with 2850 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
export interface AnimationController {
create(animationBuilder?: AnimationBuilder, baseEl?: any, opts?: any): Promise<Animation>;
}
export interface Animation {
new (): any;
parent: Animation | undefined;
hasChildren: boolean;
addElement(el: Node | Node[] | NodeList): Animation;
add(childAnimation: Animation): Animation;
duration(milliseconds: number): Animation;
easing(name: string): Animation;
easingReverse(name: string): Animation;
getDuration(opts?: PlayOptions): number;
getEasing(): string;
from(prop: string, val: any): Animation;
to(prop: string, val: any, clearProperyAfterTransition?: boolean): Animation;
fromTo(prop: string, fromVal: any, toVal: any, clearProperyAfterTransition?: boolean): Animation;
beforeAddClass(className: string): Animation;
beforeRemoveClass(className: string): Animation;
beforeStyles(styles: { [property: string]: any; }): Animation;
beforeClearStyles(propertyNames: string[]): Animation;
beforeAddRead(domReadFn: () => void): Animation;
beforeAddWrite(domWriteFn: () => void): Animation;
afterAddClass(className: string): Animation;
afterRemoveClass(className: string): Animation;
afterStyles(styles: { [property: string]: any; }): Animation;
afterClearStyles(propertyNames: string[]): Animation;
play(opts?: PlayOptions): void;
playSync(): void;
playAsync(opts?: PlayOptions): Promise<Animation>;
reverse(shouldReverse?: boolean): Animation;
stop(stepValue?: number): void;
progressStart(): void;
progressStep(stepValue: number): void;
progressEnd(shouldComplete: boolean, currentStepValue: number, dur: number): void;
onFinish(callback: (animation?: Animation) => void, opts?: {oneTimeCallback?: boolean, clearExistingCallbacks?: boolean}): Animation;
destroy(): void;
isRoot(): boolean;
hasCompleted: boolean;
}
export type AnimationBuilder = (Animation: Animation, baseEl: any, opts?: any) => Promise<Animation>;
export interface PlayOptions {
duration?: number;
promise?: boolean;
}
export interface EffectProperty {
effectName: string;
trans: boolean;
wc?: string;
to?: EffectState;
from?: EffectState;
[state: string]: any;
}
export interface EffectState {
val: any;
num: number;
effectUnit: string;
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
import { Animation, AnimationBuilder } from '../../interface';
import { Animator } from './animator';
export function create(animationBuilder?: AnimationBuilder, baseEl?: any, opts?: any): Promise<Animation> {
if (animationBuilder) {
return animationBuilder(Animator as any, baseEl, opts);
}
return Promise.resolve(new Animator() as any);
}

View File

@@ -0,0 +1,30 @@
export function transitionEnd(el: HTMLElement | null, callback: (ev?: TransitionEvent) => void) {
let unRegTrans: (() => void) | undefined;
const opts: any = { passive: true };
function unregister() {
if (unRegTrans) {
unRegTrans();
}
}
function onTransitionEnd(ev: Event) {
if (el === ev.target) {
unregister();
callback(ev as TransitionEvent);
}
}
if (el) {
el.addEventListener('webkitTransitionEnd', onTransitionEnd, opts);
el.addEventListener('transitionend', onTransitionEnd, opts);
unRegTrans = () => {
el.removeEventListener('webkitTransitionEnd', onTransitionEnd, opts);
el.removeEventListener('transitionend', onTransitionEnd, opts);
};
}
return unregister;
}

View File

@@ -0,0 +1,630 @@
// TODO: Add validation
// TODO: More tests
export interface Animation {
parentAnimation: Animation | undefined;
elements: HTMLElement[];
childAnimations: Animation[];
beforeAddClasses: string[];
beforeRemoveClasses: string[];
beforeStylesValue: { [property: string]: any };
afterAddClasses: string[];
afterRemoveClasses: string[];
afterStylesValue: { [property: string]: any };
parent(animation: Animation): Animation;
play(): Animation;
pause(): Animation;
stop(): Animation;
playStep(step: number): Animation;
destroy(): Animation;
keyframes(keyframes: any[]): Animation;
addAnimation(animationToADd: Animation | Animation[] | undefined | null): Animation;
addTarget(target: string): Animation;
addElement(el: Node | Node[] | NodeList | undefined | null): Animation;
iterations(iterations: number): Animation;
duration(duration: number): Animation;
easing(easing: string): Animation;
delay(delay: number): Animation;
name(name: string): Animation;
getKeyframes(): any[];
getDelay(): number | undefined;
getIterations(): number | undefined;
getEasing(): string | undefined;
getDuration(): number | undefined;
afterClearStyles(propertyNames: string[]): Animation;
afterStyles(styles: { [property: string]: any }): Animation;
afterRemoveClass(className: string | string[] | undefined): Animation;
afterAddClass(className: string | string[] | undefined): Animation;
beforeClearStyles(propertyNames: string[]): Animation;
beforeStyles(styles: { [property: string]: any }): Animation;
beforeRemoveClass(className: string | string[] | undefined): Animation;
beforeAddClass(className: string | string[] | undefined): Animation;
}
const animationEnd = (el: HTMLElement | null, callback: (ev?: TransitionEvent) => void) => {
let unRegTrans: (() => void) | undefined;
const opts: any = { passive: true };
function unregister() {
if (unRegTrans) {
unRegTrans();
}
}
function onTransitionEnd(ev: Event) {
if (el === ev.target) {
unregister();
callback(ev as TransitionEvent);
}
}
if (el) {
el.addEventListener('webkitAnimationEnd', onTransitionEnd, opts);
el.addEventListener('animationend', onTransitionEnd, opts);
unRegTrans = () => {
el.removeEventListener('webkitAnimationEnd', onTransitionEnd, opts);
el.removeEventListener('animationend', onTransitionEnd, opts);
};
}
return unregister;
};
const supportsWebAnimations = (): boolean => {
return !!(window as any).Animation;
};
const generateKeyframeString = (name: string | undefined, keyframes: any[] = []): string => {
if (name === undefined) { console.warn('A name is required to generate keyframes'); }
const keyframeString = [`@keyframes ${name} {`];
keyframes.forEach(keyframe => {
const offset = keyframe.offset;
delete keyframe.offset;
const frameString = [];
for (const property in keyframe) {
if (keyframe.hasOwnProperty(property)) {
frameString.push(`${property}: ${keyframe[property]};`);
}
}
keyframeString.push(`${offset * 100}% { ${frameString.join(' ')} }`);
});
return keyframeString.join(' ');
};
const createKeyframeStylesheet = (keyframeString: string): HTMLElement => {
const stylesheet = document.createElement('style');
stylesheet.appendChild(document.createTextNode(keyframeString));
document.querySelector('head')!.appendChild(stylesheet);
return stylesheet;
};
const addClassToArray = (classes: string[] = [], className: string | string[] | undefined): string[] => {
if (className !== undefined) {
const classNameToAppend = (Array.isArray(className)) ? className : [className];
return [...classes, ...classNameToAppend];
}
return classes;
};
export const createAnimation = (animationNameValue: string | undefined): Animation => {
let elements: HTMLElement[] = [];
let childAnimations: Animation[] = [];
let _name: string | undefined;
let _delay: number | undefined;
let _duration: number | undefined;
let _easing: string | undefined;
let _iterations: number | undefined;
let _keyframes: any[] = [];
let _keyframeString = '';
let initialized = false;
let stylesheet: HTMLElement | undefined;
let parentAnimation: Animation | undefined;
let beforeAddClasses: string[] = [];
let beforeRemoveClasses: string[] = [];
let beforeStylesValue: { [property: string]: any } = {};
let afterAddClasses: string[] = [];
let afterRemoveClasses: string[] = [];
let afterStylesValue: { [property: string]: any } = {};
let webAnimations: any[] = [];
/**
* Destroy this animation and all child animations.
*/
const destroy = (): Animation => {
childAnimations.forEach(childAnimation => {
childAnimation.destroy();
});
cleanUp();
elements = [];
childAnimations = [];
initialized = false;
return generatePublicAPI();
};
const cleanUp = () => {
cleanUpElements();
cleanUpStyleSheets();
};
const cleanUpElements = () => {
if (supportsWebAnimations()) {
webAnimations.forEach(animation => {
animation.cancel();
});
webAnimations = [];
} else {
elements.forEach(element => {
element.style.removeProperty('animation-name');
element.style.removeProperty('animation-duration');
element.style.removeProperty('animation-timing-function');
element.style.removeProperty('animation-iteration-count');
element.style.removeProperty('animation-delay');
element.style.removeProperty('animation-play-state');
});
}
};
const cleanUpStyleSheets = () => {
if (stylesheet) {
stylesheet.parentNode!.removeChild(stylesheet);
stylesheet = undefined;
}
};
/**
* Add CSS class to this animation's elements
* before the animation begins.
*/
const beforeAddClass = (className: string | string[] | undefined): Animation => {
beforeAddClasses = addClassToArray(beforeAddClasses, className);
return generatePublicAPI();
};
/**
* Remove CSS class from this animation's elements
* before the animation begins.
*/
const beforeRemoveClass = (className: string | string[] | undefined): Animation => {
beforeRemoveClasses = addClassToArray(beforeRemoveClasses, className);
return generatePublicAPI();
};
/**
* Set CSS inline styles to this animation's elements
* before the animation begins.
*/
const beforeStyles = (styles: { [property: string]: any } = {}): Animation => {
beforeStylesValue = styles;
return generatePublicAPI();
};
/**
* Clear CSS inline styles from this animation's elements
* before the animation begins.
*/
const beforeClearStyles = (propertyNames: string[] = []): Animation => {
for (const property of propertyNames) {
beforeStylesValue[property] = '';
}
return generatePublicAPI();
};
/**
* Add CSS class to this animation's elements
* after the animation ends.
*/
const afterAddClass = (className: string | string[] | undefined): Animation => {
afterAddClasses = addClassToArray(afterAddClasses, className);
return generatePublicAPI();
};
/**
* Remove CSS class from this animation's elements
* after the animation ends.
*/
const afterRemoveClass = (className: string | string[] | undefined): Animation => {
afterRemoveClasses = addClassToArray(afterRemoveClasses, className);
return generatePublicAPI();
};
/**
* Set CSS inline styles to this animation's elements
* after the animation ends.
*/
const afterStyles = (styles: { [property: string]: any } = {}): Animation => {
afterStylesValue = styles;
return generatePublicAPI();
};
/**
* Clear CSS inline styles from this animation's elements
* after the animation ends.
*/
const afterClearStyles = (propertyNames: string[] = []): Animation => {
for (const property of propertyNames) {
afterStylesValue[property] = '';
}
return generatePublicAPI();
};
const getEasing = (): string | undefined => {
if (_easing !== undefined) { return _easing; }
if (parentAnimation) { return parentAnimation.getEasing(); }
return undefined;
};
const getDuration = (): number | undefined => {
if (_duration !== undefined) { return _duration; }
if (parentAnimation) { return parentAnimation.getDuration(); }
return undefined;
};
const getIterations = (): number | undefined => {
if (_iterations !== undefined) { return _iterations; }
if (parentAnimation) { return parentAnimation.getIterations(); }
return undefined;
};
const getDelay = (): number | undefined => {
if (_delay !== undefined) { return _delay; }
if (parentAnimation) { return parentAnimation.getDelay(); }
return undefined;
};
const getKeyframes = (): any[] => {
return _keyframes;
};
const name = (animationName: string): Animation => {
_name = animationName;
return generatePublicAPI();
};
const delay = (animationDelay: number): Animation => {
_delay = animationDelay;
return generatePublicAPI();
};
const easing = (animationEasing: string): Animation => {
_easing = animationEasing;
return generatePublicAPI();
};
const duration = (animationDuration: number): Animation => {
_duration = animationDuration;
return generatePublicAPI();
};
const iterations = (animationIterations: number): Animation => {
_iterations = animationIterations;
return generatePublicAPI();
};
const parent = (animation: Animation): Animation => {
parentAnimation = animation;
return generatePublicAPI();
};
const addElement = (el: Node | Node[] | NodeList | undefined | null): Animation => {
if (el != null) {
const nodeList = el as NodeList;
if (nodeList.length >= 0) {
for (let i = 0; i < nodeList.length; i++) {
elements.push((el as any)[i]);
}
} else {
elements.push(el as any);
}
}
return generatePublicAPI();
};
const addTarget = (target: string): Animation => {
const els = document.querySelectorAll(target);
return addElement(els);
};
const addAnimation = (animationToAdd: Animation | Animation[] | undefined | null): Animation => {
if (animationToAdd != null) {
const parentAnim = generatePublicAPI();
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 generatePublicAPI();
};
const keyframes = (keyframeValues: any[]) => {
_keyframes = keyframeValues;
if (!supportsWebAnimations()) {
_keyframeString = generateKeyframeString(_name, keyframeValues);
}
return generatePublicAPI();
};
const beforeAnimation = () => {
const addClasses = beforeAddClasses;
const removeClasses = beforeRemoveClasses;
const styles = beforeStylesValue;
elements.forEach((el: HTMLElement) => {
const elementClassList = el.classList;
elementClassList.add(...addClasses);
elementClassList.remove(...removeClasses);
for (const property in styles) {
if (styles.hasOwnProperty(property)) {
el.style.setProperty(property, styles[property]);
}
}
});
};
const afterAnimation = () => {
const addClasses = afterAddClasses;
const removeClasses = afterRemoveClasses;
const styles = afterStylesValue;
elements.forEach((el: HTMLElement) => {
const elementClassList = el.classList;
elementClassList.add(...addClasses);
elementClassList.remove(...removeClasses);
for (const property in styles) {
if (styles.hasOwnProperty(property)) {
el.style.setProperty(property, styles[property]);
}
}
});
cleanUpElements();
};
const initializeAnimation = () => {
beforeAnimation();
if (supportsWebAnimations()) {
elements.forEach((element, i) => {
const animation = element.animate(getKeyframes(), {
delay: getDelay(),
duration: getDuration(),
easing: getEasing(),
iterations: getIterations()
});
if (i === 0) {
animation.onfinish = () => {
afterAnimation();
};
}
animation.pause();
webAnimations.push(animation);
});
} else {
if (!stylesheet) {
stylesheet = createKeyframeStylesheet(_keyframeString);
}
const animationDuration = getDuration();
const animationEasing = getEasing();
const animationIterationCount = getIterations();
const animationDelay = getDelay();
elements.forEach(element => {
if (_name !== undefined) {
(element as HTMLElement).style.animationName = _name;
}
if (animationDuration !== undefined) {
(element as HTMLElement).style.animationDuration = `${animationDuration}ms`;
}
if (animationEasing !== undefined) {
(element as HTMLElement).style.animationTimingFunction = animationEasing;
}
if (animationIterationCount !== undefined) {
(element as HTMLElement).style.animationIterationCount = (animationIterationCount === Infinity) ? 'infinite' : animationIterationCount.toString();
}
if (animationDelay !== undefined) {
(element as HTMLElement).style.animationDelay = `${animationDelay}ms`;
}
});
animationEnd(elements[0], () => {
afterAnimation();
});
}
initialized = true;
};
const playStep = (step: number): Animation => {
childAnimations.forEach(animation => {
animation.playStep(step);
});
if (!initialized) {
initializeAnimation();
}
pause();
if (getDuration() !== undefined) {
if (supportsWebAnimations()) {
webAnimations.forEach(animation => {
animation.currentTime = animation.effect.getComputedTiming().delay + (getDuration()! * step);
});
} else {
const animationDuration = `-${getDuration()! * step}ms`;
elements.forEach(element => {
(element as HTMLElement).style.animationDelay = animationDuration;
});
}
}
return generatePublicAPI();
};
const pause = (): Animation => {
childAnimations.forEach(animation => {
animation.pause();
});
if (initialized) {
if (supportsWebAnimations()) {
webAnimations.forEach(animation => {
animation.pause();
});
} else {
elements.forEach(element => {
(element as HTMLElement).style.animationPlayState = 'paused';
});
}
}
return generatePublicAPI();
};
const play = (): Animation => {
childAnimations.forEach(animation => {
animation.play();
});
if (!initialized) {
initializeAnimation();
}
if (supportsWebAnimations()) {
webAnimations.forEach(animation => {
animation.play();
});
} else {
elements.forEach(element => {
(element as HTMLElement).style.animationPlayState = 'running';
});
}
return generatePublicAPI();
};
const stop = (): Animation => {
childAnimations.forEach(animation => {
animation.stop();
});
if (initialized) {
cleanUp();
initialized = false;
}
return generatePublicAPI();
};
const generatePublicAPI = (): Animation => {
return {
parentAnimation,
elements,
childAnimations,
beforeAddClasses,
beforeRemoveClasses,
beforeStylesValue,
afterAddClasses,
afterRemoveClasses,
afterStylesValue,
parent,
play,
pause,
stop,
playStep,
destroy,
keyframes,
addAnimation,
addTarget,
addElement,
iterations,
duration,
easing,
delay,
name,
getKeyframes,
getDelay,
getIterations,
getEasing,
getDuration,
afterClearStyles,
afterStyles,
afterRemoveClass,
afterAddClass,
beforeClearStyles,
beforeStyles,
beforeRemoveClass,
beforeAddClass,
};
};
if (animationNameValue !== undefined) {
name(animationNameValue);
}
return generatePublicAPI();
};

View File

@@ -0,0 +1,361 @@
import { createAnimation } from '../animation';
describe('Animation Class', () => {
describe('addElement()', () => {
let animation;
beforeEach(() => {
animation = createAnimation();
});
it('should add 1 element', () => {
const el = document.createElement('p');
animation.addElement(el);
expect(animation.elements.length).toEqual(1);
});
it('should add multiple elements', () => {
const els = [
document.createElement('p'),
document.createElement('p'),
document.createElement('p')
];
animation.addElement(els);
expect(animation.elements.length).toEqual(els.length);
});
it('should not error when trying to add null or undefined', () => {
const el = document.createElement('p');
animation.addElement(el);
animation.addElement(null);
animation.addElement(undefined);
expect(animation.elements.length).toEqual(1);
});
});
describe('addTarget()', () => {
let animation;
beforeEach(() => {
animation = createAnimation();
document.body.innerHTML = '';
});
it('should add a target', () => {
document.body.appendChild(document.createElement('p'));
animation.addTarget('p');
expect(animation.elements.length).toEqual(1);
});
it('should add multiple targets of the same type', () => {
document.body.appendChild(document.createElement('p'));
document.body.appendChild(document.createElement('p'));
animation.addTarget('p');
expect(animation.elements.length).toEqual(2);
});
it('should add multiple targets of different types', () => {
document.body.appendChild(document.createElement('p'));
document.body.appendChild(document.createElement('p'));
document.body.appendChild(document.createElement('span'));
animation.addTarget('p, span');
expect(animation.elements.length).toEqual(3);
});
it('should not error when trying to add null or undefined', () => {
animation.addTarget('p');
expect(animation.elements.length).toEqual(0);
});
});
describe('addAnimation()', () => {
let animation;
beforeEach(() => {
animation = createAnimation();
});
it('should add 1 animation', () => {
const newAnimation = createAnimation();
animation.addAnimation(newAnimation);
expect(animation.childAnimations.length).toEqual(1);
});
it('should add multiple animations', () => {
animation.addAnimation([createAnimation(), createAnimation(), createAnimation()]);
expect(animation.childAnimations.length).toEqual(3);
});
it('should not error when trying to add null or undefined', () => {
animation.addAnimation(null);
animation.addAnimation(undefined);
expect(animation.childAnimations.length).toEqual(0);
})
});
describe('keyframes()', () => {
let animation;
beforeEach(() => {
animation = createAnimation('my-animation');
});
it('should generate a keyframe', () => {
animation.keyframes([
{ transform: 'scale(1)', opacity: 1, offset: 0 },
{ transform: 'scale(0.5)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(0)', opacity: 0, offset: 1 }
]);
expect(animation.getKeyframes().length).toEqual(3);
});
});
describe('Before and After Animation Methods', () => {
let animation;
beforeEach(() => {
animation = createAnimation();
});
it('should register all "before" styles', () => {
animation = animation.beforeStyles({ 'background': 'red', 'opacity': 1 });
expect(Object.keys(animation.beforeStylesValue).length).toEqual(2);
});
it('should register all "before" classes given arrays', () => {
const classesToAdd = ['my-class', 'hello-world'];
const classesToRemove = ['ionic-framework'];
animation = animation.beforeAddClass(classesToAdd);
animation = animation.beforeRemoveClass(classesToRemove);
expect(animation.beforeAddClasses.length).toEqual(classesToAdd.length);
expect(animation.beforeRemoveClasses.length).toEqual(classesToRemove.length);
});
it('should register all "before" classes given strings', () => {
const classesToAdd = 'my-class';
const classesToRemove = 'ionic-framework';
animation = animation.beforeAddClass(classesToAdd);
animation = animation.beforeRemoveClass(classesToRemove);
expect(animation.beforeAddClasses.length).toEqual(1);
expect(animation.beforeRemoveClasses.length).toEqual(1);
});
it('should not register "before" classes given undefined', () => {
animation = animation.beforeAddClass(undefined);
animation = animation.beforeRemoveClass(undefined);
expect(animation.beforeAddClasses.length).toEqual(0);
expect(animation.beforeRemoveClasses.length).toEqual(0);
});
it('should apply all "before" styles', () => {
const el = document.createElement('div');
el.classList.add('hello', 'world');
el.style.setProperty('opacity', "0.5");
animation
.addElement(el)
.beforeAddClass(['ionic', 'framework'])
.beforeStyles({ 'background': 'blue' })
.beforeClearStyles(['opacity'])
.beforeRemoveClass('hello');
expect(el.style.getPropertyValue('opacity')).toEqual("0.5");
expect(el.classList.contains('hello')).toEqual(true);
expect(el.classList.contains('world')).toEqual(true);
animation.play();
expect(el.style.getPropertyValue('opacity')).toEqual("");
expect(el.style.getPropertyValue('background')).toEqual('blue');
expect(el.classList.contains('hello')).toEqual(false);
expect(el.classList.contains('world')).toEqual(true);
expect(el.classList.contains('ionic')).toEqual(true);
expect(el.classList.contains('framework')).toEqual(true);
});
it('should register all "after" styles', () => {
animation = animation.afterStyles({ 'background': 'red', 'opacity': 1 });
expect(Object.keys(animation.afterStylesValue).length).toEqual(2);
});
it('should register all "after" classes given arrays', () => {
const classesToAdd = ['my-class', 'hello-world'];
const classesToRemove = ['ionic-framework'];
animation = animation.afterAddClass(classesToAdd);
animation = animation.afterRemoveClass(classesToRemove);
expect(animation.afterAddClasses.length).toEqual(classesToAdd.length);
expect(animation.afterRemoveClasses.length).toEqual(classesToRemove.length);
});
it('should register all "after" classes given strings', () => {
const classesToAdd = 'my-class';
const classesToRemove = 'ionic-framework';
animation = animation.afterAddClass(classesToAdd);
animation = animation.afterRemoveClass(classesToRemove);
expect(animation.afterAddClasses.length).toEqual(1);
expect(animation.afterRemoveClasses.length).toEqual(1);
});
it('should not register "after" classes given undefined', () => {
animation = animation.afterAddClass(undefined);
animation = animation.afterRemoveClass(undefined);
expect(animation.afterAddClasses.length).toEqual(0);
expect(animation.afterRemoveClasses.length).toEqual(0);
});
it('should apply all "after" styles', async () => {
const el = document.createElement('div');
el.classList.add('hello', 'world');
el.style.setProperty('opacity', "0.5");
animation
.name('my-animation')
.addElement(el)
.duration(500)
.keyframes([
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(0.5) rotate(-45deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 1 }
])
.afterAddClass(['ionic', 'framework'])
.afterStyles({ 'background': 'blue' })
.afterClearStyles(['opacity'])
.afterRemoveClass('hello');
expect(el.style.getPropertyValue('opacity')).toEqual("0.5");
expect(el.classList.contains('hello')).toEqual(true);
expect(el.classList.contains('world')).toEqual(true);
animation.play();
/**
* Animations don't run in spec tests
* so we have to fake the end of the animation
*/
const ev = new CustomEvent('animationend');
el.dispatchEvent(ev);
expect(el.style.getPropertyValue('opacity')).toEqual("");
expect(el.style.getPropertyValue('background')).toEqual('blue');
expect(el.classList.contains('hello')).toEqual(false);
expect(el.classList.contains('world')).toEqual(true);
expect(el.classList.contains('ionic')).toEqual(true);
expect(el.classList.contains('framework')).toEqual(true);
});
});
describe('Animation Config Methods', () => {
let animation;
beforeEach(() => {
animation = createAnimation();
});
it('should get undefined when easing not set', () => {
expect(animation.getEasing()).toEqual(undefined);
});
it('should get parent easing when child easing is not set', () => {
const childAnimation = createAnimation();
animation.addAnimation(childAnimation);
childAnimation.easing('linear');
expect(childAnimation.getEasing()).toEqual('linear');
});
it('should get prefer child easing over parent easing', () => {
const childAnimation = createAnimation();
childAnimation.easing('linear');
animation.addAnimation(childAnimation);
animation.easing('ease-in-out');
expect(childAnimation.getEasing()).toEqual('linear');
});
it('should get undefined when duration not set', () => {
expect(animation.getDuration()).toEqual(undefined);
});
it('should get parent duration when child duration is not set', () => {
const childAnimation = createAnimation();
animation.addAnimation(childAnimation);
childAnimation.duration(500);
expect(childAnimation.getDuration()).toEqual(500);
});
it('should get prefer child duration over parent duration', () => {
const childAnimation = createAnimation();
childAnimation.duration(500);
animation.addAnimation(childAnimation);
animation.duration(1000);
expect(childAnimation.getDuration()).toEqual(500);
});
it('should get undefined when delay not set', () => {
expect(animation.getDelay()).toEqual(undefined);
});
it('should get parent delay when child delay is not set', () => {
const childAnimation = createAnimation();
animation.addAnimation(childAnimation);
childAnimation.delay(500);
expect(childAnimation.getDelay()).toEqual(500);
});
it('should get prefer child delay over parent delay', () => {
const childAnimation = createAnimation();
childAnimation.delay(500);
animation.addAnimation(childAnimation);
animation.delay(1000);
expect(childAnimation.getDelay()).toEqual(500);
});
it('should get undefined when iterations not set', () => {
expect(animation.getIterations()).toEqual(undefined);
});
it('should get parent iterations when child iterations is not set', () => {
const childAnimation = createAnimation();
animation.addAnimation(childAnimation);
childAnimation.iterations(2);
expect(childAnimation.getIterations()).toEqual(2);
});
it('should get prefer child iterations over parent iterations', () => {
const childAnimation = createAnimation();
childAnimation.iterations(2);
animation.addAnimation(childAnimation);
animation.iterations(1);
expect(childAnimation.getIterations()).toEqual(2);
});
})
});

View File

@@ -0,0 +1,171 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Animation - Basic</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
import { createAnimation } from '../../../../dist/collection/utils/animation/animation.js';
import { createGesture } from '../../../../dist/ionic/index-a098653b.js';
function clamp(val) {
if (val > 1) {
return 1;
} else if (val < 0) {
return 0;
}
return val;
}
const squareA = document.querySelectorAll('.square-a');
const squareB = document.querySelectorAll('.square-b');
const squareC = document.querySelectorAll('.square-c');
const track = document.querySelector('.track');
const cursor = document.querySelector('.track .cursor');
const rootAnimation = createAnimation();
const animationA = createAnimation('animation-a');
const animationB = createAnimation('animation-b');
const animationC = createAnimation('animation-c');
const gesture = createGesture({
el: track,
gestureName: 'drag',
gesturePriority: 100,
treshold: 5,
onStart: () => {},
onMove: (ev) => {
const start = ev.startX;
const width = track.clientWidth;
const lower = start;
const upper = width - start;
const current = clamp((ev.currentX - lower) / upper);
rootAnimation.playStep(current);
cursor.style.transform = `translateX(${ev.currentX - 10}px)`;
},
onEnd: (ev) => {
rootAnimation.playStep(0);
cursor.style.transform = `translateX(${0}px)`;
}
});
gesture.setDisabled(false);
animationA
.addElement(squareA)
.duration(2000)
.delay(5000)
.easing('linear')
.iterations(Infinity)
.keyframes([
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(1.5) rotate(45deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 1 }
]);
animationB
.addElement(squareB)
.duration(500)
.delay(2000)
.easing('ease-in-out')
.iterations(Infinity)
.keyframes([
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(0.5) rotate(-45deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 1 }
]);
animationC
.addElement(squareC)
.duration(2000)
.delay(500)
.easing('ease-in-out')
.iterations(Infinity)
.keyframes([
{ transform: 'scale(1) skew(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(1.5) skew(15deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) skew(0deg)', opacity: 1, offset: 1 }
]);
rootAnimation.addAnimation([animationA, animationB, animationC]);
//rootAnimation.play();
</script>
<style>
.square {
width: 100px;
height: 100px;
background: rgba(0, 0, 255, 0.5);
text-align: center;
line-height: 100px;
margin-left: 25px;
margin-top: 25px;
margin-bottom: 25px;
}
.track {
width: 100%;
height: 100px;
background: rgba(0, 255, 0, 0.5);
text-align: center;
line-height: 100px;
user-select: none;
cursor: ew-resize;
position: relative;
}
.track .cursor {
pointer-events: none;
width: 10px;
height: 100px;
background: rgba(255, 0, 0, 0.5);
position: absolute;
top: 0;
}
</style>
</head>
<body
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Animations</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-padding">
<div class="track">
Drag along the track to animate the elements
<div class="cursor"></div>
</div>
<div class="square square-a">
Hello
</div>
<div class="square square-b">
Hello
</div>
<div class="square square-c">
Hello
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Animation - Grid</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
import { createAnimation } from '../../../../dist/collection/utils/animation/animation.js';
const grid = document.querySelector('.grid-container');
let dotsString = '';
for (let i = 0; i < 360; i++) {
dotsString += '<div class="dot"></div>';
}
grid.innerHTML = dotsString;
const dots = document.querySelectorAll('.dot');
const rootAnimation = createAnimation();
rootAnimation
.duration(500)
.easing('ease-in-out')
.iterations(Infinity);
dots.forEach((dot, i) => {
const animation = createAnimation(`dot-${i}`);
animation
.addElement(dot)
.delay(i * 10)
.keyframes([
{ transform: 'scale(1)', offset: 0 },
{ transform: 'scale(2)', offset: 0.5 },
{ transform: 'scale(1)', offset: 1 }
]);
rootAnimation.addAnimation(animation);
});
document.querySelector('.pause').addEventListener('click', () => {
rootAnimation.pause();
});
document.querySelector('.play').addEventListener('click', () => {
rootAnimation.play();
console.log('polkay',rootAnimation)
});
</script>
<style>
.grid-container {
width: 330px;
height: 470px;
background: rgba(0, 0, 255, 0.5);
}
.dot {
--width: 8px;
width: var(--width);
height: var(--width);
border-radius: var(--width);
background: rgba(0, 255, 0, 1);
float: left;
margin: 6px;
}
.dot:nth-of-type(2n) {
background: rgba(255, 0, 0, 1);
}
.dot:nth-of-type(3n) {
background: rgba(0, 0, 255, 1);
}
</style>
</head>
<body
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Animations</ion-title>
<ion-buttons slot="end">
<ion-button class="pause">Pause</ion-button>
<ion-button class="play">Play</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-padding">
<div class="grid-container">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,145 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Animation - Basic</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
import { createAnimation } from '../../../../dist/collection/utils/animation/animation.js';
const squareA = document.querySelectorAll('.square-a');
const squareB = document.querySelectorAll('.square-b');
const squareC = document.querySelectorAll('.square-c');
const rootAnimation = createAnimation();
const animationA = createAnimation('animation-a');
const animationB = createAnimation('animation-b');
const animationC = createAnimation('animation-c');
animationA
.addElement(squareA)
.duration(2000)
.delay(5000)
.easing('linear')
.iterations(1)
.keyframes([
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(1.5) rotate(45deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 1 }
])
.beforeStyles({
'background': 'rgba(0, 0, 255, 0.5'
})
.afterStyles({
'background': 'rgba(0, 255, 0, 0.5)'
});
animationB
.addElement(squareB)
.duration(500)
.delay(2000)
.easing('ease-in-out')
.iterations(1)
.keyframes([
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(0.5) rotate(-45deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 1 }
])
.beforeStyles({
'background': 'rgba(0, 0, 255, 0.5'
})
.afterStyles({
'background': 'rgba(0, 255, 0, 0.5)'
});
animationC
.addElement(squareC)
.duration(2000)
.delay(500)
.easing('ease-in-out')
.iterations(1)
.keyframes([
{ transform: 'scale(1) skew(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(1.5) skew(15deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) skew(0deg)', opacity: 1, offset: 1 }
])
.beforeStyles({
'background': 'rgba(0, 0, 255, 0.5'
})
.afterStyles({
'background': 'rgba(0, 255, 0, 0.5)'
});
rootAnimation.addAnimation([animationA, animationB, animationC]);
document.querySelector('.play').addEventListener('click', () => {
rootAnimation.play();
});
document.querySelector('.pause').addEventListener('click', () => {
rootAnimation.pause();
});
document.querySelector('.destroy').addEventListener('click', () => {
rootAnimation.destroy();
});
document.querySelector('.stop').addEventListener('click', () => {
rootAnimation.stop();
});
</script>
<style>
.square {
width: 100px;
height: 100px;
background: rgba(0, 0, 255, 0.5);
text-align: center;
line-height: 100px;
margin-left: 25px;
margin-top: 25px;
margin-bottom: 25px;
}
</style>
</head>
<body
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Animations</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-padding">
<ion-button class="play">Play</ion-button>
<ion-button class="pause">Pause</ion-button>
<ion-button class="stop">Stop</ion-button>
<ion-button class="destroy">Destroy</ion-button>
<div class="square square-a">
Hello
</div>
<div class="square square-b">
Hello
</div>
<div class="square square-c">
Hello
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Animation - Basic</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
import { createAnimation } from '../../../../dist/collection/utils/animation/animation.js';
const square = document.querySelectorAll('.square');
const animation = createAnimation('animation-a');
animation
.addElement(square)
.duration(2000)
.easing('linear')
.iterations(1)
.keyframes([
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(1.5) rotate(45deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 1 }
])
.beforeAddClass(['hello', 'ionic', 'world'])
.beforeRemoveClass('liam-was-here')
.beforeStyles({
'background': 'rgba(255, 0, 0, 0.5)',
'color': 'white'
})
.afterStyles({
'background': 'rgba(0, 255, 0, 0.5)',
});
animation.play();
</script>
<style>
.square {
width: 100px;
height: 100px;
background: rgba(0, 0, 255, 0.5);
text-align: center;
line-height: 100px;
margin-left: 25px;
margin-top: 25px;
margin-bottom: 25px;
}
</style>
</head>
<body
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Animations</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-padding">
<div class="square liam-was-here">
Hello
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>