Css3-transforms

From HTML5 Chinese Interest Group Wiki
Jump to: navigation, search

介绍

模块的交互

CSS 值

术语

二维子集

变换的渲染模式

“transform” 属性

“transform-origin” 属性

“transform-style” 属性

“perspective” 属性

“perspective-origin” 属性

“backface-visibility” 属性

SVG 的 “transform” 属性

SVG 动画

变换函数

变换函数列表

变换的插值

当对变换实现动画或者过度效果时,变换函数列表必须执行插值。对于从“源变换”到“的变换”的变换插值,使用以下描述的规则。

  • 如果“源变换”和“的变换”都为 none:
    • 这时不需要任何插值,计算后的结果依然是 none。
  • 如果“源变换”或“的变换”的其中一个为 none。
    • 那么 none 被替换为一个相当于恒等变换函数列表中的对应变换函数列表。这两个变换函数列表以下面的规则来进行插值。
示例18
例如,如果一个“源变换”为 scale(2) 并且“的变换”为 none 那么 scale(1) 将被用于“的变换”,并且动画将继续使用下一个规则。
类似地,如果“源变换”为 none 并且“的变换”为 scale(2) rotate(50deg) 那么动画将会视“源变换”为 scale(1) rotate(0) 来执行。
  • 如果“源变换”和“的变换”有着相同数量的变换函数,每个变换函数对有着相同的名称,或者它们是有着相同原生变换的派生变换。
示例19
例如,如果“源变换”为 scale(1) translate(0) 并且“的变换”为 translate(100px) scale(2)
那么 scale(1) 和 translate(100px) 与 translate(0) 和 scale(2) 都不共享一个公共的原生变换,因此根据这条规则是无法进行插值的。
  • 所有其它情况:
    • 在“源变换”和“的变换”上的变换函数列表中的每个变换函数,将他们全部相乘最终得到 4*4 的矩阵。每个矩阵都插值根据矩阵插值中的指示来插值。其计算结果是一个变换函数矩阵,除非两个矩阵初始都能以 3*2 的矩阵或 matrix3d 来描述。

在一些情况下,动画可能会让一个变换矩阵变成一个退化矩阵或不可逆的矩阵。例如一个 scale 从 1 到 -1 的缩放动画。当矩阵处于一个这样的状态时元素将不会被渲染。


变换函数的原生和派生

一些变换函数可以使用多个一般变换函数来描述。这些变换函数被称为派生变换函数,而那些一般变换函数被则为原生变换函数。下面列出了二维和三维的原生变换函数。

2D原生变换函数与其派生函数: translate()

  • 派生出 translateX()、translateY() 和 translate()。

带三个参数的 rotate()

  • 派生出 一个参数的rotate() 以及如果带三个参数的rotate()支持的话也算一个派生。

scale()

  • 派生出 caleX()、scaleY() 和 scale()。

3D原生变换函数与其派生函数: translate3d()

  • 派生出 translateX()、translateY()、translateZ() 和 translate()。

scale3d()

  • 派生出 scaleX()、scaleY()、scaleZ() 和 scale()。

rotate3d()

  • 派生出 rotate()、rotateX()、rotateY() 和 rotateZ()。

对于同时拥有一个二维和一个三维原生变换的派生变换,环境决定了它使用何种原生变换函数,见:原生与派生变换函数的插值


原生与派生变换函数的插值

两个具有相同名称和相同参数个数的变换函数使用没有任何转换的数值化插值。计算出的值将会是相同名称和相同参数个数的变换函数。特殊的规则应用于 rotate3d()、matrix()、matrix3d() 和 perspective()。

示例 20
两个变换函数 translate(0) 和 translate(100px) 具有相同的类型和相同的参数个数,因此它们可以执行数值化地插值。
translateX(100px) 和 translate(100px,0) 没有相同的类型和相同的参数个数,因此它们不能跳过转换的步骤直接进行插值。

两个共享了原生变换的不同变换函数或具有相同的类型但没有相同参数的变换函数,它们可以被插值。两个变换函数需要先变换到具有共同原生变换的形式,之后再进行数值化的插值。计算结果将会是这个原生变换的参数进行插值的结果。

示例 21
下面的实例描述当一个DIV元素在鼠标悬停状态下在 3 秒内从 translateX(100px) 变换到 translateY(100px) 的过渡效果。
两个变换函数都派生自同一个原生变换函数 translate() 因此可以插值。

div {
  transform: translateX(100px);
}

div:hover {
  transform: translateY(100px);
  transition: transform 3s;
}

在这个过渡的期间,两个变换函数都变成了一个公共的原生变换。translateX(100px) 变成 translate(100px, 0) ,而 translateY(100px) 变成 translate(0, 100px)。这样两个变换函数才得以进行数值化插值。

如果两个变换函数在一个二维空间中共享一个原生变换,那么两个变换函数都要被转换成二维的原生变换。如果存在一个或者两个变换函数都是三维变换函数,那么公共的原生变换函数将使用三维的。

示例 22
这个例子中执行了一个从二维的变换函数到三维的变换函数的动画。它们的公共原生变换是 translate3d()。

div {
  transform: translateX(100px);
}

div:hover {
  transform: translateZ(100px);
  transition: transform 3s;
}
首先,translateX(100px) 变成 translate3d(100px, 0, 0),translateZ(100px) 变成 translate3d(0, 0, 100px)。然后两个转换后的变换函数进行数值化插值。


matrix()、matrix3d() 和 perspective() 这些变换函数首先要转换到 4*4 的矩阵上,然后根据矩阵插值一章的定义进行插值。

对于原生 rotate3d() 的插值,其变换函数的方向向量应先归一化。如果归一化后的向量都相等,那么则以旋转角来数值化插值。 其它情况下,变换函数先转换成一个 4*4 的矩阵,然后根据矩阵插值一章的定义进行插值。


矩阵插值

当在两个矩阵之间插值时,每个矩阵被分解为对应的 移动、旋转、缩放、扭曲、透视(3D矩阵的)。每个矩阵分解的对应部分做数值化的插值后最终还原成矩阵。

在下面的例子中,当鼠标悬停时,元素在X和Y方向同时被移动了100像素并且旋转1170度。初始的变换值是45度旋转。
在这个过渡用法中,开发者期望得到一个顺时针旋转三又四分之一圈的动画效果。

<style>
div {
  transform: rotate(45deg);
}
div:hover {
  transform: translate(100px, 100px) rotate(1215deg);
  transition: transform 3s;
}
</style>

原变换 rotate(45deg) 中变换函数的数量与目标变换 translate(100px, 100px) rotate(1125deg) 中的变换函数数量不同。
根据变换的插值中最后提供的规则,两个变换之间必须被以矩阵插值的方式来进行插值。
那么变换函数会转换成矩阵,旋转三圈的这个信息会丢失,元素只会旋转四分之一圈(90度)。

下面,我们区分开“两个2D矩阵插值”和至少有一个矩阵不是2D矩阵的“两个矩阵插值”。 如果插值中的其中一个矩阵是不可逆的,那么使用的动画函数必须根据其对应规范的规则来退化为一个离散动画。


 支持的函数

下一个小结中的伪代码给出了支持的函数: 支持的函数(point是一个具有 3 个分量的向量,matrix 是一个 4*4 的矩阵,vector 是一个具有 4 个分量的向量):

  • double determinant(matrix) 返回 matrix 的行列式值
  • matrix inverse(matrix) 返回 matrix 的逆矩阵
  • matrix transpose(matrix) 返回 matrix 的转置矩阵
  • point multVecMatrix(point, matrix) 将 point 乘以 matrix,并返回变换后的 point
  • double length(point) 返回向量的模长
  • point normalize(point) 返回将向量的模长设置为 1 后的向量
  • double dot(point, point) 返回两个 point 的内积
  • double sqrt(double) 返回传入值的平方根 Bug.png
  • double max(double y, double x) 返回传入值中较大的值 Bug.png
  • double dot(vector, vector) 返回两个 vector 的内积
  • vector multVector(vector, vector) 对传入的向量做乘法 Bug.png
  • double sqrt(double) 返回传入值的平方根
  • double max(double y, double x) 返回传入值中较大的值
  • double min(double y, double x) 返回传入值中较小的值
  • double cos(double) 返回传入值的余弦
  • double sin(double) 返回传入职的正弦
  • double acos(double) 返回传入值的反余弦
  • double abs(double) 返回传入值的绝对值
  • double rad2deg(double) 将一个弧度转换为角度并返回
  • double deg2rad(double) 将一个角度转换为弧度并返回

分解也给出了下列函数:

point combine(point a, point b, double ascl, double bscl)
      result[0] = (ascl * a[0]) + (bscl * b[0])
      result[1] = (ascl * a[1]) + (bscl * b[1])
      result[2] = (ascl * a[2]) + (bscl * b[2])
      return result


2D 矩阵的插值

解析一个 2D 矩阵

下面的伪代码是基于《Graphics Gems II》(作者 Jim Arvo)的。

输入:   matrix       ; 一个 4*4 的矩阵
输出:   translation  ; 一个具有两个分量的向量
        scale        ; 一个具有两个分量的向量
        angle        ; 旋转的角度
        m11          ; 2*2 矩阵的 (1,1) 坐标位置
        m12          ; 2*2 矩阵的 (1,2) 坐标位置
        m21          ; 2*2 矩阵的 (2,1) 坐标位置
        m22          ; 2*2 矩阵的 (2,2) 坐标位置
如果矩阵无法分解则返回 false,否则返回 true。

double row0x = matrix[0][0]
double row0y = matrix[0][1]
double row1x = matrix[1][0]
double row1y = matrix[1][1]

translate[0] = matrix[3][0]
translate[1] = matrix[3][1]

scale[0] = sqrt(row0x * row0x + row0y * row0y)
scale[1] = sqrt(row1x * row1x + row1y * row1y)

// 如果其行列式为负,则做一次轴翻转。
double determinant = row0x * row1y - row0y * row1x
if (determinant < 0)
    // 以最小单的位向量数量积做轴翻转。
    if (row0x < row1y)
        scale[0] = -scale[0]
    else
        scale[1] = -scale[1]

// 重归一化矩阵以移除缩放。
if (scale[0])
    row0x *= 1 / scale[0]
    row0y *= 1 / scale[0]
if (scale[1]) 
    row1x *= 1 / scale[1]
    row1y *= 1 / scale[1]

// 计算旋转并重归一化矩阵。
angle = atan2(row0y, row0x); 

if (angle)
    // Rotate(-angle) = [cos(angle), sin(angle), -sin(angle), cos(angle)]
    //                = [row0x, -row0y, row0y, row0x]
// 感谢先前的归一化行为,此处可以直接使用坐标而无需重新通过三角函数计算。
    double sn = -row0y
    double cs = row0x
    double m11 = row0x
    double m12 = row0y
    double m21 = row1x
    double m22 = row1y
    row0x = cs * m11 + sn * m21
    row0y = cs * m12 + sn * m22
    row1x = -sn * m11 + cs * m21
    row1y = -sn * m12 + cs * m22

m11 = row0x
m12 = row0y
m21 = row1x
m22 = row1y

// 转换为角度,因为我们的旋转函数是这样需求的。
angle = rad2deg(angle)

return true

解析后的 2D 矩阵的插值

在最终完成之前,两个 2D 矩阵分解后的值会被插值,根据以下算法

输入:  translationA    ; 一个具有两个分量的向量
       scaleA          ; 一个具有两个分量的向量
       angleA          ; 旋转
       m11A            ; 2*2 矩阵的 (1,1) 坐标位置
       m12A            ; 2*2 矩阵的 (1,2) 坐标位置
       m21A            ; 2*2 矩阵的 (2,1) 坐标位置
       m22A            ; 2*2 矩阵的 (2,2) 坐标位置
       translationB    ; 一个具有两个分量的向量
       scaleB          ; 一个具有两个分量的向量
       angleB          ; 旋转
       m11B            ; 2*2 矩阵的 (1,1) 坐标位置
       m12B            ; 2*2 矩阵的 (1,2) 坐标位置
       m21B            ; 2*2 矩阵的 (2,1) 坐标位置
       m22B            ; 2*2 矩阵的 (2,2) 坐标位置

// 如果一个矩阵是 x 轴翻转,而另一个矩阵是 y 轴翻转,则转换为一个非翻转的旋转。
if ((scaleA[0] < 0 && scaleB[1] < 0) || (scaleA[1] < 0 && scaleB[0] < 0))
    scaleA[0] = -scaleA[0]
    scaleA[1] = -scaleA[1]
    angleA += angleA < 0 ? 180 : -180

// 就近旋转不绕远。
if (!angleA)
    angleA = 360
if (!angleB)
    angleB = 360

if (abs(angleA - angleB) > 180)
    if (angleA > angleB)
        angleA -= 360
    else 
        angleB -= 360

之后,变换分解后的结果中的每个部分,scale、angle、源矩阵的m11 到 m22 与目标矩阵的每一个对应的部分进行线性插值。

重组一个 2D 矩阵

在插值之后,产生的结果值用于转换元素的用户空间。使用这些值的一种方法是将它们重组为一个 4*4 的矩阵。这可以通过以下伪代码来完成:Question.png

输入: translation    ; 一个具有两个分量的向量
       scale          ; 一个具有两个分量的向量
       angle          ; 旋转
       m11            ; 2*2 矩阵的 (1,1) 坐标位置
       m12            ; 2*2 矩阵的 (1,2) 坐标位置
       m21            ; 2*2 矩阵的 (2,1) 坐标位置
       m22            ; 2*2 矩阵的 (2,2) 坐标位置
输出: matrix         ; 一个 4*4 的矩阵,初始为单位矩阵。

matrix[0][0] = m11
matrix[0][1] = m12
matrix[1][0] = m21
matrix[1][1] = m22

// 平移矩阵。
matrix[3][0] = translate[0] * m11 + translate[1] * m21
matrix[3][1] = translate[0] * m12 + translate[1] * m22

// 准备旋转参数。
angle = deg2rad(angle);
double cosAngle = cos(angle);
double sinAngle = sin(angle);

// 创建一个临时的旋转矩阵 rotateMatrix。
rotateMatrix[0][0] = cosAngle
rotateMatrix[0][1] = sinAngle
rotateMatrix[1][0] = -sinAngle
rotateMatrix[1][1] = cosAngle

matrix = multiply(matrix, rotateMatrix)

// 缩放矩阵。
matrix[0][0] *= scale[0]
matrix[0][1] *= scale[0]
matrix[1][0] *= scale[1]
matrix[1][1] *= scale[1]

 3D 矩阵的插值

解析一个 3D 矩阵

下面的伪代码是基于《Graphics Gems II》(作者 Jim Arvo)中的非矩阵方案,但是编辑为使用四元数代替欧拉角来避免方向节锁问题。 下面的伪代码在一个 4*4 的齐次矩阵上工作:

输入: matrix         ; 一个 4*4 的矩阵
输出: translation    ; 一个具有 3 个分量的向量
       scale          ; 一个具有 3 个分量的向量
       skew           ; 槽扭因子 XY、XZ、YZ 表示为一个具有 3 个分量的向量
       perspective    ; 一个具有 4 个分量的向量
       quaternion     ; 一个具有 4 个分量的向量
       
如果矩阵不可分解则返回 false 否则返回 true。

// 归一化矩阵。
if (matrix[3][3] == 0)
    return false

for (i = 0; i < 4; i++)
    for (j = 0; j < 4; j++)
        matrix[i][j] /= matrix[3][3]

// 虽然 perspectiveMatrix 是用于解决透视问题的,
// 但它也可以作为一种判断矩阵的 3*3 部分是否奇异的的简单方式。
perspectiveMatrix = matrix

for (i = 0; i < 3; i++)
    perspectiveMatrix[i][3] = 0

perspectiveMatrix[3][3] = 1 Note.png

if (determinant(perspectiveMatrix) == 0)
    return false

// 将透视部分从中分离出来
if (matrix[0][3] != 0 || matrix[1][3] != 0 || matrix[2][3] != 0)
    // rightHandSide 是方程的右边。
    rightHandSide[0] = matrix[0][3]
    rightHandSide[1] = matrix[1][3]
    rightHandSide[2] = matrix[2][3]
    rightHandSide[3] = matrix[3][3]

    // 求出 perspectiveMatrix 的逆矩阵,再让 rightHandSide 乘以这个逆矩阵,以此来解这个方程。
    inversePerspectiveMatrix = inverse(perspectiveMatrix)
    transposedInversePerspectiveMatrix = transposeMatrix4(inversePerspectiveMatrix)
    perspective = multVecMatrix(rightHandSide, transposedInversePerspectiveMatrix)
else
    // 没有透视的情况。
    perspective[0] = perspective[1] = perspective[2] = 0
    perspective[3] = 1

// 下一步是处理它的平移
for (i = 0; i < 3; i++)
    translate[i] = matrix[3][i]

// 现在处理缩放和裁剪。“row”是一个具有三个元素的数组,每个元素是具有三个分量的向量。
for (i = 0; i < 3; i++)
    row[i][0] = matrix[i][0]
    row[i][1] = matrix[i][1]
    row[i][2] = matrix[i][2]

// 计算 X 的缩放因子,并归一化“row”的第一行。
scale[0] = length(row[0])
row[0] = normalize(row[0])

// 计算 XY 的裁剪因子,并让“row”的第二行与第一行正交。
skew[0] = dot(row[0], row[1])
row[1] = combine(row[1], row[0], 1.0, -skew[0])

// 现在,计算 Y 的缩放因子,并归一化“row”的第二行。
scale[1] = length(row[1])
row[1] = normalize(row[1])
skew[0] /= scale[1];

// Compute XZ and YZ shears, orthogonalize 3rd row
// 计算 XZ 和 YZ 的裁剪因子,并让“row”的第三行与前面的行正交。
skew[1] = dot(row[0], row[2])
row[2] = combine(row[2], row[0], 1.0, -skew[1])
skew[2] = dot(row[1], row[2])
row[2] = combine(row[2], row[1], 1.0, -skew[2])

// 下一步,算出 Z 的缩放因子,并归一化“row”的第三行。
scale[2] = length(row[2])
row[2] = normalize(row[2])
skew[1] /= scale[2]
skew[2] /= scale[2]

// 此刻,该矩阵(在行上)是一个标准正交矩阵。
// 检查坐标系的翻转,如果其行列式为 -1 ,那么取该矩阵和其缩放因子的相反数。
pdum3 = cross(row[1], row[2])
if (dot(row[0], pdum3) < 0)
    for (i = 0; i < 3; i++)
        scale[i] *= -1;
        row[i][0] *= -1
        row[i][1] *= -1
        row[i][2] *= -1

// 现在,抽取出其旋转。
quaternion[0] = 0.5 * sqrt(max(1 + row[0][0] - row[1][1] - row[2][2], 0))
quaternion[1] = 0.5 * sqrt(max(1 - row[0][0] + row[1][1] - row[2][2], 0))
quaternion[2] = 0.5 * sqrt(max(1 - row[0][0] - row[1][1] + row[2][2], 0))
quaternion[3] = 0.5 * sqrt(max(1 + row[0][0] + row[1][1] + row[2][2], 0))

if (row[2][1] > row[1][2])
    quaternion[0] = -quaternion[0]
if (row[0][2] > row[2][0])
    quaternion[1] = -quaternion[1]
if (row[1][0] > row[0][1])
    quaternion[2] = -quaternion[2]

return true

 解析后的 3D 矩阵的插值

解析后的值的每个部分,translation、scale、skew 和源矩阵的 perspective 与目标矩阵的每一个对应部分进行线性插值。

注意:在一些实例中,源矩阵的 translate[0] 和目标矩阵的 translate[0] 被做数值化的插值, 并且其结果是用于设置动画中元素的平移。

解析后的源矩阵四元数与目标矩阵四元数做球面线性插值(Slerp),用以下伪代码来描述。

输入:  quaternionA    ; 一个具有四个分量的向量
        quaternionB    ; 一个具有四个分量的向量
        t              ; 一个满足 0 <= t <= 1 的插值参数
输出:  quaternionDst  ; 一个具有四个分量的向量


product = dot(quaternionA, quaternionB)

// 将 product 的值钳制在 -1 到 1 这个闭区间上
product = max(product, 1.0)
product = min(product, -1.0)

if (product == 1.0)
   quaternionDst = quaternionA
   return

theta = acos(dot)
w = sin(t * theta) * 1 / sqrt(1 - product * product)

for (i = 0; i < 4; i++)
  quaternionA[i] *= cos(t * theta) - product * w
  quaternionB[i] *= w
  quaternionDst[i] = quaternionA[i] + quaternionB[i]

return


重组一个 3D 矩阵

在插值之后,产生的结果值用于转换元素的用户空间。使用这些值的一种方法是将它们重组为一个 4*4 的矩阵。这可以通过以下伪代码来完成:

输入:  translation     ; 一个具有 3 个分量的向量
        scale           ; 一个具有 3 个分量的向量
        skew            ; 扭曲因子 XY,XZ,YZ 作为一个具有 3 个分量的向量来描述
        perspective     ; 一个具有 4 个分量的向量
        quaternion      ; 一个具有 4 个分量的向量
输出:  matrix          ; 一个 4*4 的矩阵

支持的函数(matrix 是一个 4*4 的矩阵):
  matrix  multiply(matrix a, matrix b)   返回 a*b 的矩阵积 4*4 的矩阵  

// 应用透视
for (i = 0; i < 4; i++)
   matrix[i][3] = perspective[i]  

// 应用平移
for (i = 0; i < 3; i++)
  for (j = 0; j < 3; j++)
    matrix[3][i] += translation[j] * matrix[j][i]

// 应用旋转
x = quaternion[0]
y = quaternion[1]
z = quaternion[2]
w = quaternion[3]

// 从四元数生成一个复合旋转矩阵
// rotationMatrix 的初始状态是一个 4*4 的单位矩阵
rotationMatrix[0][0] = 1 - 2 * (y * y + z * z)
rotationMatrix[0][1] = 2 * (x * y - z * w)
rotationMatrix[0][2] = 2 * (x * z + y * w)
rotationMatrix[1][0] = 2 * (x * y + z * w)
rotationMatrix[1][1] = 1 - 2 * (x * x + z * z)
rotationMatrix[1][2] = 2 * (y * z - x * w)
rotationMatrix[2][0] = 2 * (x * z - y * w)
rotationMatrix[2][1] = 2 * (y * z + x * w)
rotationMatrix[2][2] = 1 - 2 * (x * x + y * y)

matrix = multiply(matrix, rotationMatrix)

// 应用扭曲
// temp 的初始状态是一个 4*4 的单位矩阵
if (skew[2])
    temp[2][1] = skew[2]
    matrix = multiply(matrix, temp)

if (skew[1])
    temp[2][1] = 0
    temp[2][0] = skew[1]
    matrix = multiply(matrix, temp)

if (skew[0])
    temp[2][0] = 0
    temp[1][0] = skew[0]
    matrix = multiply(matrix, temp)

// 应用缩放
for (i = 0; i < 3; i++)
  for (j = 0; j < 3; j++)
    matrix[i][j] *= scale[i]

return