SVG SMIL动画:超越CSS的复杂动画技术

本文深入探讨SVG SMIL动画技术,详细解析其语法、属性控制、变换动画、同步机制和运动路径动画,展示如何实现复杂而精确的动画效果,超越CSS动画的局限性。

Smashing Animations Part 3: SMIL并未消亡,宝贝,SMIL并未消亡

尽管CSS动画有多种方式能让设计生动起来,但在SVG中添加简单的SMIL(同步多媒体集成语言)动画能让它们实现更多功能。Andy Clarke解释了SVG中的SMIL动画在CSS力所不及的地方如何发挥作用。

SMIL规范由W3C于1998年推出,用于同步多媒体。这远在CSS动画或基于JavaScript的动画库出现之前。它内置在SVG 1.1中,这就是为什么我们今天仍然可以使用它。

现在,你可能听说过SMIL已经消亡。然而,自从谷歌近十年前撤销了弃用该技术的决定后,它依然活跃且良好。对于希望以简单、语义化的方式为设计添加动画的设计师和开发者来说,它仍然是一个极佳的选择。

提示:现在有一个网站可以查看我所有的卡通标题。

介绍Mike Worth项目

我最近一直在为艾美奖获奖游戏作曲家Mike Worth开发一个新网站。他聘请我创建一个大胆的复古风格设计,展示他的作品。我在整个网站中使用动画,让观众在浏览时感到愉悦和惊喜。

Mike喜欢90年代的动画——尤其是迪士尼的《唐老鸭俱乐部》。不出所料,我对卡通的品味可以追溯到更早的Hanna-Barbera节目,如《达斯塔德利和穆特利在他们的飞行机器》、《史酷比》、《佩内洛普·皮特斯托普的冒险》、《疯狂赛车》以及当然的《瑜伽熊秀》。因此,为了解释这个时代的动画如何与SVG相关,我将在一些经典瑜伽熊卡通片的标题卡中添加SVG的SMIL动画。

从根本上说,动画通过一些基本技术改变元素的外观和位置随时间的变化。这可能只是简单地将元素上下左右移动,以创建运动的外观,比如瑜伽熊在屏幕上移动。

围绕固定点旋转对象可以创建从简单的旋转效果到完全正常事物的自然运动,比如一只熊在降落伞下从天空坠落。

缩放使元素增长、收缩或拉伸,可以增加戏剧性、创建透视或模拟深度。

改变颜色和过渡不透明度可以增加氛围、创造情绪并增强视觉叙事。仅这些基本原则就可以创建吸引注意力并改善某人使用设计的体验的动画。

这些结果都可以使用CSS动画实现,但一些SVG属性无法使用CSS进行动画处理。幸运的是,使用SVG中的SMIL动画,我们可以做得更多——并且更有趣。我们可以组合复杂的动画,沿路径移动对象,并控制它们何时开始、停止以及其间的一切。

动画可以嵌入任何SVG元素中,包括圆形、椭圆和矩形等基本形状。它们也可以封装成组、路径和多边形:

1
2
3
<circle ...>
  <animate>...</animate>
</circle>

动画也可以在元素外部定义,在SVG的其他地方,并使用xlink属性连接到它:

1
2
3
<g id="yogi">...</g>
...
<animate xlink:href="#yogi"></animate>

构建动画

<animate>只是SVG中几个动画元素之一。与attributeName值一起,它支持基于元素一个或多个属性的动画。

大多数动画解释从移动一个基本形状开始,比如这个令人兴奋的圆形:

1
2
3
4
5
6
7
<circle
  r="50"
  cx="50" 
  cy="50" 
  fill="#062326" 
  opacity="1"
/>

使用这个attributeName属性,我可以定义要动画化这个圆的哪个属性,在这个例子中,是它的cx(x轴中心点)位置:

1
2
3
<circle ... >
  <animate attributename="cx"></animate>
</circle>

单独这样什么也不做,直到我定义另外三个值。from关键字指定圆的初始位置,to指定其最终位置,以及duration(持续时间)在这两个位置之间:

1
2
3
4
5
6
7
8
<circle ... >
  <animate 
  attributename="cx"
  from="50" 
  to="500"
  dur="1s">
  </animate>
</circle>

如果我想要更精确的控制,可以用一组用分号分隔的值替换fromto

1
2
3
4
5
6
7
<circle ... >
  <animate 
  attributename="cx"
  values="50; 250; 500; 250;"
  dur="1s">
  </animate>
</circle>

最后,我可以定义动画重复多少次(repeatcount)甚至重复应该在多长时间后停止(repeatdur):

1
2
3
4
5
6
7
8
<circle ... >
  <animate 
  attributename="cx"
  values="50; 250; 500; 250;"
  dur="1s"
  repeatcount="indefinite"
  repeatdur="180s">
</circle>

大多数SVG元素都有可以动画化的属性。这张来自1959年"Brainy Bear"剧集的标题卡显示瑜伽熊在一个疯狂科学家的大脑实验中。瑜伽熊的头在圆顶下,能量在他周围辐射。

为了在瑜伽熊周围创建嗡嗡声,我的SVG包括三个路径元素,每个都有opacitystrokestroke-width属性,这些都可以动画化:

1
<path opacity="1" stroke="#fff" stroke-width="5" ... />

我动画化了每个路径的opacity,将其值从1改为0.5再改回:

1
2
3
4
5
6
7
8
<path opacity="1" ... >
  <animate 
    attributename="opacity"
    values="1; .25; 1;"
    dur="1s"
    repeatcount="indefinite">
  </animate>
</path>

然后,为了从瑜伽熊辐射能量,我指定了每个动画应该开始的时间,为每个路径使用不同的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<path ... >
  <animate begin="0"  >
</path>

<path ... >
  <animate begin=".5s"  >
</path>

<path ... >
  <animate begin="1s"  >
</path>

为了让动画看起来更自然,我可以应用多个animate元素,并给每个元素一个不同的attributename值。这些路径还包含一个stroke-width属性,我也可以通过将描边宽度在5和7之间变化来动画化:

1
2
3
4
<path ... >
  <animate attributename="opacity" ... ></animate>
  <animate attributename="stroke-width" ... ></animate>
</path>

最后,我可以动画化瑜伽熊头上的圆顶,在五秒内将其填充颜色在两个值之间变化,以创建疯狂科学家的机器正在加热的印象:

1
2
3
4
5
6
7
8
<path fill="#50D9E0" ... >
  <animate
    attributename="fill"
    values="#50D9E0; #E18C50;"
    dur="5s"
    begin="2s"
  >
</path>

实现该代码后,你很快会注意到圆顶在动画完成后会恢复到原始状态。为了在动画结束时保留其颜色,我可以添加——令人困惑的命名——fill属性和freeze值。这会将动画停止在最终状态,并防止它返回原始颜色:

1
2
3
<path fill="#50D9E0" ... >
  <animate fill="freeze">
</path>

动画化属性使这些标题卡设计变得生动,无论是通过调整基本形状的位置、不透明度和描边宽度,还是通过创建具有交错时间的复杂序列。但我还可以做更多,从下一个动画元素animateTransform开始。

animateTransform

如果<animate>控制属性,那么animateTransform则动画化变换,包括旋转、缩放、倾斜和平移。它通过改变transform属性的值来工作,比如这个平移:

1
<path transform="translate(0,0)"/>

然后,动画的工作方式与<animate>相同,添加一个attributename并指定变换类型,在这个例子中是rotate

1
2
3
4
<animatetransform 
  attributename="transform"
  type="rotate">
</animatetransform>

我可以使用fromtovalues属性来定义元素如何变换。

  • 缩放使用x和y值(0.5, 1)
  • 旋转使用度数(0-360)加上可选的x和y(360, 0, 0)
  • 平移也使用x和y值(50, 100)
  • 倾斜也使用x和y值(50, 100)

这些值的有趣之处在于,它们可以添加到元素的现有值中,而不是替换它们。例如,当一个属性包含平移值100, 0时:

1
<path transform="translate(100, 0)"/>

然后我通过100水平动画化该平移:

1
2
3
4
5
6
7
<animatetransform
  attributename="transform"
  type="translate"
  from="0, 0"
  to="100, 0"
  additive="sum">
</animatetransform>

使用值为sumadditive属性,动画值相对于原始值,通过在100上加100,从100开始动画,到200结束。

类似地,如果我给accumulate属性一个sum值,每个动画实例都会建立在上一个之上。因此,在一个元素平移100并重复五次的动画中,每次移动将是累积的,将元素移动500:

1
2
3
4
5
6
7
8
<animatetransform
  attributename="transform"
  type="translate"
  from="0, 0"
  to="100, 0"
  additive="sum"
  accumulate="sum" 
/>

这张来自1958年瑜伽熊"Big Break"剧集的标题卡显示瑜伽熊在降落伞下从天空飘落。

我需要两种类型的变换动画来产生瑜伽熊轻轻向下漂移的效果:平移和旋转。我首先向包含瑜伽熊和他的降落伞的组添加了一个animatetransform元素。我定义了他的初始垂直位置——在viewBox顶部上方1200——然后在15秒的持续时间内将他的下降平移到1000:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<g transform="translate(1200, -1200)">
  ...
  <animateTransform
    attributeName="transform"
    type="translate"
    values="500,-1200; 500,1000"
    dur="15s"
    repeatCount="1" 
  />
</g>

瑜伽熊似乎从天空落下,但运动看起来不真实。所以,我添加了第二个animatetransform元素,这次带有无限重复的+/-5度旋转,让瑜伽熊在下降过程中左右摆动:

1
2
3
4
5
6
7
8
<animateTransform
  attributeName="transform"
  type="rotate"
  values="-5; 5; -5"
  dur="14s"
  repeatCount="indefinite"
  additive="sum" 
/>

开始和停止

到目前为止,每个动画都在页面加载后立即开始。但有方法不仅可以延迟动画的开始,还可以使用begin属性精确定义它开始的位置:

在这张来自1959年"Robin Hood Yogi"的标题卡中,瑜伽熊向布布头上的苹果射箭。

默认情况下,箭头在页面加载时射出。眨眼,你可能会错过它。为了制造一些期待,我可以在两秒后开始动画:

1
2
3
4
5
6
7
8
9
<animatetransform
  attributename="transform"
  type="translate"
  from="0 0"
  to="750 0"
  dur=".25s"
  begin="2s"
  fill="freeze"
/>

或者,我可以让观众在点击箭头时射击:

1
2
3
4
<animatetransform
  ...
  begin="click"
/>

我可以结合点击事件和延迟,所有这些都不需要JavaScript,只需要一点SMIL:

1
2
3
4
<animatetransform
  ...
  begin="click + .5s"
/>

同步动画

在他1958年的"Pie-Pirates"剧集中,瑜伽熊试图偷一个馅饼,必须智斗一只斗牛犬。标题卡——由Lawrence Goble设计——显示了追逐,但唉,(剧透警告)没有偷到的馅饼。

为了让这个标题卡变得生动,我需要两组路径:一组用于瑜伽熊,另一组用于狗。我将它们都平移出viewBox的左边缘:

1
2
3
4
5
6
7
<g class="dog" transform="translate(-1000, 0)">
  ...
</g>

<g class="yogi" transform="translate(-1000, 0)">
  ...
</g>

然后,我对两组应用了一个animatetransform元素,将它们移回视图中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!-- yogi -->
<animateTransform
  attributeName="transform"
  type="translate"
  from="-1000,0"
  to="0,0"
  dur="2s"
  fill="freeze"
/>

<!-- dog -->
<animateTransform
  attributeName="transform"
  type="translate"
  from="-1000,0"
  to="0,0"
  dur=".5s"
  fill="freeze"
/>

这设置了动作,但效果感觉平淡,所以我添加了另一对动画,让两个角色弹跳:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!-- yogi -->
<animateTransform
  attributeName="transform"
  type="rotate"
  values="-1,0,450; 1,0,450; -1,0,450"
  dur=".25s"
  repeatCount="indefinite"
/>

<!-- dog -->
<animateTransform
  attributeName="transform"
  type="rotate"
  values="-1,0,450; 1,0,450; -1,0,450"
  dur="0.5s"
  repeatCount="indefinite"
/>

动画可以在页面加载时开始,在指定时间后开始,或在点击时开始。通过命名它们,它们还可以与其他动画同步。

我希望瑜伽熊首先进入画面以制造期待,在其他动画开始之前有一个短暂的停顿,同步到他到达的时刻。首先,我给瑜伽熊的平移动画添加了一个ID:

1
2
3
4
5
<animateTransform
  id="yogi"
  type="translate"
  ...
/>

注意:出于某种原因,我无法解释为什么Firefox在ID包含连字符时不会开始动画。这并不比普通浏览器聪明,但用下划线替换连字符可以解决这个问题。

然后,我应用了一个begin到他的旋转动画,它在#yogi动画结束后半秒开始播放:

1
2
3
4
5
<animateTransform
  type="rotate"
  begin="yogi.end + .5s"
  ...
/>

我可以使用begin属性以及命名动画是开始还是结束来构建复杂的同步动画集。追逐瑜伽熊的斗牛犬在瑜伽熊开始进入两秒后进入画面:

1
2
3
4
5
6
7
<animateTransform
  id="dog"
  type="translate"
  begin="yogi.begin + 2s"
  fill="freeze"
  ...
/>

在狗追上瑜伽熊一秒钟后,一个旋转变换也让他弹跳:

1
2
3
4
5
6
<animateTransform
  type="rotate"
  ...
  begin="dog.begin + 1s"
  repeatCount="indefinite" 
/>

飞驰而过的背景矩形也是同步的,这次是在斗牛犬结束奔跑前一秒:

1
2
3
4
5
<rect ...>
  <animateTransform
    begin="dog.end + -1s"
  />
</rect>

这个背景移动的时间与狗的到达同步,而狗的到达又相对于瑜伽熊的到达,构建了一个感觉相互连接的动画序列。

沿运动路径动画化

到目前为止,这些标题卡中的所有动画都是上、下、左、右或某种组合。但SVG中的SMIL还有一个方面可以为动画增加一个额外的维度:使用animateMotion元素沿运动路径动画化。

animateMotion接受与animateanimateTransform相同的所有属性和值,但添加了更多用于 finer 控制方向和时间。animateMotion使用path属性使元素能够沿运动路径移动。它还使用d值作为坐标数据,与任何传统路径相同。

在1959年的"The Runaway Bear"中,瑜伽熊必须避免猎人将他的头变成战利品。我希望瑜伽熊通过让他遵循一条路径跳入和跳出屏幕。我还想改变他冲刺的速度:进入和退出时加速,经过标题文本时减速。

我首先添加了一个path属性,使用其坐标数据给瑜伽熊一条要遵循的路线,并为我的动画指定了两秒的持续时间:

1
2
3
4
5
6
7
<g>
  <animateMotion
    dur="2s"
    path="..."
  >
  </animateMotion>
</g>

或者,我可以添加一个path元素,保持其可见,或通过将其放在defs元素中来防止其渲染:

1
2
3
<defs>
  <path id="yogi" d="..." />
</defs>

然后我可以通过在我的animateMotion中使用mpath元素来引用它:

1
2
3
4
<animateMotion
  ...
  <mpath href="#yogi" />
</animateMotion>

我在确定提供我想要的运动形状的路径之前尝试了几条路径:一条太弹跳,一条太平坦,但第三条运动路径刚刚好。几乎,因为我还想改变瑜伽熊冲刺的速度:进入和退出时加速,经过标题文本时减速。

keyPoints属性使我能够指定沿运动路径的点,然后调整瑜伽熊在它们之间花费的持续时间。为简单起见,我定义了五个介于0和1之间的点:

1
2
3
4
5
<animateMotion
  ...
  keyPoints="0; .35; .5; .65; 1;"
>
</animateMotion>

然后我添加了相同数量的keyTimes值,用分号分隔,以控制此动画的节奏:

1
2
3
4
5
<animateMotion
  ...
  keyTimes="0; .1; .5; .95; 1;"
>
</animateMotion>

现在,瑜伽熊快速通过前三个keyPoints,在经过标题文本时减速,然后在退出viewBox时再次加速。

SMIL并未消亡,宝贝。SMIL并未消亡

凭借其控制变换、动画化复杂运动路径和同步多个动画的能力,SVG中的SMIL动画仍然是强大的工具。它们可以使设计变得生动,而不需要框架或依赖JavaScript。它很紧凑,这使它非常适合小的SVG效果。

SMIL包括begin属性,这使得链接动画比使用CSS直观得多。此外,SMIL存在于SVG文件中,使其非常适合随资源传播的动画。因此,虽然SMIL按今天的标准并不现代,可能有点小众,但它仍然可以是神奇的。

不要让SMIL"

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计