blog icon indicating copy to clipboard operation
blog copied to clipboard

深入浅出CSS Transform

Open SamHwang1990 opened this issue 9 years ago • 0 comments

[TOC]

"transform" 属性

2D Transform

  • matrix(a, b, c, d, e, f) 以矩阵的方式声明变换函数;
  • translate(e[, f]) 仿射变换(平移变换),以向量(e, f) 的方向和长度平移图像,f 可选,默认为0;
  • translateX(e) 图像向X 轴方向平移e 的距离,派生自translate;
  • translateY(f) 图像向Y 轴方向平移f 的距离,派生自translate;
  • scale(a[, d]) 缩放变换,图像所有的点的x 轴的值缩放a 倍,y 轴的值缩放d 倍,d 可选,默认与a 相同;
  • scaleX(a) 图像所有的点的x 轴的值缩放a 倍,派生自scale;
  • scaleY(d) 图像所有的点的y 轴的值缩放d 倍,派生自scale;
  • rotate(𝚊) 旋转变换,图像沿着transform-origin指定点(默认是中心点)旋转𝚊 度,单位为deg;
  • skew(𝚊[, 𝚋]) 错切变换,图像所在坐标系的X 轴倾斜𝚊 度,Y 轴倾斜𝚋 度,𝚋 为可选值,默认为0,𝚊、𝚋 单位均为deg;
  • skewX(𝚊) 图像所在坐标系的X 轴倾斜𝚊 度,派生自skewX;
  • skewY(𝚋) 图像所在坐标系的Y 轴倾斜𝚋 度,派生自skewY;

3D Transform

  • matrix3d(),4x4 矩阵声明变换函数;
  • translate3d(tx, ty, tz) 透视投影,以向量(tx, ty, tz) 的方向和长度平移图像;
  • translateZ(tz) 图像向Z 轴方向平移tz 的距离,派生自translate3d;
  • scale3d(sx, sy, sz) 在scale 的基础上支持Z 轴的缩放;
  • scaleZ(sz) Z 轴缩放sz 倍,派生自scale3d;
  • rotate3d(tx, ty, tz, deg) 3D 旋转变换,以向量(tx, ty, tz) 为轴旋转,deg 为正则顺时针,为负则逆时针,因为(tx, ty, tz) 的意义是旋转所围绕的轴,所以不能是(0, 0, 0);
  • rotateX(angle) 围绕X 轴旋转,相当于rotate3d(1, 0, 0, deg);
  • rotateY(angle) 围绕Y 轴旋转,相当于rotate3d(0, 1, 0, deg);
  • rotateZ(angle) 围绕Z 轴旋转,相当于rotate3d(0, 0, 1, deg);

利用上面的变换函数,我们可以很简单的在2D、3D 维度上平移、缩放、旋转图像。

下面着重分析下两个让人迷惑却又揭示着变换属性本质的函数:matrixmatrix3d,允许使用者以矩阵的方式指定变换函数。

矩阵变换函数,相当于线性代数中的线性变换概念:线性变换T 中存在一个矩阵A,将一个向量x 变换为T(x),并记为:x→_A_x。简单理解为,将矩阵_A_ 点乘变换前的向量x 得到变换后的向量_T_(x),举个例子:

上面的例子使用的矩阵中的六个字母正是对应2D transfrom 中matrix 函数使用的六个数值。所有的2D 变换本质上均使用上面的矩阵_A_ 作线性转换。

注意了,上面提到了一个概念:线性转换,简单理解为,线性转换与非线性转换的区别在于,矩阵_A_ 能否转换整个子空间。即,给定矩阵_A_ 和转换后的子空间,是否 中的每个变量_T_(x) 在转换前的子空间中都对应的变量。

将上面的例子转换为线性代数中的参数方程组来理解线性转换的意思:

在上述参数方程组中,对于任一 组合,方程组均有解。

那么问题来了,既然是2D 转换,使用2x2 矩阵应该就刚好将 子空间转换啦,为什么要使用3x3 矩阵,上面矩阵中,第三行[0 0 1]像没什么用?

其实不然,2x2 的矩阵是无法完成2D 维度的线性平移变换(translate、translateX、translateY)的。具体的证明请参见线性代数的相关章节。于是,我们需要站在更高的维度上完成线性平移变换,也就是引入齐次坐标(也是线性代数的概念)。使用方式是,在2D 变换中,为方便计算,一般会将 中每个点对应与中的,也就是位于平面上方1 单位的平面上,我们称有齐次坐标

齐次坐标的意义在于,矩阵与向量点乘时引入与向量坐标无关的常量,常量往往就是平移的值,而2D 变换的齐次坐标经过运算后,z 轴的值是不需要改变的,所以,矩阵的最后一行只能为[0 0 1]。

其实,齐次方程的思想我们从小就有接触,那就是二元一次方程,比如:,该方程可以视为1D 变换,以矩阵变换的方式相当于:

矩阵与变换

正如上文所说,matrix属性是所有变换函数的本质,在半桶水地引入了上面这么多线代的知识后,终于要写我想表达的:来尝试用matrix属性代替其他的2D 变换函数。

translate 变换

形如transform: translate(e,f);的样式可以转为:transform: matrix(1,0,0,1,e,f);。对应矩阵如下:

scale 变换

形如transform:scale(a,d);的样式可以转为:transform: matrix(a,0,0,d,0,0);。对应矩阵如下:

rotate 变换

形如transform: rotate(𝒂deg);的样式可以转为:transform: matrix(cos𝒂,sin𝒂,-sin𝒂,cos𝒂);。对应矩阵如下:

矩阵的推导过程参考:推导坐标旋转公式

skew 变换

形如transform: skew(𝒂deg,𝒃deg);的样式可以转为:transform: matrix(1,tan𝒃,tan𝒂,1,0,0);。对应矩阵如下:

理解完2D 的变换矩阵再来看3D 变换矩阵就简单很多了,无非就是在3x3 的矩阵上引入齐次坐标变成4x4 的矩阵,对应的,matrix3d()函数支持16 个参数,与2D 变换矩阵不一样的是,3D 变换矩阵的最后一行不再是一成不变的[0 0 0 1],而是支持透视投影样式:perspective(下文会详细解释)。4x4 矩阵变换表示如下:

在支持的3D 变换函数中,重点关注下rotate3d(x,y,z,𝒂deg)matrix3d()的转换。

rotate3d 变换

以向量(m, n, o) 为轴旋转𝒂 度的样式为:transform:rotate3d(m,n,o,𝒂deg);,对应矩阵如下:

其中:

在我们了解了上面的基本变换与矩阵的转换之后,其实,我们还能做得更多。

在上手编写变换的效果时,我可以预想得到,至少会有两种情境:

  1. 我设计了变换步骤,但不知道怎样编写对应的样式或矩阵;
  2. 我知道变换前后的图像,但不知道怎样编写对应的样式或矩阵;(todo)

针对第一种情况,我们可以尽量拆分变换步骤,以达到每个步骤都值做很基本的变换效果,比如平移、缩放、旋转等,由于transform属性值支持多变换函数,于是,我们可以把每个步骤对应的变换函数值以列表形式传给transform属性。

例一:我想要做一个2D 变换:图像沿x 轴平移20px,然后旋转45度,然后x 坐标放大2 倍,y 轴倾斜30 度。

想完成这样一个变换,编写的变换函数是:

  transform: translateX(20px) rotate(45deg) scaleX(2) skewY(30deg);

类似上面变换函数与矩阵的转换,这里我们会使用矩阵点乘来进行矩阵转换:

矩阵点乘的结果如下:

针对第二种情况,我们利用线性变换矩阵_A_与单位矩阵有_I_ 存在的关系来求解:A I = A,即线性变换矩阵与单位矩阵的点乘等于线性矩阵本身,以2D 变换矩阵为例:

这个关系带来的启发是,只要我们知道变换前单位矩阵各列坐标在变换后的坐标位置,然后将变换后的坐标以矩阵形式写出来,就等于我们相求的矩阵_A_ 了。

变换函数与坐标系

css 中的transform属性的定义是具有继承性质的,也就是,父元素的变换会导致后代元素进行同样的变换,这是很容易理解的,否则会出现比如父元素平移了20px,如果后代元素不继承,就会出现父子脱离的现象,显然是不合理的。

transform属性不为none的元素会创建一个独立的坐标系来完成声明的变换,若元素的父元素也有不为nonetransform属性定义,则子元素的坐标系会在父元素坐标系的基础上创建。若元素的transform属性定义了多个变换,则每个变换都会根据自身需要调整子元素当前的坐标系。举个例子:

.box {
  transform: skewX(20deg);
}

.box > .son {
  transform: skewY(30deg) skewX(-45deg);
}
  • .box元素的transform属性创建了一个新的坐标系,然后使X 轴顺时针旋转20度来完成自身指定变换;
  • .box进行变换的同时,子类元素.son也进行了一样的变换,此时.son的坐标系与父元素的坐标系一样;
  • 然后,.son自身也有定义变换函数,而且还定义了两个:
    • 于是,先进行skewY变换,该变换使.son坐标系的Y 轴顺时针旋转了30度;
    • 完成后进行skewX变换,该变换使.son坐标系的X 轴逆时针旋转了45 度。

最终结果:.son元素的transform属性创建的坐标轴跟父元素的坐标轴相比,多了两次转换。

原始的CSS 变换坐标系大概以下图所示呈现:

原始的CSS 变换坐标系

即X 轴水平向右,Y 轴垂直向下,Z 轴垂直屏幕坐在平面指向用户。该坐标轴为左手坐标系

"transform-origin" 属性

CSS 变换的目的是转换坐标,而坐标的值是建立在坐标系上的,自然,坐标系的原点相对图像的位置也会影响最终的转换效果,这个在rotate类型的变换中尤其明显。可以想象,图像以左上角旋转和以右下角旋转的到的图像是不一样的。CSS 变换默认是以图像中心点作为原点进行的,要更改原点位置,可以使用transform-origin属性。举个例子:

.box1 {
  height: 100px; width: 100px;
  transform-origin: 50px 50px;
  transform: rotate(45deg);
}

.box2 {
  height: 100px; width: 100px;
  transform-origin: bottom left;
  transform: rotate(45deg);
}

.box1中的指定的transform-origin位于图像中点,与默认行为一致。.box2则将图像旋转的原点设为了左下角。

transform 变换矩阵计算总结

现在对transform-origin这个属性有了个直观感受了:图像变换坐标系的原点,那接下来,继续探讨下,CSS 在计算变换函数矩阵时是如何实现(兼顾)自定义原点位置的,以2D 变换为例:

  1. 从单位矩阵开始;
  2. 若指定了样式transform-origin:(tx, ty)属性,则将该属性的值转换为相对元素左上角位置的偏移距离,得到偏移距离后,元素所在坐标系的原点按该偏移距离做一次translate(tx, ty)
  3. 元素开始从左到优执行transform属性指定的变换函数;
  4. 最后,坐标系的原点做一次逆transform-origin变换的操作,也就是将第一步中做的偏移给回退,即最后做了一次translate(-tx, -ty)

详细的图像描述参考:

上图中黑色的点就是原点的位置,左侧的图展示了上述的第一步,原点偏移,中间的图展示了第二步,元素执行transform 变换,右侧的图展示了原点针对transform-origin的逆偏移。

以矩阵运算的角度看上面4 个步骤:

"perspective" & "perspective-origin"属性

perspective属性是控制3D 变换Z 轴纵深视感(也就是立体感)的关键属性。

在接触3D 变换之前,元素几乎都是渲染在一个只有X 轴、Y 轴的平面上(先不考虑z-index),而html 文档中的所有元素都共享着这么一个平面。但当引入3D 变换后,元素的渲染我们就可以站在三维空间的角度去看了。也就是,除却X 轴、Y 轴坐标,元素中点还拥有了各自不同的Z 轴坐标。

日常生活中,同样的一个物体,离观察者远则显小,离观察者近则显大,而我们觉得这个物体有立体感,正是因为它不是平面的,而是有些部位离我们近,有些部位离我们远。

同样的,在CSS 渲染三维空间的元素时,默认它是不知道观察者的位置的,此时,我们看到的渲染结果是没有立体感的,仿佛整个三维空间都被一束平行光投射到二维平面上了。举个例子:

<style>
div {
  height: 150px;
  width: 150px;
}
.container {
  border: 1px solid black;
  background-color: gray;
}
.transformed {
  transform: rotateY(50deg);
  background-color: blue;
}
</style>

<div class="container">
  <div class="transformed"></div>
</div>

上面这段样式渲染结果如下:

可以看到,蓝色的矩形围绕Y 轴旋转了50 度,但整个图像看起来跟二维平面是一样的,只是直观看上去,这个矩形变小了。那为什么变小了,道理也很简单,Y 轴是平面垂直向下的,元素坐标系原点默认在中心,然后整个元素围绕Y 轴旋转50 度,元素中心Z 值为0,向左Z 值越大,向右Z 值越小。然后,一束垂直屏幕所在平面的平行光将元素投射到Z 为0 的平面上,于是,元素看起来就像变小了一样。

于是,为了让三维空间的元素渲染出立体感,我们需要告诉CSS,观察者的位置,也就是给出观察者距离屏幕有多远。当CSS 确定观察者位置后,渲染元素时,Z 轴坐标越大(越靠近观察者),元素越大,Z 轴坐标越小(越远离观察者),元素越小。

使用perspective属性即可指定观察者的位置,举个例子:

<style>
div {
  height: 150px;
  width: 150px;
}
.container {
  perspective: 500px;
  border: 1px solid black;
  background-color: gray;
}
.transformed {
  transform: rotateY(50deg);
  background-color: blue;
}
</style>

<div class="container">
  <div class="transformed"></div>
</div>

上面这段样式渲染结果如下:

有了perspective的声明,通过放大中心点左侧的部分(Z 轴坐标相对大)、缩小中心点右侧的部分(Z 轴坐标相对小),终于营造出rotateY带来立体感了。

花了这么多口水终于说完perspective的作用,接下来,我们从数学关系上探讨这个属性到底是怎样影响元素大小的。一图胜千语:

图中d表示perspective设定的观察者距离屏幕的长度,Z表示元素的Z 轴坐标,虚线圆表示元素的原始大小,

蓝色背景圆表示元素实际的现实大小。可以看到,当元素的Z 轴坐标大于0,则实际渲染的要比原始的大,当元素的Z 轴坐标小于0,则实际渲染的要比原始的小。

假设元素原始大小为,经过perspective转换之后大小为,则有:

,有。当,有

perspective属性的声明有两种用法,一种是在父元素声明,一种是在当前元素的transform列表的开头声明。

除了指定观察者的距离,我们还可以使用perspective-origin:(tx, ty)属性指定观察者的视角。

我们可以把观察者所在地方看作一个平面,默认的视角到屏幕的连线与屏幕成90 度角,perspective-origin指定视角的平移值,类似于translate。再来看一图:

perspective 矩阵计算总结

perspectiveperspective-origin最终作为一个矩阵参与到整个变换,矩阵计算过程如下:

  1. 从单位矩阵开始;
  2. 单位矩阵根据perspective-origin的计算值做平移(translate);
  3. 步骤2 的矩阵点乘perspective对应的矩阵;
  4. 步骤3 的矩阵进行perspective-origin的逆平移;

perspective对应的矩阵为:

上面4 步得出的矩阵变换算式是:

上述的矩阵进行完运算后,会得到一个4x4 的矩阵,使用该矩阵对元素坐标进行变换,则元素的每个坐标会被转换为类似:(x, y, z, w),若有指定perspective,w 会等于。而齐次坐标的值在变换前后是不能改变的,所以,最终元素的坐标等于:(x/w, y/w, z/w, 1)

实际渲染时,CSS 对元素4 个角的坐标值,对应d、Z 的不同大小关系会做不同处理:

  • ,即,坐标的每个值都会乘以一个无限大的值:(xn, yn, zn, 1),其中n 无限大;
  • 若元素4 个角的w 都小于 0,即d<Z,元素会完全消失;
  • 若元素4 个角的w 不全小于0,则元素最终会被渲染成多边形,就像图像四个角中w 小于0 的角被砍掉一样;

3D render context & "transform-style" 属性

3D render context (3D 渲染上下文)影响3D 变换是否会出现元素交错的现象。

当带有3D 变换函数的函数在非3D 渲染上下文中渲染时,并不会改变该元素的显示顺序,而是按正常的渲染步骤来走,在html 文档结构中,靠前的元素先显示,靠后的元素后显示,当元素位置出现重叠时,靠后的元素会覆盖靠前的元素。即使元素3D 变换让它有了比较大的Z 轴坐标,也只能让它在显示上可能变大而已。

而3D 渲染上下文的显示规则也很简单,Z 轴坐标越大,则显示越靠近用户,Z 轴坐标越小,则显示越远离用户,并允许不同Z 轴坐标的元素间出现交错的渲染,元素交错的渲染算法使用Newell's algorithm

下面的规则描述了元素是如何创建以及加入到3D 渲染上下文的:

  • 当一个允许发生变换的元素声明了transform-style: preserve-3d且自身不在3D 渲染上下文时,则会新建一个3D 渲染上下文,同时,元素自身也会加入到这个上下文中;
  • 当一个元素处在一个已有的3D 渲染上下文时,即使声明transform-style: preserve-3d,也不会创建新的,而是共享并扩展该上下文;
  • 当一个元素创建或扩展了一个上下文时,其子元素也会加入到该上下文中;

在上面的规则中,我们使用了transform-style属性来标识元素是否需要创建或扩展3D 渲染上下文。该属性有两个可选值:

  • flat:不生成3D 渲染上下文
  • preserve-3d:创建或扩展3D 渲染上下文

3D render context 矩阵计算总结

当一个元素处于3D 渲染上下文时,其变换的矩阵计算过程如下:

  1. 从单位矩阵开始;
  2. 遍历父元素到3D 渲染上下文的根元素:
    1. 点乘父元素产生的perspective矩阵;
    2. 计算元素的框架模型相对父元素的偏移(translate),常见于absolute、relative 定位;
    3. 点乘元素的transform变换矩阵;
  3. 点乘元素自身的transform变换矩阵;

矩阵的计算过程中,有两点需要注意:

  • 3D 渲染上下文中每个元素都会继承上下文中所有祖先节点的变换过程;
  • 3D 渲染上下文中每个元素在进行transform变换前的平面都是一个独立的CSS 视觉格式化模型( visual formatting model) ,意思是,子元素如果使用fixedabsoluterelative定位,则原点都是元素的左上角,不会是屏幕左上角或祖先元素的左上角;

"backface-visibility" 属性

当一个元素进行3D 变换时,就会有正面和背面的分别,backface-visibility属性用于控制元素背面是否可见,该属性只能用于进行3D 变换的元素。

可选值有两个:

  • visible
  • hidden,默认值

Interpolation of Transform

transform属性参与到CSS 动画(animate)或过渡(transition)时,变换函数列表需要使用插值法(Interpolation)计算得出。参照以下四条规则来计算动画期间的函数列表:

  • 若起始和终点变换值均为none,则没有插值计算的需要,保持none即可;
  • 若起始或终点变换有一个为none,则会将none转化为一个个单位矩阵来对应transform中每个变换函数;
  • 若起始或终点变换的列表长度一致,且对应位置的变换类型一致(比如:translate vs translate)或都派生自同一个变换基类(比如:translateX vs translateY vs translate);
  • 若不符合以上三种情况,则会将transform的变换函数列表计算为一个矩阵,然后应用矩阵插值法

参考文献

SamHwang1990 avatar Nov 27 '16 12:11 SamHwang1990