为模糊处理添加动画效果

模糊处理是转移用户注意力的绝佳方式。使某些视觉元素模糊不清,同时保持其他元素清晰可见,这样可以自然而然地引导用户将注意力集中在清晰的元素上。用户会忽略模糊处理的内容,而专注于他们可以阅读的内容。例如,当鼠标悬停在某个图标上时,该图标会显示有关相应项的详细信息。在此期间,系统可能会模糊处理其余选项,以将用户重定向到新显示的信息。

要点

为模糊效果添加动画效果并不是一个好选择,因为速度非常慢。而是预先计算一系列模糊程度逐渐增加的版本,并在这些版本之间进行交叉淡化。我的同事 Yi Gu 编写了一个,可以为您处理所有事情!不妨看看我们的演示

不过,如果不设置任何过渡期,此技术可能会显得非常突兀。模糊动画(从不模糊到模糊的过渡)似乎是一个合理的选择,但如果您曾在网页上尝试过这样做,您可能会发现动画并不流畅,正如这个演示所示(如果您没有强大的机器)。我们能否做得更好?

问题

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

目前,我们无法高效地实现模糊动画效果。不过,我们可以找到一种看起来不错的变通方法,但从技术上讲,它并不是动画模糊效果。首先,我们来了解一下动画模糊效果缓慢的原因。如需模糊网页上的元素,可以使用两种技术:CSS filter 属性和 SVG 滤镜。由于支持力度加大且易于使用,CSS 滤镜通常会派上用场。遗憾的是,如果您必须支持 Internet Explorer,则只能使用 SVG 滤镜,因为 IE 10 和 11 支持 SVG 滤镜,但不支持 CSS 滤镜。好消息是,我们用于为模糊效果添加动画的解决方法适用于这两种技术。因此,我们不妨通过查看开发者工具来尝试找出瓶颈。

如果您在开发者工具中启用“绘制闪烁”,则根本不会看到任何闪烁。看起来没有发生任何重绘。从技术上讲,这是正确的,因为“重绘”是指 CPU 必须重绘提升元素的纹理。如果某个元素同时被突出显示模糊处理,则模糊效果由 GPU 使用着色器应用。

SVG 滤镜和 CSS 滤镜都使用卷积滤镜来应用模糊效果。卷积滤波器的开销相当大,因为对于每个输出像素,都必须考虑多个输入像素。图片越大或模糊半径越大,效果的成本就越高。

问题就出在这里,我们每帧都在运行相当耗费资源的 GPU 操作,这会超出 16 毫秒的帧预算,因此最终的帧速率远低于 60fps。

深入了解

那么,我们该怎么做才能让这个过程顺利进行呢?我们可以使用障眼法!我们没有对实际的模糊值(模糊半径)进行动画处理,而是预先计算出几个模糊副本,其中模糊值呈指数级增长,然后使用 opacity 在它们之间进行淡入淡出。

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

在我们的实验中,按阶段以指数方式增加模糊半径可获得最佳视觉效果。示例:如果我们有四个模糊阶段,则会为每个阶段应用 filter: blur(2^n),即阶段 0:1 像素,阶段 1:2 像素,阶段 2:4 像素,阶段 3:8 像素。如果我们使用 will-change: transform 将每个模糊副本强制放到各自的图层上(称为“提升”),那么更改这些元素的透明度应该会非常快。从理论上讲,这会让我们能够预先完成模糊处理这项耗费资源的工作。结果发现,该逻辑存在缺陷。如果您运行此演示,您会发现帧速率仍然低于 60fps,并且模糊效果实际上比之前更差

显示 GPU 长时间处于繁忙状态的轨迹的开发者工具。

快速查看开发者工具会发现,GPU 仍然非常繁忙,并将每帧拉伸到大约 90 毫秒。但为什么?我们不再更改模糊值,只更改不透明度,这是怎么回事?问题再次出在模糊效果的性质上:如前所述,如果元素既被提升又被模糊,则效果由 GPU 应用。因此,即使我们不再为模糊值添加动画效果,纹理本身仍然处于未模糊状态,需要由 GPU 在每一帧重新模糊。帧速率比之前更差的原因在于,与简单实现相比,GPU 实际上需要完成更多工作,因为大多数时候都有两个需要单独模糊的纹理可见。

我们想出的方法并不美观,但可让动画的播放速度非常快。 我们重新改为提升待模糊处理的元素,而是提升父封装容器。如果某个元素同时模糊化和突出显示,则效果由 GPU 应用。这导致了演示速度缓慢。如果元素已模糊处理但未提升,则模糊效果会栅格化到最近的父纹理。在本例中,它是提升的父级封装容器元素。模糊处理后的图片现在是父元素的纹理,可供所有后续帧重复使用。之所以能这样做,是因为我们知道模糊元素不会进行动画处理,缓存它们实际上是有益的。下面是一个实现此技术的演示。不知道 Moto G4 对这种方法有何看法?剧透预警:它认为自己很棒:

显示 GPU 有大量空闲时间的轨迹的开发者工具。

现在,GPU 有了充足的余量,可以实现丝滑流畅的 60fps。我们做到了!

生产化

在演示中,我们多次复制 DOM 结构,以获得不同模糊强度的内容副本。您可能想知道这在生产环境中会如何运作,因为这可能会对作者的 CSS 样式甚至 JavaScript 产生一些意想不到的副作用。您说得对。进入 Shadow DOM!

虽然大多数人认为 Shadow DOM 是一种将“内部”元素附加到 Custom Elements 的方式,但它也是一种隔离和性能原语!JavaScript 和 CSS 无法穿透 Shadow DOM 边界,这使我们能够复制内容,而不会干扰开发者的样式或应用逻辑。我们已经为每个要栅格化的副本准备了一个 <div> 元素,现在将这些 <div> 用作阴影宿主。我们使用 attachShadow({mode: 'closed'}) 创建 ShadowRoot,并将内容副本附加到 ShadowRoot,而不是 <div> 本身。我们必须确保还将所有样式表复制到 ShadowRoot 中,以保证复制的内容与原始内容具有相同的样式。

有些浏览器不支持 Shadow DOM v1,对于这些浏览器,我们只能复制内容,并希望一切正常。我们可以将 ShadyCSSShadow DOM 填充区搭配使用,但我们并未在库中实现此功能。

这样就完成了。在了解了 Chrome 的渲染流水线后,我们找到了在浏览器中高效实现模糊动画的方法!

总结

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