@happyqu/flipbook
v0.1.3
Published
A responsive WebGL 3D flipbook engine for Vue 3 and TypeScript.
Maintainers
Readme
WebGL FlipBook Engine
基于 TypeScript、Three.js、WebGL Shader 和 Vue 3 的 H5 3D 翻书引擎。页面使用真实 Mesh 变形完成卷曲、翻面和阴影效果,并针对桌面端与移动端提供不同的阅读交互。
功能
- WebGL Page Curl:纸页弯曲、翻面、动态光影和书脊折痕
- 封面模式:初始显示单页封面,翻开后进入双页书本
- 双页排版:普通跨页、首页封面和末页单页自动居中
- 跟手翻页:拖动距离实时控制纸页弯曲进度,松手后完成或回弹
- 移动端单页聚焦:保留双页展开结构,画面聚焦当前阅读页
- 快速短划:双页内部平滑切换焦点,到页边缘后连续翻到下一跨页
- 双指缩放:默认支持
1x到3x - 自由拖动:放大后可向上下左右任意移动,双指缩放可无缝衔接单指拖动
- 导航居中:书本被移动后点击上一页或下一页,会先平滑回到当前页中心再翻页
- 响应式布局:适配常见手机尺寸、横屏和动态视口
- 纹理管理:邻近页面预加载,离开缓存范围的 GPU 纹理自动释放
- 多种操作:触控、鼠标、点击左右区域、方向键、PageUp 和 PageDown
页面资源
引擎接收按阅读顺序排列的图片 URL,不在浏览器运行时解析 PDF。推荐在服务端或构建阶段将 PDF 转换为 WebP:
page001.webp
page002.webp
page003.webp
...建议移动端页面宽度约为 1200px;需要高清展示时可使用 1500–2000px。所有页面最好保持相同宽高比。
const pages = [
'/magazine/page001.webp',
'/magazine/page002.webp',
'/magazine/page003.webp',
]当 cover 为 true 时,数组第一张图片作为封面。示例阅读顺序为:
封面 → [第 1 页 | 第 2 页] → [第 3 页 | 第 4 页] → ...本地开发
环境要求:Node.js 18+。
npm install
npm run dev常用命令:
npm run typecheck # TypeScript 类型检查
npm run test # 运行单元测试
npm run build # 构建演示站点
npm run build:lib # 构建组件库Vue 3 使用
容器必须具有明确的宽度和高度,引擎会自动填满容器。
npm install @happyqu/flipbookES Module / Vite
<script setup lang="ts">
import { ref } from 'vue'
import { VueFlipBook, type PageChangeDetail } from '@happyqu/flipbook'
import '@happyqu/flipbook/style.css'
type ReaderRef = InstanceType<typeof VueFlipBook>
const reader = ref<ReaderRef>()
const zoom = ref(1)
const pages = [
'/magazine/page001.webp',
'/magazine/page002.webp',
'/magazine/page003.webp',
'/magazine/page004.webp',
]
const handleChange = (detail: PageChangeDetail) => {
console.log('当前页:', detail.currentPage)
}
</script>
<template>
<div class="reader-container">
<VueFlipBook
ref="reader"
:pages="pages"
:cover="true"
:shadow="true"
:duration="720"
:min-zoom="1"
:max-zoom="3"
@page-change="handleChange"
@zoom-change="zoom = $event.zoom"
@ready="console.log('FlipBook ready')"
/>
</div>
<div class="controls">
<button @click="reader?.previous()">上一页</button>
<button @click="reader?.animateZoom(zoom - 0.25)">缩小</button>
<button @click="reader?.animateZoom(zoom + 0.25)">放大</button>
<button @click="reader?.next()">下一页</button>
</div>
</template>
<style scoped>
.reader-container {
width: 100%;
height: min(78dvh, 820px);
min-height: 320px;
}
</style>组件会从第一张成功加载的图片自动识别页面比例,并根据容器空间自动选择单页或跨页、计算相机距离。使用者只需要用 CSS 设置外层容器尺寸。高级场景可通过像素值覆盖内部安全留白:
.reader-container {
--flipbook-inset: 16px;
}CommonJS
const { FlipBook, VueFlipBook } = require('@happyqu/flipbook')
require('@happyqu/flipbook/style.css')Vue 子路径
import { VueFlipBook } from '@happyqu/flipbook/vue'
import '@happyqu/flipbook/style.css'浏览器 CDN / UMD
UMD 版本依赖全局的 Vue 和 THREE,适合不使用打包工具的页面:
<link
rel="stylesheet"
href="https://unpkg.com/@happyqu/flipbook/dist/flipbook.css"
/>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script type="module">
import * as THREE from 'https://unpkg.com/[email protected]/build/three.module.min.js'
window.THREE = THREE
await import('https://unpkg.com/@happyqu/flipbook/dist/flipbook.umd.js')
const { FlipBook, VueFlipBook } = window.FlipBook
</script>Props
| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| pages | string[] | 必填 | 按阅读顺序排列的页面图片 URL |
| view | 'auto' \| 'single' \| 'spread' | 'auto' | 自动选择、强制单页聚焦或强制完整跨页 |
| pageRatio | number | 首张有效图片的宽高比 | 页面宽度除以高度;仅在需要覆盖自动识别时传入 |
| shadow | boolean | true | 是否启用翻页阴影 |
| duration | number | 760 | 自动翻页动画时长,单位 ms |
| cover | boolean | true | 是否将第一张图片作为单页封面 |
| minZoom | number | 1 | 最小缩放比例 |
| maxZoom | number | 3 | 最大缩放比例 |
| preloadRadius | number | 2 | 当前页前后预加载的页面范围 |
| pixelRatio | number | 设备像素比(最大 2) | 自定义 WebGL 渲染像素比 |
| ariaLabel | string | 3D flipbook reader | 阅读器画布的无障碍名称 |
| pageTurnSound | boolean \| string | true | 启用内置翻页音效;也可传入自定义音频 URL,设为 false 可关闭 |
| pageTurnSoundVolume | number | 0.55 | 翻页音量,范围为 0 到 1 |
事件
| 事件 | 参数 | 说明 |
| --- | --- | --- |
| ready | PageStateDetail | 初始页面加载完成 |
| page-change | PageChangeDetail | 当前跨页或移动端聚焦页发生变化 |
| flip-start | FlipDetail | 开始实际的纸张翻转 |
| flip-progress | FlipProgressDetail | 翻页进度发生变化,progress 范围为 0 到 1 |
| flip-end | FlipDetail | 翻页成功且目标跨页加载完成 |
| flip-cancel | FlipCancelDetail | 翻页手势撤销或被其他操作中断 |
| layout-change | LayoutChangeDetail | 容器尺寸、页面比例或单双页布局发生变化 |
| zoom-change | ZoomChangeDetail | 缩放比例发生变化 |
| pan-change | PanChangeDetail | 缩放后的平移位置发生变化 |
| page-error | PageErrorDetail | 某张页面图片加载失败 |
| error | unknown | Vue 组件初始化或更新页面失败 |
原生引擎与 Vue 组件使用相同的事件名和参数。error 仅由 Vue 包装层触发;原生方法的错误通过异常或 Promise rejection 返回。
主要事件参数:
interface PageStateDetail {
leftPage: number
rightPage: number
currentPage: number
view: 'single' | 'spread'
pageRatio: number
totalPages: number
isFirstPage: boolean
isLastPage: boolean
canPrevious: boolean
canNext: boolean
}
interface PageChangeDetail extends PageStateDetail {
reason: 'flip' | 'focus' | 'go-to' | 'pages-update' | 'layout'
}
interface FlipDetail {
direction: 'next' | 'previous'
fromPage: number
toPage: number
}
interface FlipProgressDetail extends FlipDetail {
progress: number
}
interface FlipCancelDetail extends FlipProgressDetail {
reason: 'gesture' | 'interrupted'
}成功翻页的事件顺序为 flip-start → flip-progress → page-change → flip-end。取消翻页时最后触发 flip-cancel,不会触发 page-change。
页面索引均为从 0 开始的图片数组索引;业务界面可根据是否包含封面自行转换为展示页码。
暴露方法
| 方法 | 返回值 | 说明 |
| --- | --- | --- |
| next() | Promise<boolean> | 下一页;成功切换时返回 true |
| previous() | Promise<boolean> | 上一页;成功切换时返回 true |
| goTo(pageIndex) | Promise<void> | 跳转到指定图片索引 |
| setView(view) | void | 设置自动、单页或跨页视图 |
| setPageRatio(pageRatio?) | void | 覆盖页面比例;省略参数恢复图片自动识别 |
| setZoom(zoom) | void | 立即设置缩放比例,适合实时手势 |
| animateZoom(zoom) | Promise<void> | 平滑设置缩放比例,适合按钮操作 |
| resetZoom() | void | 恢复到 1x 并清除拖动偏移 |
| getEngine() | FlipBook \| undefined | 获取底层引擎实例 |
原生 TypeScript 使用
import {
FlipBook,
type FlipCancelDetail,
type FlipDetail,
type FlipProgressDetail,
type PageChangeDetail,
type PageErrorDetail,
} from '@happyqu/flipbook'
const container = document.querySelector<HTMLElement>('#reader')!
const book = new FlipBook(container, {
pages,
view: 'auto',
cover: true,
shadow: true,
duration: 720,
minZoom: 1,
maxZoom: 3,
preloadRadius: 2,
})
await book.ready
await book.next()
await book.animateZoom(1.5)原生引擎通过容器派发以下 CustomEvent:
container.addEventListener('page-change', (event) => {
const detail = (event as CustomEvent<PageChangeDetail>).detail
console.log(detail)
})
container.addEventListener('flip-start', (event) => {
const { direction, fromPage, toPage } = (
event as CustomEvent<FlipDetail>
).detail
console.log('开始翻页', direction, fromPage, toPage)
})
container.addEventListener('flip-progress', (event) => {
const { progress } = (event as CustomEvent<FlipProgressDetail>).detail
console.log('翻页进度', progress)
})
container.addEventListener('flip-end', (event) => {
const { toPage } = (event as CustomEvent<FlipDetail>).detail
console.log('翻页完成', toPage)
})
container.addEventListener('flip-cancel', (event) => {
const detail = (event as CustomEvent<FlipCancelDetail>).detail
console.log('翻页取消', detail.reason, detail.progress)
})
container.addEventListener('page-error', (event) => {
const { pageIndex, url, error } = (
event as CustomEvent<PageErrorDetail>
).detail
console.error('页面加载失败', pageIndex, url, error)
})不再使用时应释放 WebGL 与事件资源:
book.dispose()交互规则
桌面端
- 点击或拖动页面左右区域进行翻页
- 使用
←、→、PageUp、PageDown 导航 - 放大后可自由拖动书本
- 放大且移动过位置时,上一页/下一页会先平滑回到当前页中心,再开始翻页
移动端 100%
- 双页书本保持展开,视口始终聚焦当前阅读页
- 横向慢拖可在同一跨页的左右页之间移动
- 到达当前页边缘后书本停止移动,继续拖动会直接带动纸页翻开
- 快速短划直接导航,并从松手位置连续完成移动或翻页
- 纵向拖动被锁定,避免阅读画面漂移
移动端放大后
- 双指捏合实时放大或缩小
- 抬起一根手指后,另一根手指可以直接继续拖动
- 上下左右拖动不设置位置边界
- 点击缩放按钮时建议调用
animateZoom(),双指手势应调用setZoom()
性能说明
- 使用分段页面 Mesh 和 Vertex Shader 计算卷曲,而非 CSS 旋转
- 使用
requestAnimationFrame驱动动画 - 仅按需加载当前跨页附近的图片,并自动释放远离当前页的 GPU 纹理
- 只保留当前页附近的纹理,超出缓存范围后调用
texture.dispose() pixelRatio可用于限制高 DPI 设备的渲染开销
浏览器要求
需要支持 WebGL、ES Modules、Pointer Events 和 ResizeObserver 的现代浏览器。建议在目标微信 WebView、iOS Safari 和 Android Chrome 上使用真实杂志素材进行最终兼容性测试。
