👴🏻 SMIL 驾鹤西去,万寿无疆!

SMIL,SVG 的原生动画规范,曾经风光无限,凭借着其强大的功能和高效的渲染能力,在 SVG 动画领域呼风唤雨。然而,时过境迁,SMIL 的支持在 WebKit 中日渐式微,而微软的 IE 和 Edge 浏览器更是从未支持过 SMIL,也几乎不可能在未来支持。

别担心!我们今天就来探讨一些 SMIL 特有的功能,并深入研究如何用其他方法来实现相同的效果,以确保你的动画能拥有更广泛的浏览器兼容性。

🏃🏻‍♀️ 沿着路径运动

SMIL 最吸引人的地方之一就是它能够让 SVG 对象沿着路径运动,从而实现更加逼真的动画效果。毕竟,现实生活中很少有物体是沿着直线运动的,沿着路径运动可以让我们模拟现实生活中的各种运动轨迹。

在过去,你需要将 SVG 路径数据传递给 animateMotion 元素,并使用 path 属性来定义路径数据。然后,你可以通过 xlink:href 属性来指定要进行动画的元素。

<animateMotion 
  xlink:href="#lil-guy" 
  dur="3s" 
  repeatCount="indefinite" 
  fill="freeze" 
  path="M 25,50 C 37.5,25 37.5,25 50,0 75,50 75,50 100,100 50,100 50,100 0,100 12.5,75 12.5,75 25,50 Z" />

替代方案:CSS

幸运的是,现在 CSS 也支持沿着路径运动的功能了!虽然目前支持的浏览器还不多(仅限于 Chrome、Opera 和 Android),但 Sara Soueidan 已经提议在 Edge 中加入该功能,并且得到了强烈的支持,在本文发布时已经获得了超过 420 票。请加入我们,一起呼吁该功能早日实现!Firefox 的投票页面 在这里

至于 Safari,据我所知,它的支持情况可能需要单独处理。我已经注册了一个 bug 报告,并请求在 CSS 中添加沿着路径运动的功能。

为了在 CSS 中使用沿着路径运动,你需要将路径数据传递给 offset-path 属性,就像这样:

.move-me {
  offset-path: path('M3.9,74.8c0,0,0-106.4,75.5-42.6S271.8,184,252.9,106.9s-47.4-130.9-58.2-92s59.8,111.2-32.9,126.1 S5.9,138.6,3.9,74.8z');
}

我通常会在 Illustrator 中创建 SVG,然后使用 SVGOMG 进行优化,以获取路径数据。

在这个例子中,我希望动画对象沿着路径从起点运动到终点,并且路径是一个闭合路径,因为路径数据末尾有一个 z。这意味着路径是一个循环,所以这个小生物最终会回到起点。我在关键帧中设置了这些参数,只指定了 100% 的值,因为默认值为 0

@keyframes motionpathguy {
  100% {
    motion-offset: 100%;
  }
}

然后,将动画应用于元素:

.move-me {
  animation: motionpathguy 10s linear infinite both;
}

替代方案:GreenSock 的沿着路径运动

如果你想要最广泛的浏览器支持和最灵活的实现方式,那么你应该使用 GreenSock。GSAP 的 Bezier 插件(默认情况下包含在 TweenMax 中)支持 IE7 及更高版本(非 SVG 元素),以及 IE9 及更高版本(SVG 元素),这是目前最广泛的 SVG 动画支持。它在移动设备上也运行得很好。

我之前在 David Walsh 博客上 写过关于这个插件的文章,但这里只是简要回顾一下,并介绍一些自那以后发布的新功能:

最初,你需要传递一个值数组:

bezier: {
  type: "soft",
  values:[{x:10, y:30}, {x:-30, y:20}, {x:-40, y:10}, {x:30, y:20}, {x:10, y:30}],
  autoRotate: true
}

但正如你所看到的,你还可以选择自动旋转(或不旋转),就像 SMIL 的 rotate 属性一样。如果你想使用 SMIL 中的 auto-reverseauto:n 参数来指定旋转的初始位置或旋转角度,GSAP 允许你使用 rotation:90 来更改旋转角度,或者如果你需要更精细的控制,可以使用更具体的设置:

autorotate: [
  first position property, like "x",
  second position property, like "y",
  rotation property, typically "rotation" but can be “rotationY”,
  integer for radians/degrees the rotation starts from like 10,
  boolean for radians or degrees- radians is true
]

在 SMIL 中,你可以对路径或组进行变换,以改变动画对象在运动过程中的方向。在 GSAP 中,你可以通过 autoRotate: false 轻松实现这一点,并使用 set 初始化旋转。你也可以像在 SMIL 中一样,在 SVG 属性本身对元素进行变换,但这有点不太优雅,而且在工作时更难跟踪。

TweenMax.set("#foo" {
  rotation: 90 // or whatever number
});

你还可以将 type 属性设置为 thrusoftquadraticcubic。有关这些属性的更多文档,请查看 GreenSock API 文档thru 属性的一个很好的用途是能够影响元素的弯曲程度。如果你将这些点视为弹跳的坐标,那么弯曲程度将控制在这些点之间采取的路径的直接程度。0 表示直线路径,1 表示稍微松散的路径,2 表示一个漂亮的曲线,而 3 及更高的值将开始在自身上缠绕。

graph LR
  subgraph 0
    A[0]
  end
  subgraph 1
    B[1]
  end
  subgraph 2
    C[2]
  end
  subgraph 3
    D[3]
  end
  A --> B
  B --> C
  C --> D

最近,GreenSock 也提供了将路径数据传递给 CSS 和 SMIL 模块的能力,就像使用原生 SMIL 一样。这作为他们 MorphSVG 插件的扩展,因此你需要添加该插件,并像这样使用它:

TweenMax.to("#lil-guy", 3, {
  bezier: {
    MorphSVGPlugin.pathDataToBezier("#path", {align: "#lil-guy" }), 
    type: "cubic"
  },
  ease: Linear.easeNone,
  repeat: -1
});
<path id="path" d="M 25,50 C 37.5,25 37.5,25 50,0 75,50 75,50 100,100 50,100 50,100 0,100 12.5,75 12.5,75 25,50 Z" fill="none" />

默认情况下,会将我要进行动画的组(在本例中为 #lil-guy)的左上角与路径轨迹对齐。这会导致视觉上的错位。因此,我使用 TweenLite.set#lil-guy 设置为使用中心点:

TweenLite.set("#lil-guy", {xPercent:-50, yPercent:-50});

你还可以通过将一个对象作为该方法的第二个参数传递,并在 pathDataToBezier 中定义 offsetXoffsetY 来偏移这些路径,注意,你可能需要扩展 viewBox,以确保你正在进行动画的组或属性不会被裁剪掉。

// 将路径坐标在 x 轴上偏移 125px,在 y 轴上偏移 50px:
TweenMax.to("#lil-guy", 3, {
  bezier: {
    values: MorphSVGPlugin.pathDataToBezier("#path", {
      offsetX: 125, 
      offsetY: 50, 
      align: "#lil-guy"
    }),
    type: "cubic"
  },
  ease: Linear.easeNone,
  repeat: -1
});

你甚至可以为这个定位定义一个矩阵坐标。

// 将路径坐标放大 1.25 倍
// 并将其在 x 轴上偏移 120px
// 在 y 轴上偏移 30px:
TweenMax.to("#lil-guy", 3, {
  bezier: {
    values: MorphSVGPlugin.pathDataToBezier("#path", {
      matrix:[1.5,0,0,1.5,120,-30], 
      align:"lil-guy"}),
    type: "cubic"
  },
  ease: Linear.easeNone,
  repeat: -1
});

另一个选择是将 align 属性设置为 “relative”。这将防止动画对象跳跃,因为它会保持每个坐标相对于 x:0y:0 的位置。在之前的示例中,我使用 align 将运动与 #lil-guy 组本身配对。

有关 GreenSock 的 Bezier 插件 API 中这个新功能(新是指在本文发布之日新发布的功能!)的更多信息,请查看他们的 文档,以及这个 很棒的解释视频

🎭 形状变形

以前,你可以将路径数据作为值传递给 animate 属性,以使形状变形。Noah Blon 有一个很好的例子:

替代方案:Snap.svg 或 SVG Morpheus

一些库提供了变形路径或形状值,例如 Snap.svg 和 SVG Morpheus,但需要注意的是(即使在 SMIL 中也是如此),形状必须具有相同数量的点,否则变形看起来很糟糕,或者完全失败。这在预处理方面令人失望,因为这意味着你必须仔细跟踪你正在制作的内容,或者与你的设计师良好协作,以确保你获得这些(有时是任意的)中间点数据。额外的点也会不必要地膨胀你的代码。

替代方案:GreenSock MorphSVG

我强烈推荐 GSAP 的 MorphSVG 插件,因为它可以很好地变形具有不同数量点的形状和路径。请查看本网站徽标上的切换按钮,以演示变形的效果。这里还有另一个例子:

https://codepen.io/sdras/pen/XqYxoy

因为 MorphSVG 插件可以对路径数据进行动画处理,所以如果你需要转换形状,可以使用他们的 convertToPath 选项:

MorphSVGPlugin.convertToPath("ellipse"); 
// or circle, rect, etc

这使我们能够进行非常复杂的形状动画,并且是 Web 上所有运动的改变者。

这个插件还提供了一些额外的功能,使其更加出色。第一个是实用程序插件 findShapeIndex。假设你对形状的变形方式不满意(虽然十有八九自动预设会正常工作),你可以加载该插件(别担心,你不需要在生产中添加额外的重量,因为它不需要),并将两个值传递给它:要进行动画的第一个形状的 ID 和第二个形状的 ID。一个 GUI 会弹出,你可以在其中切换值,它还会自动使用 repeat: -1,以便它会不断地在形状之间循环。

findShapeIndex("#hex", "#star");
// you can comment out above line to automatically disable findShapeIndex() UI

一旦你有了这个额外的值,你就可以在 morphSVG 对象中传递 shapeIndex

TweenLite.to("#hex", 1, {morphSVG: { shape: "#star", shapeIndex: 1 }});

第二个额外的功能是该插件能够解析剪切路径,这是其他库无法提供的。最后,你还可以重用第一个起始 ID(而不是必须存储该路径数据以供重用)。值得一提的是,当该插件首次发布时,这些功能不可用,但 GreenSock 认识到需要支持这些功能,因此将其包含在内。

现在,我们不再受限于指定的点数,我们拓宽了各种效果的可能性。下面,我制作了一些烟雾:

https://codepen.io/sdras/pen/yXqXzY

🖱️ DOM 事件

SMIL 中很好地集成了诸如悬停和点击之类的事件。为了启动动画,可以指定 begin="click"begin="hover"

<animate 
    xlink:href="#rectblue"
    attributeName="x"
    from="0"
    to="300" 
    dur="1s"
    begin="click"
    values="20; 50"
    keyTimes="0; 1"
    fill="freeze" />

替代方案:JavaScript

有像 onmouseenteronmouseleave 这样的原生 DOM 事件,用于悬停,以及 click 事件,用于点击。你可以使用它们来更改事件触发器,从而触发基于 JavaScript 的动画。

替代方案:JavaScript + CSS

你可以使用 JavaScript 来更改类名或直接更改 CSS 样式。以下是一种可能性:更改 animation-play-state 以从事件触发器启动动画。

.st0 {
  animation: moveAcross 1s linear both;
  animation-play-state: paused;
}

@keyframes moveAcross {
  to {
    transform: translateX(100px);
  }
}
document.getElementById("rectblue").addEventListener("click", function() {
  event.target.style.animationPlayState = "running";
});

或者在 jQuery 中:

$(".st0").on("click", function() {
  $(this).css("animation-play-state", "running");
});

这种实现不会像 SMIL 示例那样立即将动画重置到开头。如果你想实现这一点,CSS-Tricks 上的一篇之前的文章详细介绍了几种实现方法。

替代方案:Greensock

在 GSAP 中,重启更加简单。我们可以将动画添加到时间线中,将其设置为暂停,然后在点击时重启它。这种实现更接近你对 SMIL 的预期,因为我们不需要做任何 hacky 的事情,比如克隆/重新插入 DOM 节点或更改元素上设置的任何属性。

// 实例化一个 TimelineLite
var tl = new TimelineLite();

// 将一个动画添加到时间线
tl.to(foo, 0.5, { left: 100 });

$(".st0").on("click", function() {
  tl.restart();
});

⏱️ 在“Y”完成后运行“X”

SMIL 还允许更复杂的时间事件,例如 begin="circ-anim.begin + 1s"。这在链接动画时特别有用。

替代方案:CSS

在 CSS 中,我们可以通过在第二个值上设置延迟来链接动画:

.foo {
  animation: foo-move 2s ease both;
}

.bar {
  animation: bar-move 4s 2s ease both; 
  /* 2 秒的值对应于第一个动画的迭代长度。 */
}

这种方法有点令人沮丧,因为你必须确保记住更改第一个间隔以及延迟。

替代方案:CSS 预处理

如果我们使用(例如)Sass 中的变量,维护和管理这些间隔会更容易:

$secs: 2s;

.foo {
  animation: foo-move $secs ease both;
}

.bar {
  animation: bar-move 4s $secs ease both; 
}

现在我们知道,如果我们更新一个值,它们将保持同步。

但是,如果我们想始终检测动画何时完成,JavaScript 提供了一些不错的原生功能,比如 animationEnd

$("#rectblue").on("animationend", function() {   
  $(this).closest("svg").find("#rectblue2").css("animation-play-state", "running");     
});

#rectblue2 {
  animation: moveAcross 2s 1s ease both;
  animation-play-state: paused;
}

🕰️ 计时器

SMIL 还提供了一个 set 属性,它允许你设置一个计时器,以便在指定的时间后执行某些操作。

<set 
    attributeName="visibility" 
    to="visible" 
    begin="2s" 
    fill="freeze" />

替代方案:JavaScript

我们可以使用 setTimeout 来实现相同的行为。

setTimeout(function() {
  document.getElementById("rectblue").style.visibility = "visible";
}, 2000);

替代方案:GreenSock

GreenSock 提供了 delay 属性,可以实现相同的效果。

TweenLite.to("#rectblue", 0, {
  delay: 2,
  visibility: "visible"
});

🔄 循环

SMIL 允许你通过 repeatCount 属性来控制动画的循环次数。

<animateMotion 
  xlink:href="#lil-guy" 
  dur="3s" 
  repeatCount="indefinite" 
  fill="freeze" 
  path="M 25,50 C 37.5,25 37.5,25 50,0 75,50 75,50 100,100 50,100 50,100 0,100 12.5,75 12.5,75 25,50 Z" />

替代方案:CSS

在 CSS 中,你可以使用 animation-iteration-count 属性来控制动画的循环次数。

.foo {
  animation: foo-move 2s ease infinite both;
}

infinite 值意味着动画将无限循环。你也可以指定一个具体的数字,例如 animation-iteration-count: 3,表示动画将循环三次。

替代方案:GreenSock

GreenSock 提供了 repeat 属性,可以实现相同的行为。

TweenLite.to("#lil-guy", 3, {
  bezier: {
    values: MorphSVGPlugin.pathDataToBezier("#path", {align: "#lil-guy" }), 
    type: "cubic"
  },
  ease: Linear.easeNone,
  repeat: -1
});

repeat: -1 表示动画将无限循环。你也可以指定一个具体的数字,例如 repeat: 3,表示动画将循环三次。

🎬 动画组

SMIL 允许你使用 animate 元素来创建动画组,并通过 begin 属性来控制组内动画的执行顺序。

<animate 
    xlink:href="#rectblue"
    attributeName="x"
    from="0"
    to="300" 
    dur="1s"
    begin="click"
    values="20; 50"
    keyTimes="0; 1"
    fill="freeze" />

<animate 
    xlink:href="#rectblue"
    attributeName="y"
    from="0"
    to="300" 
    dur="1s"
    begin="click + 1s"
    values="20; 50"
    keyTimes="0; 1"
    fill="freeze" />

替代方案:CSS

在 CSS 中,你可以使用 animation-delay 属性来控制动画的延迟时间。

.foo {
  animation: foo-move 2s ease both;
}

.bar {
  animation: bar-move 4s 1s ease both; 
  /* 1 秒的值对应于第一个动画的迭代长度。 */
}

这种方法有点令人沮丧,因为你必须确保记住更改第一个间隔以及延迟。

替代方案:CSS 预处理

如果我们使用(例如)Sass 中的变量,维护和管理这些间隔会更容易:

$secs: 2s;

.foo {
  animation: foo-move $secs ease both;
}

.bar {
  animation: bar-move 4s $secs ease both; 
}

现在我们知道,如果我们更新一个值,它们将保持同步。

替代方案:GreenSock

GreenSock 提供了 TimelineLite 类,可以用来创建动画组,并通过 delay 属性来控制组内动画的执行顺序。

var tl = new TimelineLite();

tl.to("#rectblue", 1, { x: 300 });
tl.to("#rectblue", 1, { y: 300 }, "+=1");

+=1 表示第二个动画将在第一个动画完成 1 秒后开始。

总结

SMIL 曾经是 SVG 动画的王者,但随着浏览器支持的减少,我们不得不寻找其他替代方案。幸运的是,CSS、JavaScript 和 GreenSock 等工具提供了强大的功能,可以让我们实现 SMIL 中的所有功能,甚至更多。

选择哪种方法取决于你的需求和偏好。如果你需要最广泛的浏览器支持,那么 GreenSock 是一个不错的选择。如果你更喜欢使用 CSS,那么 CSS 动画是一个不错的选择。如果你需要更灵活的控制,那么 JavaScript 是一个不错的选择。

无论你选择哪种方法,都希望你能够轻松地创建出令人惊叹的 SVG 动画!

参考文献

  1. SMIL Is Dead! Long Live SMIL! A Guide To Alternatives To SMIL Features | CSS-Tricks
  2. GreenSock Animation Platform
  3. Snap.svg
  4. SVG Morpheus
  5. David Walsh 博客
0 0 投票数
Article Rating
订阅评论
提醒
0 评论
最多投票
最新 最旧
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x