UNPKG

@labsforge/flipbook

Version:

A simple angular flipbook component written in typescript

530 lines (520 loc) 27.2 kB
import { TweenLite, TimelineLite, Power2 } from 'gsap'; import { Subject, fromEvent } from 'rxjs'; import { takeUntil, tap } from 'rxjs/operators'; import * as i0 from '@angular/core'; import { Injectable, Component, Input, HostBinding, Pipe, ChangeDetectionStrategy, NgModule } from '@angular/core'; import * as i3 from '@angular/common'; import { CommonModule } from '@angular/common'; import { HammerModule } from '@angular/platform-browser'; import 'hammerjs'; var PageType; (function (PageType) { PageType[PageType["Single"] = 0] = "Single"; PageType[PageType["Double"] = 1] = "Double"; })(PageType || (PageType = {})); class FlipbookService { constructor() { this.prev = new Subject(); this.play = new Subject(); this.pause = new Subject(); this.next = new Subject(); this.goTo = new Subject(); this.currentPage = new Subject(); } } FlipbookService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.11", ngImport: i0, type: FlipbookService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); FlipbookService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.3.11", ngImport: i0, type: FlipbookService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.11", ngImport: i0, type: FlipbookService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return []; } }); class PageComponent { constructor() { } get hostLeft() { return this.width * this.zoom; } get hostRotation() { return `rotateY(${this.page.rotation}deg)`; } } PageComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.11", ngImport: i0, type: PageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); PageComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.11", type: PageComponent, selector: "flipbook-page", inputs: { page: "page", width: "width", height: "height", rotation: "rotation", zoom: "zoom" }, host: { properties: { "style.left.px": "this.hostLeft", "style.transform": "this.hostRotation" } }, ngImport: i0, template: "<div class=\"page\"\r\n [class.cover]=\"page.front?.isCover\"\r\n [style.width.px]=\"width * zoom\"\r\n [style.height.px]=\"height * zoom\"\r\n [style.z-index]=\"rotation > -90 ? 1 : 0\"\r\n [style.background-image]=\"page.front ? 'url(' + page.front.imageUrl + ')' : null\"\r\n [style.background-size]=\"page.front?.width * zoom + 'px ' + page.front?.height * zoom + 'px'\"\r\n [style.background-color]=\"page.front?.backgroundColor\"\r\n [style.opacity.%]=\"page.front?.opacity * 100\">\r\n</div>\r\n<div class=\"page back\"\r\n [class.cover]=\"page.back?.isCover\"\r\n [style.width.px]=\"width * zoom\"\r\n [style.height.px]=\"height * zoom\"\r\n [style.z-index]=\"rotation < -90 ? 1 : 0\"\r\n [style.background-image]=\"page.back ? 'url(' + page.back.imageUrl + ')' : null\"\r\n [style.background-size]=\"page.back?.width * zoom + 'px ' + page.back?.height * zoom + 'px'\"\r\n [style.background-color]=\"page.back?.backgroundColor\"\r\n [style.opacity.%]=\"page.back?.opacity * 100\">\r\n</div>\r\n", styles: [":host{position:absolute}.page{position:absolute;box-sizing:border-box;background-position-y:50%;background-position-x:0%;background-repeat:no-repeat}.back{transform:scaleX(-1);background-position-x:100%}.cover{background-size:cover}\n"] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.11", ngImport: i0, type: PageComponent, decorators: [{ type: Component, args: [{ selector: 'flipbook-page', template: "<div class=\"page\"\r\n [class.cover]=\"page.front?.isCover\"\r\n [style.width.px]=\"width * zoom\"\r\n [style.height.px]=\"height * zoom\"\r\n [style.z-index]=\"rotation > -90 ? 1 : 0\"\r\n [style.background-image]=\"page.front ? 'url(' + page.front.imageUrl + ')' : null\"\r\n [style.background-size]=\"page.front?.width * zoom + 'px ' + page.front?.height * zoom + 'px'\"\r\n [style.background-color]=\"page.front?.backgroundColor\"\r\n [style.opacity.%]=\"page.front?.opacity * 100\">\r\n</div>\r\n<div class=\"page back\"\r\n [class.cover]=\"page.back?.isCover\"\r\n [style.width.px]=\"width * zoom\"\r\n [style.height.px]=\"height * zoom\"\r\n [style.z-index]=\"rotation < -90 ? 1 : 0\"\r\n [style.background-image]=\"page.back ? 'url(' + page.back.imageUrl + ')' : null\"\r\n [style.background-size]=\"page.back?.width * zoom + 'px ' + page.back?.height * zoom + 'px'\"\r\n [style.background-color]=\"page.back?.backgroundColor\"\r\n [style.opacity.%]=\"page.back?.opacity * 100\">\r\n</div>\r\n", styles: [":host{position:absolute}.page{position:absolute;box-sizing:border-box;background-position-y:50%;background-position-x:0%;background-repeat:no-repeat}.back{transform:scaleX(-1);background-position-x:100%}.cover{background-size:cover}\n"] }] }], ctorParameters: function () { return []; }, propDecorators: { page: [{ type: Input }], width: [{ type: Input }], height: [{ type: Input }], rotation: [{ type: Input }], zoom: [{ type: Input }], hostLeft: [{ type: HostBinding, args: ['style.left.px'] }], hostRotation: [{ type: HostBinding, args: ['style.transform'] }] } }); class ReversePipe { transform(values) { if (values) { return values.slice().reverse(); } } } ReversePipe.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.11", ngImport: i0, type: ReversePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); ReversePipe.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "12.0.0", version: "13.3.11", ngImport: i0, type: ReversePipe, name: "reverse", pure: false }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.11", ngImport: i0, type: ReversePipe, decorators: [{ type: Pipe, args: [{ name: 'reverse', pure: false }] }] }); const DEFAULT_BACKGROUND_COLOR = '#fff'; class BookComponent { constructor(service, cdr, elr) { this.service = service; this.cdr = cdr; this.elr = elr; this.currentIndex = 0; this.destroyed = new Subject(); this.navigationQueue = []; this.render = () => { this.cdr.detectChanges(); }; this.sortBook = (index) => { this.navigationQueue.shift(); if (this.flipTimeLine && this.navigationQueue.length > 0) { return; } const page = this.pages.find(p => p.index === index); this.currentIndex = page ? page.rotation < -90 ? page.index : page.index - 1 : index; this.pages.sort((a, b) => { const diffa = Math.abs(a.index - this.currentIndex) + (a.rotation === -180 ? 1 : 0); const diffb = Math.abs(b.index - this.currentIndex); return diffa - diffb; }); this.render(); this.service.currentPage.next(this.currentIndex + 1); }; this.setPageAtTop = (page) => { this.pages.unshift(this.pages.splice(this.pages.indexOf(page), 1)[0]); }; cdr.detach(); service.prev.pipe(takeUntil(this.destroyed)).subscribe(() => this.navigate(-1)); service.next.pipe(takeUntil(this.destroyed)).subscribe(() => this.navigate(1)); service.play.pipe(takeUntil(this.destroyed)).subscribe(() => this.play()); service.pause.pipe(takeUntil(this.destroyed)).subscribe(() => this.pause()); service.goTo.pipe(takeUntil(this.destroyed)).subscribe(index => this.goTo(index)); } get hostWidth() { this.cdr.detectChanges(); return this.model.width * this.model.zoom; } get hostHeight() { return this.model.height * this.model.zoom; } get hostPerspective() { return this.model.width * this.model.zoom * 2; } ngOnInit() { // TODO: Implement startPageType / endPageType this.pages = []; const pages = this.model.pages.slice(); const hasCover = this.model && this.model.cover !== undefined; const pageWidth = this.model.pageWidth || this.model.width / 2; const pageHeight = this.model.pageHeight || this.model.height; if (this.model && pages.length > 1) { if (!hasCover && this.model.startPageType === PageType.Single) { // add first white page block this.pages.push({ index: this.pages.length, lock: true, front: { imageUrl: '', width: pageWidth, height: pageHeight, backgroundColor: DEFAULT_BACKGROUND_COLOR }, back: { imageUrl: '', width: pageWidth, height: pageHeight, backgroundColor: DEFAULT_BACKGROUND_COLOR }, rotation: -180 }); } else { const frontCover = { index: this.pages.length, lock: !hasCover, front: hasCover ? { imageUrl: this.model.cover.front.imageUrl, isCover: true, width: this.model.width / 2, height: this.model.height, backgroundColor: this.model.cover.front.backgroundColor, opacity: this.model.cover.front.opacity, } : undefined, back: { imageUrl: '', backgroundColor: hasCover ? this.model.cover.front.backgroundColor : DEFAULT_BACKGROUND_COLOR, opacity: hasCover ? this.model.cover.front.opacity : 1, width: pageWidth, height: pageHeight, }, rotation: hasCover ? 0 : -180 }; if (this.model.startPageType !== PageType.Single) { const firstPage = pages.shift(); frontCover.back.imageUrl = firstPage.imageUrl; frontCover.back.backgroundColor = firstPage.backgroundColor; frontCover.back.opacity = firstPage.opacity; } this.pages.push(frontCover); } while (pages.length > 1) { const frontPage = pages.shift(); const backPage = pages.shift(); this.pages.push({ index: this.pages.length, front: { imageUrl: frontPage.imageUrl, width: pageWidth, height: pageHeight, backgroundColor: frontPage.backgroundColor, opacity: frontPage.opacity, }, back: { imageUrl: backPage.imageUrl, width: pageWidth, height: pageHeight, backgroundColor: backPage.backgroundColor, opacity: backPage.opacity, }, rotation: 0 }); } if (!hasCover && this.model.endPageType === PageType.Single) { // add last white page block this.pages.push({ index: this.pages.length, lock: true, front: { imageUrl: '', width: pageWidth, height: pageHeight, backgroundColor: DEFAULT_BACKGROUND_COLOR }, back: { imageUrl: '', width: pageWidth, height: pageHeight, backgroundColor: DEFAULT_BACKGROUND_COLOR }, rotation: 0 }); } else { const backCover = { index: this.pages.length, lock: !hasCover, front: { imageUrl: '', backgroundColor: hasCover ? this.model.cover.back.backgroundColor : DEFAULT_BACKGROUND_COLOR, opacity: hasCover ? this.model.cover.back.opacity : 1, width: pageWidth, height: pageHeight, }, back: this.model.cover ? { imageUrl: this.model.cover.back.imageUrl, isCover: true, width: this.model.width / 2, height: this.model.height, backgroundColor: this.model.cover.back.backgroundColor, opacity: this.model.cover.back.opacity, } : undefined, rotation: 0 }; if (this.model.startPageType !== PageType.Single && pages.length) { const lastPage = pages.shift(); backCover.front.imageUrl = lastPage.imageUrl; backCover.front.backgroundColor = lastPage.backgroundColor; backCover.front.opacity = lastPage.opacity; } this.pages.push(backCover); } } if (this.startAt !== undefined && this.startAt !== this.currentIndex) { this.goTo(this.startAt); } else { this.sortBook(this.currentIndex - 1); } } ngOnDestroy() { this.destroyed.next(); } update() { this.render(); } onPageDown(event, page) { if (TweenLite.getTweensOf(page, true).length > 0) { return; } if (page.lock) { this.flipTimeLine = new TimelineLite({ autoRemoveChildren: true }); this.flipTimeLine.add(TweenLite.to(page, 0.3, { rotation: page.rotation < -90 ? -175 : -5, ease: Power2.easeOut, onUpdate: this.render })); this.flipTimeLine.add(TweenLite.to(page, 0.2, { rotation: page.rotation < -90 ? -180 : 0, ease: Power2.easeOut, onUpdate: this.render })); return; } const startX = event.pageX; const startY = event.pageY; let hasMoved = false; const mouseUpEvt = fromEvent(document, 'mouseup') .pipe(tap(() => { this.flipTimeLine = new TimelineLite({ autoRemoveChildren: true }); if (!hasMoved) { this.navigationQueue.push(page.rotation < -90 ? page.index - 1 : page.index); this.flipTimeLine.add(TweenLite.to(page, 1, { rotation: page.rotation < -90 ? 0 : -180, ease: Power2.easeOut, onStart: this.setPageAtTop, onStartParams: [page], onUpdate: this.render, onComplete: this.sortBook, onCompleteParams: [page.rotation < -90 ? page.index - 1 : page.index] })); } else { this.navigationQueue.push(page.rotation < -90 ? page.index : page.index - 1); this.flipTimeLine.add(TweenLite.to(page, 1, { rotation: page.rotation < -90 ? -180 : 0, ease: Power2.easeOut, onUpdate: this.render, onComplete: this.sortBook, onCompleteParams: [page.rotation < -90 ? page.index : page.index - 1] })); } })); fromEvent(document, 'mousemove') .pipe(takeUntil(mouseUpEvt)) .subscribe(movEvt => { const movEvent = movEvt; const bookBounds = this.elr.nativeElement.getBoundingClientRect(); hasMoved = startX !== movEvent.pageX || startY !== movEvent.pageY; this.setPageAtTop(page); if (movEvent.pageX < bookBounds.left) { page.rotation = -180; } else if (movEvent.pageX > bookBounds.left + bookBounds.width) { page.rotation = 0; } else { page.rotation = -180 + ((movEvent.pageX - bookBounds.left) / bookBounds.width) * 180; } this.render(); }); } onPagePan(event, page) { if (TweenLite.getTweensOf(page, true).length > 0) { return; } if (page.lock) { this.flipTimeLine = new TimelineLite(); this.flipTimeLine.add(TweenLite.to(page, 0.3, { rotation: page.rotation < -90 ? -175 : -5, ease: Power2.easeOut, onUpdate: this.render })); this.flipTimeLine.add(TweenLite.to(page, 0.2, { rotation: page.rotation < -90 ? -180 : 0, ease: Power2.easeOut, onUpdate: this.render })); return; } this.setPageAtTop(page); const bookBounds = this.elr.nativeElement.getBoundingClientRect(); if (event.center.x < bookBounds.left) { page.rotation = -180; } else if (event.center.x > bookBounds.left + bookBounds.width) { page.rotation = 0; } else { page.rotation = -180 + ((event.center.x - bookBounds.left) / bookBounds.width) * 180; } this.render(); } onPagePanEnd(event, page) { this.flipTimeLine = new TimelineLite(); this.flipTimeLine.add(TweenLite.to(page, 1, { rotation: page.rotation < -90 ? -180 : 0, ease: Power2.easeOut, onUpdate: this.render, onComplete: this.sortBook, onCompleteParams: [page.rotation < -90 ? page.index : page.index - 1] })); } onSwipe(event, page) { if (page.lock) { return; } if (TweenLite.getTweensOf(page, true).length > 0) { TweenLite.getTweensOf(page, true)[0].kill(); } const direction = event.deltaX > 0 ? -1 : 1; this.flipTimeLine.add(TweenLite.to(page, 1, { rotation: direction === 1 ? -180 : 0, ease: Power2.easeOut, onUpdate: this.render, onComplete: this.sortBook, onCompleteParams: [this.currentIndex + direction] })); } navigate(direction) { const lastNavigationIndex = this.navigationQueue.length ? this.navigationQueue[this.navigationQueue.length - 1] : this.currentIndex; const pageIndex = direction === 1 ? lastNavigationIndex + 1 : lastNavigationIndex; const page = this.pages.find(p => p.index === pageIndex); if (page === undefined || page.lock) { return; } if (direction === 1 && page.rotation === -180) { return; } if (direction === -1 && page.rotation === 0) { return; } this.navigationQueue.push(lastNavigationIndex + direction); this.flipTimeLine = new TimelineLite({ autoRemoveChildren: true }); this.flipTimeLine.add(TweenLite.to(page, 1, { rotation: direction === 1 ? -180 : 0, ease: Power2.easeOut, onStart: this.setPageAtTop, onStartParams: [page], onUpdate: this.render, onComplete: lastNavigationIndex + direction < this.pages.length ? this.sortBook : void 0, onCompleteParams: [lastNavigationIndex + direction] })); } goTo(index) { if (this.currentIndex === index - 1) { return; } for (let i = 0; i < this.pages.length; i++) { const page = this.pages.find(p => p.index === i); page.rotation = i < index ? -180 : 0; if (i <= index) { this.setPageAtTop(page); } } this.sortBook(index - 1); } play() { if (this.flipTimeLine && this.flipTimeLine.totalDuration() > this.flipTimeLine.time()) { this.flipTimeLine.resume(null, false); } else { this.flipTimeLine = new TimelineLite({ autoRemoveChildren: true }); this.pages.forEach((page, index) => { if (page.rotation === 0 && !page.lock) { this.flipTimeLine.add(TweenLite.to(page, 1, { delay: index - (page.rotation < -90 ? 0 : 1) === this.currentIndex ? 0 : 2.5, rotation: -180, ease: Power2.easeOut, onStart: this.setPageAtTop, onStartParams: [page], onUpdate: this.render, onComplete: page.index + 1 < this.pages.length ? this.sortBook : void 0, onCompleteParams: [page.index + 1] })); } }); } } pause() { if (this.flipTimeLine) { const tweens = this.flipTimeLine.getChildren(true, true, true, this.flipTimeLine.time()); if (tweens.length > 0) { this.flipTimeLine.addPause(tweens[0].startTime()); } } } } BookComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.11", ngImport: i0, type: BookComponent, deps: [{ token: FlipbookService }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); BookComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.3.11", type: BookComponent, selector: "flipbook", inputs: { model: "model", startAt: "startAt" }, host: { properties: { "style.width.px": "this.hostWidth", "style.height.px": "this.hostHeight", "style.perspective.px": "this.hostPerspective" } }, ngImport: i0, template: "<flipbook-page *ngFor=\"let page of pages | reverse\"\r\n [page]=\"page\"\r\n [width]=\"model.width / 2\"\r\n [height]=\"model.height\"\r\n [zoom]=\"model.zoom\"\r\n [rotation]=\"page.rotation\"\r\n (mousedown)=\"onPageDown($event, page)\"\r\n (pan)=\"onPagePan($event, page)\"\r\n (panend)=\"onPagePanEnd($event, page)\"\r\n (swipe)=\"onSwipe($event, page)\">\r\n</flipbook-page>", styles: [":host{display:block;box-sizing:border-box;position:relative;perspective-origin:center}\n"], components: [{ type: PageComponent, selector: "flipbook-page", inputs: ["page", "width", "height", "rotation", "zoom"] }], directives: [{ type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }], pipes: { "reverse": ReversePipe }, changeDetection: i0.ChangeDetectionStrategy.OnPush }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.11", ngImport: i0, type: BookComponent, decorators: [{ type: Component, args: [{ selector: 'flipbook', changeDetection: ChangeDetectionStrategy.OnPush, template: "<flipbook-page *ngFor=\"let page of pages | reverse\"\r\n [page]=\"page\"\r\n [width]=\"model.width / 2\"\r\n [height]=\"model.height\"\r\n [zoom]=\"model.zoom\"\r\n [rotation]=\"page.rotation\"\r\n (mousedown)=\"onPageDown($event, page)\"\r\n (pan)=\"onPagePan($event, page)\"\r\n (panend)=\"onPagePanEnd($event, page)\"\r\n (swipe)=\"onSwipe($event, page)\">\r\n</flipbook-page>", styles: [":host{display:block;box-sizing:border-box;position:relative;perspective-origin:center}\n"] }] }], ctorParameters: function () { return [{ type: FlipbookService }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }]; }, propDecorators: { model: [{ type: Input }], startAt: [{ type: Input }], hostWidth: [{ type: HostBinding, args: ['style.width.px'] }], hostHeight: [{ type: HostBinding, args: ['style.height.px'] }], hostPerspective: [{ type: HostBinding, args: ['style.perspective.px'] }] } }); class FlipBookModule { static forChild() { return { ngModule: FlipBookModule, providers: [ FlipbookService, ] }; } } FlipBookModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.3.11", ngImport: i0, type: FlipBookModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); FlipBookModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.3.11", ngImport: i0, type: FlipBookModule, declarations: [BookComponent, PageComponent, ReversePipe], imports: [CommonModule, HammerModule], exports: [BookComponent] }); FlipBookModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.3.11", ngImport: i0, type: FlipBookModule, imports: [[ CommonModule, HammerModule, ]] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.3.11", ngImport: i0, type: FlipBookModule, decorators: [{ type: NgModule, args: [{ imports: [ CommonModule, HammerModule, ], declarations: [ BookComponent, PageComponent, ReversePipe, ], exports: [ BookComponent, ] }] }] }); /* * Public API Surface of flipbook */ /** * Generated bundle index. Do not edit. */ export { BookComponent, FlipBookModule, FlipbookService, PageType }; //# sourceMappingURL=labsforge-flipbook.mjs.map