hic-pageflip 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/components/hic-pageflip-page.js +43 -0
- package/components/hic-pageflip.js +339 -0
- package/core/engines/engine-2d.js +470 -0
- package/core/engines/engine-3d.js +699 -0
- package/core/engines/engine-base.js +73 -0
- package/core/pageflip.js +657 -0
- package/core/utils/math.js +186 -0
- package/index.js +8 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bramus Van Damme <bramus@bram.us> (https://www.bram.us/)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# `<hic-pageflip>`
|
|
2
|
+
|
|
3
|
+
An interactive page flip book viewer powered by the [**HTML-in-Canvas** web API](https://developer.chrome.com/blog/html-in-canvas-origin-trial).
|
|
4
|
+
|
|
5
|
+
[](https://hic-pageflip.netlify.app/)
|
|
6
|
+
|
|
7
|
+
> [!WARNING]
|
|
8
|
+
> **HTML-in-Canvas is experimental technology.**
|
|
9
|
+
> This project relies on experimental browser APIs (`<canvas layoutsubtree>`, `ctx.drawElementImage`, `gl.texElementImage2D`, and `canvas.updateElementGeometry`) currently being developed in Chromium. Features and API surfaces are subject to change.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
`<hic-pageflip>` allows you to render interactive, flip-book style page presentations where every page is composed of **real, accessible DOM elements** (with selectable text, live links, CSS animations, and rich markup), while being deformed and rendered through high-performance 2D Canvas or 3D WebGL deformation pipelines.
|
|
16
|
+
|
|
17
|
+
[Try a live demo](https://hic-pageflip.netlify.app/)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
To view and interact with `<hic-pageflip>`, you need a browser that supports the **HTML-in-Canvas** API (e.g. Chrome):
|
|
24
|
+
|
|
25
|
+
1. **Browser**: Google Chrome (v149+ recommended).
|
|
26
|
+
2. **Flag**: Enable `chrome://flags/#canvas-draw-element`
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation & Usage
|
|
31
|
+
|
|
32
|
+
### Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install hic-pageflip
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Usage
|
|
39
|
+
|
|
40
|
+
After installation, import the **main entry point** once in your application.
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import 'hic-pageflip';
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The package will auto-register the `<hic-pageflip>` and `<hic-pageflip-page>` web components for you. No need to do anything else!
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Example
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<script type="module" src="./js/hic-pageflip/index.js"></script>
|
|
54
|
+
|
|
55
|
+
<hic-pageflip engine="3d" page-width="1024" page-height="768" page-background="#ffffff">
|
|
56
|
+
<!-- Slide 1: Cover -->
|
|
57
|
+
<hic-pageflip-page>
|
|
58
|
+
<div class="content">
|
|
59
|
+
<h1>Cover Page</h1>
|
|
60
|
+
<p>This is live HTML inside a 3D WebGL pageflip!</p>
|
|
61
|
+
</div>
|
|
62
|
+
</hic-pageflip-page>
|
|
63
|
+
|
|
64
|
+
<!-- Slide 2 -->
|
|
65
|
+
<hic-pageflip-page>
|
|
66
|
+
<div class="content">
|
|
67
|
+
<h2>Inside Left Page</h2>
|
|
68
|
+
<p>Selectable text and clickable <a href="#test">links</a> work natively.</p>
|
|
69
|
+
</div>
|
|
70
|
+
</hic-pageflip-page>
|
|
71
|
+
|
|
72
|
+
<!-- Slide 3 -->
|
|
73
|
+
<hic-pageflip-page>
|
|
74
|
+
<div class="content">
|
|
75
|
+
<h2>Inside Right Page</h2>
|
|
76
|
+
</div>
|
|
77
|
+
</hic-pageflip-page>
|
|
78
|
+
|
|
79
|
+
<!-- Slide 4: Backcover -->
|
|
80
|
+
<hic-pageflip-page>
|
|
81
|
+
<div class="content">
|
|
82
|
+
<h2>Backcover</h2>
|
|
83
|
+
<p>This is the backcover page.</p>
|
|
84
|
+
</div>
|
|
85
|
+
</hic-pageflip-page>
|
|
86
|
+
</hic-pageflip>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
To prevent a FOUC while the custom elements are not defined, add the following CSS:
|
|
90
|
+
|
|
91
|
+
```css
|
|
92
|
+
/* Prevent Flash of Undefined Custom Elements (FOUC) */
|
|
93
|
+
hic-pageflip:not(:defined),
|
|
94
|
+
hic-pageflip-page:not(:defined) {
|
|
95
|
+
display: none !important;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Fade-in hic-pageflip once defined */
|
|
99
|
+
hic-pageflip:defined {
|
|
100
|
+
transition: opacity 0.35s ease-out;
|
|
101
|
+
@starting-style {
|
|
102
|
+
opacity: 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Custom Element Reference
|
|
110
|
+
|
|
111
|
+
### `<hic-pageflip>` Attributes & Properties
|
|
112
|
+
|
|
113
|
+
| Attribute | Property | Type | Default | Description |
|
|
114
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
115
|
+
| `engine` | `engine` / `engineMode` | `string` | `'2d'` | Rendering engine mode: `'2d'` or `'3d'`. |
|
|
116
|
+
| `page-width` | `pageWidth` | `number` | `1024` | Width of a single page in CSS pixels. |
|
|
117
|
+
| `page-height` | `pageHeight` | `number` | `768` | Height of a single page in CSS pixels. |
|
|
118
|
+
| `page-background` | `pageBackground` | `string` | `'white'` | Background color/fill applied to each page sheet. |
|
|
119
|
+
| `page` | `page` / `currentPage` | `number` | `0` | Current page index (0 = Cover / Spread [0, 1]). |
|
|
120
|
+
|
|
121
|
+
### Methods
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
const pageflip = document.querySelector('hic-pageflip');
|
|
125
|
+
|
|
126
|
+
// Navigate forwards by one spread
|
|
127
|
+
pageflip.flipForward();
|
|
128
|
+
|
|
129
|
+
// Navigate backwards by one spread
|
|
130
|
+
pageflip.flipBackward();
|
|
131
|
+
|
|
132
|
+
// Jump to a specific page number
|
|
133
|
+
pageflip.gotoPage(4);
|
|
134
|
+
|
|
135
|
+
// Switch rendering engine
|
|
136
|
+
pageflip.engine = '3d'; // or '2d'
|
|
137
|
+
|
|
138
|
+
// Reload textures from DOM
|
|
139
|
+
pageflip.reloadTextures();
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Events
|
|
143
|
+
|
|
144
|
+
- **`pagechange`**: Fired when the active spread changes upon completing a flip.
|
|
145
|
+
```javascript
|
|
146
|
+
pageflip.addEventListener('pagechange', (e) => {
|
|
147
|
+
console.log('Current page:', e.detail.currentPage);
|
|
148
|
+
console.log('Current spread:', e.detail.currentSpread); // e.g. [2, 3]
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
- **`flipprogress`**: Fired continuously during drag or transition animation ticks with progress data.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Rendering Engines
|
|
156
|
+
|
|
157
|
+
`<hic-pageflip>` comes equipped with two distinct rendering engines that can be switched dynamically at runtime via the `engine` attribute or property:
|
|
158
|
+
|
|
159
|
+
### 1. 2D Engine (`engine="2d"`)
|
|
160
|
+
- **Class**: [`HICPageflipEngine2D`](src/js/hic-pageflip/core/engines/engine-2d.js)
|
|
161
|
+
- **Technology**: 2D Canvas context with direct DOM element drawing via `ctx.drawElementImage()`.
|
|
162
|
+
- **Techniques**:
|
|
163
|
+
- **Geometric Fold Clipping**: Uses half-plane clipping (`clipHalfPlane`) to separate the stationary spread, underneath revealed pages, and turning flap.
|
|
164
|
+
- **Affine Reflection**: Performs 2D affine matrix reflection across the dynamic fold crease line.
|
|
165
|
+
- **Dynamic Lighting**: Renders drop shadows cast under the fold crease and spine gutter shadows.
|
|
166
|
+
- **Pre-warmed Paint Records**: Pre-warms slide paint records to eliminate unstyled content flashing during initial corner peeks.
|
|
167
|
+
|
|
168
|
+
### 2. 3D Engine (`engine="3d"`)
|
|
169
|
+
- **Class**: [`HICPageflipEngine3D`](src/js/hic-pageflip/core/engines/engine-3d.js)
|
|
170
|
+
- **Technology**: WebGL / WebGL2 context capturing live DOM nodes to GPU textures via `gl.texElementImage2D()`.
|
|
171
|
+
- **Techniques**:
|
|
172
|
+
- **Chris Luke's Page Curl Algorithm**: Complete GLSL vertex shader implementation based on [*The Anatomy of a Page Curl*](https://blog.flirble.org/2010/10/08/the-anatomy-of-a-page-curl/).
|
|
173
|
+
- **Cylindrical & Conical Deformation**: Dynamically transitions between a uniform cylinder (for horizontal flips) and a tapered cone (for diagonal corner pulls), keeping the fold apex anchored to the page edge without bulging.
|
|
174
|
+
- **Hardware Dual-Sided Texturing**: Shaders sample front (`uSamplerFront`) and back (`uSamplerBack`) textures in a single draw pass using `gl_FrontFacing`.
|
|
175
|
+
- **Z-Fighting Prevention**: Employs sub-pixel depth offsetting (`uDepthOffset`) to eliminate z-fighting between overlapping sheets.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Package Folder Structure
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
hic-pageflip
|
|
183
|
+
├── package.json
|
|
184
|
+
├── README.md
|
|
185
|
+
├── index.js # Main entry point exporting Custom Elements
|
|
186
|
+
├── components/
|
|
187
|
+
│ ├── hic-pageflip.js # <hic-pageflip> Web Component
|
|
188
|
+
│ └── hic-pageflip-page.js # <hic-pageflip-page> Web Component
|
|
189
|
+
└── core/
|
|
190
|
+
├── pageflip.js # Core Pageflip state machine & gestures
|
|
191
|
+
├── engines/
|
|
192
|
+
│ ├── engine-base.js # BaseEngine abstract base class
|
|
193
|
+
│ ├── engine-2d.js # HICPageflipEngine2D (2D Canvas)
|
|
194
|
+
│ └── engine-3d.js # HICPageflipEngine3D (3D WebGL)
|
|
195
|
+
└── utils/
|
|
196
|
+
└── math.js # Fold math, easing curves, constraints
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Class Inheritance & Architecture Structure
|
|
202
|
+
|
|
203
|
+
```mermaid
|
|
204
|
+
classDiagram
|
|
205
|
+
direction TB
|
|
206
|
+
|
|
207
|
+
class HTMLElement {
|
|
208
|
+
<<browser>>
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
class HICPageflip {
|
|
212
|
+
+canvas: HTMLCanvasElement
|
|
213
|
+
+pageflip: Pageflip
|
|
214
|
+
+engineMode: string
|
|
215
|
+
+flipForward()
|
|
216
|
+
+flipBackward()
|
|
217
|
+
+gotoPage(n)
|
|
218
|
+
+switchEngine(mode)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
class HICPageflipPage {
|
|
222
|
+
+pageNum: number
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
class Pageflip {
|
|
226
|
+
+engine: BaseEngine
|
|
227
|
+
+activeFlip: object
|
|
228
|
+
+currentPage: number
|
|
229
|
+
+currentSpread: number[]
|
|
230
|
+
+handlePaint()
|
|
231
|
+
+startCornerPeek(corner)
|
|
232
|
+
+startDrag(x, y)
|
|
233
|
+
+flipForward()
|
|
234
|
+
+flipBackward()
|
|
235
|
+
+render()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
class BaseEngine {
|
|
239
|
+
<<abstract>>
|
|
240
|
+
+canvas: HTMLCanvasElement
|
|
241
|
+
+slides: object[]
|
|
242
|
+
+render(state)*
|
|
243
|
+
+resize()*
|
|
244
|
+
+destroy()*
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
class HICPageflipEngine2D {
|
|
248
|
+
+ctx: CanvasRenderingContext2D
|
|
249
|
+
+renderPages(ctx, state)
|
|
250
|
+
+clipHalfPlane(ctx, fold, onDragSide)
|
|
251
|
+
+drawFoldUnderShadow()
|
|
252
|
+
+warmupSlidePaintRecords()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
class HICPageflipEngine3D {
|
|
256
|
+
+gl: WebGLRenderingContext
|
|
257
|
+
+program: WebGLProgram
|
|
258
|
+
+meshBuffers: object
|
|
259
|
+
+drawTurningSheet(front, back, side, flip)
|
|
260
|
+
+drawDoubleSidedPage(front, back, side, isActive, cyl)
|
|
261
|
+
+rasterizeSlideToTexture(slide)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
HTMLElement <|-- HICPageflip
|
|
265
|
+
HTMLElement <|-- HICPageflipPage
|
|
266
|
+
HICPageflip *-- Pageflip : contains
|
|
267
|
+
HICPageflip o-- HICPageflipPage : slots light DOM
|
|
268
|
+
Pageflip *-- BaseEngine : delegates rendering to
|
|
269
|
+
BaseEngine <|-- HICPageflipEngine2D : implements
|
|
270
|
+
BaseEngine <|-- HICPageflipEngine3D : implements
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Architectural Highlights
|
|
274
|
+
|
|
275
|
+
1. **Shadow DOM & Layout Subtree**:
|
|
276
|
+
`<hic-pageflip>` encapsulates the rendering canvas in its Shadow Root using `<canvas layoutsubtree><slot></slot></canvas>`. This projects light-DOM `<hic-pageflip-page>` elements directly into the browser's canvas layout subtree without manual DOM relocation.
|
|
277
|
+
2. **Engine Decoupling**:
|
|
278
|
+
`Pageflip` acts as an engine-agnostic controller managing gesture coordinates, timeline animations, paper constraint math (`constrainPaper`), and page state. It delegates painting to whichever `BaseEngine` implementation is currently active.
|
|
279
|
+
3. **Pluggable Engine Hierarchy**:
|
|
280
|
+
Both `HICPageflipEngine2D` and `HICPageflipEngine3D` extend `BaseEngine` and implement a unified interface (`render`, `resize`, `setDimensions`, `destroy`), allowing seamless hot-switching between 2D and 3D rendering modes on the fly.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Development
|
|
285
|
+
|
|
286
|
+
### Run Locally
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
# Start local development server on port 3000
|
|
290
|
+
npm start
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Point your browser at `http://localhost:3000` to see.
|
|
294
|
+
|
|
295
|
+
### Deploy the demo to Netlify
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
npm run deploy
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Publish the package
|
|
302
|
+
|
|
303
|
+
Do not run `npm publish` but, instead, run a custom publish script:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
npm run pub
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The script will automatically build the project first into the `dist` folder, and only publish the contents of that folder.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## References
|
|
314
|
+
|
|
315
|
+
- [The Anatomy of a Page Curl by Chris Luke](https://blog.flirble.org/2010/10/08/the-anatomy-of-a-page-curl/)
|
|
316
|
+
- [Chrome HTML-in-Canvas Documentation](https://developer.chrome.com/blog/html-in-canvas-origin-trial)
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## License
|
|
321
|
+
|
|
322
|
+
[MIT](LICENSE) © [Bramus Van Damme](https://www.bram.us)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <hic-pageflip-page> Custom Element Web Component.
|
|
3
|
+
* Represents an individual page slide within <hic-pageflip>.
|
|
4
|
+
* Renders slotted content on a standardized pageflip page surface.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const BaseElement = typeof HTMLElement !== 'undefined' ? HTMLElement : class {};
|
|
8
|
+
|
|
9
|
+
const templateHTML = `
|
|
10
|
+
<style>
|
|
11
|
+
:host {
|
|
12
|
+
position: absolute;
|
|
13
|
+
top: 0;
|
|
14
|
+
left: 0;
|
|
15
|
+
transform-origin: 0 0;
|
|
16
|
+
height: var(--pageflip-height);
|
|
17
|
+
width: var(--pageflip-width);
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
user-select: text;
|
|
21
|
+
-webkit-user-select: text;
|
|
22
|
+
background: var(--pageflip-background);
|
|
23
|
+
}
|
|
24
|
+
</style>
|
|
25
|
+
<slot></slot>
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export class HICPageflipPage extends BaseElement {
|
|
29
|
+
constructor() {
|
|
30
|
+
super();
|
|
31
|
+
|
|
32
|
+
if (typeof HTMLElement !== 'undefined' && this.attachShadow) {
|
|
33
|
+
this.attachShadow({ mode: 'open' });
|
|
34
|
+
const template = document.createElement('template');
|
|
35
|
+
template.innerHTML = templateHTML;
|
|
36
|
+
this.shadowRoot.appendChild(template.content.cloneNode(true));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof customElements !== 'undefined' && !customElements.get('hic-pageflip-page')) {
|
|
42
|
+
customElements.define('hic-pageflip-page', HICPageflipPage);
|
|
43
|
+
}
|