Skip to content

Commit

Permalink
feat: 🎸 generic interface AnimationObject over type and options
Browse files Browse the repository at this point in the history
AnimationObject interface is now generic over type of the animation and
the options the animation of the type can accept. It is simplifies
implementing new animations where you can specify your type of
AnimationObject just using generic one. Also, it adds a possibility for
TypeScript to infer some of the types when using these interfaces.
  • Loading branch information
ghaiklor committed Apr 26, 2020
1 parent e93551e commit 4e09ab7
Show file tree
Hide file tree
Showing 12 changed files with 85 additions and 65 deletions.
43 changes: 27 additions & 16 deletions packages/kittik-animation-basic/src/animation/Animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,16 @@ export class Animation extends EventEmitter implements AnimationOptions {
this.on('tick', this.onTick.bind(this));
}

public static create<T extends Animation>(options?: Partial<AnimationOptions>): T
public static create<T extends Animation, O extends AnimationOptions>(options?: Partial<O>): T {
return (new this(options)) as T;
public static create <A extends Animation, O extends AnimationOptions>(options: O): A
public static create <O extends AnimationOptions>(options: O): Animation
public static create (options: AnimationOptions): Animation {
return new this(options);
}

public static fromObject<T extends Animation>(obj: AnimationObject): T
public static fromObject<T extends Animation, O extends AnimationObject>(obj: O): T {
public static fromObject <T, O extends AnimationOptions, A extends Animation>(obj: AnimationObject<T, O>): A
public static fromObject <T, O extends AnimationOptions>(obj: AnimationObject<T, O>): Animation
public static fromObject <T>(obj: AnimationObject<T, AnimationOptions>): Animation
public static fromObject (obj: AnimationObject<'Basic', AnimationOptions>): Animation {
if (obj.type !== this.name) {
throw new Error(
`You specified configuration for "${obj.type}" but provided it to "${this.name}". ` +
Expand All @@ -70,20 +73,27 @@ export class Animation extends EventEmitter implements AnimationOptions {
return this.create(obj.options);
}

public static fromJSON<T extends Animation>(json: string): T {
public static fromJSON <A extends Animation>(json: string): A {
return this.fromObject(JSON.parse(json));
}

// We need to have a possibility to override onTick in children
// Moreover, in case overridden method wants to use its `this` we need to have it here
// Even if we do not use `this` in this specific implementation, someone else can
// eslint-disable-next-line class-methods-use-this
public onTick<S extends Shape, P extends keyof S, V extends number>(shape: S, property: P, value: V): void {
public onTick <S extends Shape, P extends keyof S, V extends number>(shape: S, property: P, value: V): void {
Object.assign(shape, { [property]: value });
}

// The same scenario applies to `this` in this method
// Someone else can override it in children and make use of `this` in his own class
// eslint-disable-next-line class-methods-use-this
public onEasing (easing: EASING.Easing, options: EasingOptions): number {
return Math.round(EASING[easing](options.time, options.startValue, options.byValue, options.duration));
}

// Again, the same scenario as above
// Someone else can override it in children and make use of `this` in his own class
// eslint-disable-next-line class-methods-use-this
public async delay (ms: number): Promise<void> {
return await new Promise((resolve) => setTimeout(resolve, isFinite(ms) ? ms : 1));
Expand Down Expand Up @@ -123,15 +133,16 @@ export class Animation extends EventEmitter implements AnimationOptions {
return await new Promise(tick);
}

public toObject<T extends AnimationObject>(): T {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return {
type: this.constructor.name,
options: {
duration: this.duration,
easing: this.easing
}
} as T;
public toObject <T, O extends AnimationOptions>(): AnimationObject<T, O>
public toObject <T>(): AnimationObject<T, AnimationOptions>
public toObject (): AnimationObject<'Basic', AnimationOptions> {
const type: 'Basic' = 'Basic' as const;
const options: AnimationOptions = {
duration: this.duration,
easing: this.easing
};

return { type, options };
}

public toJSON (): string {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AnimationOptions } from './AnimationOptions';

export interface AnimationObject {
type: string
options?: Partial<AnimationOptions>
export interface AnimationObject<T, O extends AnimationOptions> {
type: T
options: O
}
17 changes: 9 additions & 8 deletions packages/kittik-animation-focus/src/Focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class Focus extends Animation implements FocusOptions, Animationable {
this.rawDuration = duration;
}

public async animate<T extends Shape>(shape: T): Promise<T> {
public async animate <T extends Shape>(shape: T): Promise<T> {
const { direction } = this;

if (direction.includes('bounce')) {
Expand All @@ -58,19 +58,20 @@ export class Focus extends Animation implements FocusOptions, Animationable {
);
}

public toObject<T extends FocusObject>(): T {
const obj: FocusObject = super.toObject();
obj.options = {
...obj.options,
public toObject (): FocusObject {
const base = super.toObject();
const type: FocusObject['type'] = 'Focus';
const options: FocusObject['options'] = {
...base.options,
direction: this.direction,
offset: this.offset,
repeat: this.repeat
};

return obj as T;
return { type, options };
}

private async animateBounce<T extends Shape>(shape: T, direction: BounceDirection): Promise<T> {
private async animateBounce <T extends Shape>(shape: T, direction: BounceDirection): Promise<T> {
const x = parseInt(shape.x, 10);
const y = parseInt(shape.y, 10);
const { offset } = this;
Expand Down Expand Up @@ -125,7 +126,7 @@ export class Focus extends Animation implements FocusOptions, Animationable {
return await sequence;
}

private async animateShake<T extends Shape>(shape: T, direction: ShakeDirection): Promise<T> {
private async animateShake <T extends Shape>(shape: T, direction: ShakeDirection): Promise<T> {
const x = parseInt(shape.x, 10);
const y = parseInt(shape.y, 10);
const { offset } = this;
Expand Down
4 changes: 1 addition & 3 deletions packages/kittik-animation-focus/src/FocusObject.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { AnimationObject } from 'kittik-animation-basic';
import { FocusOptions } from './FocusOptions';

export interface FocusObject extends AnimationObject {
options?: Partial<FocusOptions>
}
export type FocusObject = AnimationObject<'Focus', FocusOptions>;
13 changes: 12 additions & 1 deletion packages/kittik-animation-print/src/Print.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Animation, Animationable } from 'kittik-animation-basic';
import { PrintObject } from './PrintObject';
import { PrintOptions } from './PrintOptions';
import { Shape } from 'kittik-shape-basic';

Expand All @@ -8,7 +9,7 @@ export { PrintOptions } from './PrintOptions';
export class Print extends Animation implements PrintOptions, Animationable {
private originalText = '';

public onTick<S extends Shape, P extends keyof S, V extends number>(shape: S, _property: P, value: V): void {
public onTick <S extends Shape, P extends keyof S, V extends number>(shape: S, _property: P, value: V): void {
shape.text = this.originalText.slice(0, value);
}

Expand All @@ -22,4 +23,14 @@ export class Print extends Animation implements PrintOptions, Animationable {
endValue: this.originalText.length
});
}

public toObject (): PrintObject {
const base = super.toObject();
const type: PrintObject['type'] = 'Print';
const options: PrintObject['options'] = {
...base.options
};

return { type, options };
}
}
3 changes: 2 additions & 1 deletion packages/kittik-animation-print/src/PrintObject.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AnimationObject } from 'kittik-animation-basic';
import { PrintOptions } from './PrintOptions';

export type PrintObject = AnimationObject;
export type PrintObject = AnimationObject<'Print', PrintOptions>;
16 changes: 10 additions & 6 deletions packages/kittik-animation-slide/src/Slide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class Slide extends Animation implements SlideOptions, Animationable {
}
}

public async animate<T extends Shape>(shape: T): Promise<T> {
public async animate <T extends Shape>(shape: T): Promise<T> {
const { startX, startY, endX, endY } = this.parseCoordinates(shape);

return await Promise.all([
Expand All @@ -26,14 +26,18 @@ export class Slide extends Animation implements SlideOptions, Animationable {
]).then(() => shape);
}

public toObject<T extends SlideObject>(): T {
const obj: SlideObject = super.toObject();
obj.options = { ...obj.options, direction: this.direction };
public toObject (): SlideObject {
const base = super.toObject();
const type: SlideObject['type'] = 'Slide';
const options: SlideObject['options'] = {
...base.options,
direction: this.direction
};

return obj as T;
return { type, options };
}

private parseCoordinates<T extends Shape>(shape: T): { startX: number, startY: number, endX: number, endY: number } {
private parseCoordinates <T extends Shape>(shape: T): { startX: number, startY: number, endX: number, endY: number } {
const { canvas } = shape;
const x = parseInt(shape.x, 10);
const y = parseInt(shape.y, 10);
Expand Down
4 changes: 1 addition & 3 deletions packages/kittik-animation-slide/src/SlideObject.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { AnimationObject } from 'kittik-animation-basic';
import { SlideOptions } from './SlideOptions';

export interface SlideObject extends AnimationObject {
options?: Partial<SlideOptions>
}
export type SlideObject = AnimationObject<'Slide', SlideOptions>;
13 changes: 8 additions & 5 deletions packages/kittik-slide/src/animation/AnimationBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { ANIMATIONS, AnimationOptions, AnimationType } from './Animations';
import { Animationable } from 'kittik-animation-basic';
import { AnimationObject, Animationable } from 'kittik-animation-basic';

export class AnimationBuilder<T extends AnimationType, O extends AnimationOptions<T>> {
export class AnimationBuilder<T extends AnimationType, O extends AnimationOptions<T>> implements AnimationObject<T, O> {
public type: T;
public options: Partial<O>;
public options: O;

public constructor (type: T) {
this.type = type;
this.options = {};

// eslint-disable-next-line no-warning-comments
// TODO: what to do with this unknown?
this.options = {} as unknown as O;
}

public static start <T extends AnimationType, O extends AnimationOptions<T>>(type: T): AnimationBuilder<T, O> {
Expand Down Expand Up @@ -43,6 +46,6 @@ export class AnimationBuilder<T extends AnimationType, O extends AnimationOption
);
}

return ctr.fromObject(this);
return ctr.fromObject<T, O, Animationable>(this);
}
}
3 changes: 2 additions & 1 deletion packages/kittik-slide/src/animation/AnimationDeclaration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AnimationOptions, AnimationType } from './Animations';
import { AnimationObject } from 'kittik-animation-basic';

export interface AnimationDeclaration extends AnimationObject {
export interface AnimationDeclaration extends AnimationObject<AnimationType, AnimationOptions<AnimationType>> {
name: string
}
24 changes: 8 additions & 16 deletions packages/kittik-slide/src/animation/Animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,17 @@ import { Slide, SlideObject, SlideOptions } from 'kittik-animation-slide';
import { Animation } from 'kittik-animation-basic';

export type AnimationType = 'Focus' | 'Print' | 'Slide';

export type AnimationOptions<T extends AnimationType> = T extends 'Focus'
? FocusOptions
: T extends 'Print'
? PrintOptions
: T extends 'Slide'
? SlideOptions
: never;

export type AnimationObject<T extends AnimationType> = T extends 'Focus'
? FocusObject
: T extends 'Print'
? PrintObject
: T extends 'Slide'
? SlideObject
: never;
export type AnimationOptions<T extends AnimationType> = TypesMap[T]['options'];
export type AnimationObject<T extends AnimationType> = TypesMap[T]['object'];

export const ANIMATIONS = new Map<AnimationType, typeof Animation>([
['Focus', Focus],
['Print', Print],
['Slide', Slide]
]);

interface TypesMap {
Focus: { options: FocusOptions, object: FocusObject }
Print: { options: PrintOptions, object: PrintObject }
Slide: { options: SlideOptions, object: SlideObject }
}
4 changes: 2 additions & 2 deletions packages/kittik-slide/src/slide/Slide.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ANIMATIONS, AnimationType } from '../animation/Animations';
import { ANIMATIONS } from '../animation/Animations';
import { AnimationDeclaration } from '../animation/AnimationDeclaration';
import { Animationable } from 'kittik-animation-basic';
import { Canvas } from 'terminal-canvas';
Expand Down Expand Up @@ -183,7 +183,7 @@ export class Slide<

private initAnimations (declaration: AnimationDeclaration[]): void {
declaration.forEach((animationDeclaration) => {
const ctor = ANIMATIONS.get(animationDeclaration.type as AnimationType);
const ctor = ANIMATIONS.get(animationDeclaration.type);

if (typeof ctor === 'undefined') {
throw new Error(
Expand Down

0 comments on commit 4e09ab7

Please sign in to comment.