使用 CSS 过渡和动画改变渐变的状态

Avatar of Ana Tudor
Ana Tudor on

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 价值 200 美元的免费积分!

早在 2012 年,Internet Explorer 10 发布了,除了其他功能之外,它终于支持了 CSS 渐变,并且能够仅使用 CSS 来对其进行动画!当时没有其他浏览器支持此功能,但我对未来充满希望。

遗憾的是,六年过去了,在这个领域并没有什么改变。Edge 支持使用 CSS 对渐变进行动画,就像 IE 10 当时那样,但其他浏览器都没有添加对该功能的支持。虽然使用动画 background-sizebackground-position 或叠加在顶部的伪元素的 opacity 或旋转 可以帮助我们实现很多酷炫的效果,但这些变通方法仍然有限。

有一些效果,我们无法在不添加大量额外元素或额外渐变的情况下重现,例如下面看到的“百叶窗效果”。

Animated GIF showing a recording of the opening and closing blinds effect. When the blinds are closed, we only see a grey background, when the blinds start to open, we start seeing vertical orange strips (the light coming in) that grow horizontally until the blinds are fully open, so we only see an orange background. After that, the blinds start to close, so the vertical orange strips start getting narrower until they're reduced to nothing when the blinds are fully closed and we only see a grey background again. The whole cycle then repeats itself.
百叶窗效果(实时演示,仅限 Edge/IE 10+)。

在 Edge 中,使用 关键帧 animation 可以实现以上效果。

html {
  background: linear-gradient(90deg, #f90 0%, #444 0) 50%/ 5em;
  animation: blinds 1s ease-in-out infinite alternate;
}

@keyframes blinds {
  to {
    background-image: linear-gradient(90deg, #f90 100%, #444 0);
  }
}

如果这看起来 很 WET,我们可以 DRY 一点,稍微使用一下 Sass

@function blinds($open: 0) {
  @return linear-gradient(90deg, #f90 $open*100%, #444 0);
}

html {
  background: blinds() 50%/ 5em;
  animation: blinds 1s ease-in-out infinite alternate;
}

@keyframes blinds { to { background-image: blinds(1) } }

虽然我们使编写的代码以及以后需要编辑的内容更易于维护,但编译后的 CSS 中仍然存在重复,并且我们受到只能在具有相同单位的停止之间进行动画的限制。在 0%100% 之间进行动画效果很好,但尝试使用 00px 而不是 0% 时,动画将不再发生。更不用说 Chrome 和 Firefox 直接从橙色变为灰色,根本没有停止位置 animation 了!

幸运的是,如今我们有一个更好的选择:CSS 变量!

从一开始,CSS 变量就不可动画,但如果我们使用它们的属性可动画,我们可以获得 transition(但不能获得 animation!)效果。例如,在 transform 函数中使用时,我们可以对 transform 属性进行 transition

让我们考虑一个示例,当选中复选框时,一个框会移动和压缩。在这个框上,我们设置了一个取决于初始值为 1 的因子 --ftransform

.box {
  /* basic styles like dimensions and background */
  --f: 1;
  transform: translate(calc((1 - var(--f))*100vw)) scalex(var(--f));
}

当复选框 :checked 时,我们将 CSS 变量 --f 的值更改为 .5

:checked ~ .box { --f: .5 }

.box 上设置 transition 使其平滑地从一种状态过渡到另一种状态。

.box {
  /* same styles as before */
  transition: transform .3s ease-in;
}

请注意,由于 此错误,这在 Edge 的当前版本中并不真正有效。

然而,CSS 渐变是背景图像,它们只能在 Edge 和 IE 10+ 中进行动画。因此,虽然我们可以简化自身的工作并减少为过渡生成的 CSS 量(如以下代码所示),但在扩展支持方面我们仍然没有取得进展。

.blinds {
  background: linear-gradient(90deg, #f90 var(--pos, 0%), #444 0) 50%/ 5em;
  transition: .3s ease-in-out;
    
  :checked ~ & { --pos: 100%; }
}
Animated gif. The blinds opening effect happens on checking an 'open blinds' checkbox, while unchecking it triggers the closing effect.
选中/取消选中复选框时打开/关闭百叶窗(实时演示,仅限 Edge)。

Houdini 来了,它允许我们注册自定义属性并对其进行动画。目前,这仅由 Blink 浏览器在 实验性 Web 平台功能 标志后面支持,但它仍然比仅仅 Edge 支持更进一步。

Screenshot showing the Experimental Web Platform features flag being enabled in Chrome.
在 Chrome 中启用的实验性 Web 平台功能标志。

回到我们的示例,我们注册了 --pos 自定义属性。

CSS.registerProperty({
  name: '--pos', 
  syntax: '<length-percentage>', 
  initialValue: '0%', 
  inherits: true
});

请注意, 表示它不仅接受长度和百分比值,还接受它们的 calc() 组合。相反, | 仅接受长度和百分比值,但不接受它们的 calc() 组合。

请注意,现在必须明确指定 inherits,即使在规范的先前版本中它是可选的。

但是,这样做 在 Chrome 中没有任何区别,即使启用了标志,可能是因为在过渡的情况下,正在过渡的是其值取决于 CSS 变量的属性,而不是 CSS 变量本身。由于我们通常无法在 Chrome 中对两个背景图像进行过渡,因此这也失败了。

它确实在 Edge 中有效,但它在 Edge 中甚至在未注册 --pos 变量的情况下也能有效,因为 Edge 允许我们对渐变进行过渡。

在启用了标志的 Blink 浏览器中,有效的 是使用 animation 而不是 transition

html {
  background: linear-gradient(90deg, #f90 var(--pos, 0%), #444 0) 50%/ 5em;
  animation: blinds .85s ease-in-out infinite alternate;
}

@keyframes blinds { to { --pos: 100%; } }

但是,这现在在 Edge 中不再有效,因为虽然 Edge 可以对渐变背景进行动画,但它不能对自定义属性执行相同操作。

因此,我们需要对 Edge 采取另一种方法。这就是 @supports 派上用场的地方,因为我们只需检查是否支持 -ms- 前缀属性即可。

@function grad($pos: 100%) {
  @return linear-gradient(90deg, #f90 $pos, #444 0);
}

html {
  /* same as before */
    
  @supports (-ms-user-select: none) {
    background-image: grad(0%);
    animation-name: blinds-alt;
  }
}

@keyframes blinds-alt { to { background-image: grad() } }

停止位置不是我们唯一可以以此方式进行动画的属性。我们可以对渐变角度执行相同的操作。它背后的理念几乎相同,只是现在我们的 animation 不再是交替的了,而是使用 easeInOutBack 类型的计时函数。

@function grad($ang: 1turn) {
  @return linear-gradient($ang, #f90 50%, #444 0);
}

html {
  background: grad(var(--ang, 0deg));
  animation: rot 2s cubic-bezier(.68, -.57, .26, 1.65) infinite;
  
  @supports (-ms-user-select: none) {
    background-image: grad(0turn);
    animation-name: rot-alt;
  }
}

@keyframes rot { to { --ang: 1turn; } }

@keyframes rot-alt { to { background-image: grad(); } }

请记住,就像停止位置一样,我们只能在 Edge 中以相同单位表示的渐变角度之间进行动画,因此使用 grad(0deg) 而不是 grad(0turn) 调用 Sass 函数不起作用。

当然,我们现在使用的 CSS 变量接受角度值而不是长度和百分比。

CSS.registerProperty({
  name: '--ang', 
  syntax: '<angle>', 
  initialValue: '0deg', 
  inherits: true
});
Animated gif. Shows a top to bottom gradient with an abrupt change from grey to orange at 50%. The angle of this gradient is animated using a easeInOutBack timing function (which overshoots the end values at both ends).
扫过(实时演示,仅限启用了标志的 Blink 浏览器和 Edge)。

以类似的方式,我们也可以对径向渐变进行动画。关于 CSS 变量方法真正令人惊奇的一点是,它允许我们对渐变的不同组件进行不同的动画,这在将渐变作为整体进行动画时是无法实现的,就像 Edge 所做的那样(这就是为什么以下演示在 Edge 中效果不佳的原因)。

假设我们有以下 radial-gradient()

$p: 9%;

html {
  --x: #{$p};
  --y: #{$p};
  background: radial-gradient(circle at var(--x) var(--y), #f90, #444 $p);
}

我们注册了 --x--y 变量。

CSS.registerProperty({
  name: '--x', 
  syntax: '<length-percentage>', 
  initialValue: '0%', 
  inherits: true
});

CSS.registerProperty({
  name: '--y', 
  syntax: '<length-percentage>', 
  initialValue: '0%', 
  inherits: true
});

然后我们添加动画。

html {
  /* same as before */
  animation: a 0s ease-in-out -2.3s alternate infinite;
  animation-name: x, y;
  animation-duration: 4.1s, 2.9s;
}

@keyframes x { to { --x: #{100% - $p} } }
@keyframes y { to { --y: #{100% - $p} } }

我们得到的结果如下所示。

Animated GIF. Shows a moving glowing orange light on a grey background. This is achieved by animating the coordinates of the central point of a radial gradient independently with the help of CSS variables and Houdini.
移动灯光(实时演示,仅限启用了标志的 Blink 浏览器)。

我们可以使用这种在渐变函数中使用的不同自定义属性进行动画的技术,使我们初始示例中的百叶窗以相反的方向关闭,而不是后退。为此,我们引入了两个额外的 CSS 变量,--c0--c1

$c: #f90 #444;

html {
  --c0: #{nth($c, 1)};
  --c1: #{nth($c, 2)};
  background: linear-gradient(90deg, var(--c0) var(--pos, 0%), var(--c1) 0) 50%/ 5em;
}

我们注册了所有这些自定义属性。

CSS.registerProperty({
  name: '--pos', 
  syntax: '<length-percentage>', 
  initialValue: '0%', 
  inherits: true
});

CSS.registerProperty({
  name: '--c0', 
  syntax: '<color>', 
  initialValue: 'red', 
  inherits: true
});

/* same for --c1 */

我们对第一个停止位置 --pos 使用与之前相同的动画,此外,我们为另外两个变量引入了两个 steps() 动画,每次第一个 animation(更改 --pos 值的动画)完成一次迭代时,它们的值就会切换。

$t: 1s;

html {
  /* same as before */
  animation: a 0s infinite;
  animation-name: c0, pos, c1;
  animation-duration: 2*$t, $t;
  animation-timing-function: steps(1), ease-in-out;
}

@keyframes pos { to { --pos: 100%; } }

@keyframes c0 { 50% { --c0: #{nth($c, 2)} } }
@keyframes c1 { 50% { --c1: #{nth($c, 1)} } }

我们得到了以下结果。

Animated GIF. Shows the blinds effect with the blinds closing the other way. Once the vertical orange strips (openings) have expanded horizontally such that they cover the whole background, they don't start contracting again. Instead, vertical grey orange strips start expanding from nothing until they cover the whole background.
百叶窗动画的另一个版本(实时演示,仅限启用了标志的 Blink 浏览器)。

我们也可以将其应用于 radial-gradient()(只有 background 声明发生变化)。

background: radial-gradient(circle, var(--c0) var(--pos, 0%), var(--c1) 0);
Animated gif. We start with a grey background and we have an orange disc growing from nothing in the middle until it covers everything. Then we have a grey disc growing from nothing in the middle until it covers the entire background and we're back where we started from: a grey background.
增长圆盘(实时演示,仅限启用了标志的 Blink 浏览器)。

完全相同的策略也适用于 conic-gradient()

background: conic-gradient(var(--c0) var(--pos, 0%), var(--c1) 0);
Animated gif. We start with a grey background and we have an orange pie slice (circular sector) growing from nothing to covering everything around the central point. Then we have a grey pie slice growing from nothing to covering everything around the central point and we're back where we started from: a grey background.
增长切片(实时演示,仅限启用了标志的 Blink 浏览器)。

重复渐变也是一种选择,在径向情况下会产生涟漪般的效果。

$p: 2em;

html {
  /* same as before */
  background: repeating-radial-gradient(circle, 
    var(--c0) 0 var(--pos, 0px), var(--c1) 0 $p);
}

@keyframes pos { 90%, 100% { --pos: #{$p} } }
Animated gif. We start with a grey background and we have concentric orange circles growing outwards from really thin until they meet and cover everything, so now it looks like we have an orange background. Then we have grey circles growing outwards from really thin until they cover the entire background and we're back where we started from: a grey background.
涟漪(实时演示,仅限启用了标志的 Blink 浏览器)。

在圆锥形情况下,它会产生螺旋/射线效果。

$p: 5%;

html {
  /* same as before */
  background: repeating-conic-gradient(
    var(--c0) 0 var(--pos, 0%), var(--c1) 0 $p);
}

@keyframes pos { 90%, 100% { --pos: #{$p} } }
Animated gif. We start with a grey background and we have orange rays growing clockwise from really thin until they meet and cover everything, so now it looks like we have an orange background. Then we have grey rays growing clockwise from really thin until they cover the entire background and we're back where we started from: a grey background.
增长射线(实时演示,仅限启用了标志的 Blink 浏览器)。

我们还可以添加另一个 CSS 变量来使事物更有趣。

$n: 20;

html {
  /* same as before */
  background: radial-gradient(circle at var(--o, 50% 50%), 
    var(--c0) var(--pos, 0%), var(--c1) 0);
  animation: a 0s infinite;
  animation-name: c0, o, pos, c1;
  animation-duration: 2*$t, $n*$t, $t;
  animation-timing-function: steps(1), steps(1), ease-in-out;
}

@keyframes o {
  @for $i from 0 to $n {
    #{$i*100%/$n} { --o: #{random(100)*1%} #{random(100)*1%} }
  }
}

我们需要注册这个变量,才能使整个过程正常工作。

CSS.registerProperty({
  name: '--o', 
  syntax: '<length-percentage>', 
  initialValue: '50%', 
  inherits: true
});

就这样!结果如下所示。

Animated gif. We start with a grey background and we have an oranges disc, randomly positioned, growing from nothing until it covers everything, so now it looks like we have an orange background. Then we have grey disc, randomly positioned, growing from nothing until it covers the entire background and we're back where we started from: a grey background.
随机定位的增长圆盘(实时演示,仅限启用了标志的 Blink 浏览器)。

我认为使用关键帧动画更改渐变的未来看起来相当酷。但与此同时,对于跨浏览器解决方案而言,JavaScript 方法 仍然是唯一有效的方法。