ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HTML5 3D翻页电子书:用CSS 3D打造跨端翻页体验

HTML5 3D翻页电子书:用CSS 3D打造跨端翻页体验 简介一套适配 PC 与手机端的 HTML5 三维翻页电子书源码基于 three.js、jQuery 与 onebook3d 插件实现仿真翻页适合前端工程师快速搭建在线杂志、产品画册、图文作品集与互动阅读站点兼顾鼠标点击、拖拽与移动端触摸操作。压缩包共 52 个文件包括 38 张封面和内页 JPG 图片、6 个 JavaScript 脚本、2 个 HTML 示例页面、2 个 CSS 样式表以及 SVG/EOT/TTF/WOFF 图标字体资源整包仅 1.25MB便于快速部署和复用。代码采用响应式设计能自动适配不同屏幕尺寸通过修改 JS 参数还可调整翻页速度、3D 视角和交互触发方式为二次开发留下充足空间其中 Canvas/WebGL 渲染、CSS3 过渡动画与移动端事件绑定都是理解前端 3D 表现力的核心切入点。已有 311 人学习浏览对想提升动画与视觉开发水平、希望直接运行并拆解现代前端特效的读者来说是一份难得的实战案例。1. HTML5 兼容电脑手机端的 3D 翻页电子书从“能看”到“像书”差在哪做画册、产品手册或企业介绍页的时候光有 H5 页面已经不够用户期望它有实体书的翻页手感手指拖过去纸面会带着角度拱起来再在书脊处落下。HTML5 兼容电脑手机端的 3D 翻页电子书特效本质就是用 CSS 3D 变换加手势事件在 Web 里模拟书页的旋转与遮挡。它不需要引入重型框架一个页面加几张内容图就能跑适合给非技术同事交付、嵌进官网或放进公众号展示。这篇笔记把它从原理拆到可复现的最小实现再把双端适配和手势细节上踩过的坑原样摆出来。2. 翻页背后的 3D 结构盒子模型、书脊旋转与左右页的差分2.1 先理解 3D 网页渲染的基本盘perspective 与 preserve-3d所有翻页效果的第一步都是让浏览器进入 3D 网页渲染上下文。用.book容器包住整本书给它设置perspective: 1800px让内部元素获得近大远小的透视再给真正的书页容器设置transform-style: preserve-3d告诉浏览器子元素不要被压平而是共享同一个 3D 坐标系。这两个属性缺一个翻页就退化成二维旋转。常见做法是分两层结构.book负责透视和整体定位.page-wrap负责preserve-3d里面放左半页和右半页两页共用书脊这个旋转轴。很多人第一步就翻车在 perspective 放错了层级。perspective写在父容器上是以父容器中心为消失点子元素各自保留深度写在transform里变成transform: perspective(1800px) rotateY(20deg)则只对当前元素生效左右两页各算各的透视中心翻到一半就会穿模。我一般统一写在.book上并且不设overflow: hidden否则背面阴影会被裁掉。你可以把整本书想象成多张 2D 图像在 z 轴方向堆叠成一个 3D 场景。每一页是平面但它们在三维空间里的位置关系由书脊轴决定。perspective 值视觉表现适用场景800px透视很强页面近大远小明显单页卡片翻转不适合整本书1500px 2000px接近人眼观察实体书的距离感翻页电子书推荐区间3000px 以上几乎变成平行投影多页同时展示、书架场景2.2 书脊与翻页轴心旋转原点决定“这是翻书还是幻灯片”翻一页纸本质是让右半页以书脊为轴向左旋转一定角度。在 CSS 里这个轴心由transform-origin控制。右半页默认轴心在页面中心旋转起来像转盘要像书必须把旋转原点钉在页面左缘transform-origin: left center再叠加rotateY角度。容易混淆的是旋转方向。屏幕上看到“向左翻”的右半页在右手坐标系里对应的是rotateY(-60deg)。写成rotateY(60deg)会让页面朝屏幕外翻转直接穿模。如果不确定正负号就在浏览器控制台里手动改角度观察正面是“朝左倒下”还是“朝右仰起”比背坐标系结论更直观。翻页角度和拖拽位移也不是一比一的关系。常见手感映射是拖动整本书宽度的 40% 到 60% 完成 180 度翻页具体系数在 3.4 里展开。角度太大用户还没翻完就脱手角度太小又觉得书页“太重”。2.3 双页翻与单页翻为什么大多数电子书用右半页翻开真实的书左右页同时存在但 Web 端最常见的是“单页双面”结构每一张纸有自己的 front 和 backfront 显示当前右页内容back 显示下一页的左页内容。翻页开始前正面是右页翻到一半读者看到的是 back 那面也就是下一页左半页。这和真实翻书视觉一致而且省了一半 DOM 节点。双页同时翻的方案也不是不行但左右两页的背面都要额外挂一张页面贴图DOM 层级和 z-index 管理会明显变复杂。对以展示为主的 3D 翻页电子书我一般先用单页双面跑通再考虑要不要上双侧同时翻。手感和效果的差异对绝大多数用户是看不出来的。下面是最小 DOM 骨架注意翻页层.flipper只占书的一半宽轴心落在左边缘div idbook classbook div idleftStatic classstatic-page左页内容/div div idrightStatic classstatic-page右页内容/div div idflipper classflipper div idflipperFront classface当前右页/div div idflipperBack classface下一页左页/div /div /div.flipper是唯一会旋转的元素leftStatic和rightStatic在它下面垫底。翻页时.flipper盖住书页向左倒下背面的flipperBack会在角度超过 90 度后进入读者视野这就是“下一页左页出现”的时机。2.4 为什么不直接用 Canvas 或 three.js网上很多教程用 Canvas 画模拟纸张像 canvas 文字 3D 效果那一类 demo确实能做出弯曲和光影但你要自己处理纹理映射、像素级手势命中开发和调试成本都不低。three.js 能做出最逼真的纸面弯曲可它引入几百 KB 运行时还要处理加载器和内存释放为一本几十页的画册上 three.js 属于杀鸡用牛刀。CSS 3D 路线的好处是preserve-3d会让浏览器自动生成合成层旋转交给 GPUCPU 只负责改transform。页面内容可以是普通 img 或 div按钮、链接、视频都能直接嵌在页面里能点、能选中、能响应业务交互这是 Canvas 路线做不到的。在做 HTML5 兼容电脑手机端的翻开呈现时CSS 3D 是性价比最稳的选择。3. 最小可复现一次跑通鼠标和触摸拖拽的翻页页面3.1 页面结构front、back 与书页轴的 DOM 摆位套用第 2 章的骨架这里直接给出一个能跑的完整页面结构。核心是一个半宽度的.flipper盖在静态左右页上面它自身带两个.face子层正面显示当前右页背面显示下一页左页。.flipper的宽度必须是整本书的一半并且left: 50%这样它的左边缘正好压在书脊位置上。div classstage div idbook classbook div idleftStatic classstatic-page左页占位/div div idrightStatic classstatic-page右页占位/div div idflipper classflipper div classface front idflipperFront当前右页/div div classface back idflipperBack下一页左页/div /div /div /div.stage是外层缩放容器第 4 章做手机端适配时会用到它。.flipper的 z-index 要高于左右静态页因为它要盖在上面翻动。翻完一页后需要把.flipper的 z-index 降到静态页之下并把内容换成新一页这个循环逻辑在 4.3 里补上。3.2 CSS 3D 核心transform-origin、rotateY 与 backface-visibility给上面的结构配上 CSS。三个关键点透视写在.book上翻页层的轴心在左缘两个.face用backface-visibility: hidden避免翻转时互相透出。.stage { width: 800px; height: 600px; } .book { position: relative; width: 100%; height: 100%; perspective: 1800px; touch-action: none; /* 禁止浏览器手势接管 */ user-select: none; -webkit-user-select: none; } .static-page { position: absolute; top: 0; width: 50%; height: 100%; background: #f6f1e7; } #leftStatic { left: 0; } #rightStatic { left: 50%; } .flipper { position: absolute; left: 50%; top: 0; width: 50%; height: 100%; transform-origin: left center; transform-style: preserve-3d; z-index: 2; will-change: transform; } .face { position: absolute; inset: 0; backface-visibility: hidden; -webkit-backface-visibility: hidden; display: flex; align-items: center; justify-content: center; } .face.back { transform: rotateY(180deg); }.face.back提前旋转 180 度让它初始时背对读者。当.flipper整体向左旋转超过 90 度后back 面自然转入读者视野视觉上就形成了“下一页左页翻开”的效果。will-change: transform是给浏览器一个合成层提示但不要对页面里每个元素都用合成层太多会吃掉移动端内存。3.3 Pointer Events 封装一套手势兼容电脑和手机用 Pointer Events 而不是分别绑 mouse 和 touch是因为它把鼠标、触摸、笔统一成一套事件桌面和移动端共用一套逻辑。移动端需要配合 CSS 里的touch-action: none否则浏览器会在pointermove过程中接管手势做页面滚动。const book document.getElementById(book); const flipper document.getElementById(flipper); const clamp (v, min, max) Math.min(max, Math.max(min, v)); let dragging false; let startX 0; let startAngle 0; let currentAngle 0; let rafId null; let flipperTransition null; function setFlipperTransition(value) { if (flipperTransition value) return; flipperTransition value; flipper.style.transition value; } function updateFlipper() { flipper.style.transform rotateY(${-currentAngle}deg); rafId null; } book.addEventListener(pointerdown, (e) { dragging true; startX e.clientX; startAngle currentAngle; setFlipperTransition(none); book.setPointerCapture(e.pointerId); }); book.addEventListener(pointermove, (e) { if (!dragging) return; const dx e.clientX - startX; // 拖满整本书宽度映射为 180 度可按手感调整分母 currentAngle clamp(startAngle - (dx / book.clientWidth) * 180, 0, 180); if (rafId null) { rafId requestAnimationFrame(updateFlipper); } }); book.addEventListener(pointerup, () { if (!dragging) return; dragging false; if (rafId ! null) { cancelAnimationFrame(rafId); rafId null; } updateFlipper(); const shouldTurn currentAngle 90; setFlipperTransition(transform 0.5s cubic-bezier(0.22, 0.61, 0.36, 1)); flipper.style.transform shouldTurn ? rotateY(-180deg) : rotateY(0deg); currentAngle shouldTurn ? 180 : 0; if (shouldTurn) { // 这里插入“更新下一页内容、把 flipper 放到底层”的逻辑 } });逻辑说明pointerdown记录按下位置和当前角度拖拽过程中把横向位移映射成翻页角度用requestAnimationFrame合并高频pointermove避免一帧内多次改 transform。松手时根据角度是否超过 90 度决定继续翻完还是回弹。setPointerCapture保证手指或鼠标移出窗口后仍能收到pointerup否则快速甩动时容易丢事件。参数说明book.clientWidth作为映射分母。当前设置是拖满整本书宽度800px转 180 度想让页面更“跟手”可以改成flipper.clientWidth拖 400px 就翻 180 度。0.5s是自动翻页动画时长电子书一般控制在 0.4s 到 0.6s太短显得生硬太长用户会等得焦躁。3.4 可调参数翻页时长、临界角与阻尼系数实际交付时不会有人用默认手感。我一般把参数集中放在一个对象里方便反复调const CONFIG { perspective: 1800, // 书的透视距离 dragAngleRatio: 1, // 1 拖满书宽转 180 度0.5 拖半本书转 180 度 turnThreshold: 90, // 松手时超过这个角度就翻完 turnDuration: 500, // 自动翻页动画毫秒数 ease: cubic-bezier(0.22, 0.61, 0.36, 1), minDragDistance: 8, // 小于 8px 视为点击 maxDragDuration: 300 // 快速甩动判定用 };dragAngleRatio调的是“跟手感”数字越小同样位移转过的角度越大书页越轻快。turnThreshold是翻页临界角真实书籍的重力会让页面在超过中位后自然落下90 度符合直觉。perspective在 1500 到 2000 之间视觉最舒服低于 1000 会看到明显的梯形变形。这里补充一个容易忽略的点动画完成后再更新内容和 z-index不要提前换内容。否则用户会看到翻到一半页面忽然变成另一张图。翻页书这类交互视觉连续性是第一位。4. 从“能翻”到“像书”贴图质感、双端适配与动效手感4.1 页面贴图与纸张质感灰度渐变和阴影是免费的很多特效包里的页面贴图是一整张 JPEG几 MB 起步移动端加载慢不说GPU 还要扛一张超大位图的合成。常见做法是底色用 CSS 渐变模拟纸张内容图片单独切出来铺在上层。.face.front { background: linear-gradient(90deg, rgba(0, 0, 0, 0.06), transparent 8%), linear-gradient(0deg, #faf6ed, #f6f1e7 30%, #ece4d6); } .flipper { box-shadow: -3px 0 8px rgba(0, 0, 0, 0.15); } .face.back::after { content: ; position: absolute; right: 0; top: 0; bottom: 0; width: 6%; background: linear-gradient(to left, rgba(0, 0, 0, 0.18), transparent); }左边的渐变模拟页面靠近书脊处的阴影右边::after的暗角模拟卷曲面的光线衰减。这些视觉细节看起来小但用户对“像不像纸”的感知几乎全部来自阴影和底色而不是图片本身。4.2 设备宽度差异如何缩放页面而不破坏 3D 比例桌面端 800px 宽的书放到手机上不能直接改 rem 或百分比因为rotateY的轴心、perspective的消失点都是按设计尺寸算的。用百分比重新布局会让 3D 层级整体走样。最稳的做法是做固定设计稿再对外层.stage整体缩放。function resizeBook() { const BOOK_WIDTH 800; const scale Math.min(1, (window.innerWidth - 24) / BOOK_WIDTH); const stage document.querySelector(.stage); stage.style.width ${BOOK_WIDTH * scale}px; stage.style.height ${600 * scale}px; stage.style.transform scale(${scale}); stage.style.transformOrigin top left; } window.addEventListener(resize, resizeBook); resizeBook();逻辑说明先取容器可用宽度与设计稿宽度求比值不小于 1防止在宽屏上把书放大到变形。.stage只负责缩放.book仍然维持 800px 的坐标系翻页的角度、轴心、透视全部不受影响。注意缩放和翻页的 transform 不能放在同一个元素上。transform是覆盖式属性.book上如果同时写了scale和rotateY的动画会互相打架。这个方案有一个连带问题.stage被缩放后它在文档流里占的布局高度不会自动变化。要在容器外部显式给高度或者在.stage外边包一层 wrapper让 wrapper 的 height 跟随600 * scale更新。4.3 动效手感缓动、回弹与连续翻页的队列翻页手感由两段动画组成拖拽中是无 transition 的实时跟随松手后是带缓动的自动归位。缓动曲线我用cubic-bezier(0.22, 0.61, 0.36, 1)这是比较接近书籍自然落下的速度感开头略缓中段加速结尾轻收。连续翻页的队列是另一个常见坑。第一页还没翻完就触发第二次拖动会把当前角度拉回 0页面内容直接闪回。我一般加一个isAnimating标志在松手后的 transition 期间屏蔽新的 pointerdown等 transitionend 后再更新页面内容并把.flipper排到底层。flipper.addEventListener(transitionend, (e) { if (e.propertyName ! transform) return; isAnimating false; flipper.style.zIndex 1; flipper.style.transition none; flipper.style.transform rotateY(0deg); void flipper.offsetWidth; // 强制 reflow防止归位时闪烁 flipper.style.zIndex 2; /* 这里切换左右页内容准备下一次翻页 */ });void flipper.offsetWidth这一行很多人看不懂它是强制浏览器在归位前完成一次重排把归位动作和样式更新放在同一帧里避免页面闪一下旧内容。翻页书这种连续的交互里这一闪最容易被用户感知成“卡顿”。5. 避坑3D 翻页电子书最常见的五个翻车现场5.1 iOS Safari 页面闪黑backface-visibility 与 GPU 合成层的锅现象桌面端一切正常iPhone 上翻到一半整页闪黑或闪白偶尔持续好几帧。原因iOS Safari 对backface-visibility: hidden和大型位图的组合处理一直不稳定触发合成层重建时GPU 纹理没有及时跟上中间几帧变成空白。解决给所有旋转中的.face加一层translateZ(0)强制它在合成层里保持独立贴图压缩到 2048px 以内超大位图切小块再把will-change: transform只保留在.flipper上不要把每个 face 都加上否则合成层数量爆炸闪得更严重。改完以后真机刷一遍仍然闪就把backface-visibility从.face挪到具体需要隐藏的那一面。5.2 拖拽时整页跟着滚动touch-action 与 overscroll-behavior现象手机上一拖书没翻整个页面上下滚动了松手后才发现翻页角度已经乱掉。原因浏览器默认把触摸滑动当成滚动手势pointermove还没走到你的处理函数滚动已经接管了事件流。解决翻页热区必须设touch-action: none这比在 JS 里preventDefault可靠得多。如果书页上下还有正常滚动的说明文字不要把touch-action: none挂在 body 上只挂.book本身。外层再加一行overscroll-behavior: none防止拖到页面边缘触发浏览器自带的下拉刷新。.book { touch-action: none; } body { overscroll-behavior: none; }5.3 快速拖拽后角度乱跳缺 clamp 与帧锁现象快速甩一下翻页动画要么停在半空要么瞬间跳到 180 度后又弹回来。原因pointermove 触发频率高于屏幕刷新率角度累加出现跳变同时松手时的速度没做限制过渡动画从错误的起始角度出发。解决pointermove 里只记录当前位置和增量真正改 transform 的活统一放进requestAnimationFrame一帧最多执行一次。角度用clamp(currentAngle, 0, 180)锁住边界防止负角度把页面翻到屏幕外面。按 3.3 的代码结构做这个问题基本不会再出现。5.4 点击翻页在移动端总是误触把“点击”和“拖拽”分开判断现象用户想点页面里的目录按钮手指稍微一动页面就被判定成翻页按钮永远点不到。原因pointerup 时没有区分位移和按压时间。真实用户触摸屏幕时手指会有微小抖动几像素的位移如果直接触发翻页移动端几乎无法点击任何内容。解决在 pointerdown 记录起点pointerup 时判断位移小于 8px 且按压时间小于 300ms就按点击处理不触发翻页逻辑。更稳妥的做法是把所有链接接到真实a标签上让浏览器原生处理点击翻页逻辑只对非链接区域生效。5.5 图片加载完成瞬间页面错位decode 与布局时机现象翻页没动背面的下一张图加载完整个盒子跳了一下旋转轴跟着偏移。原因图片没有固定宽高加载完成后撑开.face而.flipper的transform-origin: left center按元素自身尺寸计算轴心随之移动。解决给.face和所有内容容器写死宽高图片用width: 100%; height: 100%; object-fit: cover约束在容器里。JS 侧用img.decode()等待图片解码完成后再打开第一页或者在页面初始化时把所有图片预加载到new Image()对象里资源就绪后再渲染。6. 进阶验证与性能收尾把翻页书交到业务手上翻页书交出去之前我习惯先跑一段帧率检测。浏览器 Performance 面板能看到掉帧但拿给非技术同事看不够直观。简单做法是统计一秒内requestAnimationFrame的执行次数低于 45 就该查问题let frames 0; let last performance.now(); function tick() { frames; const now performance.now(); if (now - last 1000) { console.log(${frames} fps); frames 0; last now; } requestAnimationFrame(tick); } requestAnimationFrame(tick);验证要在三台设备上做桌面 Chrome 拖拽鼠标、Android Chrome 用手指快速甩动、iOS Safari 慢速拖拽。重点看三个场景快速翻到一半松手、连续快速翻两页、页面里有大图时翻页。任何一处闪白、跳动、错位优先怀疑合成层和 z-index而不是 JS 逻辑。性能收尾时做一个类似 3D 减面工具的减法把每页图片裁到实际显示尺寸的两倍以内删除所有看不见的阴影层压缩重复的渐变背景。一本书十几页时每页省 100ms 加载时间用户感知非常明显。我现在交付翻页书项目最后一步永远是打开手机开飞行模式再清缓存加载一次因为几乎所有业务方都是在微信里用手机看效果弱网才是真实战场。先保证弱网下不白屏再谈 3D 特效有多炫。希望帮到你。本文还有配套的精品资源点击获取
返回列表