出现这些问题,主要是我想要实现一个更通用的 FLIP 动画,FLIP 这里就不赘述了。我遇到的困扰,主要是怎么记录元素状态。在做变换动画时候,希望用 transform 动画,取代会引起重绘元素属性修改,例如位置信息适用 translate 替代,长宽变化适用 scale 替代。这时候就需要获取元素信息。在这个过程中,我遇到了不少问题。越搞越觉得 MDN 上写的太简单了。尤其是我用到的 getBoundingClientRect 和 getComputedStyle API,问题一言难尽,倒也不是 MDN 刻意写简单了,里面涉及的问题本身就比较杂。
getBoundingClientRect 注意事项
getBoundingClientRect 可以获取元素相对视口的位置关系(DOMRect)。这样位置反转的思路就很简单,只需要获取开始结束的 (x/left, y/top)
,计算差值,使用 translate 进行反转即可。
实际使用中发现 DOMRect 存在几个问题。首先要明确,它计算的是相对视口的位置关系。用以下 DOM 结构进行示范。
xml
<style>
html,
body {
height: 100%;
}
body {
margin: 0;
}
.wrapper {
height: 100%;
overflow-y: scroll;
}
.box {
width: 300px;
height: 400px;
margin: auto;
}
#box {
background-color: palegreen;
margin-top: 100px;
}
</style>
<div class="wrapper">
<div id="box" class="box"></div>
</div>
获取此时 div#box
的 DOMRect,可以看到 y: 100
。如果设置 wrapper.scrollTop = 200
,就会发现 y 变成了 -100。明确了这一点,就需要注意一种情况,变换中元素相对视口位置,发生了意外的变化。
在 div#box 前插入一个 <div id="dummy" class="box"></div>
,在一秒钟后移除,重新计算 rect,看看效果。
ini
setTimeout(() => {
const dummy = document.querySelector('#dummy');
dummy.remove()
const rect = box.getBoundingClientRect();
console.log('Box Rect:', rect);
}, 1000);
这里我们没有对 div#box 进行定位操作,rect 的 y 值依旧从 500 变到了 100。这种情况可以使用 FLIP,也可以不用,完全取决于动画变现目的。多个元素的移动,只对一个元素做动画,具有一定的引导效果。
接下来的问题就比较麻烦了,DOMRect 是受 transform 影响的。
都知道一个 3 4 5 的直角三角形,每个角大概是 37、53 和 90。给 div#box 添加 rotate(53deg)
,查看此时的 DOMRect。会发现 width=499.9986877441406
,大致是矩形对角线宽度。y=59.84166717529297
,对于这种直角三角形,底边上的高长度应该是 240,原本中心到顶部位置是 200,差了 40,也是差不多的。
css
#box {
...
transform: rotate(53deg);
}
这也就说明,DOMRect 计算的是进行变换后的元素相对视口的的信息。听起来很合理,但这意味着我们需要将变换效果纳入考量。不能直接使用 DOMRect 中的 x 和 y 计算位置信息,更不能使用 width 和 height 计算内容区宽高相对关系。
所以我们需要元素的 css 信息。
getComputedStyle
getComputedStyle 是 Window 下面的一个方法,返回对应元素的所有 css 属性。它有两个参数,第一个参数 element 是目标元素,第二个参数 pseudoElt 可选,可传入伪元素选择器的字符串来匹配伪元素。属性值会实时同步,获取时进行计算。
transfrom 会影响 DOMRect 计算,移除 transform 不就可以正常计算了嘛。但是在移除前,需要保存原来的 transform,反转时需要应用原来的 transform。上面说过,DOMRect 的宽高不能直接用,也要用 css 属性。
arduino
const cssComputedStyle = window.getComputedStyle(box);
console.log(cssComputedStyle.width); // 300px
box.style.width = '500px';
console.log(cssComputedStyle.width); // 500px 可以看到是同步的
需要注意几点,css 同一属性可能有多种写法,getComputedStyle 返回的值是被统一格式化的,和我们目标相关的主要是 transform
和 transform-origin
。transform 返回的 matrix 格式的字符串,transform-origin 返回的是 offset 格式的。示例代码中 div#box 返回结果如下:
javascript
const cssComputedStyle = window.getComputedStyle(box);
cssComputedStyle.getPropertyValue('transform'); // "matrix(0.601815, 0.798636, -0.798636, 0.601815, 0, 0)"
cssComputedStyle.getPropertyValue('transform-origin'); // "150px 200px"
transform-origin 默认值是 center,也是元素x、y轴中心点。这里元素宽度是 300px,一半也就是 150px,高度同理。需要注意的是,transform-origin 是按照元素未做变形的形状确定的。给 div#box 添加 scale(0.5)
,可以看到 transform-origin 依旧是 "150px 200px"。也很好理解,transform-origin 是变换的原点,要先有这个点才能变换,不变也是合理的。
transform 要复杂一点,返回的值是 matrix,如果是 3D 变换,就是 matrix3d。通常我们使用的都是单独的 transform 函数,例如 scale、translate 等,遇到 matrix 会有一点懵。首先要明白线性代数中,矩阵如果计算,简单说就是行乘列。简单举几个例子:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> [ 1 0 o f f s e t X 0 1 o f f s e t Y ] [ o l d X o l d Y 1 ] = [ o l d X + o f f s e t X o l d Y + o f f s e t Y ] \begin{bmatrix} 1 & 0 & offsetX \\ 0 & 1 & offsetY \end{bmatrix} \begin{bmatrix} oldX \\ oldY \\ 1 \end{bmatrix} = \begin{bmatrix} oldX + offsetX \\ oldY + offsetY \end{bmatrix} </math>[1001offsetXoffsetY] oldXoldY1 =[oldX+offsetXoldY+offsetY]
上面就是 translate 的计算过程,分解看第一行乘第一列,1 \times oldX + 0 \times oldY + offsetX \times 1 = oldX + offsetX,第二行乘第一列同理。transform 可以添加多种变换,最终都可以转换成这种矩阵形式。对于单独的变换只需要用上面这种一个 2 x 3 表示变换方式,加一个一维的位置矩阵即可完成变换。多个变换,需要累乘多个矩阵,所以就填充成 3 x 3 矩阵。这样 3 x 3 矩阵相乘,结果还是 3 x 3 矩阵。
scale(x,y) | scaleX(x) | scaleY(y) | translateX(x) | translateY(y) | translate(x, y) |
---|---|---|---|---|---|
<math xmlns="http://www.w3.org/1998/Math/MathML"> [ x 0 0 0 y 0 0 0 1 ] \begin{bmatrix} x & 0 & 0 \\ 0 & y & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} </math> x000y0001 | <math xmlns="http://www.w3.org/1998/Math/MathML"> [ x 0 0 0 1 0 0 0 1 ] \begin{bmatrix} x & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} </math> x00010001 | <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 0 0 0 y 0 0 0 1 ] \begin{bmatrix} 1 & 0 & 0 \\ 0 & y & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} </math> 1000y0001 | <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 0 x 0 1 0 0 0 1 ] \begin{bmatrix} 1 & 0 & x \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} </math> 100010x01 | <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 0 0 0 1 y 0 0 1 ] \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & y \\ 0 & 0 & 1 \\ \end{bmatrix} </math> 1000100y1 | <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 0 x 0 1 y 0 0 1 ] \begin{bmatrix} 1 & 0 & x \\ 0 & 1 & y \\ 0 & 0 & 1 \\ \end{bmatrix} </math> 100010xy1 |
skewX(x) | skewY(x) | skew(x,y) | rotete( <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ) | ||
<math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 tan x 0 0 1 0 0 0 1 ] \begin{bmatrix} 1 & \tan x & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} </math> 100tanx10001 | <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 0 0 tan y 1 0 0 0 1 ] \begin{bmatrix} 1 & 0 & 0 \\ \tan y & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} </math> 1tany0010001 | <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 tan x 0 tan y 1 0 0 0 1 ] \begin{bmatrix} 1 & \tan x & 0 \\ \tan y & 1 & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} </math> 1tany0tanx10001 | <math xmlns="http://www.w3.org/1998/Math/MathML"> [ cos θ − sin θ 0 sin θ cos θ 0 0 0 1 ] \begin{bmatrix} \cos \theta & -\sin \theta & 0 \\ \sin \theta & \cos \theta & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} </math> cosθsinθ0−sinθcosθ0001 |
以上就是所有的变形矩阵,想要看细节可以看这里。
这里单独解释一下旋转,旋转很难搞明白。其他的变换都是复合变换,有一个 x 轴,有一个 y 轴变换,单独拆分看都不麻烦。其实旋转也一样,把旋转拆分成 x、y 轴两种。假设有点 (x, y),可以拆分成 x 轴上的向量 (x, 0),和 y 轴上向量 (0, y)。注意,前端的旋转是顺时针旋转,但是 y 轴是向下的。我们以前做题的坐标系,y 轴是向上的,对应过来旋转就应该是逆时针旋转。(x, 0) 旋转后应该是 (xcos <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ, xsin <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ)。(0, y)旋转后回到第二象限,最终是 (-ysin <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ, ycos <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ)。旋转后点坐标就应该是,两个的 x 坐标与 y 坐标分别相加 (xcos <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ - ysin <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ, xsin <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ + ycos <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ)。根据矩阵乘法,变换矩阵也就是上面的样子。
简单变换中有如下对应关系, matrix(a, b, c, d, tx, ty) = matrix(sclaeX, skewY, skewX, scaleY, translateX, translateY)
。这里旋转角度可以根据 a、b 的值确定。假设旋转角度是 θ,a = cosθ,b = sinθ
,这样就可以使用 Math.atan2(b, a)
计算角度,Math.atan2 返回的是弧度,可以转成角度。
上述的方法,是你在网上最容易查到的信息。不能说完全错,但是要注意上述方法,只适用简单的变换!!! 对于多个变换,要进行矩阵累乘,矩阵乘法并不适用交换律。旋转和缩放的复合过程,有两种方式,第一种,先旋转,后缩放,计算如下:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> [ cos θ − sin θ 0 sin θ cos θ 0 0 0 1 ] [ x 0 0 0 y 0 0 0 1 ] = [ x cos θ − y sin θ 0 x sin θ y cos θ 0 0 0 1 ] \begin{bmatrix} \cos \theta & -\sin \theta & 0 \\ \sin \theta & \cos \theta & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \begin{bmatrix} x & 0 & 0 \\ 0 & y & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} = \begin{bmatrix} x\cos \theta & -y\sin \theta & 0 \\ x\sin \theta & y\cos \theta & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} </math> cosθsinθ0−sinθcosθ0001 x000y0001 = xcosθxsinθ0−ysinθycosθ0001
反之,先缩放后旋转,则是这种:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> [ x 0 0 0 y 0 0 0 1 ] [ cos θ − sin θ 0 sin θ cos θ 0 0 0 1 ] = [ x cos θ − x sin θ 0 y sin θ y cos θ 0 0 0 1 ] \begin{bmatrix} x & 0 & 0 \\ 0 & y & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} \begin{bmatrix} \cos \theta & -\sin \theta & 0 \\ \sin \theta & \cos \theta & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} = \begin{bmatrix} x\cos \theta & -x\sin \theta & 0 \\ y\sin \theta & y\cos \theta & 0 \\ 0 & 0 & 1 \\ \end{bmatrix} </math> x000y0001 cosθsinθ0−sinθcosθ0001 = xcosθysinθ0−xsinθycosθ0001
对于这两种方式,计算旋转角度的方式完全不同。第一种变换方式,尚且适用我们前面说的变换方式,a、b 分别对应 x <math xmlns="http://www.w3.org/1998/Math/MathML"> cos θ \cos \theta </math>cosθ 和 x <math xmlns="http://www.w3.org/1998/Math/MathML"> sin θ \sin \theta </math>sinθ,计算后 x 会被消掉。第二种方法,则是完全行不通。
在实际代码中,也能看到,两种变换结果完全不同。遗憾的是,我们不能规定如何使用 transform 函数。各种变换后,我也没能找到规律去处理,这个问题目前就只能先放下。好处是我对 transform 的理解更深了一点,虽然用处不好。另外,万幸我要做的动画效果没有那么复杂的复合关系,简化了一些处理最终还是能用。