- #小程序云开发挑战赛#-Hi头像-小雪溪
Hi头像 让头像有趣一点 核心亮点 特性 介绍 云开发 基于腾讯云云开发,易部署、扩展性很好 人工智能 基于腾讯云的人脸五官分析服务自动为头像戴皇冠、圣诞帽或口罩,支持多人识别 多端运行 基于 Taro 打造,有微信小程序端和 Web 端 用户体验好 UI设计精美,易于操作,交互动画友好且动感 部署文档详细 本项目的部署文档非常详细,详情请查看《DEPLOYMENT》文档 前端工程化 核心配置已抽取,基于 Cloudbase Framework 完成云函数、Web 端 和小程序端部署 技术小册 独家拥有技术小册《从0到1开发一个智能头像识别小程序》,详细说明 Hi 头像主要实现思路 功能体验 小程序二维码 [图片] Web 体验版:https://face.xiaoxili.com 愿景 使用场景&目标用户&背景 在节日的时候,我们可以使用符合节日七夕的头像,比如在国庆节使用含国旗的头像表达对祖国的祝福;在圣诞节的时候,我们能给亲朋送一个带圣诞帽的头像传递快乐。 需求分析 在一些特定节日里,很多人尤其是年轻人喜欢新潮的头像,比如近几年的圣诞节,会有“@官方微信,送我一顶圣诞帽“的需求,对此,我们做这个小程序,来解决特定时候头像的制作。 设计理念 标语:让头像有趣一点 「Hi头像」结合节日特点,微信头像进行智能化处理,让头像更加符合节日氛围。 1、紧跟节日热点 国庆节头戴国旗、圣诞节智能戴帽子,增加节日乐趣 2、简单易操作 不同于各类app的繁琐,小程序更简洁易操作 3、乐于分享 借助微信社交机制,头像处理后可以分享给好友或分享到朋友圈,展示头像成果 用户画像 20-35喜欢分享的年轻人 作品效果图 [图片] 产品展示视频 #小程序云开发挑战赛# Hi头像小程序介绍视频-完整版 主创人员 小溪里,技术研发 个人网站:https://www.xiaoxili.com 公众号:小溪里 不二雪,UI设计 公众号:不二诗旅 部署文档 本项目的部署文档非常详细,详情请查看《DEPLOYMENT》文档。 RoadMap 🚀 表示已经实现的功能,👷 表示进行中的功能,⏳ 表示规划中的功能,🏹 表示技术方案设计中的功能。 功能 状态 发布版本 UI 重构、页面流程优化 🚀 已实现 V2.0 云函数合并,使用 [代码]tcb-router[代码] 进行路由匹配 🚀 已实现 V2.0 使用 [代码]cloudbase-framework[代码] 来发布 Web 端、云函数 🚀 已实现 V2.0 使用 [代码]cloudbase-framework[代码] 来部署小程序的预览、体验版 👷 进行中 V2.0 Cloudbase CMS v1 版数据模型快速生成集合字段 🚀 已实现 V2.0 Cloudbase CMS v2 版数据模型快速生成集合字段及提供默认数据 ⏳ 规划中 V3.0 贴纸编辑器调研 🏹 设计中 V3.0 海报换为 [代码]canvas 2d[代码] 版 🏹 设计中 V3.0 Web 版云开发登录鉴权设计 🏹 设计中 V3.0 云环境共享调研 🏹 设计中 V3.0 更多的腾讯云人工智能服务 🏹 设计中 V3.0 学习资源 《从0到1开发一个智能头像识别小程序》 本技术小册基于Hi 头像 v1版本编写,v2 版本正在撰写中。 从产品功能规划、技术选型到实战开发,全方位介绍基于小程序云开发来实现智能人像识别小程序,不仅能完成跨端开发,更能学习到当今炙手可热的 ServerLess 开发实战(即小程序与 Web 云开发),更能将高深的人工智能技术落地到真实项目中,甚至,我们还将探索如何将小程序与Web端完成前端工程化实践。 你会学到什么? 小程序云开发配置与开发技巧,亲历云开发的实战场景 基于 tcb-router 封装多功能的 API 服务 基于静态网站托管部署 Web 版 基于 CMS 内容管理系统来统一管理数据 基于 Cloudbase-Framework 来部署云函数、小程序和 Web 端 体验及实战腾讯云人工智能特色功能,如人脸识别、五官分析、人像转换等 Taro 跨端使用技巧,在实战中了解Canvas在小程序与web端的差异,以及图片裁剪、压缩、上传的各种技术细节 产品需求、项目规划的实战知识 项目综述 本项目在小程序和Web端使用 Taro 构建,功能服务基于腾讯云云开发及腾讯云人工智能服务,使用 CMS 内容管理系统来管理数据,基于Cloudbase Framework完成小程序端、Web端、云函数端构建。 整体关系图 [图片] 页面间调用关系图 在[代码]taro/src[代码]中,有以下文件 [代码]|-config.js 配置AppId、云环境Id及其它 |-components 全局组件 |-pages |-|- avatar-edit 头像编辑页 |-|- avatar-poster 头像分享页 |-|- theme-list 主题列表 |-|- self 我的,含个人头像列表 [代码] [图片] 头像编辑页流程图 头像编辑是Hi 头像的主逻辑,下面这个流程图充分展示了从加载主题数据、到头像编辑、再到头像生成的完整流程。 Hi 头像功能以小程序端为主,功能齐全,而在 web端能制作头像并保存图片,暂不提供头像保存和分享功能。 [图片] 五官分析时序图 v1版中使用图示“第五版”时序图来完成主逻辑,而v2版中使用“第六版”时序图,这两者差异点就是基于已有服务来完成功能,更容易初学者掌握。 [图片] 核心语法 1、云函数[代码]hiface-api[代码]基于[代码]tcb-router[代码]做路由跳转 主要功能有头像、主题、用户信息的获取与更新,还有创建小程序码的功能。 路径为[代码]cloud/functions/hiface-api[代码] 2、核心算法 基于五官分析为多个人脸戴上贴纸,具体可以查看《通过五官分析实现为人脸佩戴贴纸》
2020-09-20 - web性能优化之渲染性能优化
引子: 笔者在业务开发过程中,需要一个vue版的无限滚动组件,从github上找了一些组件后发现效果都不太好(主要是卡顿),最后自己查阅一些渲染性能优化的文章后,基于iScroll二次开发了一个组件,自己觉得效果还不错,主要是利用了硬件渲染加速和dom元素的复用,有同样需求的朋友可以试一下。(https://github.com/zuolei828/vue-virtual-infinite-scroll) 针对这次组件的优化,记录一下渲染性能优化的比较系统的知识,个人能力所限,很多方面理解的可能不对,欢迎大家指正! 一个web页面的性能优化,包括加载(loading)性能优化以及渲染(rendering)性能优化,关于加载性能的优化在另一篇文章中讨论(加载优化),这里来整理一下渲染性能优化的相关知识。 浏览器多进程模型 为了方便后面优化知识的阐述,这里先简单介绍下浏览器的多进程模型(以chrome为例)。 [图片] 主要进程如图所示: Browser进程:浏览器的主进程,负责浏览器界面的显示,各个页面的管理,其他各种进程的管理; Renderer进程:页面的渲染进程,负责页面的渲染工作,Blink的工作主要在这个进程中完成(主要分成render主线程和合成器线程); NPAPI插件进程:每种类型的插件只会有一个进程,每个插件进程可以被多个Render进程共享; GPU进程:最多只有一个,当且仅当GPU硬件加速打开的时候才会被创建,主要用于对3D加速调用的实现; Pepper插件进程:同NPAPI插件进程,不同的是为Pepper插件而创建的进程 页面渲染过程 页面渲染中每一帧的渲染最多进行了如下五个步骤。 [图片] JavaScript:通常我们会使用 JavaScript 来实现页面视觉变化的效果。比如做一个动画或者往页面里添加一些 DOM 元素等。 Style:计算样式,这个过程是根据 CSS 选择器,对每个 DOM 元素匹配对应的 CSS 样式。 Layout:在知道对一个元素应用哪些样式之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置。网页的布局模式意味着一个元素可能影响其他元素,例如 body 元素的宽度一般会影响其子元素的宽度以及树中各处的节点,因此对于浏览器来说,布局过程是经常发生的。 Paint:绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个层(Layer)上完成的。 Composite:由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上,以便正确渲染页面。对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。 换成这个图来看渲染引擎的处理流程 [图片] 这个过程比较复杂,详细的留在后面介绍Composite优化的时候再阐述。先简单说一下中间步骤,DOM树构建完成后,等待JS和CSS一起合成了Render树,每一个DOM节点对应一个Render Object,根据RenderObject的样式属性,可能将多个或者单个的object转换成RenderLayer,通常,渲染引擎的软件渲染到这就结束了,在开启硬件加速后,某些RenderLayer才会被转换成GraphicsLayer,最后利用GPU来进行合成和最终呈现。 如何检测render性能 上面说的渲染的五个步骤中的每一个都有可能造成卡顿,当然根据css属性的不同,可能会跳过layout或者paint阶段(具体每个css属性影响哪些阶段,请查看css触发器,注意chrome现在用的是blink内核),那么如何知道页面runtime中触发了哪些步骤以及各自性能了,最好的方法就是使用chrome devtool中的performance来记录分析。 打开chrome开发者工具,切换到performance tab,点击record按钮,这时你对页面的操作就会被记录下来,点击stop后就能看到性能火焰图等信息了,点击Frames中的一帧,下方的Main区域就会集中到这一帧的运行过程,如下图所示。(红圈区域即为选中一帧) [图片] 黄色为JS,紫色为Style和Layout,绿色为Paint和Composite部分,选中每个部分会显示各自的花费时间等信息,可以看出这个图片中JS运行的时间太长。目前的显示设备一般刷新率是60FPS,所以理想中每帧的时间最好为16毫秒,利用performance就能很直观的看出渲染中哪一步骤出现问题,下面介绍如何对每个步骤进行优化。 优化JS执行 JS 经常会触发视觉变化。有时是直接通过样式操作,有时是会产生视觉变化的计算,例如搜索数据或将其排序。时机不当或长时间运行的 JS 可能是导致性能问题的常见原因。通常可以通过以下几个方法来优化JS的执行。 对于动画效果的实现,避免使用 setTimeout 或 setInterval,请使用 requestAnimationFrame。 将长时间运行的 JavaScript 从主线程移到 Web Worker。 使用微任务来执行对多个帧的 DOM 更改。 使用requestAnimationFrame来执行视觉变化 先看一张图 [图片] 为了避免显示撕裂,开启垂直同步后,显示器每16ms(假设为60HZ)会发出一个VSync信号,浏览器收到信号后开启一帧的渲染,中间过程可能只用CPU完成软件渲染,也可能利用GPU硬件渲染,最终将渲染结果绘制到帧缓冲区,在下一个VSync信号到来时,显示器显示最新的渲染结果,并通知开启下一帧渲染。 在16ms的间隔中,如果一帧没有渲染完,那么这一帧就会被丢弃,显示器还是显示之前的画面,就会造成掉帧;同时如果16ms内如果完成多次渲染,显示器也只会更新一次画面,多次的渲染就会造成CPU和GPU的资源浪费。所以最理想的情况就是每16ms只渲染一次,一些老的框架会使用setTimeout来实现出这个间隔,但是会出现下图的问题。 [图片] 由于不能保证renderer主线程的运行时间,有可能setTimeout的回调会正好在间隔的中间被执行,如果渲染不能在下次间隔前完成,还是会造成卡帧。为了保证每次渲染都在一帧的开始来执行,requestAnimationFrame是唯一正确的方法,但是在使用时候也要注意一点,在requestAnimationFrame的回调执行之前,如果多次调用requestAnimationFrame,也会导致下一帧开始时多次执行这个回调,造成结果的不正确,所以需要加一下类似下面代码的控制。 [代码]function onScroll (evt) { // Store the scroll value for laterz. lastScrollY = window.scrollY; // Prevent multiple rAF callbacks. if (scheduledAnimationFrame) return; scheduledAnimationFrame = true; requestAnimationFrame(readAndUpdatePage); } window.addEventListener('scroll', onScroll); [代码] 分割长时间的JS的执行 由于长时间的JS执行会阻塞渲染,要尽量缩减一帧中JS的执行时间,不需要DOM权限的操作可以移到web worker中,但是通常我们的JS代码都会造成视觉变化,所以可以将一个耗时任务拆分成若干微任务,并利用requestAnimationFrame来执行,如下代码所示。 [代码]var taskList = breakBigTaskIntoMicroTasks(monsterTaskList); requestAnimationFrame(processTaskList); function processTaskList(taskStartTime) { var taskFinishTime; do { // Assume the next task is pushed onto a stack. var nextTask = taskList.pop(); // Process nextTask. processTask(nextTask); // Go again if there’s enough time to do the next task. taskFinishTime = window.performance.now(); } while (taskFinishTime - taskStartTime < 3 && taskList.length > 0); if (taskList.length > 0) requestAnimationFrame(processTaskList); } [代码] 优化样式的计算过程 通过添加和删除元素,更改属性、类或通过动画来更改 DOM,全都会导致浏览器重新计算元素样式。计算样式通过两个阶段来完成,首先浏览器计算出给指定元素应用哪些类、伪选择器和 ID,然后从匹配选择器中获取所有样式规则,并计算出此元素的最终样式。在Chrome的Performance记录区域,可以看到每一帧的渲染中,都有一个recalculate style的紫色矩形,记录的就是此次重新计算的耗时及影响到的元素数量等信息。通常采用下述两个方法来优化计算过程: 降低选择器的复杂性 减少必须计算其样式的元素数量 有时候我们喜欢用p:nth-of-type(2),:nth-child(n)等选择器来书写css内容,因为这样方便我们在一个父元素的所有子元素中找出一个特例来修改样式,但是这样会增加计算的复杂度,浏览器要知道其它所有子元素的情形,通常还是建议给元素一个明确的类选择器,例如BEM。 优化布局 布局是浏览器计算各元素几何信息的过程:元素的大小以及在页面中的位置。如何优化需要做到以下几点。 尽可能避免触发布局 因为布局几乎总是作用到整个文档。 如果有大量元素,将需要很长时间来算出所有元素的位置和尺寸。修改元素的几何属性(大小,位置等)都会导致整个文档重新布局,这个时候可以利用tranform的位移,放大缩小等操作来避免重新布局(前提是开启了硬件加速),这部分会在后面的composite优化部分详细描述,下面看两个demo demo1(更改top属性导致重新布局) demo2(利用translate不会导致重新布局) [图片] [图片] 利用performance分析能看出demo2没有触发layout 使用flex布局而不是浮动 早些年因为兼容性的问题,喜欢用float来实现布局,现在请使用flexbox,布局的性能会得到显著提升,看一下两个demo demo1(使用float布局) demo2(使用flex布局) 利用performance来分析,为了模拟手机上的效果,请将cpu 4x down降速 [图片] float是26.77ms [图片] flex是13.43ms 提升了一倍,看下flexbox目前的兼容性 [图片] 非IE的情况下,大家请安心使用吧(吐槽下,为啥还有人用IE),再贴一张最近一年桌面浏览器占有率 [图片] 避免强制同步布局 回忆下帧的渲染步骤,JS先运行,然后计算样式,再来布局,然而,JS可以强制布局提前,这被称为强制同步布局,看下代码。 [代码]// Schedule our function to run at the start of the frame. requestAnimationFrame(logBoxHeight); function logBoxHeight() { box.classList.add('super-big'); // Gets the height of the box in pixels and logs it out. console.log(box.offsetHeight); } [代码] JS运行时,来自上一帧的浏览器的布局信息是已知的,但是例子中的回调方法先增加了一个类,这个时候浏览器必须先应用样式修改,再重新布局,然后才能输出高度信息。通常上一帧的布局信息已经够用,这种强制同步布局会造成性能浪费。 4. 避免布局抖动 有一种情况会频繁的强制同步布局,看一下代码。 [代码] function resizeAllParagraphsToMatchBlockWidth() { // Puts the browser into a read-write-read-write cycle. for (var i = 0; i < paragraphs.length; i++) { paragraphs[i].style.width = box.offsetWidth + 'px'; } } [代码] 此代码循环处理一组段落,并设置每个段落的宽度以匹配一个称为“box”的元素的宽度。这看起来没有害处,但问题是循环的每次迭代读取一个样式值 (box.offsetWidth),然后立即使用此值来更新段落的宽度 (paragraphs[i].style.width)。在循环的下次迭代时,浏览器必须考虑样式已更改这一事实,因为 offsetWidth 是上次请求的(在上一次迭代中),因此它必须应用样式更改,然后运行布局。每次迭代都将出现此问题! [代码]此示例的修正方法还是先读取值,然后写入值: [代码] [代码]// Read. var width = box.offsetWidth; function resizeAllParagraphsToMatchBlockWidth() { for (var i = 0; i < paragraphs.length; i++) { // Now write. paragraphs[i].style.width = width + 'px'; } } [代码] 优化绘制与合成 绘制是填充像素的过程,像素最终合成到用户的屏幕上。 它往往是渲染过程中运行时间最长的任务,应尽可能避免此任务。合成是将页面的已绘制部分放在一起以在屏幕上显示的过程。这两个过程通常需要放在一起优化,而且是渲染过程中最需要关注的优化点,所以一起来详细阐述下。在介绍优化之前,我们要了解一下Blink的渲染基础知识,再来回顾一下之前放的一张图。 [图片] 这张图展示了Blink从最初的DOM树如何转换到最终的用于合成的Graphics Layer树,具体是如下步骤: Nodes 和 DOM树 网页内容在Blink内部以Node为节点的树形结构存储,称为DOM树。网页中的每一个HTML 元素,包括元素之间的text都和一个Node相关联。DOM tree的最顶层Node 永远是Document Node. From Nodes to RenderObjects DOM树中每一个可视化的Node 节点都对应着一个RenderObject。RenderObject 也存储在一棵对应的树结构中,称为Render树。 RenderObject 知道如何在一个显示设备上绘制(paint) Node 节点的内容。它通过调用GraphicsContext提供的绘制接口来完成绘制过程。GraphicsContext最终负责将像素写入一块bitmap,这块bitmap会被显示在屏幕上。在Chrome中,GraphicsContext 封装了Skia( 2D图形库)。 之前对GraphicsContext的大多数调用都转变成对SkCanvas或SkPlatformCanvas的接口调用。不过为了把绘制的实际过程移出主线程(后面会详细讲),现在这些调用命令被替换成记录到SkPicture。SkPicture是一个能够记录command,最后可以replay这些command的有序数据结构,类似于display list。 From RenderObjects to RenderLayers 每一个RenderObject 都关联着RenderLayer。这种关联是通过祖先RenderObject 节点直接或间接地建立的。分享同一坐标系的RenderObject(比如被同一CSS transform属性影响的元素)必然位于同一RenderLayer。 正是由于RenderLayer的存在,网页上的元素才可以按照正确的顺序合成,从而恰当的显示有交叠的内容,和半透明元素等效果。通常来讲,满足下列条件之一时,RenderObject就会创建RenderLayer: 根节点 有明确的CSS定位属性(relative, absolute) 透明的(opacity 小于 1) 有overflow, an alpha mask or reflection 有CSS filter 有2D加速Context或者3D(webGL)context的 canvas 元素对应的 有video元素的 需要注意的是RenderObject和RenderLayer之间并不是一一对应的。 RenderObject 或者与它所创建的RenderLayer相关联(如果它创建了的话),或者与它的第一个拥有RenderLayer的祖先RenderObject创建的RenderLayer相关联。 RenderLayer 也会形成一个树型层次结构。这个树结构的根节点是与网页的根元素相对应的RenderLayer。每一个RenderLayer 节点的后代都是包含在父亲RenderLayer内的可视化的RenderLayer. 每一个RenderLayer的子节点都被存储在两个按升序排列的有序表中。negZOrderList 有序表中存储的子节点是z-index值为负的子RenderLayer,所以这些RenderLayer在当前RenderLayer的下面;posZOrderList有序表中存储的子节点是z-index值为正的RenderLayer,所以这些RenderLayer在当前RenderLayer的上面。 事实上,在老版本的chrome里(15年之前),有一个软件渲染路径的概念,就是不需要硬件加速的情况下,渲染到这里结束了,放一张图来简单了解一下。 [图片] 所有的RenderLayer构建完成后,浏览器渲染进程调用Skia递归的将layer树绘制到共享内存中的单个位图,然后通过IPC传递到Browser Process,最终由Browser Process负责将位图drawing到屏幕。 4. From RenderLayers to GraphicsLayers 为了有效利用GPU硬件加速渲染,Blink又引入了一个新的GraphicsLayer,并且专门独立了一个专门的Compositor(合成器) Thread来管理GraphicsLayer以及协调帧的生命周期(后面会专门介绍这个合成器)。作为一个前端开发,你会经常听到用transform: translateZ(0)来开启所谓的硬件加速,实质上就是提升成了GraphicsLayer。 每一个RenderLayer或者拥有自己的GraphicsLayer(如果这个RenderLayer是compositing Layer的话),或者是使用它的第一个拥有GraphicsLayer的祖先节点的GraphicsLayer. RenderLayer与GraphicsLayer的关系类似于RenderObject与RenderLayer之间的关系。每个GraphicsLayer都拥有一个GraphicsContext,与这个GraphicsLayer相对应的每个RenderLayer都绘制到这个GraphicsContext上。合成器会负责将多个的GraphicsContext输出的位图最终合成一个最终的image。 理论上讲,每一个RenderLayer都可以将自己绘制到一个单独的backing surface上以避免不必要的重绘。但是在实际中,这种做法会导致内存的大量浪费(尤其是VRAM)。在当前的Blink实现中,只有满足以下条件之一,RenderLayer才会拥有它自己的compositing layer。 layer 有3D或者perspective transform 属性值 layer是硬解码的video 元素使用的 layer是拥有3D context或2D加速context的Canvas标签使用的 layer是一个合成的插件使用的 layer使用了动画表示它的透明度,或者使用了动画形式的webkit 变换 layer 使用了加速的CSS 滤镜 拥有compositing layer后代的layer 渲染在compositing layer之上的layer(overlap) 最后一个overlap为啥会产生合成层了?看一个例子。 [图片] 图中蓝色矩形覆盖在绿色矩形之上,同时它们的父元素是一个GraphicsLayer,假设绿色矩形也是一个GraphicsLayer,如果蓝色不是,那么它将和父元素公用一个合成层,既变成如下图情形。 [图片] 绿色矩形覆盖了蓝色矩形,渲染的顺序就发生了错误,所以为了保证正确,overlap也必须提升为合成层。 5. Layer Squashing overlap引起的合成层提升经常出现,就会导致有很多的合成层,岂不是会造成内存大量浪费,所以Blink专门有Layer Squashing(层压缩)的处理。看一下demo(层压缩)。 打开chrome的Performance工具来分析,选中一帧后,会看到下方工具栏出现一个layer tab,选中这个tab就能看到页面对应的合成层信息。 [图片] 红色圈中部分是显示有几个合成层,右侧绿色圈中部分显示这个合成层形成的原因和大小等信息。很明显,中间可视区域的深蓝色的矩形因为开启3D加速的原因被提升为合成层,绿色,红色,浅蓝三个矩形因为overlap的原因被提升成了合成层。 当我们把鼠标移到绿色矩形上,对应的CSS属性也修改成3Dtransform,所以绿色矩形也被提升为合成层,剩下的红色和浅蓝还是因为overlap被提升为另一个合成层,如下图所示。 [图片] 每一个GraphicsLayer都有对应的Composite Layer,这样Chrome的合成器才知道如何对这个GraphicsLayer进行处理,下面我们就来阐述下什么是合成器。 合成器(Compositor) Chrome的合成器是一个用来管理GraphicsLayer树和协调帧的生命周期的软件库。最初合成器也是被设计在渲染进程的主线程中的,现在合成器被拆成了两部分,一半在主线程里面,负责绘制(painting),主要工作就是把layer树的信息记录到SkPicture中,并没有实际上产生像素;另一半变成了单独的Compositor Thread(简称为cc),也被称为impl thread,这部分是真正的drawing,负责将painting中记录的layer信息经过光栅,合成等操作,最终显示到屏幕。下面分步骤来详细阐述合成器的工作。 Recording: Painting from Blink’s Perspective 兴趣区域(interest area)是要被记录到SkPicture中的viewport附近的区域。每当DOM元素改变,Blink会把兴趣区域中失效的部分layer树信息记录到 SkPicture-backed GraphicsContext。记住,这一步并没有真正的绘制像素,只是记录了可以replay出像素的命令的一个display list。 The Commit: Handoff to the Compositor Thread 合成器线程的一个关键特性就是它维护了主线程状态的一个复制,因此可以根据这个复制来生成帧而不用去询问主线程。主线程的状态信息就是一个LayerChromiumtree,对应的合成器线程复制的是CCLayerImpltree,这两棵树理论上是彼此独立的,这就意味着合成器线程可以在主线程阻塞的情况下使用当前的复制信息执行drawing内容到屏幕。 而当主线程产生了新的兴趣区域,合成器线程如何知道去修改它所维持的树的状态了?合成器线程有一个专门的调度器,使用commit来定期同步两棵树的状态。commit会将主线程更新过的LayerChromiumtree的状态以及新的SkPicture命令传给合成器线程,并同时block主线程来达成同步。这也是主线程在一个帧的生成过程中的最后一步。由于合成器线程独立于主线程,而且专门负责实际的drawing,所以浏览器传来的用户输入都是直接传到合成器线程的,一些不需要主线程参与的交互,例如用户键盘输入等,合成器线程可以直接处理完成页面的更新,但是如果主线程注册了事件的回调,这时候合成器线程就必须将更新的CCLayerImpltree状态以及一些额外任务反向commit给主线程。 Tree Activation 当合成器线程通过主线程的commit同步到更新后的layer tree信息后,会检查哪些layer是失效的并且重新光栅化这些layer。这时active tree是合成器线程保留的上一帧的layer tree信息,而新光栅化的layer tree信息被称为pending tree。为了保持展示内容的一致性,只有当pending tree已经完全光栅化后才会转换成新的active tree,从pending到active的过程被称为tree activation。 需要注意的非常重要的一点是有可能屏幕会滚动到当前的active tree之外,因为主线程只记录viewport周围的兴趣区域。这个时候合成器线程就会询问主线程去记录和commit新区域的信息,但是如果新的pending tree没能及时激活,用户就会滚动到一个所谓的 checkerboard zone。 为了减轻checkerboard zone,chrome将pending tree的光栅化分成低分辨率的部分和高分辨率的部分,当要出现checkerboard zone的时候优先光栅化低分辨率的部分并激活用来展现,这也就是为什么有时候有些页面在快速滚动时候会变模糊(例如google地图)。这部分工作是一个专门的tile manager来管理的(下一节的内容)。 Tiling 光栅化整个页面的layer tree是非常浪费CPU和内存的,所以合成器线程将layer tree分割成多个小的tile,设定好各个tile的优先级(根据离viewport的远近等因素来设置),并且专门创建了tile worker线程(一个或者多个)来执行这些tile的光栅化。在chrome的performance分析中能看到页面的tile,如图所示,勾选rending选项中的红色区域,就能看到页面中绿色border的tile。 [图片] Rasterization: Painting from cc/Skia’s perspective 主线程记录的SkPicture的display list,合成器线程通过两种方式来转变成最终上传到GPU的纹理(texture)。一种是基于CPU、使用Skia库的Software Rasterization,首先绘制进位图里,然后再作为纹理上传至GPU。这一方式中,Compositor Thread会创建出一个或多个Compositor Tile Worker Thread,然后多线程并行执行SkPicture records中的绘画操作,以之前介绍的Graphics Layer为单位,绘制Graphics Layer里的Render Object。同时这一过程是将Layer拆分为多个小tile进行光栅化后写入进tile对应的位图中的。另一种则是基于GPU的Hardware Rasterization,也是基于tile worker线程,也是分tile进行,但是这个过程不是像Software Rasterization那样在CPU里绘制到位图里,然后再上传到GPU中作为纹理。而是借助Skia’s OpenGL backend (Ganesh) 直接在GPU中的纹理中进行绘画和光栅化,填充像素。 Drawing on the GPU 一旦所有的纹理已经被填充,GPU进程就能使用深度优先遍历来遍历layer树的信息,然后调用GL/D3D命令来draw每个layer到帧的缓冲池,当然实际上每个layer的drawing还是分成tiles来进行的。下面这张图展示了GPU进程如何进行drawing。 [图片] 好了,到这里整个Compositor的部分阐述完了,我们也就知道了如何对帧渲染步骤中的绘制和合成来进行优化了–将页面频繁变化的部分提升到合成层,通常使用transform: translateZ(0),利用GPU渲染加速来进行合成。总结下,主要有以下几个优点。 合成层的位图,会交由 GPU 合成,比 CPU 处理要快 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层 对于 transform 和 opacity 效果,不会触发 layout 和 paint 当然,不能盲目的增加合成层数量,因为增加一个合成层就意味着更多的内存分配(特别是GPU内存)和更复杂的合成管理。我们应该专注于那些频繁变化的区域来进行优化。 帧的整个渲染步骤的优化都阐述完了,下面贴一张完整的流程图来总结一下。 [图片] 注意并不是每一帧中这些步骤都会发生,最多的步骤如下: Frame Start. 合成器线程收到来自浏览器的Vsync信号和Input data,一帧开始。 Input event handlers. Input data被合成器线程传给了主线程,注册的事件回调被执行,注意这里合成器线程做了优化,保证一帧中最多只会触发一次event handler,所以自带了requestAnimationFrame的节流效果。 requestAnimationFrame. 如果之前注册了raf回调,会在这里执行,这是最完美的执行更新视觉的地方。唯一要注意的就是避免发生强制布局,即导致样式计算和布局提前(红线所示)。 Parse HTML. 新增的html会在这里被解析,生成对应DOM元素。大部分你会在page load和appendChild之类操作后见到它。 Recalc Styles. 如果你在JS执行过程中修改了样式或者改动了DOM,那么便会执行这一步,重新计算指定元素及其子元素的样式。 Layout. 如果有涉及元素位置信息的DOM改动或者样式改动,那么浏览器会重新计算所有元素的位置、尺寸信息。 Update Layer Tree. 这一步实际上是更新Render Layer的层叠顺序关系,保证层叠的正确。 Paint. paint操作实际上有两步,第一步是主进程将layer tree的相关信息记录到SkPicture中,类似一个display list;第二部是合成器线程replay这个记录list来光栅化和填充上传纹理。主线程的paint只是第一步。 Composite. 这里其实也分两步,主线程这里计算出每个Graphics Layers的合成时所需要的data,包括位移(Translation)、缩放(Scale)、旋转(Rotation)、Alpha 混合等操作的参数,然后就是图中我们看到的第一个commit,主线程通知合成器线程去同步layer tree的信息。然后主线程此时会去执行requestIdleCallback。这一步并没有真正对Graphics Layers完成位图的composite。 Raster Scheduled and Rasterize。 第8步生成的SkPicture records在这个阶段被执行。合成器线程创建出若干个Compositor Tile Worker Thread,利用CPU软件光栅化或者GPU的硬件光栅化,最终将纹理写入了GPU内存中。 Frame End. 合成器线程已经完成paint和composite的工作,这时会发送一个commit给GPU进程,告诉他可以进行draw了,同时会传达主线程一个commit done,如果一个帧中视觉的变化没有主线程参与,这里合成器线程也会同步更新后的合成器layer tree信息给主线程。 draw. GPU进程按照深度优先遍历将最后的纹理draw到帧缓冲区,等待显示器的下一个Vsync到来时去显示。 结语 整个浏览器页面渲染的过程以及优化都阐述完了,性能优化是一门艺术,本文也只是很浅显的探讨了其中的一些基本概念和设计思想,如果想深入理解具体的架构和实现过程,还是要去阅读一下chrome的内核源码。对于我们前端开发来说,切忌的是为了优化而优化,实际开发过程中碰到了页面卡顿的情况,利用performance来分析找出卡顿的原因,针对卡顿的步骤不断进行改进测试,才是正确的优化方法。 参考引用 GPU Accelerated Compositing in Chrome compositor-thread-architecture rendering-performance the-anatomy-of-a-frame 无线性能优化:Composite
2019-03-15 - CSS3 Animation动画的十二原则
作为前端的设计师和工程师,我们用 CSS 去做样式、定位并创建出好看的网站。我们经常用 CSS 去添加页面的运动过渡效果甚至动画,但我们经常做的不过如此。 [代码] 动效是一个有助于访客和用户理解我们设计的强有力工具。这里有些原则能最大限度地应用在我们的工作中。 迪士尼经过基础工作练习的长时间累积,在 1981 年出版的 The Illusion of Life: Disney Animation 一书中发表了动画的十二个原则 ([] (https://en.wikipedia.org/wiki/12_basic_principles_of_animation)) 。这些原则描述了动画能怎样用于让观众相信自己沉浸在现实世界中。 [代码] 在本文中,我会逐个介绍这十二个原则,并讨论它们怎样运用在网页中。你能在 Codepen 找到它们[] (https://codepen.io/collection/AxKOdY/)。 挤压和拉伸 (Squash and stretch) [图片] 这是物体存在质量且运动时质量保持不变的概念。当一个球在弹跳时,碰击到地面会变扁,恢复的时间会越来越短。 [代码] 创建对象的时候最有用的方法是参照实物,比如人、时钟和弹性球。 当它和网页元件一起工作时可能会忽略这个原则。DOM 对象不一定和实物相关,它会按需要在屏幕上缩放。例如,一个按钮会变大并变成一个信息框,或者错误信息会出现和消失。 尽管如此,挤压和伸缩效果可以为一个对象增加实物的感觉。甚至一些形状上的小变化就可以创造出细微但抢眼的效果。 HTML [代码] [代码] <h1>Principle 1: Squash and stretch</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle one"> <div class="shape"></div> <div class="surface"></div> </article> [代码] CSS [代码].one .shape { animation: one 4s infinite ease-out; } .one .surface { background: #000; height: 10em; width: 1em; position: absolute; top: calc(50% - 4em); left: calc(50% + 10em); } @keyframes one { 0%, 15% { opacity: 0; } 15%, 25% { transform: none; animation-timing-function: cubic-bezier(1,-1.92,.95,.89); width: 4em; height: 4em; top: calc(50% - 2em); left: calc(50% - 2em); opacity: 1; } 35%, 45% { transform: translateX(8em); height: 6em; width: 2em; top: calc(50% - 3em); animation-timing-function: linear; opacity: 1; } 70%, 100% { transform: translateX(8em) translateY(5em); height: 6em; width: 2em; top: calc(50% - 3em); opacity: 0; } } body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 预备动作 (Anticipation) [图片] 运动不倾向于突然发生。在现实生活中,无论是一个球在掉到桌子前就开始滚动,或是一个人屈膝准备起跳,运动通常有着某种事先的累积。 [代码] 我们能用它去让我们的过渡动画显得更逼真。预备动作可以是一个细微的反弹,帮人们理解什么对象将在屏幕中发生变化并留下痕迹。 例如,悬停在一个元件上时可以在它变大前稍微缩小,在初始列表中添加额外的条目来介绍其它条目的移除方法。 [代码] HTML [代码]<h1>Principle 2: Anticipation</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle two"> <div class="shape"></div> <div class="surface"></div> </article> [代码] CSS [代码].two .shape { animation: two 5s infinite ease-out; transform-origin: 50% 7em; } .two .surface { background: #000; width: 8em; height: 1em; position: absolute; top: calc(50% + 4em); left: calc(50% - 3em); } @keyframes two { 0%, 15% { opacity: 0; transform: none; } 15%, 25% { opacity: 1; transform: none; animation-timing-function: cubic-bezier(.5,.05,.91,.47); } 28%, 38% { transform: translateX(-2em); } 40%, 45% { transform: translateX(-4em); } 50%, 52% { transform: translateX(-4em) rotateZ(-20deg); } 70%, 75% { transform: translateX(-4em) rotateZ(-10deg); } 78% { transform: translateX(-4em) rotateZ(-24deg); opacity: 1; } 86%, 100% { transform: translateX(-6em) translateY(4em) rotateZ(-90deg); opacity: 0; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 演出布局 (Staging) [图片] 演出布局是确保对象在场景中得以聚焦,让场景中的其它对象和视觉在主动画发生的地方让位。这意味着要么把主动画放到突出的位置,要么模糊其它元件来让用户专注于看他们需要看的东西。 [代码] 在网页方面,一种方法是用 model 覆盖在某些内容上。在现有页面添加一个遮罩并把那些主要关注的内容前置展示。 另一种方法是用动作。当很多对象在运动,你很难知道哪些值得关注。如果其它所有的动作停止,只留一个在运动,即使动得很微弱,这都可以让对象更容易被察觉。 [代码] 还有一种方法是做一个晃动和闪烁的按钮来简单地建议用户比如他们可能要保存文档。屏幕保持静态,所以再细微的动作也会突显出来。 HTML [代码]<h1>Principle 3: Staging</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle three"> <div class="shape a"></div> <div class="shape b"></div> <div class="shape c"></div> </article> [代码] CSS [代码].three .shape.a { transform: translateX(-12em); } .three .shape.c { transform: translateX(12em); } .three .shape.b { animation: three 5s infinite ease-out; transform-origin: 0 6em; } .three .shape.a, .three .shape.c { animation: threeb 5s infinite linear; } @keyframes three { 0%, 10% { transform: none; animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 26%, 30% { transform: rotateZ(-40deg); } 32.5% { transform: rotateZ(-38deg); } 35% { transform: rotateZ(-42deg); } 37.5% { transform: rotateZ(-38deg); } 40% { transform: rotateZ(-40deg); } 42.5% { transform: rotateZ(-38deg); } 45% { transform: rotateZ(-42deg); } 47.5% { transform: rotateZ(-38deg); animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 58%, 100% { transform: none; } } @keyframes threeb { 0%, 20% { filter: none; } 40%, 50% { filter: blur(5px); } 65%, 100% { filter: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 连续运动和姿态对应 (Straight-Ahead Action and Pose-to-Pose) [图片] 连续运动是绘制动画的每一帧,姿态对应是通常由一个 assistant 在定义一系列关键帧后填充间隔。 [代码] 大多数网页动画用的是姿态对应:关键帧之间的过渡可以通过浏览器在每个关键帧之间的插入尽可能多的帧使动画流畅。 [代码] 有一个例外是定时功能step。通过这个功能,浏览器 “steps” 可以把尽可能多的无序帧串清晰。你可以用这种方式绘制一系列图片并让浏览器按顺序显示出来,这开创了一种逐帧动画的风格。 HTML [代码]<h1>Principle 4: Straight Ahead Action and Pose to Pose</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle four"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].four .shape.a { left: calc(50% - 8em); animation: four 6s infinite cubic-bezier(.57,-0.5,.43,1.53); } .four .shape.b { left: calc(50% + 8em); animation: four 6s infinite steps(1); } @keyframes four { 0%, 10% { transform: none; } 26%, 30% { transform: rotateZ(-45deg) scale(1.25); } 40% { transform: rotateZ(-45deg) translate(2em, -2em) scale(1.8); } 50%, 75% { transform: rotateZ(-45deg) scale(1.1); } 90%, 100% { transform: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 跟随和重叠动作 (Follow Through and Overlapping Action) [图片] 事情并不总在同一时间发生。当一辆车从急刹到停下,车子会向前倾、有烟从轮胎冒出来、车里的司机继续向前冲。 [代码] 这些细节是跟随和重叠动作的例子。它们在网页中能被用作帮助强调什么东西被停止,并不会被遗忘。例如一个条目可能在滑动时稍滑微远了些,但它自己会纠正到正确位置。 要创造一个重叠动作的感觉,我们可以让元件以稍微不同的速度移动到每处。这是一种在 iOS 系统的视窗 (View) 过渡中被运用得很好的方法。一些按钮和元件以不同速率运动,整体效果会比全部东西以相同速率运动要更逼真,并留出时间让访客去适当理解变化。 [代码] 在网页方面,这可能意味着让过渡或动画的效果以不同速度来运行。 HTML [代码]<h1>Principle 5: Follow Through and Overlapping Action</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle five"> <div class="shape-container"> <div class="shape"></div> </div> </article> [代码] CSS [代码].five .shape { animation: five 4s infinite cubic-bezier(.64,-0.36,.1,1); position: relative; left: auto; top: auto; } .five .shape-container { animation: five-container 4s infinite cubic-bezier(.64,-0.36,.1,2); position: absolute; left: calc(50% - 4em); top: calc(50% - 4em); } @keyframes five { 0%, 15% { opacity: 0; transform: translateX(-12em); } 15%, 25% { transform: translateX(-12em); opacity: 1; } 85%, 90% { transform: translateX(12em); opacity: 1; } 100% { transform: translateX(12em); opacity: 0; } } @keyframes five-container { 0%, 35% { transform: none; } 50%, 60% { transform: skewX(20deg); } 90%, 100% { transform: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 缓入缓出 (Slow In and Slow Out) [图片] 对象很少从静止状态一下子加速到最大速度,它们往往是逐步加速并在停止前变慢。没有加速和减速,动画感觉就像机器人。 [代码] 在 CSS 方面,缓入缓出很容易被理解,在一个动画过程中计时功能是一种描述变化速率的方式。 [代码] 使用计时功能,动画可以由慢加速 (ease-in)、由快减速 (ease-out),或者用贝塞尔曲线做出更复杂的效果。 HTML [代码]<h1>Principle 6: Slow in and Slow out</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle six"> <div class="shape a"></div> </article> [代码] CSS [代码].six .shape { animation: six 3s infinite cubic-bezier(0.5,0,0.5,1); } @keyframes six { 0%, 5% { transform: translate(-12em); } 45%, 55% { transform: translate(12em); } 95%, 100% { transform: translate(-12em); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 弧线运动 (Arc) [图片] 虽然对象是更逼真了,当它们遵循「缓入缓出」的时候它们很少沿直线运动——它们倾向于沿弧线运动。 我们有几种 CSS 的方式来实现弧线运动。一种是结合多个动画,比如在弹力球动画里,可以让球上下移动的同时让它右移,这时候球的显示效果就是沿弧线运动。 HTML [代码]<h1>Principle 7: Arc (1)</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle sevena"> <div class="shape-container"> <div class="shape a"></div> </div> </article> [代码] CSS [代码].sevena .shape-container { animation: move-right 6s infinite cubic-bezier(.37,.55,.49,.67); position: absolute; left: calc(50% - 4em); top: calc(50% - 4em); } .sevena .shape { animation: bounce 6s infinite linear; border-radius: 50%; position: relative; left: auto; top: auto; } @keyframes move-right { 0% { transform: translateX(-20em); opacity: 1; } 80% { opacity: 1; } 90%, 100% { transform: translateX(20em); opacity: 0; } } @keyframes bounce { 0% { transform: translateY(-8em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 15% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 25% { transform: translateY(-4em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 32.5% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 40% { transform: translateY(0em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 45% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 50% { transform: translateY(3em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 56% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 60% { transform: translateY(6em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 64% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 66% { transform: translateY(7.5em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 70%, 100% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] [图片] 另外一种是旋转元件,我们可以设置一个在对象之外的原点来作为它的旋转中心。当我们旋转这个对象,它看上去就是沿着弧线运动。 HTML [代码]<h1>Principle 7: Arc (2)</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle sevenb"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].sevenb .shape.a { animation: sevenb 3s infinite linear; top: calc(50% - 2em); left: calc(50% - 9em); transform-origin: 10em 50%; } .sevenb .shape.b { animation: sevenb 6s infinite linear reverse; background-color: yellow; width: 2em; height: 2em; left: calc(50% - 1em); top: calc(50% - 1em); } @keyframes sevenb { 100% { transform: rotateZ(360deg); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 次要动作 (Secondary Action) [图片] 虽然主动画正在发生,次要动作可以增强它的效果。这就好比某人在走路的时候摆动手臂和倾斜脑袋,或者弹性球弹起的时候扬起一些灰尘。 在网页方面,当主要焦点出现的时候就可以开始执行次要动作,比如拖拽一个条目到列表中间。 HTML [代码]<h1>Principle 8: Secondary Action</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle eight"> <div class="shape a"></div> <div class="shape b"></div> <div class="shape c"></div> </article> [代码] CSS [代码].eight .shape.a { transform: translateX(-6em); animation: eight-shape-a 4s cubic-bezier(.57,-0.5,.43,1.53) infinite; } .eight .shape.b { top: calc(50% + 6em); opacity: 0; animation: eight-shape-b 4s linear infinite; } .eight .shape.c { transform: translateX(6em); animation: eight-shape-c 4s cubic-bezier(.57,-0.5,.43,1.53) infinite; } @keyframes eight-shape-a { 0%, 50% { transform: translateX(-5.5em); } 70%, 100% { transform: translateX(-10em); } } @keyframes eight-shape-b { 0% { transform: none; } 20%, 30% { transform: translateY(-1.5em); opacity: 1; animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 32% { transform: translateY(-1.25em); opacity: 1; } 34% { transform: translateY(-1.75em); opacity: 1; } 36%, 38% { transform: translateY(-1.25em); opacity: 1; } 42%, 60% { transform: translateY(-1.5em); opacity: 1; } 75%, 100% { transform: translateY(-8em); opacity: 1; } } @keyframes eight-shape-c { 0%, 50% { transform: translateX(5.5em); } 70%, 100% { transform: translateX(10em); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 时间节奏 (Timing) [图片] 动画的时间节奏是需要多久去完成,它可以被用来让看起来很重的对象做很重的动画,或者用在添加字符的动画中。 [代码] 这在网页上可能只要简单调整 animation-duration 或 transition-duration 值。 [代码] 这很容易让动画消耗更多时间,但调整时间节奏可以帮动画的内容和交互方式变得更出众。 HTML [代码]<h1>Principle 9: Timing</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle nine"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].nine .shape.a { animation: nine 4s infinite cubic-bezier(.93,0,.67,1.21); left: calc(50% - 12em); transform-origin: 100% 6em; } .nine .shape.b { animation: nine 2s infinite cubic-bezier(1,-0.97,.23,1.84); left: calc(50% + 2em); transform-origin: 100% 100%; } @keyframes nine { 0%, 10% { transform: translateX(0); } 40%, 60% { transform: rotateZ(90deg); } 90%, 100% { transform: translateX(0); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 夸张手法 (Exaggeration) [图片] 夸张手法在漫画中是最常用来为某些动作刻画吸引力和增加戏剧性的,比如一只狼试图把自己的喉咙张得更开地去咬东西可能会表现出更恐怖或者幽默的效果。 在网页中,对象可以通过上下滑动去强调和刻画吸引力,比如在填充表单的时候生动部分会比收缩和变淡的部分更突出。 HTML [代码]<h1>Principle 10: Exaggeration</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle ten"> <div class="shape"></div> </article> [代码] CSS [代码].ten .shape { animation: ten 4s infinite linear; transform-origin: 50% 8em; top: calc(50% - 6em); } @keyframes ten { 0%, 10% { transform: none; animation-timing-function: cubic-bezier(.87,-1.05,.66,1.31); } 40% { transform: rotateZ(-45deg) scale(2); animation-timing-function: cubic-bezier(.16,.54,0,1.38); } 70%, 100% { transform: rotateZ(360deg) scale(1); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 扎实的描绘 (Solid drawing) [图片] 当动画对象在三维中应该加倍注意确保它们遵循透视原则。因为人们习惯了生活在三维世界里,如果对象表现得与实际不符,会让它看起来很糟糕。 如今浏览器对三维变换的支持已经不错,这意味着我们可以在场景里旋转和放置三维对象,浏览器能自动控制它们的转换。 HTML [代码]<h1>Principle 11: Solid drawing</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle eleven"> <div class="shape"> <div class="container"> <span class="front"></span> <span class="back"></span> <span class="left"></span> <span class="right"></span> <span class="top"></span> <span class="bottom"></span> </div> </div> </article> [代码] CSS [代码].eleven .shape { background: none; border: none; perspective: 400px; perspective-origin: center; } .eleven .shape .container { animation: eleven 4s infinite cubic-bezier(.6,-0.44,.37,1.44); transform-style: preserve-3d; } .eleven .shape span { display: block; position: absolute; opacity: 1; width: 4em; height: 4em; border: 1em solid #fff; background: #2d97db; } .eleven .shape span.front { transform: translateZ(3em); } .eleven .shape span.back { transform: translateZ(-3em); } .eleven .shape span.left { transform: rotateY(-90deg) translateZ(-3em); } .eleven .shape span.right { transform: rotateY(-90deg) translateZ(3em); } .eleven .shape span.top { transform: rotateX(-90deg) translateZ(-3em); } .eleven .shape span.bottom { transform: rotateX(-90deg) translateZ(3em); } @keyframes eleven { 0% { opacity: 0; } 10%, 40% { transform: none; opacity: 1; } 60%, 75% { transform: rotateX(-20deg) rotateY(-45deg) translateY(4em); animation-timing-function: cubic-bezier(1,-0.05,.43,-0.16); opacity: 1; } 100% { transform: translateZ(-180em) translateX(20em); opacity: 0; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 吸引力 (Appeal) [图片] 吸引力是艺术作品的特质,让我们与艺术家的想法连接起来。就像一个演员身上的魅力,是注重细节和动作相结合而打造吸引性的结果。 [代码] 精心制作网页上的动画可以打造出吸引力,例如 Stripe 这样的公司用了大量的动画去增加它们结账流程的可靠性。 [代码] HTML [代码]<h1>Principle 12: Appeal</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle twelve"> <div class="shape"> <div class="container"> <span class="item one"></span> <span class="item two"></span> <span class="item three"></span> <span class="item four"></span> </div> </div> </article> [代码] CSS [代码].twelve .shape { background: none; border: none; perspective: 400px; perspective-origin: center; } .twelve .shape .container { animation: show-container 8s infinite cubic-bezier(.6,-0.44,.37,1.44); transform-style: preserve-3d; width: 4em; height: 4em; border: 1em solid #fff; background: #2d97db; position: relative; } .twelve .item { background-color: #1f7bb6; position: absolute; } .twelve .item.one { animation: show-text 8s 0.1s infinite ease-out; height: 6%; width: 30%; top: 15%; left: 25%; } .twelve .item.two { animation: show-text 8s 0.2s infinite ease-out; height: 6%; width: 20%; top: 30%; left: 25%; } .twelve .item.three { animation: show-text 8s 0.3s infinite ease-out; height: 6%; width: 50%; top: 45%; left: 25%; } .twelve .item.four { animation: show-button 8s infinite cubic-bezier(.64,-0.36,.1,1.43); height: 20%; width: 40%; top: 65%; left: 30%; } @keyframes show-container { 0% { opacity: 0; transform: rotateX(-90deg); } 10% { opacity: 1; transform: none; width: 4em; height: 4em; } 15%, 90% { width: 12em; height: 12em; transform: translate(-4em, -4em); opacity: 1; } 100% { opacity: 0; transform: rotateX(-90deg); width: 4em; height: 4em; } } @keyframes show-text { 0%, 15% { transform: translateY(1em); opacity: 0; } 20%, 85% { opacity: 1; transform: none; } 88%, 100% { opacity: 0; transform: translateY(-1em); animation-timing-function: cubic-bezier(.64,-0.36,.1,1.43); } } @keyframes show-button { 0%, 25% { transform: scale(0); opacity: 0; } 35%, 80% { transform: none; opacity: 1; } 90%, 100% { opacity: 0; transform: scale(0); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码]
2019-03-21