From fc1eb56da99d0ca498f20d5d47f6430f81077f9e Mon Sep 17 00:00:00 2001 From: Eugene Obrezkov Date: Sat, 4 Apr 2020 17:28:54 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20DeckBuilder=20allows=20t?= =?UTF-8?q?o=20create=20the=20whole=20decks=20via=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/kittik-deck/examples/builder.ts | 45 ++++++++++++ packages/kittik-deck/examples/complex.ts | 15 +++- packages/kittik-deck/package.json | 2 + packages/kittik-deck/spec/Deck.spec.ts | 69 ++++++++++++++++++- packages/kittik-deck/spec/DeckBuilder.spec.ts | 33 +++++++++ packages/kittik-deck/src/Deck.ts | 47 ++++++++++--- packages/kittik-deck/src/DeckBuilder.ts | 41 +++++++++++ 7 files changed, 239 insertions(+), 13 deletions(-) create mode 100644 packages/kittik-deck/examples/builder.ts create mode 100644 packages/kittik-deck/spec/DeckBuilder.spec.ts create mode 100644 packages/kittik-deck/src/DeckBuilder.ts diff --git a/packages/kittik-deck/examples/builder.ts b/packages/kittik-deck/examples/builder.ts new file mode 100644 index 0000000..1fed3e2 --- /dev/null +++ b/packages/kittik-deck/examples/builder.ts @@ -0,0 +1,45 @@ +import { DeckBuilder, SlideBuilder, AnimationBuilder, ShapeBuilder } from '..'; + +DeckBuilder + .start() + .withSlide( + SlideBuilder + .start() + .withShape( + 'Local Shape', + ShapeBuilder + .start('Rectangle') + .withText('Local Shape Here!') + .withY('bottom') + .withX('right') + .withBackground('white') + .withForeground('black') + .end() + ) + .withAnimation( + 'Local Animation', + AnimationBuilder + .start('Slide') + .end() + ) + .withOrder('Global Shape', ['Global Animation']) + .withOrder('Local Shape', ['Local Animation']) + .end() + ) + .withShape( + 'Global Shape', + ShapeBuilder + .start('FigText') + .withText('Hello, World!') + .end() + ) + .withAnimation( + 'Global Animation', + AnimationBuilder + .start('Print') + .withDuration(5000) + .end() + ) + .end() + .renderSlide() + .catch(e => console.error(e)); diff --git a/packages/kittik-deck/examples/complex.ts b/packages/kittik-deck/examples/complex.ts index ed9ad02..4a2d438 100644 --- a/packages/kittik-deck/examples/complex.ts +++ b/packages/kittik-deck/examples/complex.ts @@ -45,7 +45,7 @@ const ThanksArtShapeOptions: Partial = { new Deck({ shapes: [{ - name: 'Text', + name: 'Shape: Text', type: 'Text', options: { text: 'Shape: Text', @@ -93,6 +93,7 @@ new Deck({ options: FocusAnimationOptions }], slides: [{ + name: 'Hello, Kittik', shapes: [{ name: 'Kittik', type: 'FigText', @@ -103,6 +104,7 @@ new Deck({ animations: ['Print'] }] }, { + name: 'Demo #1', shapes: [{ name: 'Demo', type: 'Text', @@ -117,30 +119,35 @@ new Deck({ animations: ['Print'] }] }, { + name: 'Demo #2', shapes: [], order: [{ - shape: 'Text', + shape: 'Shape: Text', animations: ['Print'] }] }, { + name: 'Demo #3', shapes: [], order: [{ shape: 'Rectangle', animations: ['Slide In', 'Focus'] }] }, { + name: 'Demo #4', shapes: [], order: [{ shape: 'FigText', animations: ['Slide In', 'Focus'] }] }, { + name: 'Demo #5', shapes: [], order: [{ shape: 'Code', animations: ['Slide In'] }] }, { + name: 'Demo #6', shapes: [{ name: 'Text', type: 'FigText', @@ -151,6 +158,7 @@ new Deck({ animations: ['Print'] }] }, { + name: 'Demo #7', shapes: [{ name: 'Text', type: 'Text', @@ -165,6 +173,7 @@ new Deck({ animations: ['Print'] }] }, { + name: 'Demo #8', shapes: [{ name: 'Text', type: 'Text', @@ -179,6 +188,7 @@ new Deck({ animations: ['Print'] }] }, { + name: 'Demo #9', shapes: [{ name: 'Text', type: 'Text', @@ -193,6 +203,7 @@ new Deck({ animations: ['Print'] }] }, { + name: 'Demo #10', shapes: [{ name: 'Text', type: 'FigText', diff --git a/packages/kittik-deck/package.json b/packages/kittik-deck/package.json index bc96fd8..9f1c227 100644 --- a/packages/kittik-deck/package.json +++ b/packages/kittik-deck/package.json @@ -54,8 +54,10 @@ "eslint-plugin-promise": "4.2.1", "eslint-plugin-standard": "4.0.1", "jest": "25.2.7", + "kittik-animation-basic": "0.0.0", "kittik-animation-focus": "0.0.0", "kittik-animation-slide": "0.0.0", + "kittik-shape-basic": "0.0.0", "kittik-shape-code": "0.0.0", "kittik-shape-fig-text": "0.0.0", "rimraf": "3.0.2", diff --git a/packages/kittik-deck/spec/Deck.spec.ts b/packages/kittik-deck/spec/Deck.spec.ts index 9e6936e..d71b614 100644 --- a/packages/kittik-deck/spec/Deck.spec.ts +++ b/packages/kittik-deck/spec/Deck.spec.ts @@ -1,5 +1,7 @@ +import { Animationable } from 'kittik-animation-basic'; import { Canvas } from 'terminal-canvas'; import { Deck, DeckDeclaration } from '../src/Deck'; +import { ShapeRenderable } from 'kittik-shape-basic'; const DECK_DECLARATION: DeckDeclaration = { cursor: Canvas.create(), @@ -23,6 +25,7 @@ const DECK_DECLARATION: DeckDeclaration = { ], slides: [ { + name: 'Global Animation', shapes: [], order: [{ shape: 'Global Shape', @@ -32,6 +35,7 @@ const DECK_DECLARATION: DeckDeclaration = { }] }, { + name: 'Global Animation 2', shapes: [], order: [{ shape: 'Global Shape', @@ -54,6 +58,8 @@ describe('Deck', () => { expect(renderSpy).toBeCalledTimes(2); expect(renderSpy).toBeCalledWith(1); expect(renderSpy).toBeCalledWith(0); + + deck.exit(); }); it('Should properly handle the key press for next slide', async () => { @@ -65,6 +71,8 @@ describe('Deck', () => { expect(renderSpy).toBeCalledTimes(1); expect(renderSpy).toBeCalledWith(1); + + deck.exit(); }); it('Should properly handle the key press for exit', () => { @@ -74,6 +82,8 @@ describe('Deck', () => { process.stdin.emit('keypress', 'q'); expect(exitSpy).toBeCalledTimes(1); + + deck.exit(); }); it('Should properly render next and previous slides', async () => { @@ -88,6 +98,8 @@ describe('Deck', () => { expect(renderSpy).toBeCalledTimes(2); expect(renderSpy).toBeCalledWith(0); expect(renderSpy).toBeCalledWith(1); + + deck.exit(); }); it('Should properly render slides without custom cursor', async () => { @@ -102,10 +114,12 @@ describe('Deck', () => { expect(renderSpy).toBeCalledTimes(2); expect(renderSpy).toBeCalledWith(0); expect(renderSpy).toBeCalledWith(1); + + deck.exit(); }); it('Should properly render minimal slide without global shapes/animations', async () => { - const deck = new Deck({ slides: [{ shapes: [], order: [] }, { shapes: [], order: [], animations: [] }] }); + const deck = new Deck({ slides: [{ name: 'Test', shapes: [], order: [] }, { name: 'Test 2', shapes: [], order: [], animations: [] }] }); const renderSpy = jest.spyOn(deck, 'renderSlide').mockResolvedValue(); await deck.nextSlide(); @@ -116,10 +130,12 @@ describe('Deck', () => { expect(renderSpy).toBeCalledTimes(2); expect(renderSpy).toBeCalledWith(0); expect(renderSpy).toBeCalledWith(1); + + deck.exit(); }); it('Should not call slide renderer many times if slide is already rendering', () => { - const deck = new Deck({ slides: [{ shapes: [], order: [] }] }); + const deck = new Deck({ slides: [{ name: 'Test', shapes: [], order: [] }] }); // Though, slides is a private property, I need to access it anyway in sake of the tests // This is done to test if slides render() behaves as expected @@ -131,8 +147,55 @@ describe('Deck', () => { deck.renderSlide(); // eslint-disable-line @typescript-eslint/no-floating-promises deck.renderSlide(); // eslint-disable-line @typescript-eslint/no-floating-promises deck.renderSlide(); // eslint-disable-line @typescript-eslint/no-floating-promises - deck.exit(); expect(renderSpy).toBeCalledTimes(1); + + deck.exit(); + }); + + it('Should properly add a shape to all the slides in the deck', () => { + const deck = new Deck({ slides: [{ name: 'Test', shapes: [{ name: 'Shape', type: 'Text' }], order: [] }] }); + const shape = {} as ShapeRenderable; + + deck.addShape('Shape 2', shape); + + // I am accessing the private property to check if there is actually a new shape exists + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + expect(deck.slides[0].shapes.size).toBe(2); + + deck.exit(); + }); + + it('Should properly throw an error if shape already exists in other slides', () => { + const deck = new Deck({ slides: [{ name: 'Test', shapes: [{ name: 'Shape', type: 'Text' }], order: [] }] }); + const shape = {} as ShapeRenderable; + + expect(() => deck.addShape('Shape', shape)).toThrowError('Slides [Test] already have a shape with the name "Shape"'); + + deck.exit(); + }); + + it('Should properly add an animation to all the slides in the deck', () => { + const deck = new Deck({ slides: [{ name: 'Test', shapes: [], order: [], animations: [{ name: 'Animation', type: 'Print' }] }] }); + const animation = {} as Animationable; + + deck.addAnimation('Animation 2', animation); + + // I am accessing the private property to check if there is actually a new animation exists + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + expect(deck.slides[0].animations.size).toBe(2); + + deck.exit(); + }); + + it('Should properly throw an error if animation already exists in other slides', () => { + const deck = new Deck({ slides: [{ name: 'Test', shapes: [], order: [], animations: [{ name: 'Animation', type: 'Print' }] }] }); + const animation = {} as Animationable; + + expect(() => deck.addAnimation('Animation', animation)).toThrowError('Slides [Test] already have an animation with the name "Animation"'); + + deck.exit(); }); }); diff --git a/packages/kittik-deck/spec/DeckBuilder.spec.ts b/packages/kittik-deck/spec/DeckBuilder.spec.ts new file mode 100644 index 0000000..bf3536b --- /dev/null +++ b/packages/kittik-deck/spec/DeckBuilder.spec.ts @@ -0,0 +1,33 @@ +import { AnimationBuilder, ShapeBuilder, SlideBuilder } from '../src/Deck'; +import { Canvas } from 'terminal-canvas'; +import { DeckBuilder } from '../src/DeckBuilder'; + +describe('DeckBuilder', () => { + it('Should properly create deck using DeckBuilder', () => { + const deck = DeckBuilder + .start() + .withCursor(Canvas.create()) + .withAnimation( + 'Test Animation', + AnimationBuilder + .start('Focus') + .end() + ) + .withShape( + 'Test Shape', + ShapeBuilder + .start('Text') + .end() + ) + .withSlide( + SlideBuilder + .start() + .end() + ) + .end(); + + expect(deck.cursor).toBeInstanceOf(Canvas); + + deck.exit(); + }); +}); diff --git a/packages/kittik-deck/src/Deck.ts b/packages/kittik-deck/src/Deck.ts index 478da0e..5cdd230 100644 --- a/packages/kittik-deck/src/Deck.ts +++ b/packages/kittik-deck/src/Deck.ts @@ -1,35 +1,44 @@ +import { Animationable } from 'kittik-animation-basic'; import { Canvas } from 'terminal-canvas'; import { DeckDeclaration } from './DeckDeclaration'; +import { ShapeRenderable } from 'kittik-shape-basic'; import { Slide } from 'kittik-slide'; import readline from 'readline'; import tty from 'tty'; +export { AnimationBuilder } from 'kittik-slide'; +export { DeckBuilder } from './DeckBuilder'; export { DeckDeclaration } from './DeckDeclaration'; +export { ShapeBuilder } from 'kittik-slide'; +export { SlideBuilder } from 'kittik-slide'; export class Deck { - private readonly cursor: Canvas = Canvas.create().saveScreen().reset().hideCursor() - private readonly slides: Slide[] + private readonly slides: Slide[] = [] private isRendering = false; private currentSlideIndex = 0; + cursor: Canvas = Canvas.create().saveScreen().reset().hideCursor() - constructor (declaration: DeckDeclaration) { - if (declaration.cursor !== undefined) { + constructor (declaration?: DeckDeclaration) { + if (declaration?.cursor !== undefined) { this.cursor = declaration.cursor; } - this.slides = this.initSlides(declaration); + if (declaration !== undefined) { + this.initSlides(declaration); + } + this.initKeyboard(); } - private initSlides (declaration: DeckDeclaration): Slide[] { + private initSlides (declaration: DeckDeclaration): void { const globalShapes = declaration.shapes ?? []; const globalAnimations = declaration.animations ?? []; - return declaration.slides.map(slide => Slide.create(this.cursor, { + declaration.slides.forEach(slide => this.addSlide(Slide.create(this.cursor, { ...slide, shapes: globalShapes.concat(slide.shapes), animations: globalAnimations.concat(slide.animations ?? []) - })); + }))); } private initKeyboard (): void { @@ -56,6 +65,28 @@ export class Deck { } } + addShape (name: string, shape: ShapeRenderable): void { + const slidesWithShape = this.slides.filter(slide => slide.shapes.has(name)).map(slide => slide.name); + if (slidesWithShape.length > 0) { + throw new Error(`Slides [${slidesWithShape.join(', ')}] already have a shape with the name "${name}"`); + } + + this.slides.forEach(slide => slide.addShape(name, shape)); + } + + addAnimation (name: string, animation: Animationable): void { + const slidesWithAnimation = this.slides.filter(slide => slide.animations.has(name)).map(slide => slide.name); + if (slidesWithAnimation.length > 0) { + throw new Error(`Slides [${slidesWithAnimation.join(', ')}] already have an animation with the name "${name}`); + } + + this.slides.forEach(slide => slide.addAnimation(name, animation)); + } + + addSlide (slide: Slide): void { + this.slides.push(slide); + } + async renderSlide (index = this.currentSlideIndex): Promise { if (!this.isRendering && this.slides[index] !== undefined) { this.isRendering = true; diff --git a/packages/kittik-deck/src/DeckBuilder.ts b/packages/kittik-deck/src/DeckBuilder.ts new file mode 100644 index 0000000..a6fffd0 --- /dev/null +++ b/packages/kittik-deck/src/DeckBuilder.ts @@ -0,0 +1,41 @@ +import { Animationable } from 'kittik-animation-basic'; +import { Canvas } from 'terminal-canvas'; +import { Deck } from './Deck'; +import { ShapeRenderable } from 'kittik-shape-basic'; +import { Slide } from 'kittik-slide'; + +export class DeckBuilder { + private readonly deck: Deck = new Deck() + + withCursor (cursor: Canvas): this { + this.deck.cursor = cursor; + + return this; + } + + withShape (name: string, shape: ShapeRenderable): this { + this.deck.addShape(name, shape); + + return this; + } + + withAnimation (name: string, animation: Animationable): this { + this.deck.addAnimation(name, animation); + + return this; + } + + withSlide (slide: Slide): this { + this.deck.addSlide(slide); + + return this; + } + + end (): Deck { + return this.deck; + } + + static start (): DeckBuilder { + return new this(); + } +}