标签: SVG

  • 👴🏻 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 博客
  • 🎨 SVG魔法:解开坐标系之谜,让文字与圆圈共舞

    🌟 引言:SVG的奇妙世界

    亲爱的读者朋友们,想象一下,你正在观看一场精彩的马戏表演。突然,一个小丑拿着一个写满文字的圆环出场了。他开始旋转这个圆环,文字随之优雅地旋转,仿佛在跳一支华尔兹。这不正是我们今天要探讨的SVG动画吗?让我们一起揭开SVG坐标系的神秘面纱,看看如何让文字与圆圈完美共舞!

    🧭 SVG坐标系:数字世界的罗盘

    🏁 从起点开始

    SVG的世界就像一张巨大的画布,而坐标系就是这张画布上的”经纬线”。默认情况下,SVG的坐标系原点(0, 0)位于画布的左上角。想象你站在一个巨大的棋盘的左上角,这就是我们的起点。

    (0,0) -----> x
    |
    |
    v
    y

    🎭 <g>元素:群魔乱舞的舞台

    在SVG的世界里,<g>元素就像是一个魔术师的帽子,可以把多个元素组合在一起。通过对<g>元素使用transform属性,我们可以像变魔术一样改变整个组的坐标系。

    <g transform="translate(300, 600)">
      <!-- 这里的元素都会受到平移变换的影响 -->
    </g>

    这就像魔术师说:”abracadabra”,然后整个舞台都移动到了新的位置!

    🛣️ 路径的秘密:相对与绝对的舞步

    🔄 绝对定位:固定的舞步

    想象一下,你在跳探戈。绝对定位就像是舞蹈老师给你画好了每一步应该踩的位置。例如,M300,600就是告诉你:”无论你在哪里,请移动到舞池的(300, 600)位置”。

    🦘 相对定位:灵活的跳跃

    相对定位则更像街舞,你可以根据当前位置即兴发挥。m-70,0就是说:”不管你在哪里,往左跳70步”。这种灵活性让我们的动画更加生动有趣。

    🕵️ 揭秘问题根源:坐标系的不协调之舞

    想象一下,如果探戈舞者按照街舞的步伐跳舞,会发生什么?没错,就是一场滑稽的混乱!这正是我们遇到的问题:

    <g transform="translate(300, 600)">
      <path d="M300,600 ..." /> <!-- 这里使用了绝对坐标 -->
      <circle cx="0" cy="0" r="70" /> <!-- 这里使用了相对坐标 -->
    </g>

    路径(<path>)使用了绝对坐标,而圆(<circle>)却使用了相对坐标。结果就像是两个舞者在跳不同的舞蹈,自然对不齐了!

    🎩 魔法解决方案:让所有元素跳同一支舞

    🔧 调整路径:相对坐标的魔力

    我们的解决方案就像是给所有舞者统一了舞步:

    <g transform="translate(300, 600)">
      <path d="M-70,0 a70,70 0 1,1 140,0" />
      <circle cx="0" cy="0" r="70" />
    </g>

    现在,路径的起点从(0, 0)左移70单位,正好与圆的左边缘对齐。接着,我们用a70,70 0 1,1 140,0画出一个完美的圆弧。这就像是舞者绕着舞池中心优雅地旋转一圈。

    🎭 <g>元素的魔法:统一的舞台

    <g>元素的transform="translate(300, 600)"就像是把整个舞台移动到了新的位置。所有的舞者(元素)都跟着舞台一起移动,保持了彼此之间的相对位置。

    📝 textPath:文字的舞蹈

    textPath就像是给文字穿上了舞鞋,让它们沿着我们设定的路径翩翩起舞。通过startOffset="0%",我们让文字从路径的起点开始跳舞,而animate元素则让文字的舞步变得生动活泼。

    🎉 欢乐的结局:和谐的圆舞曲

    经过我们的魔法调教,所有元素都找到了自己的位置:

    1. 路径的圆心与<circle>完美对齐。
    2. 文字沿着正确的路径旋转,就像是在圆环上跳舞。
    3. 整个动画看起来和谐统一,仿佛一场精心编排的表演。

    🖼️ 可视化的魔法

    想象两幅画面:

    1. 混乱的舞池:舞者们各自为政,有的看着地板上的标记跳舞(绝对坐标),有的跟着舞伴移动(相对坐标)。结果就是一片混乱。
    2. 和谐的圆舞曲:所有的舞者都遵循同一个舞步指南,随着音乐旋转。圆环、文字和背景完美融合,创造出一场视觉盛宴。

    🌈 结语:SVG的无限可能

    亲爱的读者朋友们,通过这次奇妙的SVG之旅,我们不仅解决了一个技术问题,更领略了数字艺术的魅力。SVG就像是一个神奇的调色板,只要我们掌握了正确的技巧,就能创造出无限的视觉奇迹。

    下次当你看到网页上那些绚丽的动画时,别忘了,在那些看似简单的图形背后,可能隐藏着一个精心设计的坐标系舞蹈!让我们继续探索SVG的奇妙世界,创造更多令人惊叹的数字艺术品吧!


    参考文献:

    1. Eisenberg, J. D. (2014). SVG Essentials: Producing Scalable Vector Graphics with XML. O’Reilly Media.
    2. Bellamy-Royds, A. , & Cagle, K. (2017). Using SVG with CSS3 and HTML5: Vector Graphics for Web Design. O’Reilly Media.
    3. MDN Web Docs. (2021). SVG: Scalable Vector Graphics. Mozilla Developer Network.
    4. W3C. (2011). Scalable Vector Graphics (SVG) 1.1 (Second Edition). World Wide Web Consortium.
    5. Soueidan, S. (2018). Practical SVG. A Book Apart.
人生梦想 - 关注前沿的计算机技术 acejoy.com