image.png

写在最前:《精通CSS》是一本经典的CSS图书,第 1 版和第 2 版的反响都非常不错,现在,第 3 版来啦,据消息称,春节后不久就将和大家见面,经过和出版社沟通,现在将《精通CSS》第 3 版书中的部分内容(包括前言和第10章)分享给大家,让大家抢先阅读。

《精通CSS》第 3 版由李松峰老师翻译,李松峰老师还参与翻译了《JavaScript高级程序设计》第 2 版和第 3 版。

以下由人民邮电出版社图灵教育授权发布。

前言

回想 2004 年,在写本书第 1 版的时候,市面上已经有两本 CSS 方面的书了。当时,我对读者是否需要第三本并没有把握。毕竟,CSS 那时候还算小众技术,只有博主和 Web 标准的狂热粉丝才会研究。当时的大部分网站用的还是表格和框架。本地开发者邮件列表中的小伙伴都说我疯了,他们认为 CSS 只是一个美丽的梦。他们并没有想到,那时候 Web 标准运动的序幕即将拉开,而本书的出版恰逢这个领域爆发之际。在接下来的几年里,本书一直名列出版商最畅销图书榜单。

等到本书第 2 版出来的时候,CSS 的地位已经无法撼动。本书的作用也从向人们展示 CSS 的魅力,转变成帮人们更有效地使用 CSS。于是,我们找到各种新技术、解决方案,还有“黑科技”,希望打造出一本 Web 设计师和前端开发者的权威指南。当时 CSS 的发展似乎已经趋于稳定,而本书好像也能卖相当长一段时间。事实证明,我们错了。

CSS 的发展并未停滞,近几年的情况表明,CSS 最终兑现了其最初的承诺。我们进入了 Web 标准的黄金时代,即浏览器支持程度已经足够好的时代。因此,我们终于可以放弃原来那些“黑科技”,转而把时间和精力花在为最大、最复杂的网站编写优雅、巧妙、容易维护的代码上。

于是,本书第 3 版应运而生:该把所有新工具、新技术和新思路写成一本新书了。为了完成这个任务,我把好朋友埃米尔·比约克隆德拉了进来。他是一位技术与才能俱佳的开发高手,为本书加入了对现代 CSS 实践的深刻理解,还会告诉大家怎么利用新技术写出高度灵活的代码, 并且让这些代码以最优雅的方式在不同浏览器、不同屏幕和不同平台上跑起来。

应该说,我们俩通力合作,基本上完全重写了本书,并且添加了覆盖 Web 排版、动画、布局、响应式设计、组织代码等主题的新章节。这一版仍然继承了前两版的写法,整合了实例、语言解读和跨浏览器的巧妙解决方案。谙熟各路“黑科技”或者任意属性都能信手拈来,这些不再是精通 CSS 的标志。今天的 CSS 已经分化为几十个规范,演化出了几百个属性,恐怕没有谁能够对其无所不知!因此,这一版不追求让读者对 CSS 无所不知,而是强调灵活性、稳健性,并确保代码在花样不断翻新的浏览器、设备和使用场景下都能欢快地跑起来。虽然本书不会一一介绍所有语言特性,但会让你知道有什么可用,告诉你一些鲜为人知的基本技术,还有对 CSS 未来的展望。

要想真正看懂这本书,读者至少应该懂得 CSS 的基本原理,比如自己写过 CSS,甚至用它设计过一两个网站。本书前三章是科普性质的,讲了一些给网页添加样式的最基础的知识,也算是照顾一下基础不牢的读者吧。从第 4 章开始,每一章都会介绍不同的 CSS 新特性,给出的例子也会越来越复杂。相信即使是 CSS 的老手,也能从本书中学到解决常见问题的实用技术。当然,这样的读者就不用按部就班地从头看到尾了,感觉哪一章有意思就看哪一章吧。

最后,我们希望无论读者的基础如何、经验多寡,都能够借助本书领略到 CSS 的无穷魅力, 最终成为真正精通 CSS 的大师。

本书源代码地址:下载地址

第10 章 变换、过渡与动画


本章跟运动有关,既涉及空间内的变化,又涉及随时间发生的过渡或动画。一般情况下,这两类属性可以相互发生作用。

变换并不是指通过定位或其他布局属性移动元素。事实上,某个元素的变换并不会影响页面布局。变换包括旋转、变形、平移和缩放元素,甚至在三维空间里!

元素动画可以通过CSS Animation属性实现。过渡是一种简化的动画。如果某个属性只有开和关两个状态(比如悬停在元素上),那么就可以使用过渡。

这些属性相结合,为网页动起来提供了各种方式。不仅如此,它们的性能还很棒。

本章内容:
**

  • 二维变换,包括平移、缩放、旋转和变形
  • 简单与高级的过渡效果
  • 什么可以过渡,什么不可以过渡
  • 关键帧动画及动画属性
  • 三维变换与透视

10.1 概述

CSS变换用于在空间中移动物体,而CSS过渡和CSS关键帧动画用于控制元素随时间推移的变化。
虽然它们并不一样,但变换、过渡和关键帧动画经常被人们相提并论。这是因为在实践中人们通常把它们当成互补的技术来用。加个动画就意味着每秒要改变其外观60次。变换可以让浏览器根据你对这种变化的描述,进行非常高效的计算。

过渡和关键帧动画可以让我们以巧妙的方式把这些变化转换成动画效果。因此,这些CSS特性就变得密不可分了。于是,就可以做出类似Google这个拆纸书的三维效果(见图10-1)。这个三维效果用于展示其产品的创意用法

image.png

图10-1 Google创建了一个三维折纸书来展示其产品的创意用法

由于本章的例子涉及大量动画,在书本上无法描述清楚。建议大家在学习本章的同时,也通过浏览器打开相应示例看一下,以便更好地理解我们都讲了什么。很多时候,JavaScript都用于实现交互功能,但我们不会关心脚本的工作原理,大家可以自己去研究示例中包含的JS文件。

关于浏览器支持

变换、过渡和关键帧动画的规范仍然在制定中。尽管如此,其中大多数特性已经在常用浏览器中实现了,但IE8和Opera Mini除外。IE9只支持变换的二维子集,而且要使用-ms前缀,并不支持关键帧动画和过渡。在WebKit和Blink系的浏览器中,变换、过渡和关键帧动画相关的属性都要加-webkit-前缀。旧版本的Firefox还需要加-moz-前缀。

10.2 二维变换

CSS变换支持在页面中平移、旋转、变形和缩放元素。此外,还可以增加第三维。本节先从二维变换开始,之后再扩展到三维。图10-2展示了二维变换的各种方式。

image.png

图10-2 二维变换的不同方式示意

从技术角度说,变换改变的是元素所在的坐标系统。一种看待变换的角度是把它们看成“畸变场”。任何落在元素渲染空间内的像素都会被畸变场捕获,然后再把它们传输到页面上的新位置,或改变大小。元素本身还在页面上原来的位置,但它们畸变之后的“影像”已经变换了。

假设页面上有一个100像素×100像素的元素,其类名为box。这个元素的位置受页面流中其他元素外边距、定位方式和大小的影响,当然它也会影响其他元素。不管这个元素最后在哪里,我们都可以用它在视口中的坐标来描述其位置。比如,距页面顶部200像素,距页面左边200像素。这就是视口坐标系统。

这个页面会为其保留100像素×100像素的空间,以便渲染它。下面假设我们要对它执行变换,让它旋转45度角:

  1. .box {
  2. transform: rotate(45deg);
  3. }

像这样给元素应用变换,会为元素最初所在的空间创建所谓的局部坐标系统。局部坐标系统就是畸变场,用于转换元素的像素。

因为元素在页面上表现为矩形,所以我们可以想象这个矩形的四个角会如何变化。Firefox的开发者工具在检查元素时,会为此提供一个形象的可视化效果。在查看元素窗格的“规则”(Rules)面板中,把鼠标悬停到变换规则上,就可以看到变换结果(见图10-3)。

image.png

图10-3 Firefox的开发者工具展示了旋转45度角的变换过程。原始的矩形和变换后的矩形重叠显示,四角的位置变换也清晰地展示了出来

页面上元素原来的位置仍然保留了100像素×100像素的空间,但元素上所有的点都被畸变场给变换到了新位置。

此时,最重要的是理解给元素应用变换的技术背景,以及影响它们在页面上位置的其他属性。如果我们给变换后的元素再应用一个margin-top: 20px会怎样呢?朝上的那个角会不会跑到原始位置以下20像素的位置?不会。矩形所在的整个局部坐标系统都会被旋转,包括外边距,如图10-4所示。

旋转后的矩形也不会妨碍页面其他部分的布局,就好像根本没有变换过一样。如果我们把这个矩形旋转90度,让上外边距转到右边,也不会影响原来就在矩形右边的任何元素。


注意 变换会影响页面的溢出。如果变换后的元素超出设置了overflow属性的元素,导致出现了滚动条,则变换后的元素会影响可滚动区域。在从左向右书写的语言中,这意味着可以利用向上或向左(不能是向下或向右)平移来隐藏元素。


image.png

图10-4 旋转元素就是旋转其整个坐标系统,因此上外边距也会跟着旋转

10.2.1 变换原点

默认情况下,变换是以元素边框盒子的中心作为原点的。控制原点的属性叫transform-origin。比如,可以围绕距元素盒子上边和左边各10像素的点来旋转元素。

可以给transform-origin设1~3个值,分别代表x、y和z轴坐标(其中z轴在三维变换时会用到,本章后面再说)。如果只给了一个值,则第二个默认是关键字center,与background- position类似。第三个值不影响二维变换,现在可以暂时忽略。

  1. .box {
  2. transform-origin: 10px 10px;
  3. transform: rotate(45deg);
  4. }

变换原点之后再旋转元素,就会得到完全不同的结果,如图10-5所示。

image.png

图10-5 围绕距左边及上边各10像素的点旋转


注意 给SVG元素应用变换,有些地方不一样。比如,transform-origin属性的默认值是元素左上角,而不是元素中心点。


10.2.2 平移

平移就是元素移动到新位置。可以沿一个轴平移,使用translateX()或者translateY();也可以同时沿两个轴平移,使用translate()。

使用translate()函数时,要给它传入两个坐标值,分别代表x轴和y轴平移的距离。这两个值可以是任何长度值,像素、em或百分比都可以。但要注意,百分比这时候是相对于元素自身大小,而不是包含块的大小。因此,不必知道元素有多大,就可以让它向右移动自身宽度一倍的距离,如图10-6所示。

  1. .box {
  2. /* 等同于transform: translateX(100%); */
  3. transform: translate(100%, 0);
  4. }

image.png

图10-6 元素盒子向右平移100%

10.2.3 多重变换

可以同时应用多重变换。多重变换的值以空格分隔的列表形式提供给transform属性,按照声明的顺序依次应用。下面来看一个同时平移且旋转的例子。

这个例子使用“拳击俱乐部”的竞赛规则列表,我们要对每条规则的编号做相应的变换,让它们逆时针旋转90度,如图10-7所示。我们希望每个编号的阅读顺序是自下向上的,但定位在列表项顶端。

image.png

图10-7 竞赛规则列表,编号逆时针旋转了90度

首先需要在标记中准备一个有序列表。列表编号从3开始,因为前两条规则已经神秘失踪了。

  1. <ol class="rules" start="3">
  2. <li>If someone says "stop", goes limp or taps out, the fight is over.</li>
  3. <li>Only two guys to a fight.</li>
  4. <li>One fight at a time.</li>
  5. <li>No shirts, no shoes.</li>
  6. <li>Fights will go on as long as they have to.</li>
  7. <li>If this is your first night at FIGHT CLUB, you HAVE to fight.</li>
  8. </ol>

默认情况下,我们没办法控制有序列表如何渲染编号。CSS Lists and Counters Module Level 3规范定义了一个::marker伪元素来控制列表记号的样式,但在本书写作时还没有浏览器支持。我们发挥一下创意,使用支持较好的counter-属性和伪元素来达成目标。计数器通过对某些元素计数来生成编号,然后就可以将编号插入到页面中。

首先,去掉列表的默认样式(去掉编号),然后添加counter-reset规则。这条规则告诉浏览器,当前元素的计数器已经重置为rulecount的值。这个名字是我们随便起的,只是一个标识符。这个名字后面的数值告诉计数器应有的初始值。

  1. .rules {
  2. list-style: none;
  3. counter-reset: rulecount 2;
  4. }

接下来再告诉计数器,针对列表中的每一项,都递增rulecount的值。换句话说,列表第一项对应的计数器值是3,第二项对应的计数器值是4,以此类推。

  1. .rules li {
  2. counter-increment: rulecount;
  3. }

最后,通过伪元素的content属性,把rulecount计数器的值插入页面,位于每一项之前,每个编号前面还要添加一个节符号(§)。渲染后就得到了如图10-8所示的效果。

  1. .rules li:before {
  2. content: '§ ' counter(rulecount);
  3. }

image.png
图10-8 注入节符号和编号的列表

虽然不太好看,但现在有了添加样式的基础,而不是只有默认的编号了。下面要把编号从文本中挪出来,让它们与旁边的区块垂直对齐。

这些数字最好不占用页面空间,因此我们采用绝对定位。绝对定位后,编号自动跑到了区块左上角。为实现编号旋转效果,需要平移和旋转:把transform-origin设置为右下角(100% 100%),然后向上和向左各平移100%(注意这个百分比指的是被变换元素自身的空间),最后再逆时针旋转90度。图10-9展示了平移和旋转的过程。

  1. .rules li {
  2. counter-increment: rulecount;
  3. position: relative;
  4. }
  5. .rules li:before {
  6. content: '§ ' counter(rulecount);
  7. position: absolute;
  8. transform-origin: 100% 100%;
  9. transform: translate(-100%, -100%) rotate(-90deg);
  10. }

image.png
图10-9 平移并旋转列表编号,使其底部靠在列表项文本左侧

注意,这里调用变换函数的顺序非常重要。如果我们先旋转伪元素,那么变换会相对于旋转后的坐标完成,结果x和y轴偏移的方向都会旋转90度。多个变换效果会叠加,因此需要提前规划好。


修改变换
声明多个变换以后,如果想增加新变换,不能直接在原来的基础上添加,而要重新声明整套变换。假设你平移了一个元素,然后想在鼠标悬停时旋转它,那下面的规则并不会像你想的那样起作用:

  1. .thing {
  2. transform: translate(0, 100px);
  3. }
  4. .thing:hover {
  5. /* 警告:这条声明会删除平移效果 */
  6. transform: rotate(45deg);
  7. }

怎么办呢?必须重新声明整套变换,最后追加一个旋转变换:

  1. .thing:hover {
  2. /* 先平移,再旋转 */
  3. transform: translate(0, 100px) rotate(45deg);
  4. }

在完成后的例子中,我们还给列表项左侧添加了灰色边框,让编号显示在边框上方。使用边框的好处是,如果规则折行,那么边框也会自动变高。此外,还添加了一些边距,比如生成的区块编号上方的内边距。不过要注意,这里说的“上方”其实是padding-right,因为它已经旋转过了(见图10-10)。

image.png

图10-10 使用边框可以在折行的情况下保持侧边背景,而padding-right实际上添加到了旋转后的元素上方

10.2.4 缩放和变形

现在已经介绍了translate()和rotate()这两个二维变换函数,剩下的两个是scale()和skew()。这两个函数都有对应x轴和y轴的变体:scaleX()、scaleY()、skewX()和skewY()。

scale()函数很简单,它的参数是没有单位的数值,可以是一个,也可以是两个。如果只传给它一个数值,就表示同时在x轴和y轴上缩放。比如传入数值2,就会同时沿x轴和y轴放大一倍。传入数值1则意味着不会发生任何改变。

  1. .doubled {
  2. transform: scale(2);
  3. /* 等于transform: scale(2, 2); */
  4. /* 也等于transform: scaleX(2) scaleY(2); */
  5. }

只沿一个方向缩放,会导致元素被压扁(见图10-11)或拉长。

  1. .squashed-text {
  2. transform: scaleX(0.5);
  3. /* 等于transform: scale(0.5, 1); */
  4. }

image.png

图10-11 文本在x轴方向上缩放比例小于1,形如被挤压

变形是指水平或垂直方向平行的边发生相对位移,或偏移一定角度。很多人分不清x轴变形和y轴变形。x轴变形可以理解为水平线在元素变形后仍然保持水平,而垂直线则会发生倾斜。关键在于你想让哪个轴向的边发生相对位移。

仍然以“拳击俱乐部”为例,可以通过变形创造流行的“2.5D”效果(学名叫“轴侧投影构图”)。

如果给列表项交替应用深浅不同的背景和边框色,同时也交替应用不同的变形,就可以创建一种“折叠”的界面(见图10-12)。

  1. /* 为简单起见,省略了一些属性 */
  2. .rules li {
  3. transform: skewX(15deg);
  4. }
  5. .rules li:nth-child(even) {
  6. transform: skewX(-15deg);
  7. }

image.png

图10-12 通过变形实现了“2.5D”效果

10.2.5 二维矩阵变换

正如本节开头所说的,变换会计算被变换元素表面上的每一点,然后得到其在本地坐标系中的一个坐标。

我们在写CSS的时候,通常会这么想:“围绕中心点旋转这个元素,向上平移,再向左平移。”但对浏览器而言,所有这些变换都归于一个数学结构:变换矩阵。我们可以通过matrix()这个低级函数直接操纵变换矩阵的值,值一共有6个。

别急,直接操纵变换矩阵没那么简单。要通过它实现比缩放或平移更复杂的变换,需要相当强的数学能力。

下面来看一个例子,这个例子中的元素会旋转45度,然后放大两倍,再沿x轴平移100像素,最后在x轴方向上变形10度。乍一看,输入maxtrix()函数的值(为节省空间已做舍入处理)与实现个别变换传入的值毫无共同点。

  1. .box {
  2. width: 100px;
  3. height: 100px;
  4. transform: matrix(1.41, 1.41, -1.16, 1.66, 70.7, 70.7);
  5. /* 等于 transform: rotate(45deg) translate(100px, 0) scale(2) skewX(10deg); */
  6. }

这不好理解,是吧?

总而言之,变换矩阵就像一个“黑盒子”,它接受一批数值,生成最终的变换,这个变换是组合几个变换之后的结果。如果我们懂其中的数学原理,可以先计算好这些值,然后把它们传入matrix()函数。但我们单看这个函数的值,无法知晓它包含哪些个别的变换。

从数学角度说,一个矩阵就可以简洁地表达任意数量变换的组合。matrix()函数的主要用途不是节省空间和展示我们的数学能力,而是通过JavaScript编程调用。事实上,当我们给一个元素应用了某种变换之后,通过JavaScript读取变换后的计算样式,就可以得到相应的矩阵表达式。

矩阵可以通过脚本灵活操纵,然后再应用回matrix()函数,因此很多基于JavaScript的动画库都大量使用它。如果你手动编写CSS,那还是使用一般的变换函数更简单(也更好理解)。

如果想了解CSS变换矩阵背后的数学原理,可以读一读Zoltan Hawryluk的科普文章The CSS3 matrix() Transform for the Mathematically Challenged

10.2.6 变换与性能

浏览器在计算CSS效果时,会在某些情况下多花一些时间。比如,如果修改文本大小,那么生成的行盒子可能会随着文本折行而变化,而元素本身也会变高。元素变高会把下方的元素向页面下方推挤,这样一来又会迫使浏览器进一步重新计算布局。

在使用CSS变换时,相应的计算只会影响相关元素的坐标系统,既不会改变元素内部的布局,又不会影响外部的其他元素。而且,这时的计算基本上可以独立于页面上的其他计算(比如运行脚本或布局其他元素),因为变换不会影响它们。多数浏览器也都会尽量安排图形处理器(如果有的话)来做这些计算,毕竟图形处理器是专门设计来做这种数学计算的。

换句话说,变换从性能角度讲是很好的。如果你想实现的效果可以用变换来做,那么变换的性能一定更好。连续多重变换的性能更佳,比如实现某个元素的动画或过渡效果。


关于变换的最终赠言
变换的性能很好,而且也很容易实现。可是变换也有一些副作用。

  • 有些浏览器会为变换的元素切换抗锯齿方法。这意味着什么呢?举个例子,如果你动态应用了一个变换,那么文本可能会瞬间变得不一样。为避免这个问题,可以在页面加载时尝试只使用初始值应用变换,而不去动元素。这样,渲染模式会在最终变换应用前完成切换。
  • 应用给元素的任何变换都会创建一个堆叠上下文。这意味着在使用变换时,要注意z-index。这是因为变换后的元素有自己的堆叠上下文。换句话说,子元素上的z-index值再大,也不会出现在父元素上方。
  • 对于固定定位,变换后的元素也会创建一个新的包含块。如果发生变换的元素中有一个元素使用了positon: fixed,那么它会将发生变换的元素当成自己的视口。

10.3 过渡

过渡是一种动画,可以从一种状态过渡到另一种状态,比如按钮从常规状态变成被按下的状态。正常情况下,这种变化是瞬间完成的,至少浏览器会尽快实现这种状态变换。在点击或按下按钮时,浏览器会计算页面的新外观,然后在几毫秒之内完成重绘。而应用过渡时,我们要告诉浏览器完成类似变换要花多长时间,然后浏览器再计算在此期间屏幕上该显示哪些过渡状态。

过渡会自动双向运行,因此只要状态一反转(比如释放鼠标按键),反向动画就会运行。

下面就拿第9章中表单的按钮为例,看看怎么创建平滑的按下按钮的动画。我们的目标是让按钮被按下时向下移动几个像素,同时减少其阴影的偏移量,以进一步强化按钮被按下的视觉效果(见图10-13)。

  1. <button>Press me!</button>

image.png

图10-13 按钮的正常状态和:active状态

下面是第9章中按钮的代码(为简单起见,省略了一些属性)。这里已经添加了transition属性:

  1. button {
  2. border: 0;
  3. padding: .5em 1em;
  4. color: #fff;
  5. border-radius: .25em;
  6. background-color: #173b6d;
  7. box-shadow: 0 .25em 0 rgba(23, 59, 109, 0.3), inset 0 1px 0 rgba(0, 0, 0, 0.3);
  8. transition: all 150ms;
  9. }
  10. button:active {
  11. box-shadow: 0 0 0 rgba(23, 59, 109, 0.3), inset 0 1px 0 rgba(0, 0, 0, 0.3);
  12. transform: translateY(.25em);
  13. }

在按钮被激活时,我们把它沿y轴向下平移与y轴阴影相同的距离。同时,也把阴影偏移量减少为0。为避免页面重新布局,这里使用transform来移动按钮。

前面的代码告诉按钮使用过渡来改变所有受影响的属性,而且要花150毫秒的时间,即0.15秒。使用动画就要涉及时间单位:毫秒(ms)和秒(s)。用户界面组件的过渡多数都应该在0.3秒内完成,否则会让人觉得拖泥带水。其他视觉效果用时可以稍长。

transition属性是一个简写形式,可以一次性设置多个属性。设置过渡的持续时间,以及告诉浏览器在两个状态间切换时动画所有属性也可以这样写:

  1. button {
  2. transition-property: all;
  3. transition-duration: .15s;
  4. }

如果我们只在状态切换时让transform和box-shadow属性有动画,而其他属性的变化(如背景颜色变化)应该立即完成,那就必须分别指定个别属性,而非使用关键字all。

单个简写的transition形式中无法指定多个属性,但可以指定多个不同的过渡,以逗号分隔。换句话说,我们可以重复相同的值,但分别针对不同的属性关键字:

  1. button {
  2. transition: box-shadow .15s, transform .15s;
  3. }

注意,必须对两个过渡重复指定持续时间。对于时间不能同步的过渡,这种重复是必要的。但不重复自己(DRY,don’t repeat yourself)也是写代码的一个基本要求。随着过渡变得复杂,为避免重复,还是使用transition-property更好:

  1. button {
  2. /* 首先,使用transition-property指定一组属性 */
  3. transition-property: transform, box-shadow;
  4. /* 然后,再给这些属性设置持续时间 */
  5. transition-duration: .15s;
  6. }

在transition声明中指定多个逗号分隔的值时,效果与多重背景属性类似。而transition-property指定的值则决定了要应用的过渡数量,如果其他过渡列表持续时间更短,则会重复。

在前面的例子中,transition-duration只有一个值,但定义了两个过渡属性,因此该持续时间是公共的。


注意 在指定带前缀的属性时,transition-property本身也要加前缀。比如transition: transform.25s,针对旧版本WebKit浏览器要写成-webkit-transition: -webkit-**transform .25s**,即属性和作为值的属性都加前缀。



10.3.1 过渡计时函数

默认情况下,过渡变化的速度并不是每一帧都相同,而是开始时稍慢些,然后迅速加快,到接近最终值时再逐渐变慢。

这种速度的变化在动画术语中叫缓动,能让动画效果更自然和流畅。CSS通过相应的数学函数控制这些变化,而这些函数由transition-timing-function属性来指定。

有一些关键字分别代表不同类型的缓动函数。前面提到的默认值对应的关键字是ease。其他关键字还有linear、ease-in、ease-out和ease-in-out。

ease-in表示开始慢后来快。ease-out相反,表示开始快后来慢。最后,二者合起来(ease-in-out)就是两头慢,中段快。

要通过书本表达这几种情形可不容易,图10-14大致能传达一些信息。其中的矩形表示背景颜色在1秒钟内由黑到白的变化情况。

image.png

图10-14 1秒钟的动画内,每100毫秒一个采样点得到的结果

如果想使用ease-in函数来改变按钮动画,可以这样写:

  1. button {
  2. transition: all .25 ease-in;
  3. /* 也可以使用transition-timing-function: ease-in; */
  4. }

1. 三次贝塞尔函数和“弹性”过渡
**
在底层,控制速度变化的数学函数基于三次贝塞尔函数生成。每个关键字都是这些函数带特定参数的简写形式。通常,这些函数随时间变化的值可以绘制成一条曲线,起点表示初始时间和初始值(左下角),终点表示结束时间和结束值(右上角),如图10-15所示。
image.png

图10-15 ease-in-out过渡计时函数对应的曲线

三次贝塞尔函数需要4个参数来计算随时间的变化,在CSS变换中可以使用cubic-bezier()函数作为缓动值。换句话说,可以通过给这个函数传入自己的参数来自定义缓动函数。这4个参数是两对x和y坐标,分别代表调整曲线的两个控制点。

与矩阵变换类似,自定义计时函数的参数也不需要手动输入,因为这样做需要数学背景。好在很多人基于数学原理为我们写好了工具,比如Lea Verou的工具(见图10-16)。

image.png

图10-16 在cubic-bezier.com上,可以调整预设值创建自己的计时函数

使用自定义计时函数可以让过渡中段的值超出开始和结束值(如图10-16所示)。实践中可以基于这一点实现目标的越界效果,也就是弹性过渡,让元素看似具备弹性。可以在http://cubic-bezier.com上试试相应的例子!

2. 步进函数
**
除了可以通过预设的关键字和cubic-bezier()函数指定缓动效果,还可以指定过渡中每一步的状态。这非常适合创建定格动画。比如,假设有一个元素,其背景图片由7个不同的图像组成,放在同一个文件里。通过定位,我们让元素每次只显示其中一个图像(见图10-17)。

image.png
图10-17 通过background-position实现7帧定格动画

在鼠标悬停于这个元素之上时,我们希望通过改变background-position属性来实现背景动画。如果使用线性或缓动过渡,那么背景图片只会滑过,无法构成动画。为此,我们需要通过6个步骤来完成过渡。

  1. .hello-box {
  2. width: 200px;
  3. height: 200px;
  4. transition: background-position 1s steps(6, start) ;
  5. background: url(steps-animation.png) no-repeat 0 -1200px;
  6. }
  7. .hello-box:hover {
  8. background-position: 0 0;
  9. }

这里的transition-timing-function指定为steps(6, start),意思就是“把过渡过程切分为6个步骤,在每一次开始时改变属性”。总之,包括起始状态在内,就创建了7个不同的帧。

默认情况下,steps(6)会在每一步结束时改变属性,但也可以通过传入start或end作为第二个参数来明确指定。我们希望用户在悬停鼠标时直接看到变化,所以选择在每一步开始时启动过渡。

好了,下面说一说在过渡中使用steps()函数的一个问题。如果在过渡完成前反转状态(比如鼠标快速移开),过渡则会反向发生。这符合直觉,但不符合直觉的是反转过渡仍然有6个步骤。此时这几个步骤就不会与原来的背景位置吻合了,从而导致动画错乱。

在当前规范中,这还是一个未定义的行为。好像所有浏览器都会以这种明显不合理的方式来处理步进函数。为避免这种情况发生,下面介绍两个有用的过渡技术。

10.3.2 使用不同的正向和反向过渡

有时候,我们会希望某个方向的过渡快一些,而反方向的过渡慢一些,或者反之。在前面步进过渡的例子中,我们无法完美处理过渡完成前元素失焦的反向过渡。但是,我们可以让反向过渡直接完成。

为此,我们得定义不同的过渡属性集合:一个针对非悬停状态,另一个针对悬停状态。关键是要在正确的位置设置正确的过渡属性。

初始状态下,我们把过渡的持续时间设置为0,然后在悬停状态下,再设置“真实的”持续时间。这样一来,悬停状态会触发动画,而悬停取消时背景会立即恢复初始状态。

  1. .hello {
  2. transition: background-position 0s steps(6);
  3. }
  4. .hello:hover {
  5. transition-duration: .6s ;
  6. }

10.3.3 “粘着”过渡

另一个方法是根本不让过渡反向,这与前面的例子相反。为了“粘着”过渡,可以指定一个非常大的持续时间。技术角度讲,反向还是会反向,只不过速度极慢,慢到浏览器标签页要保持打开数年时间才能看到一些变化。

  1. .hello {
  2. transition: background-position 9999999999s steps(6);
  3. }
  4. .hello:hover {
  5. transition-duration: 0.6s ;
  6. }

10.3.4 延迟过渡

通常,过渡会随状态变化立即发生,比如类名被JavaScript修改或按钮被按下。但是可以通过transition-delay属性来推迟过渡的发生。比如,让用户鼠标悬停于元素上超过1秒才开始定格动画。

简写的transition属性对于其值的顺序是非常宽容的,但延迟时间必须是第二个时间值,第一个始终是持续时间。

  1. .hello {
  2. transition: background-position 0s 1s steps(6);
  3. /* 等于添加了transition-delay: 1s; */
  4. }

延迟时间也可以是负值。这样虽然不会实现时光穿越,却可以让我们一开始就直接跳到过渡的中段。如果在一个持续时间为10秒的过渡中使用了transition-delay: -5s,那么过渡一开始就会跳到一半的位置。

10.3.5 过渡的能与不能

前面几个过渡的例子涉及了transform、box-shadow和background-position等属性。并非所有CSS属性都可以拿来实现过渡动画。多数情况下,涉及长度和颜色的都是可以的,比如边框、宽度、高度、背景颜色、字体大小,等等。这取决于能否计算值的中间状态。比如,100像素和200像素有中间状态,红和蓝也有(因为颜色其实也是通过数值来表示的)。但display属性的两个值block和none就没有中间状态。当然,这个规则本身其实也有例外。

1. 可插值
有些属性虽然没有明确的中间值,却可以实现动画。比如,在使用z-index时,不能指定值为1.5,但1或999都没问题。很多属性,比如z-index或column-count只接受整数值,浏览器会自动插入整数值,类似前面的steps()函数。

有些可以插值的属性还有点怪。比如,可以对visibility属性实现过渡动画,但浏览器会在过渡经过中点后突变为两个终点值中的一个。

设计师Oli Studholme总结过一个可实现动画的属性列表,包括CSS规范中的属性和SVG中可通过CSS实现动画的属性(http://oli.jp/2010/css-animatable-properties/)。

2. 过渡到内容高度
关于过渡,要注意的最后一个问题是,对于有些可以实现动画的属性,比如height,只能在数值之间过渡。也就是说,像auto这样的关键字就不能用于表示要过渡到的一个状态。

常见的一个应用是折叠后的元素过渡到完整高度,比如折叠列表。这时候浏览器不知道怎么从0过渡到auto,甚至也不能过渡到max-content这种内置关键字。

在图10-18中,有一个餐馆菜单组件,初始状态只显示最受欢迎的3个菜,展开更多时会下滑并淡入其他选项。

image.png

图10-18 可扩展的菜单列表组件

在这种场景下,我们知道列表大概的高度,因为一共有10个菜。当然,由于菜名长短不一,折行的菜名可能导致列表高度更高。此时可以把它过渡到max-height。这样,可以从一开始设置的一个长度值过渡到比元素扩展后的高度还要高。而且,这里我们限制展开菜名的数量为7个。

这个组件的HTML标记包含两个有序列表,其中第二个编号从4开始:

  1. <div class="expando">
  2. <h2 class="expando-title">Top menu choices</h2>
  3. <ol>
  4. <li>Capricciosa</li>
  5. <li>Margherita</li>
  6. <li>Vesuvio</li>
  7. </ol>
  8. <ol class="expando-list" start="4" aria-label="Top menu choices, continued.">
  9. <li>Calzone</li>
  10. <!-- 还有更多…… -->
  11. <li>Fungi</li>
  12. </ol>
  13. </div>

这里面有一个aria-label属性,这是为了告诉使用屏幕阅读器的用户为什么会有两个列表。

为了切换状态,我们先使用了一点JavaScript作为铺垫。在实际运行的例子中,这个脚本会为我们创建一个按钮,添加到标题后面,然后在按钮被单击时再给容器元素添加一个is-expanded类。

这个脚本也给html元素添加了js类。然后就可以根据这个类名来声明样式,以便在JavaScript没有运行时,用户能从一开始就看到完整的菜单。

  1. .js .expando-list {
  2. overflow: hidden;
  3. transition: all .25s ease-in-out;
  4. max-height: 0;
  5. opacity: 0;
  6. }
  7. .js .is-expanded .expando-list {
  8. max-height: 24em;
  9. opacity: 1;
  10. }

扩展后的max-height设置为一个值,这个值比实际列表展开后的高度大很多。这是因为多加一些会比较保险。如果这个值偏小,那么万一在小屏幕上有多个菜名折行,就可能导致实际高度超过max-height。

这样做也有缺点。过渡会以列表高度24em为准,这会导致缓动和终点都有点过头。如果你看这个例子,最明显的一点就是折叠动画会有一点延迟。更稳妥的方案是通过脚本把初始的max-height设置为一个很大的值,然后在过渡之后测量元素的实际高度,再用实际高度更新max-height。

10.4 CSS关键帧动画

CSS过渡是一种隐式动画。换句话说,我们给浏览器指定两个状态,让浏览器在元素从一个状态过渡到另一个状态的过程中,给指定的属性添加动画效果。有时候,动画的范围不仅限于两个状态,或者要实现动画的属性一开始也不一定存在。

CSS Animations规范引入了关键帧的概念来帮我们实现这一类动画。此外,关键帧动画还支持对动画时间及方式的控制。

10.4.1 动画与生命的幻象

动画的一个优点是通过展示而非讲述传达信息。可以通过它吸引注意力(比如动态的箭头告诉你“看这里,很重要”),解释刚刚发生了什么(比如使用淡入动画告诉你增加了列表项),或者只是让网页看起来更有活力,以便跟用户建立起情感联系。

迪士尼动画工作室认为通过动画表达角色和个性有12个原理。这些原理后来被集合成一本书,叫《生命的幻象》。动画师Vincezo Lodigiani后来制作了一个动画片来解释这些原理(https://vimeo.com/93206523),其中以一个小立方体作为主角。有空就看看吧。

受此启发,我们打算实现一个动画的方形标志,通过它介绍一下关键帧动画。这个标志由一个正方形和旁边的文字“Boxmodel”构成(见图10-19),这是我们虚构的一个公司名。

image.png
图10-19 静态的标志

标记很简单,一个标题中包含几个span元素,分别用于包含文本和绘制方形。虽然使用空元素并不是我们提倡的表现手段,但稍后你会明白这样做是有必要的。

  1. <h1 class="logo">
  2. <!-- 这是我们要做动画的正方形盒子 -->
  3. <span class="box-outer"><span class="box-inner"></span></span>
  4. <span class="logo-box">Box</span><span class="logo-model">model</span>
  5. </h1>

基本的样式包括页面的背景颜色,标志的字体,以及方形的边距、颜色等。我们用两个span元素表示要动画的方形,把它们的显示属性设置为inline-block,因为行内文本不能转为动画。

  1. body {
  2. background-color: #663399;
  3. margin: 2em;
  4. }
  5. .logo {
  6. color: #fff;
  7. font-family: Helvetica Neue, Arial, sans-serif;
  8. font-size: 2em;
  9. margin: 1em 0;
  10. }
  11. .box-outer {
  12. display: inline-block;
  13. }
  14. .box-inner {
  15. display: inline-block;
  16. width: .74em;
  17. height: .74em;
  18. background-color: #fff;
  19. }

1. 创建动画关键帧块
接下来创建动画。我们打算模仿《生命的幻象》动画开场的一些帧,即方块在屏幕上费力地滚动前行。

CSS动画的语法有点古怪,需要使用@keyframes规则来定义并命名一个关键帧序列,然后再通过animation-*属性将该序列连接到一个或多个规则。

以下是第一个关键帧块:

  1. @keyframes roll {
  2. from {
  3. transform: translateX(-100%);
  4. animation-timing-function: ease-in-out;
  5. }
  6. 20% {
  7. transform: translateX(-100%) skewX(15deg);
  8. }
  9. 28% {
  10. transform: translateX(-100%) skewX(0deg);
  11. animation-timing-function: ease-out;
  12. }
  13. 45% {
  14. transform: translateX(-100%) skewX(-5deg) rotate(20deg) scaleY(1.1);
  15. animation-timing-function: ease-in-out;
  16. }
  17. 50% {
  18. transform: translateX(-100%) rotate(45deg) scaleY(1.1);
  19. animation-timing-function: ease-in;
  20. }
  21. 60% {
  22. transform: translateX(-100%) rotate(90deg);
  23. }
  24. 65% {
  25. transform: translateX(-100%) rotate(90deg) skewY(10deg);
  26. }
  27. 70% {
  28. transform: translateX(-100%) rotate(90deg) skewY(0deg);
  29. }
  30. to {
  31. transform: translateX(-100%) rotate(90deg);
  32. }
  33. }

一口气看下来需要点毅力。首先,我们把这个关键帧序列命名为roll。这个名字只要不是CSS中预定义的关键字就行。这里并没有指定动画持续多长时间,因此这里通过关键帧选择符来选择时间点,即表示进度的百分比。

此外也可以同时使用关键字from和to,它们分别是0%和100%的别名。如果既没指定from(或0%),又没指定to(或100%),则浏览器会根据元素现有属性自动创建这两个值。关键帧选择符的值从1开始,没有上限。

第一个关键帧(0%)设置了animation-timing-function属性。这个属性与过渡中相对应的那个属性类似,值是预设的关键字或cubic-bezier()函数。这里设置的计时函数用于控制这一帧与下一帧之间的过渡变化。

而且,在第一个关键帧里,我们还通过translateX(-100%)将方块向左移动了100%。

接下来,我们设置了所有关键帧,应用了不同的变换,个别帧还添加了计时函数。图10-20展示了每一帧中元素的样子。注意,有些关键帧是一样的,比如最后两个帧,这是为了控制动画速度。

image.png
图10-20 动画中的不同关键帧

这个元素首先会向后仰一点,好像在积聚力量,然后旋转并拉伸(在旋转到45度时几乎停止),最终完成90度的旋转,并在旋转轴上略有变形,停下时还有个缓冲。这就是我们的第一个动画。

2. 将关键帧块连接到元素
定义了动画关键帧序列后,需要把它跟标志中的方块连接起来。跟使用过渡属性类似,关键帧动画也有相应的属性控制持续时间、延迟和计时函数,但可控制的方面更多一些:

  1. .box-inner {
  2. animation-name: roll;
  3. animation-duration: 1.5s;
  4. animation-delay: 1s;
  5. animation-iteration-count: 3;
  6. animation-timing-function: linear;
  7. transform-origin: bottom right;
  8. }

通过animation-name把元素的动画序列指定为roll。再通过animation-duration设置动画的时长。而animation-delay属性告诉浏览器在运行动画前先等1秒。我们希望这个方块翻滚3次,因此animation-iteration-count的值设置为3。

计时函数可以在关键帧选择符中通过animation-timing-function来设置,也可以在要实现动画的元素上设置。这里把整个动画序列的计时函数设置为linear,而前面在关键帧选择符中设置的计时函数可以覆盖这个设置。


注意 可以给同一个元素应用多个动画,就像过渡一样,只要用逗号分隔相应的名字即可。如果某一时刻两个动画都要加给同一个属性,则后声明的动画优先。


最后,设置transform-origin属性为bottom right,因为我们想让方块的旋转中心点位于右下角。

使用简写的animation属性,可以把前面的多行代码简化为一行,也跟过渡中的很像。

  1. .box-inner {
  2. animation: roll 1.5s 1s 3 linear;
  3. transform-origin: bottom right;
  4. }

不过现在还没完。目前,我们可以让方块原地旋转了。但我们希望方块能从视口外面进入并移动到其最终位置。这个动画也可以附加到前一个动画里实现,但那样的话关键帧有点多。因此我们再单独定义一个动画,对外部的span元素做一下变换。这个动画简单得多,我们只要它从左向右移动,距离大约是边长的3倍。

  1. @keyframes shift {
  2. from {
  3. transform: translateX(-300%);
  4. }
  5. }

因为我们想让动画从某个值开始,到初始值结束,所以这里省略了to关键帧,只指定了from关键帧。


关键帧代码与前缀
**
本章的代码只使用标准的不加前缀的属性。但示例代码中的属性是加了前缀的。

在需要使用前缀的浏览器中,不仅动画属性要加前缀,关键帧规则也要加前缀。换句话说,每种前缀都要写一套关键帧规则!好在时至今日,多数浏览器都可以支持不加前缀的属性,因此实践中可能只要多写-webkit一个前缀就行了。


现在可以通过步进计时函数,把shift序列应用给外部的span元素。这里有3步,以便每次旋转动画完成时,都可以把方块恢复到其初始位置,而步进函数会将它向前移动相同的距离。这样就会造成一种假象,好像方块滚过了整个屏幕。这个效果很难在纸面上展示,读者可以自己看看示例。

  1. .box-outer {
  2. display: inline-block;
  3. animation: shift 4.5s 1s steps(3, start) backwards;
  4. }

最后一个关键字backwards,设置的是动画序列的animation-fill-mode属性。这里的填充模式(fill mode)会告诉浏览器在动画运行之前或之后如何处理动画。默认情况下,第一个关键帧中的属性在动画运行前不会被应用。如果我们指定了关键字backwards,那相应的属性就会反向填充,即第一个关键帧中的属性会立即应用,即使动画有延迟或一开始被暂停。关键字forward表示应用最后一个关键帧的计算样式。both表示同时应用正向和反向填充。

本例中,我们希望动画直接位于屏幕外部,但保持其最终值(因为与方块的初始位置相同),所以我们使用了backwards。

这样我们第一个关键帧动画就完成了。在浏览器中打开这个示例,你会发现一个小方块欢喜地蹒跚前行。

10.4.2 曲线动画

通常,元素在两点间的位移动画都是走直线的。通过多使用一些关键帧,每一帧稍微改变一点方向,可以实现元素沿曲线运行。但更好的办法是以特殊方式组合旋转和平移,比如下面要讲的Lea Verou实现的例子(http://lea.verou.me/2012/02/moving-an-element-along-a-circle/)。

本书示例文件中包含一个基于这种技术实现的文件加载动画,用于表示文件被上传到了服务器。文件会沿一个半圆形路径从计算机“跳”到服务器,而在服务器图标后面会稍微缩小一些(见图10-21)。

image.png

图10-21 文件图标会沿曲线移动到服务器图标后面

下面就是这个动画的关键帧代码:

  1. @keyframes jump {
  2. from {
  3. transform: rotate(0) translateX(-170px) rotate(0) scale(1);
  4. }
  5. 70%, 100% {
  6. transform: rotate(175deg) translateX(-170px) rotate(-175deg) scale(.5);
  7. }
  8. }

开始的关键帧将元素向左平移170像素(以计算机图标作为起点)。第二个关键帧把元素旋转了175度,同时也平移了相同的距离,然后再向相反方向旋转175度。因为这是在平移之后的位置上发生的,所以元素仍然保持竖直,不会因旋转而倾斜。最后把元素缩小一半。

图10-22展示了这两种变换组合创建沿曲线移动效果的过程。
image.png

图10-22 因为旋转是在平移之前应用的,所以图标会沿曲线移动。此处展示的是动画进行到1/4,也就是旋转45度时的样子

接下来把这个动画连接到file-icon元素,同时设置持续时间和缓动函数。因为这是一个加载动画,所以将它设置为无限循环(大家都很熟悉吧),即把infinite作为animation-iteration-count的值:

  1. .file-icon {
  2. animation: jump 2s ease-in-out infinite;
  3. }

你会发现最后的关键帧选择符同时选择了70%和100%两个点。这是因为我们希望动画在完成状态暂停一小会儿,然后再重新开始。

这里没有专门的属性来设置延迟,因此我们让从70%到100%这段时间的状态保持相同。对于共享相同属性值的关键帧,我们都可以这样设置,就像组合多个普通选择符一样。

动画事件、播放状态和方向
过一段时间,文件上传就会完成,但愿如此。在完整的例子中,我们也添加了相应的按钮,用于模拟完成、重新开始和暂停动画。这两个按钮所做的只是把两个类之一添加给文件图标。这两个类将属性animation-play-state的值设置为paused。这个属性有两个值:paused和running。其中running是默认值。

停止操作与暂停不同,停止是通过动画开始、停止或再次启动时触发的JavaScript事件来实现的。当前动画完成后,文件图标会消失,然后服务器图标上会出现一个对勾。具体的过程可以参考本书源代码,而关于JavaScript与动画事件的内容,可以通过MDN进一步了解。

最后,还可以通过animation-direction属性控制动画的方向。默认值是normal,而reverse关键字可以让动画反向播放,可以用于实现“下载”的动画。

此外还有alternate和alternate-reverse关键字,会在动画不同循环期间交替播放方向。它们的区别在于,alternate开始是正常方向,而alternate-reverse开始是相反方向。


动画的几点注意事项
**
CSS关键帧动画的实际使用中有不少问题和冲突。因此,大家应该注意下面几点。

  • 有些动画会在页面加载后立即开始,也可能稍有延迟。但有些浏览器不能保证页面加载后能平滑流畅地启动动画。可以在不同浏览器中打开翻滚方块的例子看看。因此,最好等页面确实完全加载后,再通过JavaScript去触发动画。
  • 关键帧中的属性没有任何特殊性。那些选择符只会简单地改变元素的属性。但有的浏览器(不是全部)却允许动画中的属性覆盖普通规则中使用了!important的属性。这有时候会引起困惑。
  • 关键帧中的属性不允许添加!important,加了的会被忽略。
  • Android OS的第2个和第3个版本支持 CSS 动画,但每个关键帧只允许一个属性!如果想给两个或多个属性加动画,那么元素会完全消失。为了解决这个问题,可以把动画代码拆分成多个关键帧块。

10.5 三维变换

学习了二维变换、过渡和动画之后,下一步自然要接触 CSS 中可能最令人激动的新特性——三维变换。

前面我们已经熟悉了二维空间中的基本变换和坐标系统。到了三维空间中,概念还是一样的,只不过要多考虑一个维度,也就是z轴。三维变换允许我们控制坐标系统,旋转、变形、缩放元素,以及向前或向后移动元素。想要实现这些效果,那就要先了解透视的概念。

10.5.1 透视简介

提到三维,就意味着要在三个轴向上表示变换。其中x轴和y轴跟以前一样,而z轴表示的是用户到屏幕的方向(见图10-23)。屏幕的表面通常被称为“z平面”(z-plane),也是z轴默认的起点位置。
image.png

图10-23 三维坐标系中的z

这意味着离用户远的位置(z轴负方向)上的元素,在屏幕上看起来应该小一些,离用户近的位置上的元素则应该大一些。而围绕x或y轴旋转,也会导致元素某一部分变大,而其余部分变小。

咱们通过一个例子来开始吧。拿二维空间中的一个边长为100像素的元素为例,让它沿y轴旋转60度:

  1. .box {
  2. margin: auto;
  3. border: 2px solid;
  4. width: 100px;
  5. height: 100px;
  6. transform: rotateY(60deg) ;
  7. }

单纯一个轴的变换只会导致元素变窄(因为围绕y轴旋转嘛),体现不出任何三维效果(见图10-24中最左侧的图)。
image.png

图10-24 旋转元素在没有透视(左)、perspective: 140px(中)和perspective: 800px(右)情况下的效果

这是因为我们还没有定义perspective(透视)。要定义透视,先得确定用户距离这个元素有多远。离得越近变化越大,离得越远变化越小。默认的距离是无穷远,因此不会发生明显的变化。

因此,我们在要变换的元素的父元素上设置perspective属性:

  1. body {
  2. perspective: 800px;
  3. }

这个数值表示观察点位于屏幕前方多远。恰当的距离一般是600~1000像素,具体数值大家可以自己测试。

1. 透视原点
默认情况下,假定观察者的视线与应用透视的元素相交于元素的中心。用术语来说,这意味着“消失点”在元素的中心。可以通过perspective-origin属性来修改消失点的位置。该属性与transform-origin属性类似,可以接受x/y坐标值(带关键字top、right、bottom和left)、百分比或长度值。

图10-25展示了给body元素设置perspective属性后的三维对象。其中所有元素都围绕x轴旋转90度(面朝前),但左右两幅图中的透视原点不同。

image.png

图10-25 左侧浏览器窗口的透视原点为默认的perspective-origin: 50%,右侧浏览器窗口的透视原点为perspective-origin: top left

2. perspective()变换函数
在父元素上设置perspective属性,可以让其中所有元素的三维变换共享同样的透视关系。这通常都是我们希望的,因为它很接近现实效果。

要设置个别变换元素的透视,可以使用perspective()函数。比如要实现之前的那个例子,可以使用如下代码,只不过透视只应用给它自己,不会被其他元素共享:

  1. .box {
  2. transform: perspective(800px) rotateY(60deg);
  3. }


10.5.2 创建三维部件

知道了如何在三维透视图中移动和显示元素,就可以动手实现一些有用的效果了。除了通过平移来增加一点活力和解释发生了什么,还可以将运动和三维效果相结合来节省屏幕空间,同时精简设计。

我们的目标是通过CSS和JavaScript构建一个三维部件,让用户界面的一部分隐藏在元素背面。这里重用之前的菜单组件,并添加了筛选而不是扩展菜名的选项。单击“Show filters”按钮,这个元素会翻转180度,显示背面的面板(见图10-26)。单击“Show me pizzas!”又会翻过来。在实际应用中,翻过来以后,菜名(比萨饼名)应该是根据筛选条件筛选过的。
image.png
图10-26 可翻转部件

首先,需要一套组织得当的标记,以保证在不支持三维变换的浏览器中或JavaScript因故不能运行时部件的可用性。如果浏览器不支持三维变换,可以同时前后显示部件的两面,如图10-27所示。理论上,单击“Show me pizzas!”按钮可以刷新页面,显示筛选后的结果。
image.png
图10-27 二维版:相继显示前后两面

HTML标记与本章前面的类似,但增加了几个新类名,还有一个外围容器包含整个结构。

  1. <div class="flip-wrapper menu-wrapper">
  2. <div class="flip-a menu">
  3. <h1 class="menu-heading">Top menu choices</h1>
  4. <ol class="menu-list">
  5. <li>Capricciosa</li>
  6. <!-- 后面的省略,共10个选项 -->
  7. </ol>
  8. </div>
  9. <div class="flip-b menu-settings">
  10. <!-- 这里是部件背面的表单 -->
  11. </div>
  12. </div>

我们会使用Modernizr来检测浏览器是否支持三维变换,因此增强后部件的类名都会带相应的前缀,这个前缀是在浏览器支持三维变换时添加到html元素的一个类名。

首先,在body元素上设置perspective,并让包装元素成为其后代的定位上下文。然后再针对包装元素的transform属性来添加过渡。

  1. .csstransforms3d body {
  2. perspective: 1000px;
  3. }
  4. .csstransforms3d .flip-wrapper {
  5. position: relative;
  6. transition: transform .25s ease-in-out;
  7. }

接下来让背面对应的元素绝对定位,以便它占据跟前面一样大的空间,同时将其围绕y轴翻转180度。我们还需要在两面被翻错时两面都不可见,以免相互干扰。可以通过backface- visibility属性来控制,默认值是visible,但设置成hidden可以让元素从背面看不到。

  1. .csstransforms3d .flip-b {
  2. position: absolute;
  3. top: 0; left: 0; right: 0; bottom: 0;
  4. margin: 0;
  5. transform: rotateY(-180deg);
  6. }
  7. .csstransforms3d .flip-b,
  8. .csstransforms3d .flip-a {
  9. backface-visibility: hidden;
  10. }

旋转部件时,我们希望所有内容都会随之旋转,包括已经翻过去的背面。默认情况下,任何应用给父元素的三维变换都会让子元素上的三维变换失效,并使其变平。我们得创建一个三维上下文,让子元素的变换与父元素在同一个三维空间中。为此要用到transform-style属性,在包装元素上将它的值设置为preserve-3d。

  1. .csstransforms3d .flip-wrapper {
  2. position: relative;
  3. transition: all .25s ease-in-out;
  4. transform-style: preserve-3d; /*默认值为flat */
  5. }

最后一步,在用户点击按钮时,通过JavaScript切换包装元素上的类名。添加is-flipped类会触发整个部件沿y轴旋转180度:

  1. .csstransforms3d .flip-wrapper.is-flipped {
  2. transform: rotateY(180deg);
  3. }

至此样式已部署到位。但现实很残酷,我们必须为跨浏览器兼容再多做一些工作。

1. 兼容IE
IE10和IE11不支持preserve-3d关键字。这意味着元素不能与父元素共享三维空间,也就意味着不能同时翻转整个部件的两面。因此,在IE中必须分别给两面添加过渡才行。

另外,在父元素应用perspective及变换多个元素时,IE也有严重的bug。这意味着必须使用perspective()函数。

更新后的代码将部件前面的初始变换设置为0度,后面的设置为-180度,然后在父元素切换类名时同时翻转两者。而且,perspective()函数需要在变换声明列表中第一个出现。

  1. .csstransforms3d .flip-b,
  2. .csstransforms3d .flip-a {
  3. transition: transform .25s ease-in-out;
  4. }
  5. .csstransforms3d .flip-a {
  6. transform: perspective(1000px) rotateY(0);
  7. }
  8. .csstransforms3d .flip-b {
  9. transform: perspective(1000px) rotateY(-180deg);
  10. }
  11. .csstransforms3d .flip-wrapper.is-flipped .flip-a {
  12. transform: perspective(1000px) rotateY(180deg);
  13. }
  14. .csstransforms3d .flip-wrapper.is-flipped .flip-b {
  15. transform: perspective(1000px) rotateY(0deg);
  16. }

iOS 8上的Safari有一个跟IE相反的bug,即应用了perspective()函数的元素有时候会在过渡期间消失。因此,需要在body元素上重复声明一个perspective属性:

  1. .csstransforms3d .flip-wrapper {
  2. perspective: 1000px;
  3. }

2. 键盘访问与无障碍
开发会隐藏信息的组件时,前几章提到过,如何隐藏至关重要。旋转之类的变换不会导致文档的制表键切换功能失效。在三维部件(以及相应的JavaScript)的最终代码中,我们还增加了另外一些机制,以确保代码的健壮性。

  • 除了使用Modernizr检测浏览器是否支持三维变换,我们还检测了浏览器是否支持JavaScript的classList API。这个API用于在部件状态变化时切换类名。这意味着最终代码中的CSS规则都会前缀.csstransforms3d.classList类。

一般来说,如果浏览器支持三维变换,那么基本就会支持classList API,但为了避免一些极端情况,我们还是“多此一举”了。万一这两个特性中有一个不被支持,就会显示原始的二维版本。

  • 在部件一面隐藏的情况下,它会自动带上一个类名is-disabled,同时aria-hidden属性也会被设置为true。其中is-disabled类用于将visibility属性设置为hidden。
  • 这样可以避免使用键盘的用户意外按下制表键,把焦点切换到看不到的筛选表单上,也会防止屏幕阅读器读取其中的内容。(aria-hidden属性只针对屏幕阅读器,因此不需要依赖CSS隐藏技术。)隐藏会在翻转完成后发生,因此它依赖transitionend事件。
  • 相应地,另一面通过使用类名is-enabled对用户保持可用。
  • 当部件再被翻转过来时,键盘焦点转移到Show filters按钮。

10.5.3 高级三维变换

本节只介绍一些不太常用却值得玩味的三维变换特性。

1. rotate3d()
除了rotateX()、rotateY()和rotateZ()(以及二维版本rotate())这几个单维旋转函数之外,还有一个rotate3d()函数。这个函数可以围绕穿越三维空间的任意一条线翻转元素:

  1. .box {
  2. transform: rotate3d(1, 1, 1, 45deg);
  3. }

这个rotate3d()函数接受4个参数:前3个数值分别表示x轴、y轴和z轴的向量坐标,最后一个是角度。其中坐标定义了一条线,作为翻转环绕的轴。比如,如果向量坐标是1,1,1,那么翻转围绕的假想线会穿过transform-origin和另一点,另一点的三维坐标是x轴、y轴和z轴各相对原点1个单位远。

这里不用指定什么单位,因为点与点之间的位置是相对的。使用100, 100, 100的结果与上面的相同,因为假想线相同。

实际上,这个三维旋转等价于每个轴上的某些旋转(0度或更多)的叠加,至于每个轴旋转多少,那就有点复杂了。只要把它想像成可以围绕你指定的线旋转元素就行了。如果确实需要同时指定每个轴旋转的度数,最好还是组合使用单维旋转函数。

2. 三维矩阵变换
与二维矩阵变换类似,也有一个matrix3d()函数可以组合多个轴向上的平移、缩放、变形和旋转。
这里不打算详细介绍三维矩阵变换,但可以告诉你,这个函数接受16个参数,以便计算最终呈现在坐标系中的对象。这个函数堪称“有史以来最复杂的CSS属性”。

与二维版本一样,三维矩阵也不是我们日常用来手工计算传参的,而是通过JavaScript和CSS组合实现游戏等高性能交互体验时才用的。比如,本章最前面那个Digital Creativity Guidebook的例子(见图10-1)就大量使用了matrix3d()来计算这本动画书中各个角色的变换形态。

10.6 小结

本章介绍了如何在空间和时间维度上操作元素。我们了解了二维和三维空间中的变换会怎样改变页面上呈现的元素,同时又不会影响页面中的其他元素。此外,还点到为止地提及了matrix()和rotate3d()等高级变换特性。

将以上技术与动画相结合,比如CSS过渡和CSS关键帧动画,就可以创造出动画标志或可翻转菜单等特效。

在致力于实现这些响应式设计效果的技术的同时,从始至终,我们都没有抛开那些浏览器不支持这些特性或只使用键盘导航的用户,乃至那些使用屏幕阅读器的用户。我们希望这些用户同样能无障碍地浏览我们的网页。