pustak
v1.0.4
Published
A premium, dependency-free page-flip library written in pure TypeScript.
Downloads
743
Maintainers
Readme
📖 Pustak Pageflip Engine
A premium, high-performance, and dependency-free page-flip library written in pure TypeScript.
Pustak mimics the physical tactile experience of flipping a book page. By utilizing GPU-accelerated CSS 3D transforms, real-time geometry calculations, and dynamic CSS clip-path polygons, it runs smoothly (60+ FPS) even on mobile web browsers, keeping all inner content fully readable by SEO crawlers, screen readers, and selectable by users.
🛠️ Prerequisites & System Requirements
To run, build, or contribute to Pustak, verify your environment meets the following specifications:
| Requirement | Supported Version | Notes |
| :--- | :--- | :--- |
| Node.js | v18.0.0 or higher | Recommended: v20.x LTS or higher. |
| NPM | v9.0.0 or higher | Standard package manager (Yarn / PNPM work perfectly as well). |
| Browsers | Modern Browsers | Requires modern CSS standard support for 3D perspective (transform-style: preserve-3d) and clip-path polygons. |
🔌 Integration Examples
Pustak can be integrated into any front-end environment. Below are complete examples for Normal HTML / Vanilla JavaScript, React, and Angular.
1. Normal HTML / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pustak - HTML Integration</title>
<!-- Import Pustak Styles -->
<link rel="stylesheet" href="node_modules/pustak/dist/style.css">
<style>
#book-container {
width: 760px; /* 380px * 2 for double-spread */
height: 500px;
margin: 50px auto;
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
}
.page {
background: #fbf8eb;
padding: 30px;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="book-container">
<div class="page">Page 1 (Cover)</div>
<div class="page">Page 2</div>
<div class="page">Page 3</div>
<div class="page">Page 4 (Back Cover)</div>
</div>
<div style="text-align: center;">
<button id="prev-btn">Previous</button>
<button id="next-btn">Next</button>
</div>
<!-- Initialize with ES Modules -->
<script type="module">
import { Book } from './node_modules/pustak/dist/pustak.js';
const container = document.getElementById('book-container');
const book = new Book(container, {
width: 380,
height: 500,
mode: 'double',
autoCenter: true,
hardCovers: true
});
document.getElementById('prev-btn').addEventListener('click', () => book.prev());
document.getElementById('next-btn').addEventListener('click', () => book.next());
</script>
</body>
</html>2. React
import React, { useEffect, useRef } from 'react';
import { Book } from 'pustak';
import 'pustak/style.css'; // Ensure you import CSS in your entrypoint or component
export const BookViewer: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const bookRef = useRef<Book | null>(null);
useEffect(() => {
if (!containerRef.current) return;
// Initialize Pustak
const book = new Book(containerRef.current, {
width: 380,
height: 500,
mode: 'double',
autoCenter: true,
hardCovers: true
});
bookRef.current = book;
// Clean up on unmount
return () => {
book.destroy();
bookRef.current = null;
};
}, []);
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '20px' }}>
{/* Book Container */}
<div ref={containerRef} style={{ boxShadow: '0 10px 30px rgba(0,0,0,0.15)' }}>
<div className="page" style={{ background: '#fbf8eb', padding: '30px' }}>Cover Page</div>
<div className="page" style={{ background: '#fbf8eb', padding: '30px' }}>Page 2</div>
<div className="page" style={{ background: '#fbf8eb', padding: '30px' }}>Page 3</div>
<div className="page" style={{ background: '#fbf8eb', padding: '30px' }}>Back Cover</div>
</div>
{/* Controls */}
<div>
<button onClick={() => bookRef.current?.prev()}>Previous</button>
<button onClick={() => bookRef.current?.next()} style={{ marginLeft: '10px' }}>Next</button>
</div>
</div>
);
};3. Angular
import { Component, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import { Book } from 'pustak';
@Component({
selector: 'app-book-viewer',
standalone: true,
template: `
<div class="viewer-wrapper">
<div #bookContainer class="book-container">
<div class="page">Front Cover</div>
<div class="page">Page 2</div>
<div class="page">Page 3</div>
<div class="page">Back Cover</div>
</div>
<div class="controls">
<button (click)="onPrev()">Previous</button>
<button (click)="onNext()">Next</button>
</div>
</div>
`,
styles: [`
.viewer-wrapper { display: flex; flex-direction: column; align-items: center; gap: 20px; }
.book-container { box-shadow: 0 10px 30px rgba(0,0,0,0.15); }
.page { background: #fbf8eb; padding: 30px; box-sizing: border-box; }
.controls button { margin: 0 5px; padding: 8px 16px; cursor: pointer; }
`]
})
export class BookViewerComponent implements AfterViewInit, OnDestroy {
@ViewChild('bookContainer') bookContainer!: ElementRef<HTMLElement>;
private book: Book | null = null;
ngAfterViewInit(): void {
this.book = new Book(this.bookContainer.nativeElement, {
width: 380,
height: 500,
mode: 'double',
autoCenter: true,
hardCovers: true
});
}
onPrev(): void {
this.book?.prev();
}
onNext(): void {
this.book?.next();
}
ngOnDestroy(): void {
if (this.book) {
this.book.destroy();
}
}
}Note: Make sure to import pustak/style.css in your global stylesheet (e.g., styles.scss or angular.json).
⚙️ API Documentation
Constructor Options (BookOptions)
When calling new Book(container, options), configure the system using these options:
| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| width | number | Required | Width of a single page in pixels (px). |
| height | number | Required | Height of a page in pixels (px). |
| mode | 'single' \| 'double' | 'double' | Display spread mode. double shows left + right spreads; single is ideal for mobile screens. |
| autoCenter | boolean | true | When true in double mode, centers single cover/back cover pages on screen. |
| hardCovers | boolean | true | If true, treats the first page and last page as rigid card covers. |
| paginationRequired | boolean | false | If true, enables DOM virtualization/pruning to keep only active pages inside the DOM to improve performance. |
| pagesLimit | number | 2 | Number of buffer pages (enforced as an even number). Keeps currentPage - pagesLimit/2 to currentPage + pagesLimit/2 in the DOM and prunes the rest. E.g., if set to 6, it maintains 3 previous and 3 next pages in the DOM. |
Public Methods
| Method | Parameters | Return Type | Description |
| :--- | :--- | :--- | :--- |
| next() | None | void | Triggers a smooth page-turn animation forward to the next page. |
| prev() | None | void | Triggers a smooth page-turn animation backward to the previous page. |
| jumpTo(pageIndex) | pageIndex: number | void | Jumps instantly to the selected page index (without turn animations). |
| setMode(mode) | 'single' \| 'double' | void | Switches orientation mode on the fly between single page and double-spread. |
| getCurrentPage() | None | number | Returns the current visible page index. |
| getPageCount() | None | number | Returns the total count of pages detected inside the container. |
| on(event, handler) | event: EventKey, handler: EventHandler | void | Adds a listener for custom engine events. |
| off(event, handler) | event: EventKey, handler: EventHandler | void | Removes an active event listener. |
| destroy() | None | void | Cleans up engine events and resources. |
Custom Event Hooks
Listen to key library lifecycles using .on('eventName', data => { ... }):
'flip': Dispatched when a page-turn starts.book.on('flip', (data) => { console.log(`Flipping started. Current page: ${data.page}, Direction: ${data.direction}`); });'update': Dispatched on every pixel of page-dragging or auto-flipping progress (0 to 1).book.on('update', (data) => { console.log(`Flip progress: ${data.progress} on corner ${data.corner}`); });'flipped': Dispatched when a page turn finishes completely.book.on('flipped', (data) => { console.log(`Flip completed. Active Page: ${data.page}`); });'jump': Dispatched after calling.jumpTo().'orientation': Dispatched when.setMode()toggles display layout.
🎨 Interactive DOM & Sandbox Elements
Unlike canvas libraries, Pustak leaves all inner controls responsive. If you place interactive widgets on a page (like text boxes, canvas sketches, slider bars, or buttons):
- Bind your event listeners as usual.
- In your button click handlers or interactive inputs, call
event.stopPropagation()onmousedown,touchstart, orclickevents to prevent the Pustak interaction engine from capturing mouse coordinates as a book drag.
For example, a widget inside the book page can be safely set up like this:
const widgetButton = document.getElementById('widget-btn');
widgetButton.addEventListener('click', (e) => {
e.stopPropagation(); // Prevents page from turning when clicking this button
// Run custom action
});📬 Contact & Support
For commercial licensing, custom integration assistance, inquiries, or support, please reach out to:
- Email: [email protected]
🛡️ License
Pustak is licensed under a dual-license model:
- Personal / Non-Commercial Use: Free for personal, educational, and non-commercial projects.
- Commercial Use: Requires a paid commercial license. Please contact the author at [email protected] to purchase a commercial license or discuss commercial usage rights.
