为模糊处理添加动画效果

模糊处理是重定向用户注意力的绝佳方式。让某些视觉元素看起来模糊,同时让其他元素保持焦点,这样可以自然地引导用户的注意力。用户会忽略经过模糊处理的内容,专注于他们可以阅读的内容。例如,将鼠标悬停在各个项目上时显示相关详细信息的图标列表。在此期间,其余选项可能会进行模糊处理,以将用户重定向到新显示的信息。

要点

为模糊处理添加动画效果并不是可行的,因为动画速度非常慢。相反,应预先计算一系列模糊程度越来越高的版本,并在它们之间实现淡入淡出。我的同事 Yi Gu 编写了一个,可以为您处理所有工作!查看我们的演示

不过,如果没有任何过渡期,这种方法可能会非常令人反感。为模糊处理添加动画效果(从未模糊处理过渡到模糊处理)似乎是合理的选择,但如果您曾尝试过在网页上执行此操作,可能会发现动画效果不流畅,正如此演示演示的那样,如果您的机器不具备强大的功能。我们能做得更好吗?

问题

CPU 会将标记转换为纹理。纹理会上传到 GPU。GPU 会使用着色器将这些纹理绘制到帧缓冲区。模糊处理发生在着色器中。

目前,我们无法高效地为模糊处理添加动画效果。不过,我们可以找到一个看上去足够好但从技术上讲不是动画模糊处理的解决方法。首先,我们先了解动画模糊处理速度缓慢的原因。如需对网页上的元素进行模糊处理,可以使用以下两种方法:CSS filter 属性和 SVG 过滤器。由于增加了支持和易用性,通常使用 CSS 过滤器。遗憾的是,如果您需要支持 Internet Explorer,则只能使用 SVG 过滤器,因为 IE 10 和 11 支持这些过滤器,但不支持 CSS 过滤器。好消息是,我们为模糊处理添加动画效果的解决方法适用于这两种技术。让我们通过查看开发者工具来尝试找出瓶颈所在。

如果您在开发者工具中启用“Paint Flashing”,则看不到任何闪烁。似乎没有进行重绘。这在技术上是正确的,因为“重绘”是指 CPU 必须重绘所提升元素的纹理。每当某个元素同时进行提升和模糊处理时,GPU 都会使用着色器应用模糊处理。

SVG 过滤器和 CSS 过滤器都使用卷积过滤器来应用模糊处理。卷积过滤器的代价相当高,因为对于每个输出像素,都必须考虑一定数量的输入像素。图片越大或者模糊处理半径越大,效果的成本就越高。

这正是问题所在,我们每帧都要运行一个开销相当高的 GPU 操作,导致 16 毫秒的帧预算耗尽,最终会远低于 60fps。

探索兔子洞

那么,我们如何才能让它顺利运行呢?我们能靠巧手!我们不是为实际模糊处理值(模糊处理的半径)添加动画效果,而是预先计算一组模糊处理副本,其中模糊处理值呈指数级增长,然后使用 opacity 在它们之间交替淡出。

交叉淡入淡出是一系列重叠的不透明度淡入和淡出。例如,如果我们有四个模糊处理阶段,第一个阶段会淡出,同时第二个阶段也会淡出。当第二个阶段的不透明度达到 100% 且第一个阶段达到 0% 时,我们将淡出第二个阶段,同时在第三个阶段淡出。完成后,我们最终将淡出第三阶段,并淡入第四阶段,也就是最终版本。在这种情况下,每个阶段将占用所需总时长的 1⁄4。从视觉上看,这与真实的动画模糊处理非常相似。

在我们的实验中,按指数方式增加每个阶段的模糊处理半径可产生最佳的视觉效果。示例:如果我们有四个模糊处理阶段,则会将 filter: blur(2^n) 应用于每个阶段,即阶段 0:1px、阶段 1:2px、阶段 2:4px 和阶段 3:8px。如果我们使用 will-change: transform 将每个经过模糊处理的副本强制放到各自的层上(称为“提升”),那么更改这些元素的不透明度应该会非常快速。从理论上讲,这将使我们能够前载开销很大的模糊处理工作。结果发现,逻辑是有问题的。如果运行此演示,您会发现帧速率仍低于 60fps,而模糊处理实际上比之前更差

显示 GPU 长时间处于忙碌状态的跟踪记录。

快速查看一下开发者工具就会发现,GPU 仍然非常繁忙,并将每一帧拉伸至约 90 毫秒。但为什么呢?我们不再更改模糊处理值,而是更改不透明度。这样会发生什么?问题同样出在模糊效果的本质上:如前所述,如果该元素既被提升又进行了模糊处理,则 GPU 会应用该效果。因此,即使我们不再为模糊处理值添加动画效果,纹理本身仍然不会模糊处理,并且需要由 GPU 逐帧重新进行模糊处理。帧速率比之前更差的原因在于,与初始实现相比,GPU 实际上需要完成更多工作,因为大多数情况下,两个纹理都是可见的,它们需要单独进行模糊处理。

我们的想法并不好看,但它让动画变得极快。回到这里,提升待模糊处理的元素,而是提升父封装容器。如果某个元素既进行了模糊处理,又进行了提升,GPU 会应用相应的效果。这就是我们的演示速度变慢的原因。如果元素已经过模糊处理,但未提升,系统会改为将模糊处理光栅化为最近的父纹理。在本例中为提升的父封装容器元素。经过模糊处理后的图片现在是父元素的纹理,可以重复用于未来的所有帧。这只是因为我们知道经过模糊处理的元素不是动画形式,缓存它们实际上是有益的。此处提供了一个实现此方法的演示。不好意思,Moto G4 对这个方法有什么想法?提前剧透:它认为很棒:

显示 GPU 存在大量空闲时间的跟踪记录。

现在我们在 GPU 上有很大的提升空间,而且流畅的 60fps。我们成功了!

量身打造

在演示中,我们多次复制了 DOM 结构,以复制要以不同强度进行模糊处理的内容。您可能想知道这在生产环境中是怎样运作的,因为这可能会对作者的 CSS 样式甚至其 JavaScript 产生一些意外的副作用。你是对的。进入 Shadow DOM!

虽然大多数人认为 Shadow DOM 是一种将“内部”元素附加到其自定义元素的方法,但它也是一种隔离且性能基元!JavaScript 和 CSS 无法穿透 Shadow DOM 边界,这样一来,我们就可以在不干扰开发者样式或应用逻辑的情况下复制内容。每个要光栅化的副本都已有 <div> 元素,现在这些 <div> 用作影子宿主。我们使用 attachShadow({mode: 'closed'}) 创建 ShadowRoot,并将内容的副本附加到 ShadowRoot(而非 <div> 本身)。我们还必须将所有样式表复制到 ShadowRoot 中,以保证副本的样式与原始样式相同。

有些浏览器不支持 Shadow DOM v1,对于此类浏览器,我们会回退到仅复制内容并希望尽可能避免发生中断。我们可以将 Shadow DOM polyfillShadyCSS 配合使用,但在库中并未实现这一点。

就是这样。在深入探索 Chrome 的渲染管道后 我们研究了如何 在各种浏览器中高效地为模糊处理添加动画效果

总结

请谨慎使用这种效果。由于我们复制 DOM 元素并将它们强制到其自己的层中,因此我们可以突破低端设备的极限。将所有样式表复制到每个 ShadowRoot 也会带来潜在的性能风险,因此您应该决定是调整您的逻辑和样式,使其不受 LightDOM 中副本的影响,还是使用我们的 ShadowDOM 方法。但有时,我们的技术可能值得投资。请查看 GitHub 代码库中的代码以及演示。如果您有任何疑问,请通过 Twitter 与我联系!