记录--怎么实现一个3d翻书效果

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

本篇主要讨论以下两种翻书动画的实现:

第一种是整页翻转的效果:

这种整页翻转的效果主要是做rotateY的动画,并结合一些CSS的3d属性实现。

第二种折线翻转的效果,如下图所示:

主要是通过计算页面翻折过来的位置。

这两种原理上都不是很复杂,需要各个细节配合好,形成一个连贯的翻书动画。

我们先重点说一下第一种翻页效果的实现。

1. 基本布局

这种的实现相对比较简单,我们先把DOM结构准备好,如下代码所示:

<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水平翻转一下:

.page-1-back,
.page-2-back {transform: scale(-1, 1);
}
并且.page-1的z-index要比在后面的.page-1-back要高:
.page-1,
.page-2 {z-index: 1;
}

通过这样排版之后,就得到了以下的布局:

接下来让右边的那一页翻过来。

2. 翻书动画

就是做.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可以理解为摄像机的位置,如果值越大摄像机就推得越远。不同值对比如下:

这样翻书的动画基本就完成了,从左向右翻也是同样道理。接下来的问题是怎么形成连续翻书的动画。

3. 连续翻书

可以给动画加一个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-leftlet $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那么就缩短动画的时间,让它翻得快一点。相似的处理我已经在《实现内部组件轮播切换效果》讨论过,这里不再重复。

4. 适配的问题

你可能会担心动画结束后修改了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提升作用的属性去掉之后,在缩放的过程会模糊,但是最终状态是清晰的。如下图所示:

5. 变成一个插件

当把上面的问题都解决了之后,可以把它变成一个插件,用的人只要引入,然后初始化一下就搞定了,不用关心这些类怎么变之类的问题。

并且,由于一个paper容器有两个page是正反面的关系,一旦中间突然插入了一页就会导致page的正反面关系发生变化,所以这个结构不是很灵活,最好是动态生成,也就是说使用插件的人,把所有的page并列排就好了,然后在插件里面再重新组织下DOM结构,把在正反面的两个page放到一个paper里面。

接着讨论下第二种翻书效果的实现。

6. 折线翻书效果的实现

这个有一个现成的插件turn.js,使用起来非常简单,我们简单讨论一下它的内部实现。

这个东西乍看一下,似乎有曲面的效果:

 但实际上是没有的,这个曲线效果是它添加的阴影和渐变产生的视觉效果,当我们把background-image的渐变去掉之后对比一下就能看出来了:

 没有渐变的伪装之后一下子就平了。它就变成了一个折纸的模型——给定一张纸和一个折过去的点,计算一下折过去的旋转角度和位移。它的源代码是在fold函数里面计算的:

它里面有各种余弦正弦的计算和角度的判断,具体实现还是比较复杂的,没有深入去研究,代码可见turn.js.

还有一个问题是它是怎么实现三角形裁剪展示的效果?它是在上层又盖了一个div:

7. 小结

本文讨论了两种翻书效果的实现,重点讨论了一下比较简单的整体翻页的方式,这种方式主要是做rotateY动画,同时打开perspective让其具有景深效果,并且用preserve-3d结合translateZ制造上下层级关系,这种方式可能会存在闪动和模糊的问题,为了让翻过去不会闪动关键的地方是保证每一个page占宽50%,模糊的问题是因为用了scale加上GPU提升导致的,所以只能通过不写3d属性保证清晰。

第二种的效果模型相对比较复杂,简单分析了一下它的原理和实现方式。主要是计算折纸过来的角度和位移,上层再盖一个div隐藏不露出来的部分。然后再加上阴影和渐变制造一种曲面的效果。这种翻书的效果还是挺好玩的。

本文转载于:

https://juejin.cn/post/6844903665216520206

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/112549.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【项目 计网7】4.20 多进程实现并发服务器 4.22 多线程实现并发服务器

文章目录 4.20 多进程实现并发服务器server_process.cclient.c4.22 多线程实现并发服务器客户端代码&#xff1a;服务端代码&#xff1a; 4.20 多进程实现并发服务器 要实现TCP通信服务器处理并发的任务&#xff0c;使用多线程或者多进程来解决。 思路&#xff1a; 1、一个父进…

企业微信cgi-bin/gateway/agentinfo接口存在未授权访问漏洞 附POC

文章目录 企业微信cgi-bin/gateway/agentinfo接口存在未授权访问漏洞 附POC1. 企业微信cgi-bin/gateway/agentinfo接口简介2.漏洞描述3.影响版本4.fofa查询语句5.漏洞复现6.POC&EXP7.整改意见8.往期回顾 企业微信cgi-bin/gateway/agentinfo接口存在未授权访问漏洞 附POC 免…

java ReentrantLock 锁 await、signal的用法

背景 在并发编程中&#xff0c;为了保证线程的原子执行&#xff0c;需要使用锁&#xff0c;jvm 内 可以使用 synchronized 和 ReentrantLock&#xff0c;如果是集群部署&#xff0c;我们可以使用Redis 分布式锁 其他的锁后面再介绍。 ReentrantLock 和 synchronized 1、Reent…

深入浅出AXI协议(3)——握手过程

一、前言 在之前的文章中我们快速地浏览了一下AXI4协议中的接口信号&#xff0c;对此我们建议先有一个简单的认知&#xff0c;接下来在使用到的时候我们还会对各种信号进行一个详细的讲解&#xff0c;在这篇文章中我们将讲述AXI协议的握手协议。 二、握手协议概述 在前面的文章…

nowcoder NC236题 最大差值

目录 题目描述&#xff1a; 示例1 示例2 题干解析&#xff1a; 暴力求解&#xff1a; 代码展示&#xff1a; 优化&#xff1a; 代码展示&#xff1a; 题目跳转https://www.nowcoder.com/practice/a01abbdc52ba4d5f8777fb5dae91b204?tpId128&tqId33768&ru/exa…

云南森林火灾vr消防模拟安全演练系统训练消防员火灾和事故的适应和应对能力

据统计,每一场破坏性地震发生后,会引发次生的灾害,而火灾是其中之一。导致火灾的原因,推测是地震时使供电线路短路,引燃易燃物,火灾就随即发生。所以,在日常生活中,定期的消防演练还是非常必要的, VR消防&#xff0c;是VR公司深圳华锐视点利用VR虚拟现实技术&#xff0c;将VR和…

汽车摩托车零部件出口管理ERP解决方案

近年来&#xff0c;随着全球经济的发展&#xff0c;人们对交通工具的需求增加&#xff0c;国内汽车、摩托车市场的不断扩大&#xff0c;以及国内制造技术的不断提高&#xff0c;中国汽车、摩托车零部件出口业务迎来了广阔的发展前景&#xff0c;带动了汽车配件和摩托车配件市场…

java企业工程项目管理系统源码(三控:进度组织、质量安全、预算资金成本、二平台:招采、设计管理)

工程项目管理软件&#xff08;工程项目管理系统&#xff09;对建设工程项目管理组织建设、项目策划决策、规划设计、施工建设到竣工交付、总结评估、运维运营&#xff0c;全过程、全方位的对项目进行综合管理 工程项目各模块及其功能点清单 一、系统管理 1、数据字典&#xff…

postman-使用Postman的模拟服务来模拟(mock)后端数据,完成前端模拟API调用

最近项目上比较忙&#xff0c;任务多时间紧&#xff0c;导致后端开发任务繁多&#xff0c;无法及时开发完毕&#xff0c;但是前端同学已经把对应功能开发完成&#xff0c;需要进行前后端联调来验证API及一些交互问题&#xff1b;这不能因为后端的进度来影响前端的工作完成情况&…

uniapp返回上一页并刷新

在uniapp中&#xff0c;经常会有返回上一页的情况&#xff0c;官方提供有 uni.navigateBack 这个api来实现效果&#xff0c;但是此方法返回到上一页之后页面并不会更新&#xff08;刷新&#xff09;。 例如有这样一个场景&#xff1a;从地址列表页点击添加按钮进入添加地址页面…

【大模型】基于 LlaMA2 的高 star 的 GitHub 开源项目汇总

【大模型】基于 LlaMA2 的高 star 的 GitHub 开源项目汇总 Llama2 简介开源项目汇总NO1. FlagAlpha/Llama2-ChineseNO2. hiyouga/LLaMA-Efficient-TuningNO3. yangjianxin1/FireflyNO4. LinkSoul-AI/Chinese-Llama-2-7bNO5. wenge-research/YaYiNO6. michael-wzhu/Chinese-LlaM…

vue2 组件库之vetur提示

当我们开发完自定义UI组件库后&#xff0c;在项目中使用时&#xff0c;想要达到以下提示效果&#xff0c;组件提示与属性提示&#xff0c;有什么解决方案呢&#xff1a; 事实上&#xff0c;这是vetur的功能&#xff0c;原文如下&#xff1a; Component Data | Vetur If a pac…

器件手册识读之 :运放

器件手册识读之 &#xff1a;运放 一、基本信息 二、引脚排列 三、最大额定参数 四、电气特性 五、应用电路 1、称重传感器放大器 2、热电偶低偏置&#xff0c;低漂移环路测量二极管冷端补偿。

MySQL 保存日期用哪种数据类型

写在前面 在设计数据库表时不可避免的需要用到时间类型&#xff0c;到底选择那种数据类型来表示时间是一个值的讨论的问题&#xff0c;本文就一起来看下&#xff01; 1&#xff1a;能用哪些数据类型 1:字符串&#xff1a;不要用,占用空间大&#xff0c;至少需要19个字节&…

Qt应用开发(基础篇)——字体选择器 QFontDialog

一、前言 QFontDialog类继承于QDialog&#xff0c;是一个设计用来选择字体的对话框部件。 对话框窗口QDialog QFontDialog字体选择对话框&#xff0c;设计用来让用户选择某一种字体&#xff0c;一般用于文本编辑窗口、标签显示和一些需要文本输入的场景。你可以直接使用静态函数…

Docker笔记

学习了神光大佬的《Nest 通关秘籍》后&#xff0c;对docker做了个笔记&#xff0c;并实操部署了一下个人项目&#xff0c;在此记录一下 是什么 Docker是一种开源的容器化平台&#xff0c;它可以将应用程序及其依赖项打包到一个可移植的容器中&#xff0c;使得应用程序能够在任…

Kubernetes(七)修改 pod 网络(flannel 插件)

一、 提示 需要重启服务器 操作之前备份 k8s 中所有资源的 yaml 文件 如下是备份脚本&#xff0c;仅供参考 # 创建备份目录 test -d $3 || mkdir $3 # $1 命名空间 # $2 资源名称&#xff1a; sts deploy configMap svc 等 # $3 资源备份存放的目录名称for app in kubec…

MySQL DATE_SUB的实践

函数简介DATE_SUB()函数从DATE或DATETIME值中减去时间值(或间隔)。 下面说明了DATE_SUB()函数的语法&#xff1a; DATE_SUB(start_date,INTERVAL expr unit); DATE_SUB()函数接受两个参数&#xff1a; start_date是DATE或DATETIME的起始值。 expr是一个字符串&#xff0c;用于确…

接口幂等性设计的最佳实现

一、什么是幂等 二、为什么需要幂等 三、接口超时了&#xff0c;到底如何处理&#xff1f; 四、如何设计幂等 全局的唯一性ID 幂等设计的基本流程 五、实现幂等的8种方案 selectinsert主键/唯一索引冲突 直接insert 主键/唯一索引冲突 状态机幂等 抽取防重表 token令牌 悲观锁…

【Jetpack】Navigation 导航组件 ⑤ ( NavigationUI 类使用 )

文章目录 一、NavigationUI 类简介二、NavigationUI 类使用流程1、创建 Fragment2、创建 NavigationGraph3、Activity 导入 NavHostFragment4、创建菜单5、Activity 界面开发 NavigationUI 的主要逻辑 ( 重点 )a、添加 Fragment 布局b、处理 Navigation 导航逻辑 ( 重点 )c、启…