使用 JavaScript 控制 CSS 动画和过渡

Avatar of Zach Saucier
Zach Saucier

DigitalOcean 为您的旅程各个阶段提供云产品。立即开始使用 $200 免费积分!

以下是由 Zach Saucier 撰写的一篇客座文章。Zach 给我写信说,作为 Stack Overflow 等编码论坛的常客,他经常看到关于使用 JavaScript 控制 CSS 动画的问题,并用一堆链接来证明这一点。我已经把这件事列在我的待办事项清单里太久了,所以我很高兴让 Zach 深入研究并写下这篇全面的教程。

网页设计师有时认为在 CSS 中进行动画比在 JavaScript 中进行动画更难。虽然 CSS 动画确实有一些局限性,但大多数情况下它比我们想象的更有能力!更不用说,通常性能更佳。

结合一点 JavaScript,CSS 动画和过渡能够比大多数 JavaScript 库更有效地实现硬件加速动画和交互。

让我们直接进入主题!

快速说明:动画和过渡不同

CSS 中的过渡应用于元素,并指定当属性更改时,它应该在一段时间内逐渐发生变化。 动画 则不同。应用后,它们只是运行并完成它们的任务。它们提供了更细粒度的控制,因为您可以控制动画的不同停止点。

在本文中,我们将分别介绍它们。

操控 CSS 过渡

编码论坛上有无数关于触发和暂停元素过渡的问题。使用 JavaScript,解决方法实际上非常简单。

要触发元素的过渡,请切换该元素上的类名,该类名会触发过渡。

要暂停元素的过渡,请在您想要暂停过渡的点使用 getComputedStylegetPropertyValue。然后将该元素的这些 CSS 属性设置为您刚刚获取的值。

以下是一个该方法的示例。

相同的技术可以以更高级的方式使用。以下示例也通过更改类名来触发过渡,但这次一个变量会跟踪当前的缩放率。

请注意,这次我们正在更改 background-size。有许多不同的 CSS 属性可以进行过渡或动画,通常是那些具有数值或颜色值的属性。Rodney Rehm 还撰写了一篇关于 CSS 过渡的特别有用且信息丰富的文章,可以在这里找到

使用 CSS “回调函数”

操控 CSS 过渡和动画的一些最有用但鲜为人知的 JavaScript 技巧是它们触发的 DOM 事件。例如:animationendanimationstartanimationiteration 用于动画,transitionend 用于过渡。您可以猜到它们的功能。当元素上的动画分别结束、开始或完成一次迭代时,这些动画事件就会触发。

目前,这些事件需要使用供应商前缀,因此在这个演示中,我们使用 Craig Buckler 开发的一个名为 PrefixedEvent 的函数,它具有参数 elementtypecallback,有助于使这些事件跨浏览器。这是他关于使用 JavaScript 捕获 CSS 动画的有用文章。这里还有另一篇文章,它可以确定触发事件的动画(名称)。

本演示中的想法是放大心脏并在鼠标悬停时停止动画。

纯 CSS 版本很生硬。除非您在完美的时间悬停其上,否则它将在放大到最终悬停状态之前跳到特定状态。JavaScript 版本则平滑得多。它通过在应用新状态之前让动画完成来消除跳跃。

操控 CSS 动画

正如我们刚刚了解到的,我们可以观察元素并对动画相关事件做出反应:animationStartanimationIterationanimationEnd。但是,如果您想在动画中途更改 CSS 动画怎么办?这需要一些技巧!

动画-播放状态属性

CSS 的 animation-play-state 属性在您只需要暂停动画并可能稍后继续时非常有用。您可以像这样通过 JavaScript 更改该 CSS(注意您的前缀)

element.style.webkitAnimationPlayState = "paused";
element.style.webkitAnimationPlayState = "running";

但是,当使用 animation-play-state 暂停 CSS 动画时,元素将无法像动画运行时那样进行变换。您无法暂停它、变换它、恢复它,并期望它从新的变换状态流畅地运行。为了做到这一点,我们必须更深入地研究。

获取当前键值百分比

不幸的是,目前还没有办法获取 CSS 关键帧动画的准确当前“完成百分比”。最接近的近似方法是使用一个 setInterval 函数,该函数在动画期间迭代 100 次,这实际上是:动画持续时间(以毫秒为单位)/ 100。例如,如果动画持续 4 秒,则 setInterval 需要每 40 毫秒运行一次(4000/100)。

var showPercent = window.setInterval(function() {
  if (currentPercent < 100) {
    currentPercent += 1;
  } else {
    currentPercent = 0;
  }
  // Updates a div that displays the current percent
  result.innerHTML = currentPercent;
}, 40);

这种方法远非理想,因为该函数的实际运行频率实际上低于每 40 毫秒一次。我发现将其设置为 39 毫秒更准确,但依赖于此是不好的做法,因为它很可能因浏览器而异,而且在任何浏览器上都不是完美的匹配。

获取动画的当前 CSS 属性值

在理想的世界中,我们将能够选择一个正在使用 CSS 动画的元素,删除该动画,并赋予它一个新的动画。然后,它将开始新的动画,从其当前状态开始。我们并不生活在那个完美的世界,所以它要复杂一些。

下面我们有一个演示来测试获取和更改 CSS 动画“中途”的技术,就好像它是一个流一样。动画以圆形路径移动一个元素,起始位置位于顶部中心(如果您愿意,可以称为“十二点钟”。)当点击按钮时,它应该将动画的起始位置更改为元素的当前位置。它沿着相同的路径移动,只是现在“开始”于您按下按钮时所在的位置。原点的这种改变,因此也改变了动画,这可以通过在第一个关键帧中将元素的颜色更改为红色来表示。

我们需要深入挖掘才能做到这一点!我们必须深入到样式表本身以找到原始动画。

您可以使用 document.styleSheets 访问与页面关联的样式表,并使用 for 循环遍历它。以下是如何使用 JavaScript 在 CSSKeyFrameRules 对象中查找特定动画的值

function findKeyframesRule(rule) {
  var ss = document.styleSheets;
  for (var i = 0; i < ss.length; ++i) {
    for (var j = 0; j < ss[i].cssRules.length; ++j) {
      if (ss[i].cssRules[j].type == window.CSSRule.WEBKIT_KEYFRAMES_RULE && 
      ss[i].cssRules[j].name == rule) { 
        return ss[i].cssRules[j]; }
    }
  }
  return null;
}

一旦我们调用了上面的函数(例如:var keyframes = findKeyframesRule(anim)),您可以使用 keyframes.cssRules.length 获取对象的动画长度(该动画中关键帧的总数)。然后,我们需要从每个关键帧中剥离“%”,使它们只是数字,JavaScript 可以将它们用作数字。为此,我们使用以下方法,该方法使用 JavaScript 的 .map 方法。

// Makes an array of the current percent values
// in the animation
var keyframeString = [];  
for(var i = 0; i < length; i ++)
{
  keyframeString.push(keyframes[i].keyText); 
}
  
// Removes all the % values from the array so
// the getClosest function can perform calculations
var keys = keyframeString.map(function(str) {
  return str.replace('%', '');
});

此时,keys 将是一个包含所有动画关键帧的数组,以数字格式表示。

更改实际动画(终于!)

在我们的圆形动画演示中,我们需要两个变量:一个用于跟踪圆形自上次开始位置移动了多少度,另一个用于跟踪圆形自最初开始位置移动了多少度。我们可以使用 setInterval 函数(使用经过的时间和圆形中的度数)来更改第一个变量。然后,我们可以使用以下代码在单击按钮时更新第二个变量。

totalCurrentPercent += currentPercent;
// Since it's in percent it shouldn't ever be over 100
if (totalCurrentPercent > 100) {
  totalCurrentPercent -= 100;
}

然后,我们可以使用以下函数根据上面获得的可能关键帧百分比数组,找到最接近当前总百分比的动画关键帧。

function getClosest(keyframe) {
  // curr stands for current keyframe
  var curr = keyframe[0];
  var diff = Math.abs (totalCurrentPercent - curr);
  for (var val = 0, j = keyframe.length; val < j; val++) {
    var newdiff = Math.abs(totalCurrentPercent - keyframe[val]);
    // If the difference between the current percent and the iterated 
    // keyframe is smaller, take the new difference and keyframe
    if (newdiff < diff) {
      diff = newdiff;
      curr = keyframe[val];
     }
  }
  return curr;
}

为了获得新动画的第一个关键帧值,以便在后面的计算中使用,我们可以使用 JavaScript 的 .indexOf 方法。然后,我们删除原始关键帧,以便可以重新创建新的关键帧。

for (var i = 0, j = keyframeString.length; i < j; i ++) {
  keyframes.deleteRule(keyframeString[i]);
}

接下来,我们需要将百分比转换为圆形中的度数。我们可以通过简单地将新的第一个百分比乘以 3.6 来实现这一点(因为 100 * 3.6 = 360)。

最后,我们根据上面获得的变量创建新的规则。每个规则之间 45 度的差异是因为我们有 8 个不同的关键帧围绕圆形移动。360(圆形中的度数)除以 8 等于 45。

// Prefix here as needed

keyframes.insertRule("0% { 
  -webkit-transform: translate(100px, 100px) rotate(" + (multiplier + 0) + "deg) 
                     translate(-100px, -100px) rotate(" + (multiplier + 0) + "deg);
  background-color: red; 
}");
keyframes.insertRule("13% { 
  -webkit-transform: translate(100px, 100px) rotate(" + (multiplier + 45) + "deg)
                     translate(-100px, -100px) rotate(" + (multiplier + 45) + "deg); 
}");

// ...continued...

然后,我们重置当前百分比的 setInterval,以便它可以再次运行。请注意,以上是 WebKit 前缀。为了使它更跨浏览器兼容,你可以尝试进行一些 UA 探测来猜测需要哪些前缀。

// Gets the browser prefix
var browserPrefix;
navigator.sayswho= (function(){
  var N = navigator.appName, ua = navigator.userAgent, tem;
  var M = ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
  if(M && (tem = ua.match(/version\/([\.\d]+)/i))!= null) M[2] = tem[1];
  M = M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
  M = M[0];
  if(M == "Chrome") { browserPrefix = "webkit"; }
  if(M == "Firefox") { browserPrefix = "moz"; }
  if(M == "Safari") { browserPrefix = "webkit"; }
  if(M == "MSIE") { browserPrefix = "ms"; }
})();

如果你想进一步研究,Russell Uresti 在 这篇文章 中的答案和 相应的示例 会有所帮助。

将动画转换为过渡

正如我们所见,使用 JavaScript 可以简化 CSS 过渡的操纵。如果你最终没有获得你想要的结果,你可以尝试将其转换为过渡,并以这种方式进行操作。它们在编码方面难度差不多,但可能更容易设置和编辑。

将 CSS 动画转换为过渡的最大问题是,当我们将 animation-iteration 转换为等效的 transition 命令时。过渡没有直接的等效项,这就是它们首先是不同事物的原因。

与我们的旋转演示相关的是,一个小技巧是将 transition-durationrotation 都乘以 x。然后你需要有一个/应用一个类来触发动画,因为如果你直接将更改后的属性应用于元素,那么就不会有太多过渡。要启动过渡(模拟动画),你将类应用于元素。

在我们的示例中,我们在页面加载时执行此操作。

操纵 CSS 矩阵

操纵 CSS 动画也可以通过使用 CSSMatrix 来完成。例如

var translated3D = 
  new WebKitCSSMatrix(window.getComputedStyle(elem, null).webkitTransform); 

但这个过程可能会让人困惑,尤其是对于那些刚开始使用 CSS 动画的人来说。

有关 CSS 矩阵的更多信息,请参见 文档(虽然承认并不太有用),这个工具 允许你玩转矩阵值,或者有关该主题的文章,比如这里的一篇

重置 CSS 动画

CSS Tricks 上 可以找到以正确方式执行此操作的技巧。这个技巧本质上是(如果可能)删除启动动画的类,以某种方式触发重排,然后再次应用该类。如果所有其他方法都失败,请将元素从页面上移除,然后放回。

动动脑筋

在开始编码之前,思考和计划过渡或动画的运行方式是最大程度减少问题并获得所需效果的最佳方法。甚至比以后在谷歌上搜索解决方案更好!本文概述的技术和技巧可能并不总是创建项目所需的动画的最佳方法。

这是一个小例子,说明使用 HTML 和 CSS 本身可以解决你可能想要使用 JavaScript 来解决的问题。

假设我们想要一个图形连续旋转,然后在悬停时切换旋转方向。了解本文中介绍的内容,你可能想要跳进来并使用 animationIteration 事件来更改动画。但是,可以使用 CSS 和一个附加的容器元素找到一个更有效、性能更好的解决方案。

这个技巧是让螺旋以 x 速度在一个方向旋转,当悬停时,让父元素以 2x 速度向相反方向旋转(从相同位置开始)。这两个旋转相互作用,创造出螺旋以相反方向旋转的净效果。

这个例子 中,用于一个 StackOverflow 问题使用了相同的概念。

你可能觉得有趣的一些相关内容。

总结

  • getComputedStyle 有助于操纵 CSS 过渡。
  • transitionend 及其相关事件在使用 JavaScript 操纵 CSS 过渡和动画时非常有用。
  • 可以通过在 JavaScript 中获取样式表来更改 CSS 动画的当前值,但这可能非常复杂。
  • 在 JavaScript 中,CSS 过渡通常比 CSS 动画更容易操作。
  • CSS 矩阵通常难以处理,尤其是对于初学者来说。
  • 思考应该做什么以及如何做对于编码动画至关重要。