SVG SMIL动画:经典动画技术的现代应用

本文详细介绍了SMIL(同步多媒体集成语言)在SVG动画中的应用,包括基础属性动画、变换动画、同步控制和路径动画,展示了如何通过SMIL实现复杂而精美的动画效果,无需依赖JavaScript或框架。

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指定其最终位置,以及dur-ation(持续时间)介于这两个位置之间:

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更改为.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值(.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”的标题卡中,瑜伽向Boo-Boo头上的苹果射箭。

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

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包含连字符时不会开始带有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相同的所有属性和值,但添加了一些用于更精细控制方向和时间的内容。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 设计