使用SVG创建Cel动画

编辑推荐: 掘金是一个高质量的技术社区,从 CSS 到 Vue.js,性能优化到开源类库,让你不错过前端开发的每一个技术干货。 点击链接查看最新前端内容,或到各大应用市场搜索「 掘金」下载APP,技术干货尽在掌握中。

如果我告诉你存在一种图像格式和GIF一样,但它是矢量格式的?如果我告诉你这个动画的方向还可以反转?如果我告诉你可以直接拿一张普通的图像,然后让它里边的每一个不同的部分都单独地动起来,而且不同部分的速度也不一样?其实,这种图像格式就是SVG,而且已经存在,并不是我想象出来的,只是还需要一点点温柔的鼓励。

在这篇文章中,我会结合新旧内容进行探讨,有点原始艺术的感觉,但会为它注入新的生命力。在Sass的帮助下,我精简了必要的工作流程,并希望能够证明automation有时候也可以是creativity的朋友。

动画,Old-School-Style老派风格

我不是动画方面的专家,但是我知道我喜欢什么:那些可以四处活动的东西。这样也好,因为我做网页设计时,Web动画是一大优势。事实上,现在对动画技术的支持是非常强大的,我们已经可以直接把注意力放到创作动画的目的和意义上了。

尽管CSS动画规范非常富有表现力以及灵活性,但有一个东西是它并不那么擅长的。我这里指的动画模式是在我出生之前就已经占有主导地位的,这有点令人失望。

动画的恒久魅力

“Do everything by hand, even when using the computer.” ——宫崎骏

宫崎骏的吉卜力工作室,诞生了很多非常棒的作品,如《千与千寻》、《哈尔的移动城堡》和《幽灵公主》,忠于传统动画技术——手绘独立动画“cels”。和CGI、我们使用CSS创建的关键帧动画是不同的,这些动画本质上是非常耗时的。宫崎骏还因为他为动画师们煮大桶的拉面而出名,因为他们总是工作到深夜,连续无数个夜晚。

但是他们选择了这种工作方式,并不是以前我们说的盲目崇拜。绘画之所以有一种持久的吸引力,即使是在相机面世之后,是因为它是生活的画笔啊:艺术家讨论的既是内源的又是明显的。这同样适用于动画。每个帧都是艺术家绘制的,并不是矢量制图生产的工件,产生的作品必然更加丰富深刻。大家都知道,卓越的技术精度,每一帧都是通过仔细的观察了解,然后再纯手工制作的。

友好的挥手动画:左边的动画采用了一个平滑的、基于关键帧的变换;右边的动画是由三张独立绘制的图片(或cels)一张一张播放的,此起彼伏。Firefox用户可能注意到transform-origin的动画实现得并不好,因为一个bug

让人不满意的SMIL

“在web上创建基于cel的动画是完全不可能的”,这样的说法是不正确的。同步多媒体集成语言(SMIL)就可以做到。事实上,Jonathan Ingram有一篇很棒的教程是关于“如何使用SMIL来创建一个基于cel的循环动画”,格斗快打的角色。

<animate 
  id="frame2" 
  attributeName="display" 
  values="none;inline;none;none" 
  keyTimes="0;0.33;0.66;1" 
  dur="1s" 
  begin="0s" 
  repeatCount="indefinite" />

SMIL的animate元素用来定义父路径的动画状态。

但是还是有很多问题。尽管它是一个比较旧的规范,SMIL一直没有在IE中得到支持。不仅如此,它可能都不会在IE12、15甚至38中得到支持了。同时,它在Blink内核中也被遗弃了,意味着Chrome的支持也在减少。Google的Paul Kinlan告诉我,Chrome 45 beta已经直接抛出相关警告

除了浏览器支持方面的缺失之外,我发现使用XML标签来定义动画的想法也有些古怪。我已经习惯将动画作为单独的额外的东西去特别关注了,在样式表中定义,我觉得这才是属于它的地方。毕竟,移动某些东西的视觉位置并没有真正改变它在文档中的位置。只有JavaScript可以改变文档流。

可惜的是,现在还没有很明确或简洁的方式可以使用CSS来创建基于cel的动画,但是我要说明,可能可以利用CSS关键帧动画很少被使用的某个功能。

开始

说到@keyframe动画,你一定对它支持一系列时间函数的情况非常熟悉,通过animation-timing-function属性即可。例如,值为ease-in,代表动画在接近完成的时候速度会变慢。

大家比较少用的steps()时间函数我们是感兴趣的,因为它让动画顺序排列,然后一步一步播放,给人一种仿真jerky——或者说“janky”的感觉,引用白话——运动的时候看起来就像cels在一片一片按照顺序播放。例如,steps(5)就是按照五个独立的步骤执行一个流畅的动画。

所有的steps()值完成的就是在一个关键帧动画里加一个小中断;它不会奇迹般地切换成一帧接一帧的模式。但是通过应用steps(1),我有效地对它进行了压制,使得关键帧之间的切换变得简单一些。通过添加opacity属性从1变成0的动画,使用step(1),我们可以在单步内让动画元素显示和隐藏:它本来还在的,然后就消失了。这对于显示和隐藏我将要创建的手绘cels是至关重要的。

Cels元素

目前为止,我一直担心自己只有一个元素,或一个cel,完全还不能做成宫崎骏自制的拉面动画的主体的一部分。它只是一张会出现和消失的图像。

这里它还有一个任务,就是创建更多的图像:需要一组cels才组成我的动画。这部分是不可避免的,而且任务相当繁重,但是我不会觉得这样不好:绘制单独的cels才使得这些动画变得特别。我要做的只是帮忙在后边生成动画逻辑,将其组合成完整的画。

标签

我是使用SVG来完成的。从技术上说,没有理由为什么同样的动画不能被应用于不同组相邻标签元素,但是在SVG中我们可以很快速地定义复杂的路径并覆盖它们。在下面的示例中,我已经使用SVG的<g>(group)元素创建了动画的容器,用于放置我们的cels。这些cel元素需要按照你希望动画进行的顺序引入。

<g class="animation-name">
<path d="[path coords for first cel]"></path> 
<path d="[path coords for second cel]"></path> 
<path d="[path coords for third cel]"></path> 
</g>

制作cels

有许多可视化编辑器可以处理SVG,但是Inkscape是专门为SVG设计的,并包括一个内置的XML编辑面板。这可以让我们的活儿变轻松一些,重点是它还是免费下载的!!!

我想要保持这种快速简单的状态,方便演示说明。打开一个新的Inkscape文档,然后绘制三种同样大小的形状。不需要和我绘制的完全一样。

Cel SVG

接下来,把形状一个一个叠起来放,调整文档的尺寸来适应。最简单的方法是选择File > Document Properties,然后选择Resize page to drawing or selection。先确定原先是没有选中任何单独的形状的,否则文件就会按照那个选中的形状来调整大小。

Cel SVG

现在选中所有形状,然后点击Object → Group。这会创建容器<g>元素。然后,新组仍处于选中状态,打开Edit &→ XML Editor,然后给你的组一个.shapes的类名。

Cel SVG

非常好。现在你只需要保存SVG。从下拉菜单中选择Optimized SVG,确认Enable viewboxing选项已经勾上。现在你的SVG已经可以用于制作动画了。

Cel SVG

注意优化

处理像这样的简单形状,要保证SVG文件的大小是最小的。在这里,用于动画的SVG文件大小只有2.3KB(包括了我们接下来要写的CSS)。一般来说,每个cel的路径数据越简单,你能引入的cel数量就越多。在处理一些更复杂的图纸时,像这篇文章前面展示的颤抖涂鸦,我建议使用Jake Archibald的视觉优化工具,SVGOMG

动画分层

正如我前面提到的,使用step(1),我可以使用opacity切换一个元素的可见和不可见状态。使用visibilitydisplay属性是不可能做到的,因为它们并不接受有序值(动画之间没有过渡)。首先,我要把我们容器中的所有cel设置opacity: 0,默认隐藏。

.shapes > * { 
  opacity: 0; 
  animation-duration: 0.75s; 
  animation-iteration-count: infinite; 
  animation-timing-function: steps(1);
}

还有设置基于step的时间函数,我选择了一个无限的迭代值,并把延迟设置为0.75s。因为每个cel的出场时间是同样长的,而且总共有三个cel,所以模拟帧速率为0.25s,即4帧/秒。

所以,我要如何让每个cel一个接一个地出现和消失呢?答案是给每个元素一个0.75s的动画,然后同时运行,将它们分层。现在有这三个cel,每个都是相继出现的。我用百分比表示了这些比例(如预期中的@keyframe语法),然后把每个已命名的动画绑到相应的nth-child上。

@keyframes shape-1 { 
  0% { 
    opacity: 1; 
  } 
  33.33333% { 
    opacity: 0; 
  } 
}  

.shapes > :nth-child(1) { 
  animation-name: shapes-1; 
}  

@keyframes shapes-2 { 
  33.33333% { 
    opacity: 1; 
  } 
  66.66667% { 
    opacity: 0; 
  } 
}  
.shapes > :nth-child(2) { 
  animation-name: shapes-2; 
}  

@keyframes shapes-3 { 
  66.66667% { 
    opacity: 1; 
  } 
  100% { 
    opacity: 0; 
  } 
}  

.shapes > :nth-child(3) {  
  animation-name: shapes-3; 
}

Cel SVG

嵌入和引用

现在我的CSS也写好了,我可以把它嵌入到我的SVG文件的头部,最后简单地创建一个,可扩展的“GIF”。因为我比较懒,我使用了一个自动添加前缀的在线工具autoprefixer为我的动画添加了前缀。

<svg viewBox="0 0 100 100"> 
<style>
<!-- CSS here --> 
</style>
<g class="animation-name"> 
<path d="[path coords for first cel]"></path> 
<path d="[path coords for second cel]"></path> 
<path d="[path coords for third cel]"></path> 
</g>  
</svg>

有些浏览器在通过<img/>标签引入文件的时候,会保存你的SVG动画,但不是所有的浏览器都是。一个更加一致和可靠的方法是使用<object>,如下:

<object type="image/svg+xml" data="path_to/shapes.svg" role="img" aria-label="shapes animation"> 
  <div role="img" class="shapes-fallback" aria-label="shapes animation"></div>
</object>

注意在对象及其降级<div>上使用一个WAI-ARIA img role以及aria-label,以提供适当的语义辅助技术用户。我避免使用<img />降级,因为一些浏览器会加载这个资源,而不是它可支持的SVG。当然,你也可以通过CSS的background-image属性为.shapes-fallback提供静态图像。

一个图像,多个动画

除了SVG的可扩展性,可能相比传统GIF格式最大的提升就是可以让同一张图像的不同部分动起来,以(如果需要的化)不同的速率和总体持续时间。在我的鲨鱼示例中,眼睛和尾巴是以不同的速率在动的,使用了不同数目的cel。

左边的图像展示了SVG的分组情况,右边的图像是我的小鲨鱼动起来了。

从数学上讲,这里发生的事情是非常好的:如果你把两个独立的动画看成一个组合动画,然后总体持续时间会更长。也就是说,如果我有一个动画是三帧,还有一个是四帧的,复合动画长度就是十二帧——是组成这个动画中“最长的那部分动画”的时间的三倍长。

使用GIF,每一帧直接映射到一个cel,这可能可以通过多引入几个图片解决。但重点是,文件大小也会相应增加。

Cel SVG

交替动画

在我的shark.svg示例中,摆动的尾巴和眨眨的眼睛都使用了交替、对称的动画,其中cel显示先正序,然后反序,正序,这样循环往复。幸运的是,每个组成部分的cel动画同时启动,这样给每个cel添加animation-direction: alternate就可以完成这个效果:

.tail > * { 
  animation-direction: alternate; 
}

这是一个更经济的方法。在GIF中如果要达到相同的效果,我必须将相同给的图片引入两次,第一张在一个方向,另一张往另一个方向。

基于cel的动画

我在开始写这篇文章的时候就承认了传统动画制作和现代的关键帧动画相比艰难很多。但是,对动画cel的创新就是减少劳动成本。在cel动画出现之前,动画中的每一帧都是按照“完整画面”绘制的——不是只画要添加动画的那部分。通过在透明cel上绘制要动的部分,静态背景可以重复使用。不仅节省了时间,还增加了一致性。

Cel SVG

SVG是一种基于文本的图像格式,可以被分开成很多“子树”标签,使得静态和动态图片在相同的上下文中结合成为可能。

使用Sass实现自动化控制

因为制作动画是一项比较有创造力的东西,给动画中的每个cel命名其实是很乏味的。在这里,我在一些比较繁琐的地方使用了sass。通过使用一个@for指令和一些计算,我可以让动画自动生成,如下:

$cels: 6; 
$fraction: 100 / $cels;  

@for $i from 1 through $cels {  
  $name: shapes; 
  $start: ($fraction * $i) - $fraction;  

  @keyframes #{$name}-#{$i} { 
    #{$start * 1%} { 
      opacity: 1; 
    } 
    #{($start + $fraction) * 1%} { 
      opacity: 0;   
    } 
  } 

  > :nth-child(#{$i}) { 
    animation-name: #{$name}-#{$i}; 
  } 
}

注意到我使用了“shapes”字符串,并使用了1到6的叠加数字来创建每个命名动画,例如:@keyframes shapes-1 {}。我在后面的mixin中引入这个逻辑,并使用unique-id()来动态创建具有唯一标识符的名称。

$name: unique-id();

在下面的示例中,参数3代表cel的数目,唯一标识的字符串用来动态创建命名动画,cel-u358d90ae, cel-uebf9a21ccel-u05cf8ffe

.shapes { 
  @include cel-animation(3); 
}

精细的控制

在设置的时候,只定义cel的数目太过于简单化了,这一点变得越来越明显。我必须给每个cel同样长的显示时间。就像我的鲨鱼动画中的眼睛,我希望眼睛在打开状态的时间比例,超过组成眨眼动画的cel序列。

首先,我简单地复制了眼睛睁开状态的cel,并引入了6个相同的版本来模拟动画中的暂停,但是我并不满意:因为这非常笨拙,而且会产生冗余的路径数据,增加了文件的大小。最后,我需要一个可以让我定义cel数量的接口,并有相同的时长:一个二维参数。答案是sass列表。

.eyes { 
  @include frame-animation((6 1 1 1), 0.15); 
}

在这个示例中,第一个参数$cels用于产生我的鲨鱼的眼睛,定义了一列cel,第一个cel的出现时长是其他三个cel的6倍。cel的数目可以用length($cels)来表示,帧的数目为列表中每个cel的整数值之和。第二个参数是帧速率,为0.15,所以动画的持续时间为(6 + 1 + 1 + 1) * 0.15 * 1s

minxin的更加成熟的版本在github上有发布。欢迎查看,如果有什么修改请发给我,这样我可以组成一个小展示。你的名字也会被列入名单中~

总结

作为一个设计师,我本身并不喜欢代码。我无法对一项技术产生兴趣,除非它可能解决一个大问题。我构思出了这个技巧,并写出了必要的代码,只是因为我真的希望我的插画能够变成可爱的动画。这是一个目标。虽然Web还缺少一些特性(有些东西觉得它们应该可以更容易实现的),通常的方法是普通的技术阵列来完成我们的目标。设计的部分是确定我们要摆在首位的东西,对它们来说,是否真的是一个好想法才是最重要的。

非常感谢Hugo Giraudel帮忙处理了cel动画sass mixin的内容,以及Sara Soueidan提供的使用<object>元素来引进动画的建议。使用内联SVG也是一个选择。

本文根据@Heydon Pickering的《Creating Cel Animations With SVG》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://www.smashingmagazine.com/2015/09/creating-cel-animations-with-svg/

彦子

在校学生,本科计算机专业。逗比一枚,热爱前端热爱生活,喜欢CSS喜欢JavaScript喜欢SVG,爱玩PS玩AI玩啊逗比的软件。努力向上,厚积薄发。

如需转载,烦请注明出处:http://www.w3cplus.com/svg/creating-cel-animations-with-svg.html

返回顶部