平面设计已成为2016年可见的趋势,显然,这就是SVG用法又重新走入人们的视野的原因。好处有很多: 独立的分辨率、跨浏览器兼容性以及DOM节点的可访问性。本文中,我们将看看如何使用SVG从简单的插图创建看似复杂的动画。
简明介绍
图1. 创建的效果如何?从简单的SVG插图创建看似复杂的动画。
该项目始于一个简单的实验想法: 我们可将SVG动画效果进行到多远?
那时候,设计师Chris Halaska和我是一个插图竞选网站的同事。在所有的创意搜索中,作品虽然很美观,但是缺乏所需的“魅力”。我们在“摄像系列”中找到了答案,即图形动画。可以使用动画将插图赋予生命,而SVG是做到这一点的最佳媒介。
我们所面临的问题(当然也是今天依旧所存在的问题)是,SVG动画应该委托给正在尝试艺术方向的前端开发人员还是正在试图学习JavaScript的设计师。当然,这些场景都是本质上的错误,但是使用少量的应用程序解决问题,实现动画在视觉的天然显示,需要在代码和设计之间搭建一个桥梁。
我们的想法是创建一个数据驱动程序,使设计师可以从静态插图到原型动画的快速实现。
动画的相关规则
在生命的幻象中,Disney概述了添加人物到动画的12个基本原则。适用于将所有无生命或者其他对象赋予生命的: 挤压与伸展(squash and stretch),预期动作(anticipation),渐快与渐慢(slow in and slow out),时间控制(timing)以及夸张(exaggeration)。在我们的项目中想要遵守这些原则,使僵硬的DOM操作更加流畅与自然。围绕变形(transformation),时间控制(timing)以及延缓过渡(easing),我们可以创建风格统一的动画,但是各自又有自己的特点。
变形(transformations)
因为插画的简单性,平面设计本身有点趋向于SVG用法。我们在动画中模仿这一点,将简单几何图形与简单几何运动进行配对。这里遵守一个单一规则: 使用基本原点(left
,right
,top
,bottom
以及center
)和变形(translate
,rotate
,scale
)。
图2. 9个可能的动画原点,组合使用left
,right
,center
,top
以及bottom
。
时间控制(timing)
为了保持类似的步调与节奏,我们约束到具体的时间增量。动画持续2s
并且由10
个单独的步骤组成。一个补间中,动画一个单一的变形(translate
,rotate
以及scale
)也必须始于并终止于这些步骤中,我们称之为 关键帧。
图3. 具有三个补间,每200ms
进行递增的一个动画示例。
延缓过渡(easing)
变形以及时间控制足以创建具有运动的视觉感知,但是延缓过渡则赋予了生命力。我们发现了三个延缓过渡的公式——添加角色到运动时提供更多的变化: easeOutBack
,easeInOutBack
以及easeOutQuint
。
图4. 有无延缓过渡的动画比较。注意,使用easingBack
的任何变化都会在某种程度上影响变形。
让我们开始吧
前期准备
随着Sketch以及Inkscape的流行,插图应用程序近年来成熟了很多,我们选择在Adobe Illustrator中绘制SVGs。
图5. 最终动画元素地分解。
图6. 使用Illustrator导出SVG时,会自动创建图层ID。
在导出SVG时,分组以及标释每一图层。Illustrator导出步骤中会自动根据图层名称创建ID。对于每一个动画元素,输出应该类似于如下所示的XML。需要注意即使没有子元素,仍旧需要一个g
标签进行包裹。这是为SVG添加变形做准备,后续会进行解释。
<g id="zipper"> <path fill="#272C40" d="…"/> </g>
图7. SVG导出设置。这里没有勾选”responsive”是因为动画单位是基于像素的。
处理蒙版
你可能已经在图6注意到了<Clip Group>
图层。本质上它们是在Illustrator中创建的剪切蒙版。当导出SVG时,它们会自动被重新定义为clipPaths
,可以以相同的方式遮挡元素。
<g> <defs> <rect id="SVGID_1_" x="235" y="-106.3" width="500" height="309"/> </defs> <clipPath id="SVGID_2_"> <use xlink:href="#SVGID_1_" overflow="visible"/> </clipPath> <g id="strap-right" clip-path="url(#SVGID_2_)"> <path fill="#93481F" stroke="#000000" stroke-width="1.5" stroke-miterlimit="10" d="…" /> </g> </g>
图8. 动画之前,使用clipPath
隐藏皮带。
原型,原型,原型
前期准备的完成,开始制作。我们在创建原型以及测试各种技术之间创建一个迭代过程,以查找解决方案。这里简单概述一下我们每一个尝试的优点以及缺点,以及为什么我们从一个方案转到了另一个。
CSS 以及 VELOCITY.JS
最初使用CSS创建动画的尝试也是很有希望的。我们坚信基于硬件加速的变形,动画将平滑运行,而执行也会变得简单不需要加载多余的库。虽然我们可以在Chrome中创建一个功能版本,但是却不适用于其它浏览器。
Firefox不支持SVG transform-origin
属性,当然Internet Explorer的支持更无从谈起了。最后,随着CSS以及JavaScript的紧密耦合,我们需要在自认为很优雅的众多解决方案文件中来回跳转。
当我们转向Velocity.js的使用时也出现了同样的问题。因为动画引擎也需要使用CSS 变形,但是Firefox以及Internet Explorer的支持性问题仍未解决。
GSAP
自从Flash之后,GSAP一直是工业标准,被移植到JavaScript之后所受欢迎程度愈加明显,凭借着链式书写语法,SVG的支持以及无与伦比的性能,GSAP是一个明显的竞争者——除一个问题外: 矫枉过正。导入TweenMax 以及 TimelineMax 就会立即使我们的文件大小翻一番,并且这被证明有些过度。Chris Gannon使我们知道,这里存有一个误区。TimelineMax 包含在TweenMax 之中,结合起来也只有37kb。
SNAP.SVG
最后的尝试中我们使用了Snap.svg —— Raphael的继承者。Snap在DOM操作方面提供了丰富的功能,但是在动画方面的支持却很受限。我们认识到了这一受限点,决定使用JavaScript来进行空白填补。这是一个轻量级的解决方案,可以实现动画的逼真度追求。
MO.JS,ANIME以及WEB ANIMATIONS API
书写这篇文章时,三个很有前途的SVG动画库在社区得到了一定的吸引力: Mo.js,Anime以及Web Animation API。如果我们可以重新审视这个问题,肯定会考虑这些方案。尽管如此,这篇文章背后的概念应该转向你想要使用的任何动画库。
文件结构
首先在我们的项目中导入Snap.svg库以及基本样式表。当然还有之后会用到的 Robert Penner的延缓过度函数。
图9. 项目最后的文件结构。“Hello world”支撑仅仅和高亮文件相关。
<!DOCTYPE html> <html lang="en" class="no-js"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>The Illusion of Life: An SVG Animation Case Study</title> <!-- Styles --> <link rel="stylesheet" type="text/css" href="css/style.css" /> <!-- Libraries --> <script src="js/libs/snap.svg.min.js"></script> <script src="js/libs/snap.svg.easing.min.js"></script></html> </head> </html> /* Full screen */ html, body { position: relative; width: 100%; height: 100%; margin: 0; overflow: hidden; background-color: #E6E6E6; font-family: sans-serif; } /* Centered canvas */ #canvas { position: absolute; top: 50%; left: 50%; -webkit-transform: translateX(-50%) translateY(-50%); -ms-transform: translateX(-50%) translateY(-50%); transform: translateX(-50%) translateY(-50%); overflow: hidden; }
Hello World
-
“Hello World,” GitHub
“Hello world” ——一个小的,简单的实验胜利成果。对于我们而言,只是在屏幕上显示了一些内容。首先实例化一个Snap
对象,使用DOM ID代表画布。使用Snap.load 函数声明外部的SVG资源以及使用一个匿名回调将节点附加到DOM树。
<body> <div id="canvas"></div> <script> (function() { var s = Snap('#canvas'); Snap.load("svg/backpack.svg", function (data) { s.append(data); }); })(); </script> </body>
制作一个简单的插件
-
Plugin, GitHub
创建一个可重用的多个动画的组件,我们使用 原型模式创建了一个“插件”。使用了一个 立即调用函数表达式(IIFE)确保数据的封装,同时仍将SVGAnimation
添加到全局命名空间。如果我们将现有的代码放在一个独立的init
函数中,就打下了SVGAnimation
的基础。
; (function(window) { 'use strict'; var svgAnimation = function () { var self = this; self.init(); }; svgAnimation.prototype = { constructor: svgAnimation, init: function() { var s = Snap('#canvas'); Snap.load("svg/backpack.svg", function (data) { s.append(data); }); } }; // Add to global namespace window.svgAnimation = svgAnimation; })(window);
添加选项
-
Options, GitHub
解析Snap.load
,可以发现两个潜在的可以作为参数传递的选项,画布以及外部SVG的资源。让我们创建一个loadJSON
函数来进行处理。
/* Loads the SVG into the DOM @param {Object} canvas @param {String} svg */ loadSVG: function(canvas, data) { Snap.load(svg, function(data) { canvas.append(svg); }); }
OBJECTS AS PARAMETERS
对象作为参数
现在需要一种方式将选项传递到SVGAnimation
函数中。有许多方式可以实现,以标准方式进行各个参数的传递。
var backpack = new svgAnimation(Snap('#canvas'), 'svg/backpack.svg');
但是这里有一个更好的解决方案。传递一个对象参数,不仅增强了代码的可读性,也会更具有灵活性。我们无需对顺序进行追踪。可以使参数可选,同时之后还可以再次使用对象。现在我们重写一下之前的代码,传递一个options
对象参数。
var backpack = new svgAnimation({ canvas: new Snap('#canvas'), svg: 'svg/backpack.svg' });
合并对象
有了options
对象后,需要使值可以被其余的插件访问到。在做这一步之前,先将传入的对象值用我们的默认值进行合并。尽管将这两个值设置为null
,我们仍然会将他们作为期望接收到的引用类型值的参考。
svgAnimation.prototype = { constructor: svgAnimation, options: { canvas: null, svg: null } };
设置了默认值,使用一个extend
函数进行两个对象的合并。本质上,函数会循环遍历每个对象的所有属性并将它们复制到另外一个对象之中。
/* Merges two objects @param {Object} a @param {Object} b @return {Object} sum http://stackoverflow.com/questions/11197247/javascript-equivalent-of-jquerys-extend-method */ function extend(a, b) { for (var key in b) { if (b.hasOwnProperty(key)) { a[key] = b[key]; } } return a; }
定义了extend
函数,需要对SVGAnimation
函数进行修改。你会注意到这里将this
设置为了self
。对this
进行缓存,保证内部的作用域可以接收到当前对象的值与方法。
var svgAnimation = function (options) { var self = this; self.options = extend({}, self.options); extend(self.options, options); self.init(); }
最后,更新init
,调用loadSVG
,将实例化过程中设置的canvas
以及svg
进行引用传递。
init: function() { var self = this; self.loadSVG(self.options.canvas, self.options.svg); }
硬编码原型
-
Hardcoded prototype, GitHub
添加SVG变形组
正如之前提到的,Snap.svg的动画引擎类似于CSS,相当原始,仅支持单一的字符串变形。意味着如果你想要对多个类型的动画变形,要么按照顺序依次进行,要么全部一次进行(共享持续时间以及过渡延缓)。我们可以添加额外的节点到DOM树中来解决这个问题,尽管不是最优解决方案。为每一个translate
、rotate
以及scale
变形,使用一个单独的分组元素,可以独立控制每一个动画补间。最好的用例说明是zipper
,也就是我们的最初原型。
首先在createTransformGroup
函数中进行zipper
元素的传递,之后对其进行定义。
var $zipper = canvas.select("#zipper"); self.createTransformGroup($zipper);
选择所有的子节点后,使用Snap.g
函数进行各自变形组的嵌套。
/* Create scale, rotate and transform groups around an SVG DOM node @param {object} Snap element */ createTransformGroup: function(element) { if (element.node) { var childNodes = element.selectAll('*'); element.g().attr('class', 'translate') .g().attr('class', 'rotate') .g().attr('class', 'scale') .append(childNodes); } }
这会创建独立的变形群组,也就可以对动画进行目标设置。
<!-- Old node --> <g id="zipper"> <path fill="#272C40" d="…"/> </g> <!-- New node --> <g id="zipper"> <g class="translate"> <g class="rotate"> <g class="scale"> <path fill="#272C40" d="…"></path> </g> </g> </g> </g>
一个SNAP.SVG动画
现在准备对第一个元素进行动画处理。Snap.svg提供了两个函数来执行此操作: transform
以及animate
。在第一个关键帧,使用transform
进行动画设置,使用animate
进行之后的处理。
Snap.svg支持标准的SVG变形符号,但是我们选择使用 变形字符串作为参数设置的一种手段。官方网站上的相关解释很少,但是可以在Raphael上找到原始文档。最初的大写字母是变形的缩写。参数 x
,y
以及我们期望的动画的angle
表示值,中心原点的cx
、cy
。
// Scale Snap.animate({transform: 'S x y cx cy'}, duration, easing, callback); // Rotation Snap.animate({transform: 'R angle cx cy'}, duration, callback); // Translate Snap.animate({transform: 'T x y'}, duration, callback);
计算原点
定义原点时,我们出现了一个有趣的问题。Snap.svg中,animate
以及transform
函数只接受参数作为像素值,这导致难以对其进行衡量。理想情况下,我们想要结合top
,right
,bottom
,left
以及center
定义原点。
幸运的是,Snap.svg提供了getBBox,可以衡量任何给定元素的边界并返回大量的描述符,当然包括我们想要获取的值。这里我们书写了两个函数,getOriginX
以及getOriginY
,它们接收一个bBox
对象以及一个direction
字符串参数,返回所需的像素值。
/* Translates the horizontal origin from a string to pixel value @param {Object} Snap bBox @param {String} "left", "right", "center" @return {Object} pixel value */ getOriginX: function (bBox, direction) { if (direction === 'left') { return bBox.x; } else if (direction === 'center') { return bBox.cx; } else if (direction === 'right') { return bBox.x2; } }, /* Translates the vertical origin from a string to pixel value @param {Object} Snap bBox @param {String} "top", "bottom", "center" @return {Object} pixel value */ getOriginY: function (bBox, direction) { if (direction === 'top') { return bBox.y; } else if (direction === 'center') { return bBox.cy; } else if (direction === 'bottom') { return bBox.y2; } }
动画实践
创建一个缩放动画进行实践。根据类名对变形组进行选择,进行缩放直至隐藏,之后再返回原来的大小。你会注意到我们从拉链的顶部进行缩放,时间间隔为400ms
,将原始的过延缓设置为easeOutBack
。
// Scale Tween var $scaleElement = $zipper.select('.scale'); var scaleBBox = $scaleElement.getBBox(); $scaleElement.transform('S' + 0 + ' ' + 0 + ' ' + self.getOriginX(scaleBBox, 'center') + ' ' + self.getOriginY(scaleBBox, 'top')); $scaleElement.animate({transform: 'S' + 1 + ' ' + 1 + ' ' + self.getOriginX(scaleBBox, 'center') + ' ' + self.getOriginY(scaleBBox, 'top')}, 400, mina['easeOutBack']);
旋转也遵循相同的模式,有几个复杂性。在这种情况下,有三个连续的补间播放。当每一个动画结束时,使用回调函数进行下一个动画的有序播放。
// Rotate Tween var $rotateElement = $zipper.select('.rotate'); var rotateBBox = $rotateElement.getBBox(); $rotateElement.transform('R' + 45 + ' ' + rotateBBox.cx + ' ' + rotateBBox.cy); $rotateElement.animate({ transform: 'R' + -60 + ' ' + self.getOriginX(rotateBBox, 'center') + ' ' + self.getOriginY(rotateBBox, 'top')}, 400, mina['easeOutBack'], function() { $rotateElement.animate({ transform: 'R' + 30 + ' ' + self.getOriginX(rotateBBox, 'center') + ' ' + self.getOriginY(rotateBBox, 'top')}, 400, mina['easeOutBack'], function() { $rotateElement.animate({ transform: 'R' + 0 + ' ' + self.getOriginX(rotateBBox, 'center') + ' ' + self.getOriginY(rotateBBox, 'top')}, 400, mina['easeInOutBack']); }); });
translate
的补间类似于scale
以及rotate
,但是有一个关键的区别。因为translate
动画并不立即开始,我们使用setTimeout
设置了400毫秒的开始延时。
// Translate Tween var $translateElement = $zipper.select('.translate'); $translateElement.transform('T' + 110 + ' ' + 0); setTimeout(function() { $translateElement.animate({ transform: 'T' + 0 + ' ' + 0 }, 600, mina['easeOutQuint']); }, 400);
关键帧是关键
关于这一点,你可能会想,“这么简单的一个动画就相当这么复杂了。”我们不会反对你的观点。
我们的目标是创建一个数据驱动程序,快速到达原型动画。通过创建的单独的补间类以及关于关键帧的概念介绍,我们可以书写如下的代码…
// Translate Tween var $translateElement = $zipper.select('.translate'); $translateElement.transform('T' + 110 + ' ' + 0); setTimeout(function() { $translateElement.animate({ transform: 'T' + 0 + ' ' + 0 }, 600, mina['easeOutQuint']); }, 400);
…像这样:
// Translate Tween new svgTween({ element: $zipper.select('.translate'), keyframes: [ { "step": 2, "x": 110, "y": 0 }, { "step": 5, "x": 0, "y": 0, "easing": "easeOutQuint" } ], duration: 2000/10 });
将每一个动画分解为单一的步骤,让我们看看这种格式如何使成型更加容易。分解位移补间的参数并思考这些数字的来源。
在原始的代码中,你可能已经注意到持续时间以及延迟时间都可以被200ms
整除。这不是一个巧合。如果整个动画持续2000ms
并包括10
个步骤,我们只需要由后者来划分前者计算某个单一步骤的持续时间。现在我们可以使用相同的逻辑思考为什么关键帧始于步骤2
止于步骤5
。初始的400ms
延迟的setTimeout
对应于这两个步骤。此外,动画的持续时间为600ms
,这被计算为三个步骤,步骤2
到步骤5
。
svgTween: Translate
-
Tween translate, GitHub
随着定义的小黑箱的输出,让我们书写SVGTween
类的功能。使用和SVGAnimation
相同的模式,我们可以很快书写出一个基本的框架。
/* svgTween.js v1.0.0 Licensed under the MIT license. http://www.opensource.org/licenses/mit-license.php Copyright 2015, Smashing Magazine http://www.smashingmagazine.com/ http://www.hellomichael.com/ */ ; (function(window) { 'use strict'; var svgTween = function (options) { var self = this; self.options = extend({}, self.options); extend(self.options, options); self.init(); }; svgTween.prototype = { constructor: svgTween, options: { element: null, keyframes: null, duration: null }, init: function () { var self = this; } }; /* Merges two objects @param {Object} a @param {Object} b @return {Object} sum http://stackoverflow.com/questions/11197247/javascript-equivalent-of-jquerys-extend-method */ function extend(a, b) { for (var key in b) { if (b.hasOwnProperty(key)) { a[key] = b[key]; } } return a; } // Add to namespace window.svgTween = svgTween; })(window);
使用之前的算法,设置动画的初始隐藏状态,之后进行动画处理。不使用Snap.svg的transform
以及animate
函数,我们重写了resetTween
以及playTween
来处理关键帧。
resetTween
会接收一个元素以及一个关键帧数组。唯一的区别在于,不是直接设置变形字符串,我们使用第一个关键帧中的值。
/* Resets the animation to the first keyframe @param {Object} element @param {Array} keyframes */ resetTween: function (element, keyframes) { var self = this; var translateX = keyframes[0].x; var translateY = keyframes[0].y; element.transform('T' + translateX + ',' + translateY); }
因为Snap.svg不提供链式的动画方法,我们必须为连续动画设置回调函数。
Snap.animation(attr, duration, [easing], [callback]);
然而,如果多于两个关键帧,这一瞬间就会变的不规则,本质上是把我们送入回调函数的一种方式。处理这个问题,我们将playTween
作为一个递归函数,允许我们不进行嵌套就可以进行动画循环。
首先定义动画的参数。在resetTween
中将变形字符串设置为关键帧值。过渡延缓已多次使用这种方式。持续时间设置为导致第一个动画暂停或者两个步骤之间计算的时间跨度。
/* Recursively loop through keyframes to create pauses or tweens @param {Object} element @param {Array} keyframes @param {Int} duration @param {Int} index */ playTween: function(element, keyframes, duration, index) { var self = this; // Set keyframes we’re transitioning to var translateX = keyframes[index].x; var translateY = keyframes[index].y; // Set easing parameter var easing = mina[keyframes[index].easing]; // Set duration as an initial pause or the difference of steps between keyframes var newDuration = index ? ((keyframes[index].step - keyframes[(index-1)].step) * duration) : (keyframes[index].step * duration); }
准备好参数后,对暂停的条件语句进行书写,暂停或者终止动画。第一个条件语句用于判断动画是否立即开始于步骤0
。如果是的话,继续进行,因为变形函数已经处理了第一个关键帧。如果我们试图使用resetTween
进行相同值的动画处理,有时会发现产生一个短暂的闪动,这是后期发现的一个bug。之后两个条件语句用于检测是否需要暂停动画或者开始补间的播放。需要注意的一点是使用的嵌套条件语句,检查递归函数是否应该再次启用。没有相关说明,playTween
会无终止的运行。
// Play first tween immediately if starts on step 0 if (index === 0 && keyframes[index].step === 0) { self.playTween(element, keyframes, duration, (index + 1)); } // Or pause tween if initial keyframe else if (index === 0 && keyframes[index].step !== 0) { setTimeout(function() { if (index !== (keyframes.length - 1)) { self.playTween(element, keyframes, duration, (index + 1)); } }, newDuration); } // Or animate tweens if keyframes exist else { element.animate({ transform: 'T' + translateX + ' ' + translateY }, newDuration, easing, function() { if (index !== (keyframes.length - 1)) { self.playTween(element, keyframes, duration, (index + 1)); } }); }
最后一个步骤就是更新init
函数,对resetTween
以及playTween
进行调用。
init: function () { var self = this; self.resetTween(self.options.element, self.options.keyframes); self.playTween(self.options.element, self.options.keyframes, self.options.duration, 0); }
svgTween: Rotation And Scale
-
Tween: rotate and scale, GitHub
现在,我们的拉链可以从右侧移动到左侧,组合使用了旋转以及缩放。现在修改选项使其包含type
,originX
以及originY
。因为svgTween
会处理所有的变形,这里会包含一个变量用于处理对象的声明。我们也将跟踪originX
以及originY
来设置正确的用于缩放和旋转的transform-origin
。变形永远不会受到transform-origin
的影响,所以默认设置为center center
。
options: { element: null, type: null, keyframes: null, duration: null, originX: null, originY: null }
更新resetTween
以及playTween
处理这些新值。首先检查类型,然后构造各自的变形字符串。我们将创建单独的translateX
,translateY
,rotationAngle
,scaleX
以及scaleY
变量,这样就可以观察变形字符串是如何生成的。
/* Resets the animation to the first keyframe @param {Object} element @param {String} type - "scale", "rotate", "translate" @param {Array} keyframes @param {String} originX - "left", "right", "center" @param {String} originY - "top", "bottom", "center" */ resetTween: function (element, type, keyframes, originX, originY) { var transform, translateX, translateY, rotationAngle, scaleX, scaleY; if (type === 'translate') { translateX = keyframes[0].x; translateY = keyframes[0].y; transform = 'T' + translateX + ' ' + translateY; } else if (type === 'rotate') { rotationAngle = keyframes[0].angle; transform = 'R' + rotationAngle + ' ' + originX + ' ' + originY; } else if (type === 'scale') { scaleX = keyframes[0].x; scaleY = keyframes[0].y; transform = 'S' + scaleX + ' ' + scaleY + ' ' + originX + ' ' + originY; } element.transform(transform);
在playTween
中进行相同模式的模仿,在递归函数中取代相关的索引。我们也会使用新type
,originX
以及originY
参数进行function
回调更新。
/* Recursively loop through keyframes to create pauses or tweens @param {Object} element @param {String} type - "scale", "rotate", "translate" @param {Array} keyframes @param {String} originX - "left", "right", "center" @param {String} originY - "top", "bottom", "center" @param {Int} duration @param {Int} index */ playTween: function(element, type, keyframes, originX, originY, duration, index) { var self = this; // Set keyframes we're transitioning to var transform, translateX, translateY, rotationAngle, scaleX, scaleY; if (type === 'translate') { translateX = keyframes[index].x; translateY = keyframes[index].y; transform = 'T' + translateX + ' ' + translateY; } else if (type === 'rotate') { rotationAngle = keyframes[index].angle; transform = 'R' + rotationAngle + ' ' + originX + ' ' + originY; } else if (type === 'scale') { scaleX = keyframes[index].x; scaleY = keyframes[index].y; transform = 'S' + scaleX + ' ' + scaleY + ' ' + originX + ' ' + originY; } // Set easing parameter var easing = mina[keyframes[index].easing]; // Set duration as an initial pause or the difference of steps between keyframes var newDuration = index ? ((keyframes[index].step - keyframes[(index-1)].step) * duration) : (keyframes[index].step * duration); // Skip first tween if animation immediately starts on step 0 if (index === 0 && keyframes[index].step === 0) { self.playTween(element, type, keyframes, originX, originY, duration, (index + 1)); } // Or pause tween if initial keyframe else if (index === 0 && keyframes[index].step !== 0) { setTimeout(function() { if (index !== (keyframes.length - 1)) { self.playTween(element, type, keyframes, originX, originY, duration, (index + 1)); } }, newDuration); } // Or animate tweens if keyframes exist else { element.animate({ transform: transform }, newDuration, easing, function() { if (index !== (keyframes.length - 1)) { self.playTween(element, type, keyframes, originX, originY, duration, (index + 1)); } }); } }
最后,在调用resetTween
以及playTween
之前,更新init
函数设置type
,originX
,originY
。我们可以对传入元素进行类名处理,从而设置type
。在这一点,可以在SVGAnimation
进行getOriginX
以及getOriginY
的转移。然后使用三元操作符进行原点设置,如果值不确定,默认为center
。
init: function () { var self = this; // Set type self.options.type = self.options.element.node.getAttributeNode('class').value; // Set bbox to specific transform element (.translate, .scale, .rotate) var bBox = self.options.element.getBBox(); // Set origin as specified or default to center self.options.originX = self.options.keyframes[0].cx ? self.getOriginX(bBox, self.options.keyframes[0].cx) : self.getOriginX(bBox, 'center'); self.options.originY = self.options.keyframes[0].cy ? self.getOriginY(bBox, self.options.keyframes[0].cy) : self.getOriginY(bBox, 'center'); // Reset and play tween self.resetTween(self.options.element, self.options.type, self.options.keyframes, self.options.originX, self.options.originY); self.playTween(self.options.element, self.options.type, self.options.keyframes, self.options.originX, self.options.originY, self.options.duration, 0); }
让我们通过补间旋转和缩放的实例化完成拉链的动画效果。使用translate
,可以通过动画步骤的总数量以及动画的总长度计算出关键帧和持续时间。现实中,更多的是有机的定义了所有这些参数:通过查看动画的进展以及不断微调的数字。
// Rotate tween new svgTween({ element: $zipper.select('.rotate'), keyframes: [ { "step": 0, "angle": 45, "cy": "top" }, { "step": 2, "angle": -60, "easing": "easeOutBack" }, { "step": 4, "angle": 30, "easing": "easeOutQuint" }, { "step": 6, "angle": 0, "easing": "easeOutBack" } ], duration: duration }); // Scale tween new svgTween({ element: $zipper.select('.scale'), keyframes: [ { "step": 0, "x": 0, "y": 0, "cy": "top" }, { "step": 2, "x": 1, "y": 1, "easing": "easeOutBack" } ], duration: duration });
JSON配置
-
JSON, GitHub
构建的最后一步是从SVGAnimation
中提取硬编码值并将其添加到我们的构造函数中。在实例中添加关键帧,持续时间以及steps
的数量。
(function() { var backpack = new svgAnimation({ canvas: new Snap('#canvas'), svg: 'svg/backpack.svg', data: 'json/backpack.json', duration: 2000, steps: 10 }); })();
通过传入一个JSON文件定义关键帧,设计师可以无需潜入文档创建原型。事实上,如果你使用GSAP,Mo.js或者Web Animation API来替换Snap.svg,这一概念可能就是完全库独立。
JSON文件格式化为单独的补间,包括ID元素以及关键帧。我们使用拉链动画作为示例,但是backpack.json
文件包括所有元素的数组(拉链、口袋、标志等)。
{ "animations": [ { "id": "#zipper", "keyframes": { "translateKeyframes": [ { "step": 6, "x": 110, "y": 0 }, { "step": 9, "x": 0, "y": 0, "easing": "easeOutQuint" } ], "rotateKeyframes": [ { "step": 4, "angle": 45, "cy": "top" }, { "step": 6, "angle": -60, "easing": "easeOutBack" }, { "step": 8, "angle": 30, "easing": "easeOutQuint" }, { "step": 10, "angle": 0, "easing": "easeOutBack" } ], "scaleKeyframes": [ { "step": 4, "x": 0, "y": 0, "cy": "top" }, { "step": 6, "x": 1, "y": 1, "easing": "easeOutBack" } ] } } ] } options: { data: null, canvas: null, svg: null, duration: null, steps: null }
关于加载JSON文件的相关细节已经超出了本文的讲述范围。重点在于回调函数返的使用,返回JSON数据以供后期使用 – 在本例中,将动画数组传递给loadSVG
。
/* Get JSON data and populate options @param {Object} data @param {Function} callback */ loadJSON: function(data, callback) { var self = this; // XML request var xobj = new XMLHttpRequest(); xobj.open('GET', data, true); xobj.onreadystatechange = function() { // Success if (xobj.readyState === 4 && xobj.status === 200) { var json = JSON.parse(xobj.responseText); if (callback && typeof(callback) === "function") { callback(json); } } }; xobj.send(null); }
现在使用animation
数组可以实现loadSVG
的循环更新,以动态创建svgTweens
。如果translateKeyframes
,rotateKeyframes
或者scaleKeyframes
其中一者被定义,从options
文件获取关键帧以及持续时间进行传递,从而实例化一个新的svgTween
。
loadSVG: function(canvas, svg, animations, duration) { var self = this; Snap.load(svg, function(data) { // Placed SVG into the DOM canvas.append(data); // Create tweens for each animation animations.forEach(function(animation) { var element = canvas.select(animation.id); // Create scale, rotate and transform groups around an SVG node self.createTransformGroup(element); // Create tween based on keyframes if (animation.keyframes.translateKeyframes) { self.options.tweens.push(new svgTween({ element: element.select('.translate'), keyframes: animation.keyframes.translateKeyframes, duration: duration })); } if (animation.keyframes.rotateKeyframes) { self.options.tweens.push(new svgTween({ element: element.select('.rotate'), keyframes: animation.keyframes.rotateKeyframes, duration: duration })); } if (animation.keyframes.scaleKeyframes) { self.options.tweens.push(new svgTween({ element: element.select('.scale'), keyframes: animation.keyframes.scaleKeyframes, duration: duration })); } }); }); }
最后,调用loadJSON
更新init
函数,返回来又调用loadSVG
。完美的结束了整个教程。
init: function() { var self = this; self.loadJSON(self.options.data, function (data) { self.loadSVG(self.options.canvas, self.options.svg, data.animations, (self.options.duration/self.options.steps)); }); }
关于性能
我们的目标是看SVG动画能够走多远;所以我们青睐动画的逼真度多于动画的性能。立足于这一点是因为可以比预期更好地推进动画。但是我们不能完全的忽略性能。
查看Chrome DevTools时间轴,可以看出动画以每帧60ms
稳定播放。将背包动画进行分解,有19个
元素 以及3
个可能的变形。意味着,最糟糕情况下,一次可能出现57
个补间。幸运的是,这种情况不会出现,因为动画的补间在生存期是交错的。在CPU图表中可以看出,它们的用法稳步加大,重叠区域的动画达到了峰值,之后每个补间随之结束。视觉上,Firefox以及Internet Explorer在性能播放方面没有显著的差异。
图10. Chrome DevTools时间轴,展示CPU用法以及桌面帧率。
不出所料,移动设备的性能大大受挫。在旧的Android设备上进行远程调试,我们的帧率每秒下降至60
,在30 ~ 60
之间徘徊。虽然不是很完美,对于我们的需求而言还是可以接受的。不过还有一线希望,因为在iPhone5以及iPhone6上的最新测试表现堪称完美。
图11. Android远程调试,手机性能表现较差。
接下来是什么?
不幸的是,这场竞选在没有完成之前就已经结束了,所以我们没有机会深入了解该项目。所提供的源代码也不是完整就绪的;我们本来希望可以解决以下几个关键问题的。
事件驱动
Codepen提供了一个“return”按钮,但是实现不是基于事件驱动的。理想情况下,动画不会立即回放,除非由某种类型的互动(点击鼠标,航点等等)引发。
移动设备
如前所述,虽然这些动画可以在移动设备上运行,但是处理器所承担的代价是沉重的。考虑到项目整体设计的重要性。排除它们就可以大大提高性能并节约文件大小。如果不是绝对的必要性,需要长远的考虑如何使移动视区变得更加灵敏。
向后兼容
动画解决方案在所有现代浏览器均可运行,已经在Internet Explorer 9+,Firefox以及Chrome测试过。这主要是由于Snap.svg的支持。如果你的项目需要得到旧版浏览器的支持可以考虑使用Snap.svg的上一个版本 – Raphael。更易于访问的方法是使用渐进增强,提供一个最初的静态SVG,之后再为一些新版浏览器添加动画。
结尾
现在完成了简单插图到复杂动画的制作。你可以在GitHub下载完整的代码。