Snap 动画状态

Avatar of Briant Diehl
Briant Diehl on

DigitalOcean 为您的旅程每个阶段提供云产品。立即开始使用 价值 200 美元的免费积分!

为网站制作图标有很多方法。内联 SVG 可缩放,易于使用 CSS 修改,甚至可以进行动画处理。如果您有兴趣详细了解使用内联 SVG 的优点,我建议您阅读 内联 SVG 与图标字体。随着浏览器支持的不断增加,现在是开始使用 SVG 的最佳时机。Snap 动画状态是一个围绕 Snap.svg 构建的 JavaScript 插件,旨在帮助使用可缩放、可编辑的 SVG 图标创建和扩展图标库。Snap 动画状态使您可以轻松地使用简单的模式加载和动画化这些 SVG。

入门

让我们从一个基本的 SVG 汉堡菜单开始。这个菜单是用 Affinity Designer 制作的,但还有许多其他免费(Inkscape)和付费(Adobe Illustrator)选项可用于制作矢量图像。

<svg width="100%" height="100%" viewBox="0 0 65 60" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:square;stroke-miterlimit:1.5;"  fill="none" stroke="#000" stroke-width="10">
   <g>
      <path class="hamburger-top" d="m 5,10 55,0" />
      <path class="hamburger-middle" d="m 5,30 55,0" />
   </g>
   <path class="hamburger-bottom" d="m 5,50 55,0" />
</svg>

虽然这是一个非常基本的 SVG,但它仍然在 HTML 文档中占用了多行。如果您想在多个网页的多个位置使用 SVG,这可能会很麻烦。如果必须修改 SVG 呢?然后您需要拼命地记住所有使用过 SVG 的地方,以便对其进行更新。这很不干净也不可重用。这就是 Snap 动画状态要解决的问题。

让我们继续使用相同的 SVG,但这次我们将使用该插件将其加载到 DOM 中。该插件的模式至少需要两个属性:selector: ".some-css-selector"svg: "svg 字符串"。查看以下演示

查看 CodePen 上 Briant Diehl (@bkdiehl) 的 Lydgoo

您会在上面的 Pen 中注意到,我像调用字体图标一样调用 icon-hamburger。请记住,selector 属性需要一个 CSS 选择器作为其值。

由于该插件是 Snap.svg 的扩展,因此我们可以做更多事情,这是一个用于创建和动画化 SVG 的 JavaScript 库。因此,让我们看看需要什么才能为这个汉堡图标添加一些基本动画。

在创建 SVG 时,我在我知道将要进行动画处理的元素中添加了类。

<g>
   <path class="hamburger-top" d="m 5,10 55,0" />
   <path class="hamburger-middle" d="m 5,30 55,0" />
</g>
<path class="hamburger-bottom" d="m 5,50 55,0" />

在我的模式中,我可以开始包含动画所需的属性,并且我首先为其提供 transitionTime: 250。过渡时间应用于转换链中的每个步骤,并且可以稍后由单个转换覆盖。

现在是包含动画状态的时候了。我首先设置属性 states:{}。此对象的属性名称应与动画将导致的状态相关联。在本例中,我将属性命名为 openclosed。此对象的属性值是转换对象的数组。到目前为止,对模式的添加应如下所示

transitionTime: 250,
states: {
  open:[],
  closed: []
}

接下来,我们需要包含定义如何转换 SVG 元素的转换对象。

open:[
  { id: "top-lower", element: ".hamburger-top", y:20 },
  { id: "bottom-raise", element: ".hamburger-bottom", y:-20 },
  { waitFor: "top-lower", element: "g", r:45 },
  { waitFor: "bottom-raise", element: ".hamburger-bottom", r:-45},
]

每个转换对象都有一个 id、一个 waitFor 或两者的组合。每个 id 都需要是唯一的。具有 id 的对象表示动画链中的一个链接。waitFor 始终需要引用在其之前的链接 id。在本例中,有一个对象带有 id:"top-lower" 和一个对象带有 waitFor:"top-lower"。当动画开始时,id:top-lower 将是链中的第一个链接,它将运行 250 毫秒。当它完成时,waitFor:"top-lower" 将运行 250 毫秒。

每个转换对象都必须引用一个元素。元素值可以是 CSS 选择器或直接元素引用。例如,一个元素属性的值为 "g",引用 SVG 中的 <g> 元素,而另一个属性的值为 ".hamburger-bottom",引用我添加到 <path> 元素中的类。

现在我们知道了动画顺序和需要转换的元素,我们只需要定义转换对象。对于那些不熟悉 SVG 转换工作原理的人,您可以从 SVG 元素上的转换 开始。否则,简单地说,想象一下您正在操作的 SVG 元素在 x/y 轴上的点 [0, 0] 处开始。还要记住,x 从左到右,而 y 从上到下。在上面的示例中,我们看到

{ id: "top-lower", element: ".hamburger-top", y:20 },

这个转换对象引用了汉堡菜单的顶线。y: 20 告诉插件,从元素的原点 [0, 0] 开始,我想将顶线向下移动 20px。对于以下内容,反之亦然

{ id: "bottom-raise", element: ".hamburger-bottom", y:-20 },

这里我告诉插件将我的元素向上移动 20px。相同的原理适用于旋转

{ waitFor: "top-lower", element: "g", r:45 },
{ waitFor: "bottom-raise", element: ".hamburger-bottom", r:-45}

正在旋转的元素以 0 度的旋转开始。r: 45 告诉插件从 0 度旋转到 45 度,反之亦然,对于 r: -45 也是如此。

我们 states 对象中的第二个状态如下所示

closed: [
  { id: "top-angle", element: "g", r: 0 },
  { id: "bottom-angle", element: ".hamburger-bottom", r: 0 },                   
  { waitFor: "top-angle", element: ".hamburger-top", y: 0 },
  { waitFor: "bottom-angle", element: ".hamburger-bottom", y: 0 }
]

您会注意到,对于所有正在转换的元素,它们的 yr 值都设置为 0。这是因为此状态的目的是将 SVG 元素恢复到其原始状态。由于 0 是原点,我们只是对每个元素执行一个转换,这将使它们回到其原点。

我们快完成了。现在已经定义了动画状态,我必须决定什么将启动这些动画。这需要模式上的另一个属性:eventsevents 接受一个对象数组,因为您可能希望通过多个事件启动动画。对于汉堡图标,它将如下所示

events: [
  { event: "click", state: ["open", "closed"] }
]

数组中的对象可能包含以下属性

  1. event:旨在监听 javascript 事件。汉堡图标监听 <i class="icon-hamburger"</i> 上的 “click” 事件,因为这就是模式中 selector 引用的内容。
  2. state:接受一个字符串或一个数组。如果此处的 state"open",那么当单击 <i class="icon-hamburger"></i> 时,只会在单击事件上运行 “open” 动画。由于 state 的值为一个数组,因此单击事件实际上将在 “open” 和 “closed” 动画之间切换。该数组仅设计为接受两个值并启用切换。
  3. 最后一个属性是可选的:selector。默认情况下,此值是您的模式 selector + “animate”。在本例中,它将是 icon-hamburger-animate。如果需要,您可以更改选择器。它的目的是使 javascript 事件能够绑定到 SVG 的父级元素或兄弟元素。例如,如果我有一个我希望在按钮被单击时在按钮内部进行动画处理的 SVG,那么我需要这样做
<button class="icon-hamburger-animate">
  <i class="icon-hamburger"></i>
</button>

哇,我们做到了。现在是查看最终产品的时候了。

查看 CodePen 上 Briant Diehl (@bkdiehl) 的 bWwQJZ

值得吗?您可能在想,仅仅为了一个图标就需要做这么多工作。我同意您的观点。这就是我创建 Gulp 插件来帮助完成繁重工作的原因。

Gulp 动画状态

到目前为止,我们只有一个图标,可以在任何包含模式的地方使用它。理想情况下,icon-hamburger 的模式应保存到一个 js 文件中,该文件将被捆绑在一起并包含在整个站点中,这意味着我可以在任何需要的地方调用 icon-hamburger。如果此 js 文件是自动生成的,并且包含您能够访问的尽可能多的 SVG 图标的模式和插件调用怎么办?您可以轻松访问 SVG 图标库!这就是 Gulp 动画状态 的目的。请务必查看 此处 的文档。

让我们从文件结构开始。假设我去了 IcoMoon 并为我的新项目生成了所有需要的 SVG 文件。我想将所有这些新生成的 文件放入项目中的一个文件夹中。让我们将该文件夹命名为 `svg`。我的文件结构将如下所示

svg
|-- icon-folder.svg
|-- icon-hamburger.svg
|-- icon-mic.svg
|-- icon-wall.svg
|-- icon-wrench.svg

使用 Gulp 动画状态,我可以将 `svg` 文件夹中的所有 SVG 文件合并到一个 js 文件中,并根据 SVG 的文件名设置每个图标的 selector。文件内容将如下所示

var iconFolder = {"selector": ".icon-folder","svg": "<svg>Content</svg>"};
SnapStates(iconFolder);
var iconHamburger= {"selector": ".icon-hamburger","svg": "<svg>Content</svg>"};
SnapStates(iconHamburger);
var iconMic= {"selector": ".icon-mic","svg": "<svg>Content</svg>"};
SnapStates(iconMic);
var iconWall= {"selector": ".icon-wall","svg": "<svg>Content</svg>"};
SnapStates(iconWall);
var iconWrench= {"selector": ".icon-wrench","svg": "<svg>Content</svg>"};
SnapStates(iconWrench);

此文件可以与网站的其余关键 JavaScript 捆绑在一起,从而允许在任何需要的地方使用 SVG 图标。但是动画呢?它们是如何包含在该 JavaScript 文件中的?

我们已经有了汉堡图标的动画,因此我们将使用它。在 `svg` 文件夹中,我们需要创建一个名为 `icon-hamburger.js` 的新文件。请注意,它与相应的 SVG 文件同名。以下是新的文件结构

svg
|-- icon-folder.svg
|-- icon-hamburger.svg
|-- icon-hamburger.js
|-- icon-mic.svg
|-- icon-wall.svg
|-- icon-wrench.svg

`icon-hamburger.js` 的内容将是

{
  transitionTime: 250,
  states: {
    open:[
      { id: "top-lower", element: ".hamburger-top", y:20 },
      { id: "bottom-raise", element: ".hamburger-bottom", y:-20 },
      { waitFor: "top-lower", element: "g", r:45 },
      { waitFor: "top-lower", element: ".hamburger-bottom", r:-45},
    ],
    closed: [
      { id: "top-angle", element: "g", r: 0 },
      { id: "bottom-angle", element: ".hamburger-bottom", r: 0 },                   
      { waitFor: "top-angle", element: ".hamburger-top", y: 0 },
      { waitFor: "bottom-angle", element: ".hamburger-bottom", y: 0 },
    ]
  },
  events: [
    { event: "click", state: ["open", "closed"] }
  ]
}

Gulp 插件将查找与它正在为其创建模式的 SVG 文件同名的 js 文件。再次使用动画状态演示输出

var iconFolder = {"selector": ".icon-folder","svg": "<svg>Content</svg>"};
SnapStates(iconFolder);
var iconHamburger= {"selector": ".icon-hamburger","svg": "<svg>Content</svg>", "transitionTime":250,"states":{"open":[{"id":"top-lower","element":".hamburger-top","y":20},{"id":"bottom-raise","element":".hamburger-bottom","y":-20},{"waitFor":"top-lower","element":"g","r":45},{"waitFor":"top-lower","element":".hamburger-bottom","r":-45}],"closed":[{"id":"top-angle","element":"g","r":0},{"id":"bottom-angle","element":".hamburger-bottom","r":0},{"waitFor":"top-angle","element":".hamburger-top","y":0},{"waitFor":"bottom-angle","element":".hamburger-bottom","y":0}]},"events":[{"event":"click","state":["open","closed"]}};
SnapStates(iconHamburger);
var iconMic= {"selector": ".icon-mic","svg": "<svg>Content</svg>"};
SnapStates(iconMic);
var iconWall= {"selector": ".icon-wall","svg": "<svg>Content</svg>"};
SnapStates(iconWall);
var iconWrench= {"selector": ".icon-wrench","svg": "<svg>Content</svg>"};
SnapStates(iconWrench);

使用 Gulp 动画状态,您可以保留更小、更易于管理的文件,以便在需要更改内容时轻松编辑。这些小块文件很好地编译成一个文件,该文件可以与站点的其他关键组件捆绑在一起,从而允许快速轻松地调用以将 SVG 包含在您的 HTML 文档中。

更多示例

汉堡菜单图标非常简单,所以让我们看看一些更复杂的图标。 我们将从一个扬声器图标开始。

查看 Pen WjoOoy by Briant Diehl (@bkdiehl) on CodePen.

您会注意到,总体而言,模式基本相同。 您会注意到属性easing是新的。 easing的默认值为easeinout。 除此之外,唯一值得注意的更改是在我的变换对象中。

{ id: "waveline1", element: ".wave-line-1", x:-10, s:0.1, attr:{ opacity:.8 }, transitionTime: 250 },
{ id: "waveline2", element: ".wave-line-2", x:-16, s:0.1, attr:{ opacity:.8 }, transitionTime: 300 },
{ id: "waveline3", element: ".wave-line-3", x:-22, s:0.1, attr:{ opacity:.8 }, transitionTime: 350 }

s代表缩放,就像在css中一样,对象的缩放始终从1开始。 attr属性允许您修改SVG元素上的任何属性,在本例中是透明度。 最后,请记住,在本文开头,我提到过transitionTime可以被单个变换覆盖? 嗯,这就是它的实现方式。 我甚至没有在主模式中声明transitionTime。 这是因为我希望每个变换都有一个唯一的过渡时间。

接下来,让我们看一下线条绘制动画。

查看 Pen OmbxVV by Briant Diehl (@bkdiehl) on CodePen.

我想让您看到的第一个主要区别是我没有在模式中声明svg。 SVG位于<i class="icon-new-document"></i>中。 这主要用于演示目的,这样就不会膨胀我想让您查看的模式。 但是,该插件确实允许此功能。 此用例适用于那些文档中只需要少量SVG图标且不想使用gulp插件的用户。

我真正想在这里关注的是变换对象。 这里有很多新东西。

{ id: 'line1-init', element: ".new-document-line1", drawPath: { min: 25, max: 75 }, transitionTime: { min: 500, max: 1000 }, repeat: {times:1} },        
{ id: 'line2-init', element: ".new-document-line2", drawPath: { min: 25, max: 75 }, transitionTime: { min: 500, max: 1000 }, repeat: {times:1} },
{ id: 'line3-init', element: ".new-document-line3", drawPath: { min: 25, max: 75 }, transitionTime: { min: 500, max: 1000 }, repeat: {times:1} },
{ id: 'line4-init', element: ".new-document-line4", drawPath: { min: 25, max: 75 }, transitionTime: { min: 500, max: 1000 }, repeat: {times:1} },
{ id: 'line5-init', element: ".new-document-line5", drawPath: { min: 25, max: 75 }, transitionTime: { min: 500, max: 1000 }, repeat: {times:1} },
{ waitFor: 'line1-init', element: ".new-document-line1", drawPath: 100, transitionTime: { min: 500, max: 1000 } },
{ waitFor: 'line2-init', element: ".new-document-line2", drawPath: 100, transitionTime: { min: 500, max: 1000 } },
{ waitFor: 'line3-init', element: ".new-document-line3", drawPath: 100, transitionTime: { min: 500, max: 1000 } },
{ waitFor: 'line4-init', element: ".new-document-line4", drawPath: 100, transitionTime: { min: 500, max: 1000 } },
{ waitFor: 'line5-init', element: ".new-document-line5", drawPath: 100, transitionTime: { min: 500, max: 1000 } },

如果您查看了Pen,您会注意到,将鼠标悬停在新文档图标上会导致线条收缩和增长。 每条线都是一条path,而path可以绘制。 上面的第一个变换对象包含drawPathdrawpath接受一个数字或一个具有minmax属性的对象。 该数字代表一个百分比。 假设变换对象具有drawPath: 0。 这意味着我希望当前path绘制到其长度的0%。 变换对象实际上具有drawPath: { min: 25, max: 75 }。 当drawpath的值是一个对象时,我告诉我的插件我希望路径绘制到minmax之间的随机百分比。 在这种情况下,它将是25到75之间的随机数。 如果您再次将鼠标悬停在图标上,您会看到每次动画发生时线条长度都会发生变化。 使用minmax设置随机数的相同原理适用于transitionTime

此动画模式的最后一个新成员是repeatrepeat接受一个具有四个有效属性的对象。

  1. loop:接受一个布尔值。 如果设置为true,动画和链中所有后续变换将一直重复,直到另行指示。 为了退出循环,您必须设置loopDuration或更改为另一个动画状态。
  2. loopDuration:接受一个整数。 如果我将loop设置为true并将loopDuration设置为5000,那么动画链将重复自身5000毫秒。 如果动画循环的持续时间不完全是5000毫秒,那么循环将继续其最终动画,超过设定的时间。
  3. times:接受一个整数。 如果我将times设置为2,那么我的动画将总共运行3次。 一次是因为动画总是至少运行一次,然后又运行2次。
  4. delay:接受一个整数。 代表您希望动画结束和重复循环开始之间的等待时间。

接下来,我想说明一个更长的动画链。

查看 Pen KmNXdW by Briant Diehl (@bkdiehl) on CodePen.

看一下shake状态。 第一个和最后一个变换对象具有idwaitFor属性。 每个其他变换对象都具有idwaitFor属性。

shake: [
  { id: "shake-right", element: '.wrench', r: 10 },
  { id: "shake-left", waitFor: 'shake-right', element: '.wrench', r: -10 },
  { id: "back-to-right", waitFor: 'shake-left', element: '.wrench', r: 10 },
  { id: "back-to-left", waitFor: 'back-to-right', element: '.wrench', r: -10 },
  { waitFor: 'back-to-left', element: '.wrench', r: 0 }
]

三个中间变换对象中的每一个都使用其waitFor引用前一个变换对象的id。 第一个动画启动一个链,该链最终导致最后的重置值r:0

最后,我想演示一下如何通过设置stroke-dashoffsetstroke-dasharray来绘制线条。

查看 Pen rmWzyW by Briant Diehl (@bkdiehl) on CodePen.

首先,我想让您注意到,在我的许多path元素上,我包含了stroke-dashoffset:1000; stroke-dasharray:1000, 1000;

<path class="right-upper-branch" d="M45.998,21.196C43.207,23.292 44.195,27.857 47.629,28.59C48.006,28.671 48.399,28.699 48.784,28.672C49.659,28.611 50.276,28.34 50.994,27.849C51.413,27.563 51.839,27.05 52.092,26.616C53.906,23.507 50.981,19.611 47.489,20.486C46.946,20.622 46.446,20.86 45.998,21.196L41.015,14.571" style="fill:none;stroke:#fff;stroke-width:1.7px;stroke-dashoffset:1000; stroke-dasharray:1000, 1000;"/>

有关stroke-dasharray的更详细说明,请查看stroke-dasharray。 就我而言,我们假设stroke-dasharray基本上是设置我不希望显示的路径的长度。 现在,我的路径当然不是1000px长。 这有点夸张,但这种夸张确保了我的路径的任何部分都不会过早地显示。 路径在以下变换中被绘制到完成状态。

{ id:["right-upper-branch", 600], element: ".right-upper-branch", drawPath:100  },

当我将drawPath重置为0时,它将相应地调整stroke-dasharraystroke-dashoffset。 我想指出关于这行代码的最后一件事是id。 它是一个数组而不是一个字符串。 数组中的第一个值是id的名称。 第二个值将始终是一个整数,表示超时。 如果我只在动画中使用此变换对象,我只会看到在mouseover事件发生后600毫秒才绘制的路径。

有关更多示例和进一步的文档,您可以查看我的演示页面

结论

你们中可能还有很多人仍在犹豫,是否将图标系统切换到新的系统是一个好主意。 目前可用的不同图标系统各有优缺点。 我试图为您创建一个简单的方法,让您迁移到SVG图标。 希望您觉得它有用。