同时设置多种变换
撰写时间:2026-02-23
修订时间:2026-02-27
以字符串方式设置
上面代码依序进行了平移、旋转、缩放的变换。
函数参数之间以逗号,
或空白符隔开,函数之间也以逗号,
或空白符隔开。
多种变换的先后顺序
当存在多种变换时,变换结果与变换次序相关。变换次序不一样,其结果也不一样。
取旋转与平移为例。下面先旋转30o,再进行平移。
先旋转,则局部坐标系的原点不变,局部坐标系在原点位置顺时针旋转了30o,局部坐标系朝向发生改变。然后,再沿着局部坐标系朝向向右平移50px的位移。
下面先进行平移,然后再旋转30o。
先平移,则局部坐标系的原点向右平移,导致旋转中心发生改变。然后再围绕新的旋转中心进行旋转。
在不同场合下,当我们决定到底先平移还是先旋转时,只需记住一点:平移改变局部坐标系原点;旋转围绕局部坐标系原点进行旋转。
由此,当我们需要围绕任意位置旋转时,可先将局部坐标系原点先平移至该点位置,再旋转,然后再反向平移回来。
下面代码围绕矩形的左下角逆时针旋转45o。
对于最后一步的animateTransform,需要解决的问题是,应如何平移变换后的局部坐标系的原点,才能让变换后的元素的左下角返回至变换前的元素的左下角的位置?显然,这需要经过精细的数学计算才能确定当前点与终点。但通过使用by属性值,我们只需将变换后的局部坐标系的原点平移其原先平移的偏移值的相反数值就行了,这样可无需计算当前点与终点的确切位置。
SVGTransformList
除了以字符串方式设置SVG元素的transform属性 (attribute) 值以实现变换之外,还可以对象的方式来设置。
在上面代码,rect是SVGGraphicsElement的一个实例,每个这样的实例都有一个名为transform属性 (property)。该属性的类型为SVGAnimatedTransformList。该属性的baseVal代表一个SVGTransformList对象。正如其名,该对象是多个变换 (transforms) 的集合,并提供了以下方法以维护集合元素:
- voidclear()
- SVGTransforminitialize(SVGTransformnewItem)
- getterSVGTransformgetItem(unsigned longindex)
- settervoid(unsigned longindex, SVGTransformnewItem)
- SVGTransforminsertItemBefore(SVGTransformnewItem, unsigned longindex)
- SVGTransformreplaceItem(SVGTransformnewItem, unsigned longindex)
- SVGTransformremoveItem(unsigned longindex)
- SVGTransformappendItem(SVGTransformnewItem)
- SVGTransformcreateSVGTransfomFromMatrix(DOMMatrixReadOnlymatrix)
- SVGTransform? consolidate()
多数方法的参数都需传入一个代表变换的SVGTransform对象。
上面的rect元素尚未设置任何变换,但其transformList照样存在,只不过该对象的length属性值为0。
SVGTransform
初窥SVGTransform
上面先以字面符的形式,将rect元素的transform属性值设置为translate(10,35),这样,transformList就含有了一个SVGTransform对象,其type属性值为2,对应于常量SVG_TRANSFORM_TRANSLATE的值,因此该对象代表一个平移的变换。matrix属性值以SVGMatrix的方式存储了其所有变换的内部矩阵数据。
每种表示变换的字面符函数,都会自动生成一个对应的SVGTransform对象。而对于每一个SVGTransform对象,我们均可选择下面其中的一个方法来设置(或改变)其矩阵数据:
- voidsetMatrix(DOMMatrixReadOnlymatrix)
- voidsetTranslate(floattx, floatty)
- voidsetScale(floatsx, floatsy)
- voidsetRotate(floatangle, floatcx, floatcy)
- voidsetSkewX(floatangle)
- voidsetSkewY(floatangle)
直接修改现有的SVGTransform
因此,对于上面已有的的平移变换,我们可以直接将该变换设置为新的变换,如旋转。
上面的rect原来的变换为平移,通过调用setRotate方法,让原有的变换改变为一个旋转的变换。
有几个要点需注意:
- 在一个原来的transform上面调用setXXX方法,将直接覆盖原来的变换。尽管原来的变换已平移至(10,35)的位置,但调用setRotate方法后,平移变换被移除,只有旋转一种变换。
- 调用setRotate后,transform对象的matrix属性值已被替换为旋转变换的内容,并且,type属性值由原来的SVG_TRANSFORM_TRANSLATE变更为SVG_TRANSFORM_ROTATE。
- setRotate方法很方便,可围绕第2个参数及第3个参数所表示的坐标值上旋转第1个参数所表示的角度。它在后台自动帮我们完成了上面所谈到的
平移、旋转、再反向平移
的一系列过程。 - setScale方法可没这么方便,它只有2个参数,分别代表X轴及Y轴上的缩放量,无法指定在特定位置上进行缩放,因此需组合多个SVGTransform才能达到定点缩放的效果。
一般情况下,我们较少这样直接修改一个已经存在的SVGTransform,而是通过SVGTransformList所提供的APIs来增删集合中的各种变换。
手工创建新的SVGTransform对象
开始时,rect没有设置任何变换。
先调用根元素SVGSVGElement的createSVGTransform方法,创建一个新的SVGTransform对象,其type类型默认为SVG_TRANSFORM_MATRIX。然后调用变量transform的setTranslate方法设置为平移变换,type类型自动更新为SVG_TRANSFORM_TRANSLATE。最后,调用变量transformList的appendItem方法,将其添加到变换集合中。这就完成了插入一个新变换对象的完整过程。
核心代码归结如下:
Web Animations APIs 简介
最简单的动画
等效于下面通过CSS Animations所设置的CSS Transforms效果:
两种技术,殊途同归。实际上,Web Animations是CSS-TRANSITIONS-1, CSS-ANIMATIONS-1及SVG11的统一规范应用接口。
通过对比,不难看出,animate方法第1个参数用于设置动画关键帧,第2个参数用于设置动画时间轴选项。
在大脑中将参数归位后,一切均可以再精简:
只有1个关键帧,动画时长为2000毫秒(也即2秒)。
设置关键帧
可以显式地指定关键帧的帧数:
因为未显式地指定首尾帧,则它们都默认使用无变换的值:
在上一节的例子中,因为只设置了一个未指定关键帧数的关键帧,则将该关键帧用作尾帧。
如果设置了两个以上的关键帧,则先分别确定首尾帧,其余的关键帧均匀地分布在时间轴上。
很多时候,因为偷懒,我们往往倾向于不显式地指定关键帧数:
此时,很容易出现一个小bug: 误识首帧。我们原以为动画的首帧应从未作设置任意变换开始,但实际上,因为我们指定了超过2个以上的关键键,首帧则从(50px, 50px)的位置开始。
尾帧不容易出现这种误识,即使关键帧数只有1个,我们都会地潜意识认为它必定是尾帧。
因此,个人较倾向于较好的习惯是,可以不显式地指定关键帧(好处是无需自己静态地计算百分比),但关键帧数至少为2个,且第1个专门用作首帧,即使它未设置任意变换:
这样,首尾关键帧始终非常明确。当我们根据需求修改第1帧时,我们会清醒地意识到是在修改首帧。从而避免了因隐式的首帧所带来的烦恼。
在单个关键帧中同时设置多个变换
某些变换必须同时包含多种变换才能达到特定要求。例如,需围绕元素中心进行旋转。此时我们可在一个关键帧中同时设置平移及旋转的多个变换。
设置了2帧动画,第1个关键帧取值于单位矩阵,第2个关键帧完成了围绕矩形中心的旋转。系统将自动在首尾帧之间自动生成插值 (interpolation),从而形成平滑的动画效果。
在此可以看出,Web Animation是基于编程语言的接口,在动态设置动画方面有足够的先天优势。
与SVG不同的是,代码中的translate及rotate等函数采用的是CSSTransforms的方式,函数参数必须明确带有单位。
上面的代码虽然确实展现了如何围绕元素的中心位置进行旋转的动画,但中间的动画过程是由系统自动生成的,我们所看到的动画效果,就是元素转着转着就转到元素的中心位置上去了,为何能有这种效果,光看动画而不看代码,我们无从知晓。
下面,我们将试着将第2帧动画中的多种变换拆分为多个关键帧,看看效果如何。
每个关键帧只有一种变换
现在,将上节例子中的每个变换,均单独归置于一个关键帧中:
拆分为每个关键键只有一种变换后,动画效果明显不同于上节,并且,旋转后的矩形并未定位于旋转前的中心位置。现在逐帧分析是何原因所导致。
第1帧为初始状态,平移到第2帧,没有任何问题。
第3帧确实旋转了45o,但,这一帧的效果是围绕着画布的左上角原点进行的旋转,而不是我们所希望的围绕矩形中心而旋转。这一步开始出错了。
第4帧虽反向进行了平移,但在平移的过程中,矩形转着转着又回到了旋转前的状态。最终的状态是,矩形只是向左上角进行了平移,但并未旋转。
其最终效果等同于将中间的两个关键帧删除,只保留首尾帧:
中间的平移与旋转这两个关键帧,忙活了个寂寞。
原因在于,从第2帧到第4帧,每一帧的起始状态都是最原始的状态。
将第4帧删除,只保留前3帧的动画,以看清第3帧出错的原因:
第3帧确实是旋转了,但发现它是围绕着画布左上角原点进行的旋转。当我们准备责怪电脑时,电脑说,你所给出的第3的代码为:
我确实做到了。只不过你没告诉我,在这一关键帧中,需要先平移到矩形的中心位置后才旋转。
第2帧不是告诉过你了吗?而且你第2帧做得非常正确,为何第3帧就不能保留第2帧的结果呢?
你想多了,每一个关键帧的初始状态均取自第1帧所设置的初始状态,而不是上一帧的状态。为简化问题,现假设全世界只有平移及旋转两种状态。第1帧代码:
则第1帧所设置的初始状态为:
第2帧代码:
则第2帧的最终状态为:
你在第2帧结束时所看到的,就是这个状态。
而第3帧代码:
则第3帧的最终状态为:
也即,第3帧结束时,矩形围绕画布原点进行了旋转。这不就是你给我下达的指令吗?
你如果想在第3帧围绕(80,80)旋转,则在第3帧中也需显式地设置平移的状态,如下所示:
同理,如果你想在第4帧中保留第3帧的状态,则应在第4帧代码的前面复制第3帧的代码后,再进行最终的平移。
毕竟,去杭州开会必须同时具备两个条件:第一:人先去到杭州;第二:开会,二者不可缺一。你没明确告诉我要去杭州,我敢事先给你订去杭州的机票吗?
要保留上一帧的状态,如果每一帧都必须复制上一帧的代码,又笨又麻烦,你就没有自动保留上一帧状态的机制吗?
有,但不能在每个关键帧上设置,而应将不同状态的动画拆解为不同的Animation对象,Animation对象可以自动将上一个Animation对象的状态添加进来。
