[聚合文章] 使用Web Animations API让动画效果做加法

CSS 2018-01-05 10 阅读

这些特性在写这篇文章的时候还没有在任何的浏览器得到稳定的支持。但是,接下来要讨论的内容在 Firefox Nightly 中得到了支持,而且关键部分在 Chrome Canary (开启Web实验性特性),所以我建议使用这些浏览器中的一个(在阅读本文时)。

无论你在Web上使用什么方法,你都会在不同的动画中使用相同的属性。也许你有一个悬停效果缩放一个图像和一个点击事件触发位移 —— 都会影响 transform 。默认情况之下,这些动画并不会了解其他动画,只有一个动画会被应用(因为它们影响了相同的 CSS属性,其他的值将被覆盖)。

element.animate({
    transform: ['translateY(0)', 'translateY(10px)']
}, 1000);

// 完全覆盖前面的动画
element.animate({
    transform: ['scale(1)', 'scale(1.15)']
}, 1500);

这个示例中Web Animations API中的只有第二个才会呈现动画,因为这两个动画同时播放,只有最后一个定义的才会使用。

有时候我们甚至有更强大的想法,我们要一个基本的动画,然后基于一些用户交互的变化,在不影响其现有的持续时间,关键帧或缓动函数就可以平滑的修改动画。在浏览器中,CSS动画和当前的Web Animations API提供的特性是无法做到这一点。

新的选择

Web动画规范中引入了 composite 属性 (以及相关的 iterationComposite )。 composite 默认的属性值是 replace ,它具有多年来我们一直想要使用而没有的功能。一个动画属性的值会替换任何先前设置的值 —— 要么是规则集,要么是另一个动画。

add 值就是从以前的规范中改变的地方。

element.animate({
    transform: ['scale(1)', 'scale(1.5)']
}, {
    duration: 1000,
    fill: 'both'
});
element.animate({
    transform: ['rotate(0deg)', 'rotate(180deg)']
}, {
    duration: 1500,
    fill: 'both',
    composite: 'add'
});

现在这两个动画都将被运用,在元素的时间轴上的指定点上进行适当的变换,也就实现两个转换(以前是后者会覆盖前者,前面提到过)。在我们的例子中,默认情况下, animation-timing-function 的值是 linear ,以及动画在同一时间开始,所以我们可以在任何给定的点上进行有效的 transform 。比如:

  • 0msscale(1) rotate(0deg)
  • 500msscale(1.25) rotate(60deg) (第一个动画完成了一半,第二个动画完成了三分之一)
  • 1000msscale(1.5) rotate(120deg) (第一个动画结束,第二个动画完成三分之二)
  • 1500msscale(1.5) rotate(180deg) (第二个动画结束)

创造力

单个动画不只是由一个起始状态和结束状态组成 —— 它可以有自己的缓动函数,播放次数、持续时间以及更多的关键帧。当一个元素是中间动的话,你可以用它自己的时间选项对它进行额外的转换。

这个示例允许你在相同的元素上应用多个动画,这些都将会影响 transform 属性。为了避免示例在运行中有所操作,将每个动画限为一次一个变换函数(比如只有一个 scale )。从默认值开始(例如 scale(1)translateX(0) ),并以一个合理的随机值结束相同的变换函数,重复无限次。下一个动画将会影响另一个单独的函数,它有自己的随机时间和缓动函数。

element.animate(getTransform(), //e.g. { transform: ['rotate(0deg), 'rotate(45deg)']}
{
    duration: getDuration(), //between 1000 and 6000ms
    iterations: Infinity,
    composite: 'add',
    easing: getEasing() //one of two options
});

当每个动画开始时,浏览器将有效地找到它在以前应用于动画中的位置,并在指定的时间启动一个新的放置动画。即使已经有相反方向的旋转,浏览器也会计算出需要旋转多少角度。

由于每个动画都有自己的计时选项,所以在这个示例中,如果添加一些选项,就不太可能看到重复的动画。当你看到动画时,这给了动画一个新鲜的感觉。

我们的示例中的每个动画都以默认( translate0scale1 )开始,这样一来有一个平滑的开始。如果我们使用的是关键帧,比如 {transform: ['scale(.5)', 'scale(.8)']} ,将会得到一个跳跃的效果,因为在此之前没有这样的 scale ,而是动画开始后的半中间添加的。

如何添加值?

变换值应该遵循 规范中的语法 ,如果你添加一个变换,将附加到一个列表中。

对于 ABC 三个 transform 动画,其计算后的 transform 值将是 [A当前的值] [B当前的值] [C当前的值] 。比如下面这个示例,假设我们有以下三个动画:

element.animate({
    transform: ['translateX(0)', 'translateX(10px)']
}, 1000);

element.animate({
    transform: ['translateY(0)', 'translateY(-20px)']
}, { 
    duration:1000,
    composite: 'add'
});

element.animate({
    transform: ['translateX(0)', 'translateX(300px)']
}, { 
    duration:1000,
    composite: 'add'
});

每个动画将会使用一个 linear 的缓动函数运行 1s ,因此在动画的中途, transform 的值将会是 translateX(5px) translateY(-10px) translateX(150px) 。动画中的缓动函数、持续时间、延迟时间和其他有效的值都将会对动画有影响。

然而,动画中不只有 transform 。像 filter (比如 hue-rotate()blur() 等)都会遵循这样的模式,将这些项附加到 filter 列表中。

有些属性会使用 数字作为值 ,比如 opacity 。这里的数字将会加起来成为一个和。

element.animate({
    opacity: [0, .1]
}, 1000);

element.animate({
    opacity: [0, .2]
}, { 
    duration:1000,
    composite: 'add'
});

element.animate({
    opacity: [0, .4]
}, { 
    duration:1000,
    composite: 'add'
});

每个动画都使用 linear 缓动函数持续 1s ,所以我们可以计算出每一个点上动画的值。

  • 0msopacity:0 ( 0 + 0 + 0 )
  • 500msopacity: .35.05 + .1 + .2
  • 1000msopacity: .7.1 + .2 + .4

因此,如果你有几个包含 1 的值作为关键帧的动画,你将不会看到有什么太多的效果。因为 opacity 的值为 1 时就完全不透明,这也是它的视觉状态的最大值,所以加起来的值会看起来和 1 一样。

接受数字值的其他属性和 opacity 属性类似,比如接受长度,百分比或颜色的属性也会对单个值求和。使用颜色时,你必须记住它们也有一个最大值( rgb() 中最大的值是 255hsl() 中的饱和度和亮度的值是 100% ),这样你的颜色结果有可能就是白色。使用长度,你可以在单位之间切换(例如 pxvmin ),就像在 calc() 中一样。

如果想了解更多的细节, 该规范讲述了不同类型的动画以及如何计算值的结果

填充模式工作原理

当你不做无限次播放动画(不管你是否使用了 composite ),默认情况下动画将不会保持它的结束状态。 fill 属性允许我们改变这种行为。如果你想要在添加一个有限动画的时候有一个平滑的过渡,你很可能想要一个 forwardsboth 或两个的填充模式来确保动画的最终的状态。

这个示例通过指定一个 rotatetranslate 的螺旋路径的动画。有两个按钮添加一个新的 1s 动画和一个额外的 translate 。由于它们指定的 fill :是 forwards ,每个额外的 translatetransform 列表的一部分。扩展(或收缩)螺旋在每个 translate 中调整值能更好的平滑适应每个位置。因为它是一个从 translate(0) 到一个新的值,并且保持那个新的值。

多个动画累积

新的 composite 有第三个值 —— accumulate 。它在概念上与 add 是一致的,除非某些类型的动画会有不同的表现。为了让我们的 transform 保持一致,让我们从一个使用 add 的新示例开始,然后再讨论 accumulate 有何不同。

element.animate({
    transform: ['translateX(0)', 'translateX(20px)']
}, {
    duration: 1000,
    composite: 'add'
});
element.animate({
    transform: ['translateX(0)', 'translateX(30px)']
}, {
    duration: 1000,
    composite: 'add'
});
element.animate({
    transform: ['scale(1)', 'scale(.5)']
}, {
    duration: 1000,
    composite: 'add'
});

1s 动画的结束处,其有效的值是:

transform: translateX(20px) translateX(30px) scale(.5)

这将一个元素沿 x 轴向右移动 50px ,然后整体缩小一半。

如果每个动画都使用 accumulate 替代 add ,那么其结果将是:

transform: translateX(50px) scale(.5)

效果同样是把一个元素沿 x 轴向右移动 50px ,然后整体的缩小一半。

不需要双重的 translateX() ,但从视觉效果上来看,两者完全是相同的 —— 那么 accumulate 有何不同呢?

从技术上讲,当我们积累了一个 transform 动画时,我们不再总是要将它附加到一个列表中。如果一个转换函数已经存在(比如我们示例中的 translateX() ),当我们开始第二个动画时,将不会追回这个值。相反,将在现有的函数中添加内部的值(例如,长度值)。

如果我们的视觉效果是相同的,那么为什么会有 accumulate 这个值存在呢?

示例中的 transform ,其函数列表的顺序很重要。 translateX(20px) translateX(30px) scale(.5)translateX(20px) scale(.5) translateX(30px) 是不同的,因为每个函数都会影响到与它相关的函数的坐标系统。当你在中间做一个 scale(.5) 时,下一个函数也会在缩放一半时发生。因此,在这个示例中 translateX(30) 将会以 15px 的方式向右移。

因此,随着积累,当我们总是将值添加到列表时,我们可以有一个不同的顺序。

每个迭代(Iteration)的累积

我之前提到过,我们还有一个新的属性 iterationComposite 。它提供了我们已经讨论过的一些行为能力,除了从一个迭代到下一个迭代的单个动画。

composite 不同,此属性只有两个有效的值 replace (默认值)和 accumulate 。使用 accumulate 值,我们已经讨论了过了,它是一个累积的过程(比如 transform ),或者将其添加到基于数字的属性,比如 opacity

下面的这个示例,两个动画的视觉效果是相同的:

intervals.animate([{ 
    transform: 'rotate(0deg) translateX(0vmin)',
    opacity: 0
}, { 
    transform: 'rotate(50deg) translateX(2vmin)',
    opacity: .5
}], {
    duration: 2000,
    iterations: 2,
    fill: 'forwards',
    iterationComposite: 'accumulate'
});

intervals2.animate([{ 
    transform: 'rotate(0deg) translateX(0vmin)',
    opacity: 0
},{ 
    transform: 'rotate(100deg) translateX(4vmin)',
    opacity: 1
}], {
    duration: 4000,
    iterations: 1,
    fill: 'forwards',
    iterationComposite: 'replace' //default value
});

第一个动画只是增加了它的不透明度为 .5 ,旋转 50deg 和右移 2vmin ,整个过程持续了 2000ms 。它具有新的 iterationComposite 值,并将运行 2 次迭代(Iteration)。因此,当动画结束时,它将运行 2 * 2000ms ,透明度为 12 * .5 ,不透明),旋转 100deg2 * 50deg )并位移 4vmin2 * 2vmin )。

太棒了!我们刚刚使用了一个新属性,只不过它只在Firefox Nightly中得到支持,可以重现使用Web 动画API(或CSS)所做的事情。

当你将它与即将到来的Web动画规范中的其他选项组合在一起时, iterationComposite 更有趣的方面就开始发挥其作用了(Firefox Nightly已经得到支持)。

设置新效果选项

今天的Web动画API在支持的浏览器中,很大程度上与CSS动画相当,只不过添加了一些细节,比如 playbackRate 选项和跳转/寻找不同的点的能力。但是,动画对象正在获得更新已经运行的动画的效果和时间选项的能力。

这里我们有一个元素,它有两个影响 transform 属性的动画,并依赖于 composite:'add' —— 一个使元素跨越整个屏幕,另一个以交错的方式垂直移动元素。最终在屏幕上的状态比第二次动画的启动状态稍高一些,并使用 iterationComposite:'accumulate' ,将会让元素不断地变得越来越高。在 8 次迭代之后,动画完成并将自己还原为另一个 8 次迭代,回到屏幕的底部,动画的过程重新开始。

我们可以通过改变飞行中的迭代次数来改变动画在屏幕上的距离。这些动画是无限播放的,但是你可以将下拉菜单更改为动画中间的不同迭代计数。例如,如果你从 7 次迭代到 9 次,你现在看到的是第 6 次迭代,那么你的动画就会一直运行,就好像什么都没有改变一样。但是,你将会看到,接下来的(第七次)迭代之后,它将不再开始相反的工作,它将会继续执行两个。你也可以交换新的关键帧,动画时间将保持不变。

animation.effect.timing.iterations = 4;
animation.effect.setKeyframes([
    { transform: 'scale(1)' },
    { transform: 'scale(1.2)' }
]);

在中途中修改动画可能不是你每天都会用到的东西,但是由于它是浏览器的新特性,我们就应该学习它,因为该功能可能会变得更广泛。当用户获得额外的奖励时,不断变化的迭代计数对于游戏来说是很方便的,而且游戏的持续时间比原定的要长。当用户从某个错误状态转到成功状态时,不同的关键帧会有意义。

我们该何去何从?

新的 composite 选项提借了改变动画的关键帧和时间的能力,这给编辑动画打开了新的大门。 CSS工作组 还在讨论如何将此功能添加到CSS中,甚至超出动画的范围。我们有时间,任何一个主流浏览器都将会支持这些新的特性,甚至令人更兴奋的是,今天有一些浏览器中可以试验这些令人兴奋的特性。

扩展阅读

本文根据 @DAN WILSON 的《 Additive Animation with the Web Animations API 》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处: https://css-tricks.com/additive-animation-web-animations-api/

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《 图解CSS3:核心技术与案例实战 》。

如需转载,烦请注明出处: https://www.w3cplus.com/animation/additive-animation-web-animations-api.html

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。