Add new keyframes proof of concept

This commit is contained in:
Liam DeBeasi
2019-07-10 16:14:39 -04:00
parent f16b118794
commit bd734c05ae
8 changed files with 2123 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,260 @@
// TODO: Add validation
// TODO: More tests
const addElement = (animationElements: any[], el: Node | Node[] | NodeList | undefined | null) => {
if (el != null) {
const nodeList = el as NodeList;
if (nodeList.length >= 0) {
for (let i = 0; i < nodeList.length; i++) {
animationElements.push((el as any)[i]);
}
} else {
animationElements.push(el);
}
}
};
const addAnimation = (parentAnimation: Animation, childAnimations: Animation[], animationToAdd: Animation | Animation[] | undefined | null) => {
if (animationToAdd != null) {
const animationsToAdd = animationToAdd as Animation[];
if (animationsToAdd.length >= 0) {
for (const animation of animationsToAdd) {
animation.parentAnimation = parentAnimation;
childAnimations.push(animation);
}
} else {
(animationToAdd as Animation).parentAnimation = parentAnimation;
childAnimations.push(animationToAdd as Animation);
}
}
};
const addTarget = (animationElements: any[], target: string) => {
const els = document.querySelectorAll(target);
addElement(animationElements, els);
};
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(' ');
};
export class Animation {
private elements: Element[] = [];
private childAnimations: Animation[] = [];
private _delay: number | undefined;
private _duration: number | undefined;
private _easing: string | undefined;
private _iterations: number | undefined;
private _keyframes: any[] = [];
private _keyframeString = '';
private initialized = false;
private stylesheet?: HTMLElement;
parentAnimation: Animation | undefined;
constructor(public _name: string | undefined) {}
getEasing(): string | undefined {
if (this._easing !== undefined) { return this._easing; }
if (this.parentAnimation && this.parentAnimation.getEasing() !== undefined) { return this.parentAnimation.getEasing(); }
return undefined;
}
getDuration(): number | undefined {
if (this._duration !== undefined) { return this._duration; }
if (this.parentAnimation && this.parentAnimation.getDuration() !== undefined) { return this.parentAnimation.getDuration(); }
return undefined;
}
getIterations(): number | undefined {
if (this._iterations !== undefined) { return this._iterations; }
if (this.parentAnimation && this.parentAnimation.getIterations() !== undefined) { return this.parentAnimation.getIterations(); }
return undefined;
}
getDelay(): number | undefined {
if (this._delay !== undefined) { return this._delay; }
if (this.parentAnimation && this.parentAnimation.getDelay() !== undefined) { return this.parentAnimation.getDelay(); }
return undefined;
}
getKeyframes(): any[] {
return this._keyframes;
}
name(name: string): Animation {
this._name = name;
return this;
}
delay(delay: number): Animation {
this._delay = delay;
return this;
}
easing(easing: string): Animation {
this._easing = easing;
return this;
}
duration(duration: number): Animation {
this._duration = duration;
return this;
}
iterations(iterations: number): Animation {
this._iterations = iterations;
return this;
}
addElement(el: Node | Node[] | NodeList | undefined | null): Animation {
addElement(this.elements, el);
return this;
}
addTarget(target: string): Animation {
addTarget(this.elements, target);
return this;
}
addAnimation(childAnimation: Animation | undefined | null): Animation {
addAnimation(this, this.childAnimations, childAnimation);
return this;
}
keyframes(keyframes: any[]): Animation {
this._keyframes = keyframes;
this._keyframeString = generateKeyframeString(this._name, keyframes);
return this;
}
private initializeAnimation(): void {
if (!this.stylesheet) {
const stylesheet = document.createElement('style');
stylesheet.appendChild(document.createTextNode(this._keyframeString));
document.querySelector('head')!.appendChild(stylesheet);
this.stylesheet = stylesheet;
}
const animationName = this._name;
const animationDuration = this.getDuration();
const animationEasing = this.getEasing();
const animationIterationCount = this.getIterations();
const animationDelay = this.getDelay();
this.elements.forEach(element => {
if (animationName !== undefined) {
(element as HTMLElement).style.animationName = animationName;
}
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`;
}
});
this.initialized = true;
}
playStep(step: number): Animation {
if (!this.initialized) {
this.initializeAnimation();
}
this.childAnimations.forEach(animation => {
animation.playStep(step);
});
this.pause();
if (this.getDuration() !== undefined) {
const animationDuration = `-${this.getDuration()! * step}ms`;
this.elements.forEach(element => {
(element as HTMLElement).style.animationDelay = animationDuration;
});
}
return this;
}
pause(): Animation {
if (!this.initialized) {
this.initializeAnimation();
}
this.childAnimations.forEach(animation => {
animation.pause();
});
this.elements.forEach(element => {
(element as HTMLElement).style.animationPlayState = 'paused';
});
return this;
}
play(): Animation {
if (!this.initialized) {
this.initializeAnimation();
}
this.childAnimations.forEach(animation => {
animation.play();
});
this.elements.forEach(element => {
(element as HTMLElement).style.animationPlayState = 'running';
});
return this;
}
}

View File

@@ -0,0 +1,221 @@
import { Animation } from '../animation';
describe('Animation Class', () => {
describe('addElement()', () => {
let animation;
beforeEach(() => {
animation = new Animation();
});
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 = new Animation();
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 = new Animation();
});
it('should add 1 animation', () => {
const newAnimation = new Animation();
animation.addAnimation(newAnimation);
expect(animation.childAnimations.length).toEqual(1);
expect(animation.childAnimations[0].parentAnimation).toEqual(animation);
});
it('should add multiple animations', () => {
animation.addAnimation([new Animation(), new Animation(), new Animation()]);
expect(animation.childAnimations.length).toEqual(3);
expect(animation.childAnimations[0].parentAnimation).toEqual(animation);
expect(animation.childAnimations[1].parentAnimation).toEqual(animation);
expect(animation.childAnimations[2].parentAnimation).toEqual(animation);
});
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 = new Animation('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._keyframes.length).toEqual(3);
});
});
describe('Animation Config Methods', () => {
let animation;
beforeEach(() => {
animation = new Animation();
});
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 = new Animation();
animation.addAnimation(childAnimation);
childAnimation.easing('linear');
expect(childAnimation.getEasing()).toEqual('linear');
});
it('should get prefer child easing over parent easing', () => {
const childAnimation = new Animation();
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 = new Animation();
animation.addAnimation(childAnimation);
childAnimation.duration(500);
expect(childAnimation.getDuration()).toEqual(500);
});
it('should get prefer child duration over parent duration', () => {
const childAnimation = new Animation();
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 = new Animation();
animation.addAnimation(childAnimation);
childAnimation.delay(500);
expect(childAnimation.getDelay()).toEqual(500);
});
it('should get prefer child delay over parent delay', () => {
const childAnimation = new Animation();
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 = new Animation();
animation.addAnimation(childAnimation);
childAnimation.iterations(2);
expect(childAnimation.getIterations()).toEqual(2);
});
it('should get prefer child iterations over parent iterations', () => {
const childAnimation = new Animation();
childAnimation.iterations(2);
animation.addAnimation(childAnimation);
animation.iterations(1);
expect(childAnimation.getIterations()).toEqual(2);
});
})
});

View File

@@ -0,0 +1,173 @@
<!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 { Animation as IonicAnimation } from '../../../../dist/collection/utils/animation/animation.js';
import { createGesture } from '../../../../dist/ionic/index-a098653b.js';
window.IonicAnimation = IonicAnimation;
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 = new IonicAnimation();
const animationA = new IonicAnimation('animation-a');
const animationB = new IonicAnimation('animation-b');
const animationC = new IonicAnimation('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,112 @@
<!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 { Animation as IonicAnimation } from '../../../../dist/collection/utils/animation/animation.js';
import { createGesture } from '../../../../dist/ionic/index-a098653b.js';
window.IonicAnimation = IonicAnimation;
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 = new IonicAnimation();
rootAnimation
.duration(500)
.easing('ease-in-out')
.iterations(Infinity);
dots.forEach((dot, i) => {
const animation = new IonicAnimation(`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();
});
</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>