以下是由 Zach Saucier 撰写的一篇客座文章。Zach 给我写信说,作为 Stack Overflow 等编码论坛的常客,他经常看到关于使用 JavaScript 控制 CSS 动画的问题,并用一堆链接来证明这一点。我已经把这件事列在我的待办事项清单里太久了,所以我很高兴让 Zach 深入研究并写下这篇全面的教程。
网页设计师有时认为在 CSS 中进行动画比在 JavaScript 中进行动画更难。虽然 CSS 动画确实有一些局限性,但大多数情况下它比我们想象的更有能力!更不用说,通常性能更佳。
结合一点 JavaScript,CSS 动画和过渡能够比大多数 JavaScript 库更有效地实现硬件加速动画和交互。
让我们直接进入主题!
快速说明:动画和过渡不同
CSS 中的过渡应用于元素,并指定当属性更改时,它应该在一段时间内逐渐发生变化。 动画 则不同。应用后,它们只是运行并完成它们的任务。它们提供了更细粒度的控制,因为您可以控制动画的不同停止点。
在本文中,我们将分别介绍它们。
操控 CSS 过渡
编码论坛上有无数关于触发和暂停元素过渡的问题。使用 JavaScript,解决方法实际上非常简单。
要触发元素的过渡,请切换该元素上的类名,该类名会触发过渡。
要暂停元素的过渡,请在您想要暂停过渡的点使用 getComputedStyle
和 getPropertyValue
。然后将该元素的这些 CSS 属性设置为您刚刚获取的值。
以下是一个该方法的示例。
相同的技术可以以更高级的方式使用。以下示例也通过更改类名来触发过渡,但这次一个变量会跟踪当前的缩放率。
请注意,这次我们正在更改 background-size
。有许多不同的 CSS 属性可以进行过渡或动画,通常是那些具有数值或颜色值的属性。Rodney Rehm 还撰写了一篇关于 CSS 过渡的特别有用且信息丰富的文章,可以在这里找到。
使用 CSS “回调函数”
操控 CSS 过渡和动画的一些最有用但鲜为人知的 JavaScript 技巧是它们触发的 DOM 事件。例如:animationend
、animationstart
和 animationiteration
用于动画,transitionend
用于过渡。您可以猜到它们的功能。当元素上的动画分别结束、开始或完成一次迭代时,这些动画事件就会触发。
目前,这些事件需要使用供应商前缀,因此在这个演示中,我们使用 Craig Buckler 开发的一个名为 PrefixedEvent
的函数,它具有参数 element
、type
和 callback
,有助于使这些事件跨浏览器。这是他关于使用 JavaScript 捕获 CSS 动画的有用文章。这里还有另一篇文章,它可以确定触发事件的动画(名称)。
本演示中的想法是放大心脏并在鼠标悬停时停止动画。
纯 CSS 版本很生硬。除非您在完美的时间悬停其上,否则它将在放大到最终悬停状态之前跳到特定状态。JavaScript 版本则平滑得多。它通过在应用新状态之前让动画完成来消除跳跃。
操控 CSS 动画
正如我们刚刚了解到的,我们可以观察元素并对动画相关事件做出反应:animationStart
、animationIteration
和 animationEnd
。但是,如果您想在动画中途更改 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-duration
和 rotation
都乘以 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 问题使用了相同的概念。
链接!
你可能觉得有趣的一些相关内容。
- Animo.js – “一个功能强大的用于管理 CSS 动画的小工具”
- 感谢上帝,我们有规范! – Smashing Magazine 关于过渡怪癖的文章
总结
getComputedStyle
有助于操纵 CSS 过渡。transitionend
及其相关事件在使用 JavaScript 操纵 CSS 过渡和动画时非常有用。- 可以通过在 JavaScript 中获取样式表来更改 CSS 动画的当前值,但这可能非常复杂。
- 在 JavaScript 中,CSS 过渡通常比 CSS 动画更容易操作。
- CSS 矩阵通常难以处理,尤其是对于初学者来说。
- 思考应该做什么以及如何做对于编码动画至关重要。
或者直接使用 TweenLite – 市面上最好的 JS 动画库:http://www.greensock.com/gsap-js/
当然,每个东西都有一个插件,但这篇文章不仅是关于学习这种技术,还包括思考它,以及如何在不同的地方以不同的方式使用它。我的意思是,是的,你可以直接使用你提到的那个插件,但这篇文章是关于学习的。我实际上并不知道这篇文章中的所有内容,所以它真的很开眼界。
很棒的文章,Zach。作为一名经常使用过渡的人,这绝对有助于我理清(和清除)一些事情。
干杯!
如果你不需要支持 IE7/8/9(或者不介意这些用户的体验略有不同),这里有一个小技巧。
使用
translate3d
而不是translate
;你会注意到性能显著提高,尤其是在使用较大和较多的图像时。那怎么样,对于 IE 就不进行任何过渡 :)
做得好。我非常喜欢阅读这篇文章,它将对我的编码问题产生重大影响,尤其是 Ritz
很棒的文章,它透明地介绍了与这些东西相关的复杂性。感谢分享。只需指出几件事……
那个例子不是真正的暂停/恢复,因为缓动没有保持(每次都会重新启动)。
你不能像 seek() 一样跳转到 css 动画/过渡中的特定位置。所以没有擦除。如果我错了,请纠正我。
独立控制变换的各个方面,如缩放、旋转、倾斜和位置,并使用不同的时间和/或缓动,是不可能的(或者极其复杂)。想象一下,旋转 5 秒,但在中途开始放大,然后在最后一秒使用独特的缓动移动。这是一个**重大**问题(在我看来)。
css 中的缓动选项非常有限。你不能做弹性、反弹等。
根据我的经验,简单的动画对于 CSS 来说是可以的(假设与旧浏览器的兼容性不是问题),但任何中等复杂的动画都会很快变成噩梦。专门的库(如 felix 提到的 GSAP)如此流行是有原因的。对于围绕 css 动画/过渡的所有炒作,它们并不适合稳健的动画工作流程。很少有人坦率地谈论局限性和复杂性,所以我非常感谢你的文章。
我知道你的目标不是说 JS 动画“不好”或 CSS“更好”——我只是想说一些我在与 css 动画作斗争时遇到的注意事项。我没有任何争论的意思。
无耻的宣传:有一篇“笼子比赛”文章详细比较了 css 动画/过渡和 GSAP,网址是 http://www.greensock.com/transitions/ 和 http://www.greensock.com/why-gsap/。你可能会感到惊讶。
我完全赞成使用附加库来获得想要的东西!我对目前操纵 CSS 动画的局限性也不满意,但我认为我会分享一些让它更容易、比大多数人所知的更可行的方法。
你说它不是真正的暂停,你是对的,我知道这一点。缓动确实重置了,但在没有库的情况下,这与我们能够实现的最接近的方式一样好,除非我们像旋转圆圈示例那样创建全新的动画。我认为 CSS 动画的目的是不是成为动画的强大资源,以取代库,但我确实认为库并不总是必要的,并且相信这篇文章包含一些有用的技巧,可以在某些情况下避免使用附加库的必要性。
我认为 Zach 用 css 做到的非常值得称赞,鉴于 css 动画和过渡的限制性极强。
作为一个运动图形动画师和前端 Web 开发人员,这些文章总是让我感到不寒而栗。人们不得不做出多么大的努力才能使极其基本的动画工作,即使最终大多数动画都有问题,这使得它们在很多情况下都无法使用。
一个动画师需要理解(相对)高级的 javascript 才能使一个圆形转动,而且它还很卡顿,这对 Web 来说是一种悲惨的状况。
p.s. 我是一个狂热的 Greensock 用户,从 AS2/AS3 开始,现在使用 js 版本进行复杂的 Web 动画,我强烈推荐它。
关于缓动选项,你可以设置自己的时序函数,因此理论上你可以拥有 js 可以执行的任何缓动函数。看看这个 http://matthewlein.com/ceaser/
事实上,不,css 中的 cubic-bezier() **并不**允许你执行诸如反弹或弹性缓动之类的操作,或者 JS 和 GSAP 中可用的许多其他操作,因为 cubic-bezier() 只允许两个控制点。但它确实很好地处理了基本的缓动函数。
cubic-bezier 可以做到超过结束点然后返回的事情。 http://cubic-bezier.com/#.47,-0.36,.59,1.33,但它不完全像 http://james.padolsey.com/demos/jquery/newstick/ 那样反弹。
这篇文章写得很好,而且很全面,但是一些 CSS 功能在 Mac 上的 Firefox 24 中仍然无法正常运行。
在使用 CSS “回调函数”时,纯 CSS 示例会展开心脏,但不会结束动画,它会循环回到默认动画。
在将动画转换为过渡时,“是这个吗?”示例什么也不做。
我在“回调函数”示例中添加了一些浏览器前缀。你现在可以试试看是否有效?我认为你的问题可能是由于嵌入造成的,你也可以直接在 CodePen 上试试看你的问题是否仍然存在?
没有名为
transitionStart
或transitionIteration
的事件类型(尽管至少transitionstart
会很有用)。这里有很多很棒的信息。很高兴在一个地方找到所有这些信息!
我认为现在可以安全地删除动画回调事件名称中的“ms”、“moz”和“o”前缀。我注意到(桌面)Opera 正在使用 webkit 事件名称,而 IE10 和 Firefox 正在使用无前缀的事件名称。
webkit 事件名称和“常规”事件名称似乎至少在这个基本测试中涵盖了所有基础
http://codepen.io/valhead/pen/uKfEy
感谢你的提示和测试!
Zach
好文章,animo.js 非常有用。
谢谢
太棒了
太棒了!!!
有点超出我的理解范围。
但我相信当我在未来遇到问题时会回到这个讨论中哈哈。
:)
仅供参考,iOS 7 中的 Mobile Safari 具有
transitionEnd
,没有前缀(虽然我相信webkitTransitionEnd
仍然受支持)。好文章,Animo 库非常有用!。
你也可以使用 Modernizr 前缀为每个浏览器添加正确的带前缀的事件名称。这非常有用,因为你可能正在使用它来检测浏览器是否支持 CSS3 动画和过渡。
“获取动画的当前 CSS 属性值”示例对于 Firefox 24 似乎完全不起作用(无论是在内联,还是在 http://codepen.io/Zeaklous/pen/GwBJa )
感谢你提供如此棒的资源;我本来要对 CSS-JS 动画进行深入学习,而你为我节省了很多研究时间!
这在文章的该部分底部有说明。感谢你让我们知道潜在的问题!
大多数开发人员喜欢把事情复杂化,这真是太神奇了。我在这里看到的大多数东西都可以用 css3 来更轻松地完成。成为一名真正优秀的程序员是关于找到最简单的解决方案,而不是最花哨或最复杂的解决方案。
我同意简单性!一切都是关于简单性的——另一个主要因素是性能(尤其是当我们谈论移动设备时)。使用 CSS3 过渡,性能非常出色,因为它像这里看到的那样是“硬件加速的”
jQuery 动画 VS. CSS 过渡
..但是,我听说 CSS 过渡也有一些缺点(我相信这篇文章中包含了一个链接)。
我认为最好的方法是,以最简单的方式结合使用 javascript 和 css 过渡,我们就会朝着正确的方向前进。
P.S. Polymere(Web 的新动画)即将到来。我迫切想知道它会带来什么!
我想出了一个方法,可以使帧计数脚本更加精确。添加几行
这将计数器在动画重新开始的确切时刻重置为 0,从而消除并消除可能发生的任何重叠。
感谢你的改进!
我注意到循环心脏动画的 javascript 版本在我的 chrome 版本 33.0.1750.117 中存在错误——动画只停止了大约 3 次悬停中的 1 次?