本篇主要讨论如下两种翻书动画的实现:javascript
第一种是整页翻转的效果:css
这种整页翻转的效果主要是作rotateY的动画,并结合一些CSS的3d属性实现。html
第二种折线翻转的效果,以下图所示:vue
主要是经过计算页面翻折过来的位置。java
这两种原理上都不是很复杂,须要各个细节配合好,造成一个连贯的翻书动画。git
咱们先重点说一下第一种翻页效果的实现。github
这种的实现相对比较简单,咱们先把DOM结构准备好,以下代码所示:web
<ul class="pages"> <!--一个li.paper包含了正反两页--> <li class="paper" data-left> <!--一个.page就是一页内容--> <div class="page page-1-back"> <img src="1.jpg" alt> </div> <div class="page page-1"> <img src="2.jpg" alt> </div> </li> <li class="paper" data-right> <div class="page page-2"> <img src="3.jpg" alt> </div> <div class="page page-2-back"> <img src="4.jpg" alt> </div> </li> <!--其它页内容省略--> </ul>复制代码
一个li.paper
就表示一张纸,包含了正反两页,data-left属性表示它是在左边的,而data-right表示是在右侧,经过absolute定位把它们放到相应的位置。因此若是是下一页,应该让data-right作左翻的动画,相反上一页则让data-left作右翻的动画。小程序
.page-1是当前显示在左边的那一页,.page-2表示当前右边的那一页,而.page-1-back和.page-2-back则分别表示在.paeg-1和.page-2后面的那一页。它们置于背后是水平翻转的,这一点应该不难想象,因此须要借助transform: scale水平翻转一下:windows
.page-1-back, .page-2-back { transform: scale(-1, 1); }复制代码
而且.page-1的z-index要比在后面的.page-1-back要高:
.page-1, .page-2 { z-index: 1; }复制代码
经过这样排版以后,就获得了如下的布局:
接下来让右边的那一页翻过来。
就是作.paper的rotateY动画,很简单,以下代码所示:
@keyframes flip-to-left { from { transform: rotateY(0); } to { transform: rotateY(-180deg); } } .paper[data-right] { transform-origin: left center; animation: flip-to-left 2s ease-in-out; }复制代码
须要设置变换中心为左边中间的位置,效果以下:
咱们发现有几个问题,第1个问题是翻过去的后面的那个paper没有显示出来,由于一开始把没显示出来的paper都隐藏了,因此须要把后面那个paper显示出来:
.paper { display: none; position: absolute; /* 默认放在右边 */ right: 0; } .paper[data-left], .paper[data-right] { display: block; z-index: 1; } .paper[data-left] { right: auto; left: 0; } /* 把相邻的paper显示出来 */ .paper[data-right] + .paper { display: block; }复制代码
这样翻过来以后就能显示后面的那个paper了,以下图所示:
第二个问题是:为何.page-2-back没显示出来,仍然显示的是.page-2,猜想是由于.page-2的z-index比较高,把.page-2-back盖住了,因此即便总体rotate属性变了,它也是被盖住的状态。
因此第一个方法能够在翻转一半的时候就把z-index的高低关系互换一下,让page-2-back比page-2更高,可是这个方法不太好控制,由于动画的变化不是linear的,即便是linear的这个方法也不灵活,容易出现闪动的状况。
第二个方法是调整它们俩的translateZ关系,让page-2的translateZ值比page-2-back高1px就能够了,而不是直接设置z-index关系。为了让translateZ能生效,须要设置它们容器的transform-style为preserve-3d,以下代码所示:
.paper { transform-style: preserve-3d; } .page-1, .page-2 { transform: translateZ(1px); }复制代码
这个可让子元素从扁平空间(flat)变成一个3维空间,translateZ就能发挥做用了,效果以下所示:
这样基本的效果就出来了,可是总感受哪里不太对,就是这个翻转有点平,没有景深的效果。说到景深会想起另一个CSS属性transform-perspective,咱们不妨给它加一个perspective看看效果如何:
@keyframes flip-to-left { from { transform: perspective(1000px) rotateY(0); } to { transform: perspective(1000px) rotateY(-180deg); } }复制代码
效果以下图所示:
这样看起来感受就立体多了。perspective能够理解为摄像机的位置,若是值越大摄像机就推得越远。不一样值对好比下:
这样翻书的动画基本就完成了,从左向右翻也是一样道理。接下来的问题是怎么造成连续翻书的动画。
能够给动画加一个forwards属性,让动画保持在最后结束的那个状态:
.paper[data-right] { transform-origin: left center; animation: flip-to-left 2s ease-in-out forwards; }复制代码
停住以后,上面那些类的关系须要从新更新一下,例如翻过来以后本来的.page-2-back会变成.page-1。
比较科学的方法是使用element.animate作动画,由于它有一个onfinish的回调告诉咱们动画结束了,动画因为这个API的兼容性不是很好,要么找个polyfill,要么仍是用上面CSS的方法而后借助setTimeout。polyfill的库比较大,这里仍是用setTimeout模拟动画结束,使用setTimeout的风险是可能会不太准。
代码逻辑比较简单,就是找到对应的dom结点设置对应的类或者属性,就是代码比较繁琐一点,以下所示:
let currentPage = 1; let $ = document.querySelector.bind(document); $('#next-page').addEventListener('click', goToNextPage); const flipAnimateTime = 1000; function goToNextPage () { // 触发CSS动画 $('.paper[data-right]').setAttribute('data-begin-animate', true); setTimeout(() => { // data-right变成data-left let $rightPaper = $('.paper[data-right]'), $leftPaper = $('.paper[data-left]'); $rightPaper.removeAttribute('data-right'); $rightPaper.setAttribute('data-left', true); // data-left没有了 $leftPaper.removeAttribute('data-left'); $leftPaper.querySelector('.page-1').classList.remove('page-1'); $leftPaper.querySelector('.page-1-back').classList.remove('page-1-back'); // 从新设置类的关系 $leftPaper = $rightPaper; $rightPaper = $leftPaper.nextElementSibling; $leftPaper.querySelector('.page').classList.add('page-1-back'); $leftPaper.querySelector('.page + .page').classList.add('page-1'); // 若是还有下一页 if ($rightPaper) { $rightPaper.setAttribute('data-right', true); $rightPaper.querySelector('.page').classList.add('page-2'); $rightPaper.querySelector('.page + .page').classList.add('page-2-back'); } currentPage++; }, flipAnimateTime); }复制代码
效果以下图所示:
向左翻页也是相似。
这里有个问题,若是用户点下一页点得很快那应该怎么办?若是他点得很快的话前面的翻页尚未结束,会致使setTimeout里的代码尚未执行,那么整个模型就乱了。有两个解决方法,第一种是在翻页过程当中禁掉下一页的操做,可是彷佛不太友好,第二种是把翻页的过程看成一个task任务,一旦点了下一页或者上一页,就push一个task进来,每一个task按顺序同步执行,若是task数组长度大于1那么就缩短动画的时间,让它翻得快一点。类似的处理我已经在《实现内部组件轮播切换效果》讨论过,这里再也不重复。
你可能会担忧动画结束后修改了dom结构,致使CSS属性变了会闪一下,例如本来的page-2-back是水平翻转的,可是在JS里面设置了以后它就变成非水平翻转了,虽然展现的效果是同样的,可是会不会闪一下呢?只要改以前和改以后浏览器进行layout计算的结果如出一辙它就不会闪的,就像上面的例子,可是一旦位移差了1px,就会有闪动。
在实际的例子,你可能须要中间有1px的书缝的阴影,因此左右页的宽度就不是恰好50%,而是要减去1px,因此若是你的transform-origin仍是left center的话翻过去以后就会往右移了1px,当动画结束重置状态,1px的偏移就会被修正,这个时候就会小闪一下。而当你把transform-origin改为-1px center以后,又会致使翻过去以后往左移了1px。因此最好别把中间的阴影单独弄出来,能够改为在每个page里面用before/after画,每一个page仍是要占50%,这样就没问题。
另一个要考虑的问题是,使用了transform: scale + translateZ可能会致使模糊,一个直接的例子能够见这个codepen,就是由于用了translateZ或者will-change: transform等触发了GPU渲染致使模糊了,这个过程多是浏览器把当前图层截一张图给GPU计算,GPU把这张静态图缩放就会模糊。而当咱们把translateZ等有promotion提高做用的属性去掉以后,在缩放的过程会模糊,可是最终状态是清晰的。以下图所示:
在上面的例子里面咱们用了transform: scale(-1, 1)作水平翻转,而后还用了translateZ(1px)作上下图层关系。理论上咱们使用scale可是并无放大或者缩小不该该模糊才对,可是在windows上的Chrome能够明显看到模糊了(在mac上的Chrome是不会模糊的),把translateZ去掉就不会模糊了。因此我想到的解法方法是一开始图层不要translateZ(使用z-index),只有开始作动画了才加上translateZ(并去掉z-index),动画结束后再把translateZ去掉。
当把上面的问题都解决了以后,能够把它变成一个插件,用的人只要引入,而后初始化一下就搞定了,不用关心这些类怎么变之类的问题。
而且,因为一个paper容器有两个page是正反面的关系,一旦中间忽然插入了一页就会致使page的正反面关系发生变化,因此这个结构不是很灵活,最好是动态生成,也就是说使用插件的人,把全部的page并列排就行了,而后在插件里面再从新组织下DOM结构,把在正反面的两个page放到一个paper里面。
接着讨论下第二种翻书效果的实现。
这个有一个现成的插件turn.js,使用起来很是简单,咱们简单讨论一下它的内部实现。
这个东西乍看一下,彷佛有曲面的效果:
但其实是没有的,这个曲线效果是它添加的阴影和渐变产生的视觉效果,当咱们把background-image的渐变去掉以后对比一下就能看出来了:
没有渐变的假装以后一会儿就平了。它就变成了一个折纸的模型——给定一张纸和一个折过去的点,计算一下折过去的旋转角度和位移。它的源代码是在fold函数里面计算的:
它里面有各类余弦正弦的计算和角度的判断,具体实现仍是比较复杂的,没有深刻去研究,代码可见turn.js.
还有一个问题是它是怎么实现三角形裁剪展现的效果?它是在上层又盖了一个div:
本文讨论了两种翻书效果的实现,重点讨论了一下比较简单的总体翻页的方式,这种方式主要是作rotateY动画,同时打开perspective让其具备景深效果,而且用preserve-3d结合translateZ制造上下层级关系,这种方式可能会存在闪动和模糊的问题,为了让翻过去不会闪动关键的地方是保证每个page占宽50%,模糊的问题是由于用了scale加上GPU提高致使的,因此只能经过不写3d属性保证清晰。
第二种的效果模型相对比较复杂,简单分析了一下它的原理和实现方式。主要是计算折纸过来的角度和位移,上层再盖一个div隐藏不露出来的部分。而后再加上阴影和渐变制造一种曲面的效果。这种翻书的效果仍是挺好玩的。