@labsforge/flipbook
Version:
A simple angular flipbook component written in typescript
530 lines (520 loc) • 27.2 kB
JavaScript
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