UNPKG

page-flip

Version:

Powerful, simple and flexible JS Library for creating realistic and beautiful page turning effect

450 lines (363 loc) 14.2 kB
import { Orientation, Render } from '../Render/Render'; import { PageFlip } from '../PageFlip'; import { Helper } from '../Helper'; import { PageRect, Point } from '../BasicTypes'; import { FlipCalculation } from './FlipCalculation'; import { Page, PageDensity } from '../Page/Page'; /** * Flipping direction */ export const enum FlipDirection { FORWARD, BACK, } /** * Active corner when flipping */ export const enum FlipCorner { TOP = 'top', BOTTOM = 'bottom', } /** * State of the book */ export const enum FlippingState { /** The user folding the page */ USER_FOLD = 'user_fold', /** Mouse over active corners */ FOLD_CORNER = 'fold_corner', /** During flipping animation */ FLIPPING = 'flipping', /** Base state */ READ = 'read', } /** * Class representing the flipping process */ export class Flip { private readonly render: Render; private readonly app: PageFlip; private flippingPage: Page = null; private bottomPage: Page = null; private calc: FlipCalculation = null; private state: FlippingState = FlippingState.READ; constructor(render: Render, app: PageFlip) { this.render = render; this.app = app; } /** * Called when the page folding (User drags page corner) * * @param globalPos - Touch Point Coordinates (relative window) */ public fold(globalPos: Point): void { this.setState(FlippingState.USER_FOLD); // If the process has not started yet if (this.calc === null) this.start(globalPos); this.do(this.render.convertToPage(globalPos)); } /** * Page turning with animation * * @param globalPos - Touch Point Coordinates (relative window) */ public flip(globalPos: Point): void { if (this.app.getSettings().disableFlipByClick && !this.isPointOnCorners(globalPos)) return; // the flipiing process is already running if (this.calc !== null) this.render.finishAnimation(); if (!this.start(globalPos)) return; const rect = this.getBoundsRect(); this.setState(FlippingState.FLIPPING); // Margin from top to start flipping const topMargins = rect.height / 10; // Defining animation start points const yStart = this.calc.getCorner() === FlipCorner.BOTTOM ? rect.height - topMargins : topMargins; const yDest = this.calc.getCorner() === FlipCorner.BOTTOM ? rect.height : 0; // Сalculations for these points this.calc.calc({ x: rect.pageWidth - topMargins, y: yStart }); // Run flipping animation this.animateFlippingTo( { x: rect.pageWidth - topMargins, y: yStart }, { x: -rect.pageWidth, y: yDest }, true ); } /** * Start the flipping process. Find direction and corner of flipping. Creating an object for calculation. * * @param {Point} globalPos - Touch Point Coordinates (relative window) * * @returns {boolean} True if flipping is possible, false otherwise */ public start(globalPos: Point): boolean { this.reset(); const bookPos = this.render.convertToBook(globalPos); const rect = this.getBoundsRect(); // Find the direction of flipping const direction = this.getDirectionByPoint(bookPos); // Find the active corner const flipCorner = bookPos.y >= rect.height / 2 ? FlipCorner.BOTTOM : FlipCorner.TOP; if (!this.checkDirection(direction)) return false; try { this.flippingPage = this.app.getPageCollection().getFlippingPage(direction); this.bottomPage = this.app.getPageCollection().getBottomPage(direction); // In landscape mode, needed to set the density of the next page to the same as that of the flipped if (this.render.getOrientation() === Orientation.LANDSCAPE) { if (direction === FlipDirection.BACK) { const nextPage = this.app.getPageCollection().nextBy(this.flippingPage); if (nextPage !== null) { if (this.flippingPage.getDensity() !== nextPage.getDensity()) { this.flippingPage.setDrawingDensity(PageDensity.HARD); nextPage.setDrawingDensity(PageDensity.HARD); } } } else { const prevPage = this.app.getPageCollection().prevBy(this.flippingPage); if (prevPage !== null) { if (this.flippingPage.getDensity() !== prevPage.getDensity()) { this.flippingPage.setDrawingDensity(PageDensity.HARD); prevPage.setDrawingDensity(PageDensity.HARD); } } } } this.render.setDirection(direction); this.calc = new FlipCalculation( direction, flipCorner, rect.pageWidth.toString(10), // fix bug with type casting rect.height.toString(10) // fix bug with type casting ); return true; } catch (e) { return false; } } /** * Perform calculations for the current page position. Pass data to render object * * @param {Point} pagePos - Touch Point Coordinates (relative active page) */ private do(pagePos: Point): void { if (this.calc === null) return; // Flipping process not started if (this.calc.calc(pagePos)) { // Perform calculations for a specific position const progress = this.calc.getFlippingProgress(); this.bottomPage.setArea(this.calc.getBottomClipArea()); this.bottomPage.setPosition(this.calc.getBottomPagePosition()); this.bottomPage.setAngle(0); this.bottomPage.setHardAngle(0); this.flippingPage.setArea(this.calc.getFlippingClipArea()); this.flippingPage.setPosition(this.calc.getActiveCorner()); this.flippingPage.setAngle(this.calc.getAngle()); if (this.calc.getDirection() === FlipDirection.FORWARD) { this.flippingPage.setHardAngle((90 * (200 - progress * 2)) / 100); } else { this.flippingPage.setHardAngle((-90 * (200 - progress * 2)) / 100); } this.render.setPageRect(this.calc.getRect()); this.render.setBottomPage(this.bottomPage); this.render.setFlippingPage(this.flippingPage); this.render.setShadowData( this.calc.getShadowStartPoint(), this.calc.getShadowAngle(), progress, this.calc.getDirection() ); } } /** * Turn to the specified page number (with animation) * * @param {number} page - New page number * @param {FlipCorner} corner - Active page corner when turning */ public flipToPage(page: number, corner: FlipCorner): void { const current = this.app.getPageCollection().getCurrentSpreadIndex(); const next = this.app.getPageCollection().getSpreadIndexByPage(page); try { if (next > current) { this.app.getPageCollection().setCurrentSpreadIndex(next - 1); this.flipNext(corner); } if (next < current) { this.app.getPageCollection().setCurrentSpreadIndex(next + 1); this.flipPrev(corner); } } catch (e) { // } } /** * Turn to the next page (with animation) * * @param {FlipCorner} corner - Active page corner when turning */ public flipNext(corner: FlipCorner): void { this.flip({ x: this.render.getRect().left + this.render.getRect().pageWidth * 2 - 10, y: corner === FlipCorner.TOP ? 1 : this.render.getRect().height - 2, }); } /** * Turn to the prev page (with animation) * * @param {FlipCorner} corner - Active page corner when turning */ public flipPrev(corner: FlipCorner): void { this.flip({ x: 10, y: corner === FlipCorner.TOP ? 1 : this.render.getRect().height - 2, }); } /** * Called when the user has stopped flipping */ public stopMove(): void { if (this.calc === null) return; const pos = this.calc.getPosition(); const rect = this.getBoundsRect(); const y = this.calc.getCorner() === FlipCorner.BOTTOM ? rect.height : 0; if (pos.x <= 0) this.animateFlippingTo(pos, { x: -rect.pageWidth, y }, true); else this.animateFlippingTo(pos, { x: rect.pageWidth, y }, false); } /** * Fold the corners of the book when the mouse pointer is over them. * Called when the mouse pointer is over the book without clicking * * @param globalPos */ public showCorner(globalPos: Point): void { if (!this.checkState(FlippingState.READ, FlippingState.FOLD_CORNER)) return; const rect = this.getBoundsRect(); const pageWidth = rect.pageWidth; if (this.isPointOnCorners(globalPos)) { if (this.calc === null) { if (!this.start(globalPos)) return; this.setState(FlippingState.FOLD_CORNER); this.calc.calc({ x: pageWidth - 1, y: 1 }); const fixedCornerSize = 50; const yStart = this.calc.getCorner() === FlipCorner.BOTTOM ? rect.height - 1 : 1; const yDest = this.calc.getCorner() === FlipCorner.BOTTOM ? rect.height - fixedCornerSize : fixedCornerSize; this.animateFlippingTo( { x: pageWidth - 1, y: yStart }, { x: pageWidth - fixedCornerSize, y: yDest }, false, false ); } else { this.do(this.render.convertToPage(globalPos)); } } else { this.setState(FlippingState.READ); this.render.finishAnimation(); this.stopMove(); } } /** * Starting the flipping animation process * * @param {Point} start - animation start point * @param {Point} dest - animation end point * @param {boolean} isTurned - will the page turn over, or just bring it back * @param {boolean} needReset - reset the flipping process at the end of the animation */ private animateFlippingTo( start: Point, dest: Point, isTurned: boolean, needReset = true ): void { const points = Helper.GetCordsFromTwoPoint(start, dest); // Create frames const frames = []; for (const p of points) frames.push(() => this.do(p)); const duration = this.getAnimationDuration(points.length); this.render.startAnimation(frames, duration, () => { // callback function if (!this.calc) return; if (isTurned) { if (this.calc.getDirection() === FlipDirection.BACK) this.app.turnToPrevPage(); else this.app.turnToNextPage(); } if (needReset) { this.render.setBottomPage(null); this.render.setFlippingPage(null); this.render.clearShadow(); this.setState(FlippingState.READ); this.reset(); } }); } /** * Get the current calculations object */ public getCalculation(): FlipCalculation { return this.calc; } /** * Get current flipping state */ public getState(): FlippingState { return this.state; } private setState(newState: FlippingState): void { if (this.state !== newState) { this.app.updateState(newState); this.state = newState; } } private getDirectionByPoint(touchPos: Point): FlipDirection { const rect = this.getBoundsRect(); if (this.render.getOrientation() === Orientation.PORTRAIT) { if (touchPos.x - rect.pageWidth <= rect.width / 5) { return FlipDirection.BACK; } } else if (touchPos.x < rect.width / 2) { return FlipDirection.BACK; } return FlipDirection.FORWARD; } private getAnimationDuration(size: number): number { const defaultTime = this.app.getSettings().flippingTime; if (size >= 1000) return defaultTime; return (size / 1000) * defaultTime; } private checkDirection(direction: FlipDirection): boolean { if (direction === FlipDirection.FORWARD) return this.app.getCurrentPageIndex() < this.app.getPageCount() - 1; return this.app.getCurrentPageIndex() >= 1; } private reset(): void { this.calc = null; this.flippingPage = null; this.bottomPage = null; } private getBoundsRect(): PageRect { return this.render.getRect(); } private checkState(...states: FlippingState[]): boolean { for (const state of states) { if (this.state === state) return true; } return false; } private isPointOnCorners(globalPos: Point): boolean { const rect = this.getBoundsRect(); const pageWidth = rect.pageWidth; const operatingDistance = Math.sqrt(Math.pow(pageWidth, 2) + Math.pow(rect.height, 2)) / 5; const bookPos = this.render.convertToBook(globalPos); return ( bookPos.x > 0 && bookPos.y > 0 && bookPos.x < rect.width && bookPos.y < rect.height && (bookPos.x < operatingDistance || bookPos.x > rect.width - operatingDistance) && (bookPos.y < operatingDistance || bookPos.y > rect.height - operatingDistance) ); } }