SMIL动画揭秘:SVG中的动态魔法

本文深入探讨SMIL(同步多媒体集成语言)在SVG动画中的应用,通过实际案例展示如何利用SMIL实现复杂动画效果,包括路径动画、同步控制和属性变换,为设计师和开发者提供强大的动画工具。

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

虽然CSS动画有多种方式可以让设计变得生动,但在SVG中添加简单的SMIL(同步多媒体集成语言)动画可以让它们做得更多。Andy Clarke解释了SVG中的SMIL动画在CSS力所不及的地方如何发挥作用。

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

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

提示: 现在有一个网站,你可以在那里看到我所有的卡通标题。

Smashing Animations Part 1: 经典卡通如何启发现代CSS Smashing Animations Part 2: CSS遮罩如何增加额外维度

介绍Mike Worth

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

Mike喜爱90年代的动画——尤其是迪士尼的《鸭宝宝历险记》。不出所料,我对卡通的品味可以追溯到更早的Hanna-Barbera节目,如《Dastardly and Muttley in Their Flying Machines》、《Scooby-Doo》、《The Perils of Penelope Pitstop》、《Wacky Races》,当然还有《The Yogi Bear Show》。因此,为了解释这个时代的动画如何与SVG相关,我将在一些经典的Yogi Bear卡通标题卡中添加SMIL动画。

从根本上说,动画使用一些基本技术随时间改变元素的外观和位置。这可能只是简单地上下、左右移动元素,以创造运动的外观,比如Yogi Bear在屏幕上移动。

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

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

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

这些结果都可以使用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”剧集的标题卡显示Yogi在一个疯狂科学家的大脑实验中。Yogi的头在圆顶下,能量在他周围辐射。

为了创建围绕Yogi的嗡嗡声,我的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>

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

 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>

我会在短暂的商业休息后解释更多关于begin属性以及如何开始动画。

自己尝试: 参见CodePen上的Brainy Bear SVG animation [forked] by Andy Clarke。

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

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

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

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年Yogi Bear的“Big Break”剧集的标题卡显示Yogi在降落伞下从天空飘落。

我需要两种类型的变换动画来产生Yogi轻轻向下漂移的效果:平移和旋转。我首先向包含Yogi和他的降落伞的组添加了一个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>

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

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

自己尝试: 参见CodePen上的Big Break SVG animation [forked] by Andy Clarke。

开始和停止

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

在这张来自1959年“Robin Hood Yogi”的标题卡中,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"
/>

自己尝试点击箭: 参见CodePen上的Robin Hood Yogi CSS animation [forked] by Andy Clarke。

同步动画

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

为了使这个标题卡变得生动,我需要两组路径:一组用于Yogi,另一组用于狗。我将它们都平移出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"
/>

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

我希望Yogi首先进入画面以制造期待,在其他动画开始之前有一个短暂的停顿,同步到他到达的时刻。首先,我向Yogi的平移动画添加了一个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属性以及命名动画是否开始或结束来构建复杂的同步动画集。追逐Yogi的斗牛犬在Yogi开始进入两秒后进入画面:

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

在狗追上Yogi一秒钟后,一个旋转变换也使他弹跳:

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>

自己尝试: 参见CodePen上的Pie-Pirates SVG animation [forked] by Andy Clarke。

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

沿运动路径动画化

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

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

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

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

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>

我在确定提供我想要的运动形状的路径之前尝试了几条路径:

一条太弹跳,一条太平坦,但第三条运动路径刚刚好。几乎,因为我还想改变Yogi冲刺的速度:进入和退出时加速他,经过标题文本时减速他。

keyPoints属性使我能够指定沿运动路径的点,然后调整Yogi在它们之间花费的持续时间。为了简单起见,

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