假设我告诉你,我们只需要一个 HTML 元素和五个 CSS 属性就可以实现以下结果。没有 SVG,没有图像(除了根目录上的 `background`,它只是为了清楚地说明我们的一个 HTML 元素有一些透明部分),没有 JavaScript。你会想到什么?

嗯,这篇文章将解释如何做到这一点,并展示如何通过添加一些动画来使事情变得有趣。
CSS 化渐变光线
HTML 代码只是一个 `<div>`。
<div class='rays'></div>
在 CSS 中,我们需要设置此元素的尺寸,并为其提供一个 `background`,以便我们能够看到它。我们还使用 `border-radius` 使其成为圆形。
.rays {
width: 80vmin; height: 80vmin;
border-radius: 50%;
background: linear-gradient(#b53, #f90);
}
而且... 我们已经使用了五个属性中的四个来获得以下结果。
查看由 thebabydino (@thebabydino) 在 CodePen 上创建的 Pen。
那么第五个是什么?`mask` 与 `repeating-conic-gradient()` 值!
假设我们想要有 20 条光线。这意味着我们需要为一条光线及其后的间隙分配圆圈的 `$p: 100%/20` 部分。
在这里,我们将光线之间的间隙保持与光线相等(因此对于光线或空间来说都是 `.5*$p`),但我们可以使它们中的任何一个更宽或更窄。我们希望在不透明部分(光线)的结束停止位置之后出现突然的变化,因此透明部分(间隙)的开始停止位置应该等于或小于它。因此,如果光线的结束停止位置是 `.5*$p`,那么间隙的开始停止位置不能大于它。但是,它可以更小,这有助于我们保持事情简单,因为它意味着我们可以简单地将其置零。
$nr: 20; // number of rays
$p: 100%/$nr; // percent of circle allocated to a ray and gap after
.rays {
/* same as before */
mask: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p);
}
请注意,与线性渐变和径向渐变不同,圆锥渐变的停止位置不能是无单位的。它们需要是百分比或角度值。这意味着使用 `transparent 0 $p` 之类的东西不起作用,我们需要 `transparent 0% $p`(或使用 `0deg` 而不是 `0%`,我们选择哪个无关紧要,它不能是无单位的)。

在支持方面需要注意一些事情。
- Edge 目前不支持在 HTML 元素上进行蒙版,尽管这被列为 开发中,并且已经出现在 `about:flags` 中的一个标志(目前没有任何作用)。
Edge 中的 **启用 CSS 蒙版** 标志。 conic-gradient()
仅在 **实验性 Web 平台功能** 标志(可以在 `chrome://flags` 或 `opera://flags` 中启用)后面的 Blink 浏览器中原生支持。Safari 也将提供支持,但在此之前,Safari 仍然依赖于 polyfill,就像 Firefox(或 Edge 在下一个版本中支持蒙版时)一样。**更新**: 从 Chrome 69 开始,`conic-gradient()` 不再受标志限制 - 它现在可以在任何最新的 Blink 浏览器中使用,无论标志是否启用。
在 Chrome 中启用的 **实验性 Web 平台功能** 标志。 - WebKit 浏览器仍然需要在 HTML 元素上的 `mask` 属性中使用 `-webkit-` 前缀。您可能会认为这不是问题,因为我们使用的是依赖于 -prefix-free 的 polyfill,所以如果我们使用 polyfill,那么我们无论如何都需要在 polyfill 之前包含 -prefix-free。可悲的是,情况比这更复杂。这是因为 -prefix-free 通过功能检测工作,在这种情况下它会失败,因为所有浏览器都支持 `mask`,没有在 SVG 元素上添加前缀...!但我们在这里在 HTML 元素上使用 `mask`,所以我们处于 WebKit 浏览器需要 `-webkit-` 前缀,但 -prefix-free 不会添加它。所以我想这意味着我们需要手动添加它。
$nr: 20; // number of rays $p: 100%/$nr; // percent of circle allocated to a ray and gap after $m: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p); // mask .rays { /* same as before */ -webkit-mask: $m; mask: $m; }
我想我们也可以使用 Autoprefixer,即使我们需要包含 -prefix-free,但只为此目的使用两者感觉有点像用猎枪打苍蝇。
添加动画
`conic-gradient()` 在 Blink 浏览器中原生支持的一件很酷的事情是,我们可以在其中使用 CSS 变量(在使用 polyfill 时我们无法做到这一点)。CSS 变量现在也可以在 Blink 浏览器中使用一些 Houdini 魔法进行动画化(我们需要启用 **实验性 Web 平台功能** 标志才能做到这一点,即使从 Chrome 69+ 开始,我们不再需要它来支持原生 `conic-gradient()`)。
为了为动画准备我们的代码,我们更改蒙版渐变,使其使用可变 alpha 值。
$m: repeating-conic-gradient(
rgba(#000, var(--a)) 0% .5*$p,
rgba(#000, calc(1 - var(--a))) 0% $p);
然后我们注册 alpha `--a` 自定义属性。
CSS.registerProperty({
name: '--a',
syntax: '<number>',
initialValue: 1,
inherits: true
})
请注意,规范现在要求明确指定 `inherits`,即使它以前是可选的。因此,如果任何不指定它的 Houdini 演示程序都坏了,至少这是一个原因。
最后,我们在 CSS 中添加一个 `animation`。
.rays {
/* same as before */
animation: a 2s linear infinite alternate;
}
@keyframes a { to { --a: 0 } }
这将给我们以下结果。

嗯。看起来不太好。但是,我们可以通过使用多个 alpha 值来使事情变得更有趣。
$m: repeating-conic-gradient(
rgba(#000, var(--a0)) 0%, rgba(#000, var(--a1)) .5*$p,
rgba(#000, var(--a2)) 0%, rgba(#000, var(--a3)) $p);
下一步是注册每个自定义属性。
for(let i = 0; i < 4; i++) {
CSS.registerProperty({
name: `--a${i}`,
syntax: '<number>',
initialValue: 1 - ~~(i/2),
inherits: true
})
}
最后,在 CSS 中添加动画。
.rays {
/* same as before */
animation: a 2s infinite alternate;
animation-name: a0, a1, a2, a3;
animation-timing-function:
/* easings from easings.net */
cubic-bezier(.57, .05, .67, .19) /* easeInCubic */,
cubic-bezier(.21, .61, .35, 1); /* easeOutCubic */
}
@for $i from 0 to 4 {
@keyframes a#{$i} { to { --a#{$i}: #{floor($i/2)} } }
}
请注意,由于我们正在为自定义属性设置值,因此我们需要 插值 floor()
函数。

现在看起来有趣多了,但我们肯定可以做得更好?
让我们尝试为光线和间隙之间的停止位置使用 CSS 变量。
$m: repeating-conic-gradient(#000 0% var(--p), transparent 0% $p);
然后我们注册此变量。
CSS.registerProperty({
name: '--p',
syntax: '<percentage>',
initialValue: '0%',
inherits: true
})
我们使用关键帧 `animation` 从 CSS 中对其进行动画化。
.rays {
/* same as before */
animation: p .5s linear infinite alternate
}
@keyframes p { to { --p: #{$p} } }
在这种情况下,结果更有趣。

但是,我们仍然可以通过在每次迭代之间水平翻转整个内容来使其更有趣,这样它在反向迭代时始终被翻转。这意味着当 `--p` 从 `0%` 变为 `$p` 时不翻转,而当 `--p` 从 `$p` 变回 `0%` 时翻转。
我们水平翻转元素的方法是为其应用 `transform: scalex(-1)`。由于我们希望此翻转在第一次迭代结束时应用,然后在第二次(反向)迭代结束时移除,因此我们也将其应用在关键帧 `animation` 中 - 在具有 `steps()` 定时函数和 `animation-duration` 两倍的关键帧中。
$t: .5s;
.rays {
/* same as before */
animation: p $t linear infinite alternate,
s 2*$t steps(1) infinite;
}
@keyframes p { to { --p: #{$p} } }
@keyframes s { 50% { transform: scalex(-1); } }
现在我们终于得到了一个看起来确实很酷的结果。

CSS 化渐变光线和波纹
要获得光线和波纹的效果,我们需要在 `mask` 中添加第二个渐变,这次是 `repeating-radial-gradient()`。
$nr: 20;
$p: 100%/$nr;
$stop-list: #000 0% .5*$p, transparent 0% $p;
$m: repeating-conic-gradient($stop-list),
repeating-radial-gradient(closest-side, $stop-list);
.rays-ripples {
/* same as before */
mask: $m;
}
可悲的是,使用多个停止位置 仅在启用了相同 **实验性 Web 平台功能** 标志的 Blink 浏览器中有效。虽然 `conic-gradient()` polyfill 在支持 HTML 元素上的 CSS 蒙版,但不支持原生圆锥渐变的浏览器(Firefox、Safari、没有启用标志的 Blink 浏览器)中涵盖了 `repeating-conic-gradient()` 部分,但没有任何东西可以修复这些浏览器中 `repeating-radial-gradient()` 部分的问题。
这意味着我们被迫在代码中进行一些重复。
$nr: 20;
$p: 100%/$nr;
$stop-list: #000, #000 .5*$p, transparent 0%, transparent $p;
$m: repeating-conic-gradient($stop-list),
repeating-radial-gradient(closest-side, $stop-list);
.rays-ripples {
/* same as before */
mask: $m;
}
我们显然越来越接近,但还没有完全达到预期。

要获得我们想要的结果,我们需要使用 mask-composite
属性并将其设置为 `exclude`。
目前,`mask-composite` 仅在 Firefox 53+ 中支持,尽管 WebKit 浏览器(从 Chrome 1.0 和 Safari 4.0 开始)对一个类似的非标准属性 -webkit-mask-composite
有很好的支持,该属性帮助我们在使用 `xor` 值时获得相同的结果,而 Edge 应该加入,因为它最终支持 HTML 元素上的 CSS 蒙版。但是请注意,即使任何使用 `-webkit-mask-composite` 来定位 WebKit 浏览器的用户都可能会使用非标准值,因为它们在 WebKit 浏览器中根本无法工作,但 Edge 将支持 `mask-composite` 和 `-webkit-mask-composite`,并使用标准值。
$lyr1: repeating-conic-gradient($stop-list);
$lyr0: repeating-radial-gradient(closest-side, $stop-list);
.xor {
/* same as before */
-webkit-mask: $lyr1, $lyr0;
-webkit-mask-composite: xor;
mask: $lyr1 exclude, $lyr0
}
请注意,非标准的 `-webkit-mask-composite` 不能像我们在 Firefox 中在 `mask` 简写中使用标准的 `mask-composite` 一样在 `-webkit-mask` 简写中使用。

如果你认为在不支持 `conic-gradient()` 的浏览器中,射线和射线之间的间隙看起来不均匀,那么你是对的。这是由于 polyfill 的 问题。
添加动画
由于标准的 `mask-composite` 目前只在 Firefox 中有效,而 Firefox 尚未原生支持 `conic-gradient()`,因此我们无法将 CSS 变量放入 `repeating-conic-gradient()` 中(因为 Firefox 仍然会回退到 polyfill,而 polyfill 不支持 CSS 变量的使用)。但是,我们可以将它们放入 `repeating-radial-gradient()` 中,即使我们无法使用 CSS 关键帧动画来动画它们,我们也可以使用 JavaScript 来实现!
由于我们现在将 CSS 变量放入 `repeating-radial-gradient()` 中,但没有放入 `repeating-conic-gradient()` 中(因为我们希望获得更好的浏览器支持,而 Firefox 不原生支持圆锥渐变,因此它回退到 polyfill,而 polyfill 不支持 CSS 变量的使用),因此我们无法再对 `mask` 的两个渐变层使用相同的 `$stop-list`。
但是,如果我们不得不无论如何都要重写 `mask`,而没有共同的 `$stop-list`,那么我们可以借此机会对两个渐变使用不同的停止位置。
// for conic gradient
$nc: 20;
$pc: 100%/$nc;
// for radial gradient
$nr: 10;
$pr: 100%/$nr;
我们动画的 CSS 变量是 alpha `--a` 变量,就像射线情况下的第一个动画一样。我们还引入了 `--c0` 和 `--c1` 变量,因为在这里我们无法为每个停止点设置多个位置,并且希望尽可能避免重复。
$lyr1: repeating-conic-gradient(#000 0% .5*$pc, transparent 0% $pc);
$lyr0: repeating-radial-gradient(closest-side,
var(--c0), var(--c0) .5*$pr,
var(--c1) 0, var(--c1) $pr);
body {
--a: 0;
/* layout, backgrounds and other irrelevant stuff */
}
.xor {
/* same as before */
--c0: #{rgba(#000, var(--a))};
--c1: #{rgba(#000, calc(1 - var(--a)))};
-webkit-mask: $lyr1, $lyr0;
-webkit-mask-composite: xor;
mask: $lyr1 exclude, $lyr0
}
alpha 变量 `--a` 是我们用一点点原生 JavaScript 来回动画的变量(从 `0` 到 `1`,然后再次回到 `0`)。我们首先设置动画发生的总帧数 `NF`、当前帧索引 `f` 和当前动画方向 `dir`。
const NF = 50;
let f = 0, dir = 1;
在 `update()` 函数中,我们更新当前帧索引 `f`,然后将当前进度值 ( `f/NF` ) 设置为当前 alpha `--a`。如果 `f` 达到 `0` 或 `NF`,我们改变方向。然后 `update()` 函数在下次刷新时再次被调用。
(function update() {
f += dir;
document.body.style.setProperty('--a', (f/NF).toFixed(2));
if(!(f%NF)) dir *= -1;
requestAnimationFrame(update)
})();
这就是 JavaScript 的全部内容!我们现在得到了一个动画效果。

这是一个线性动画,alpha 值 `--a` 被设置为进度 `f/NF`。但是,我们可以将计时函数更改为其他内容,如我在之前关于 使用 JavaScript 模拟 CSS 计时函数 的文章中所述。
例如,如果我们想要一个 `ease-in` 类型的计时函数,我们将 alpha 值设置为 `easeIn(f/NF)` 而不是 `f/NF`,其中我们有 `easeIn()` 函数:
function easeIn(k, e = 1.675) {
return Math.pow(k, e)
}
使用 `ease-in` 计时函数的结果可以在 这个 Pen 中看到(Firefox 53+ 使用标准的 `mask-composite` 和 Chrome 1.0+/ Safari 4.0+ 使用非标准的 `-webkit-mask-composite`)。如果你对如何得到这个函数感兴趣,它在之前链接的关于计时函数的文章中得到了详细解释。
完全相同的做法适用于 `easeOut()` 或 `easeInOut()`。
function easeOut(k, e = 1.675) {
return 1 - Math.pow(1 - k, e)
};
function easeInOut(k) {
return .5*(Math.sin((k - .5)*Math.PI) + 1)
}
由于我们无论如何都在使用 JavaScript,我们可以使整个过程具有交互性,例如,使动画仅在点击/点击时发生。
为此,我们添加了一个请求 ID 变量 ( `rID` ),它最初为 `null`,然后在 `update()` 函数中取 `requestAnimationFrame()` 返回的值。这使我们能够在需要时使用 `stopAni()` 函数停止动画。
/* same as before */
let rID = null;
function stopAni() {
cancelAnimationFrame(rID);
rID = null
};
function update() {
/* same as before */
if(!(f%NF)) {
stopAni();
return
}
rID = requestAnimationFrame(update)
};
点击时,我们停止任何可能正在运行的动画,反转动画方向 `dir` 并调用 `update()` 函数。
addEventListener('click', e => {
if(rID) stopAni();
dir *= -1;
update()
}, false);
由于我们从当前帧索引 `f` 为 `0` 开始,因此我们希望在第一次点击时朝正方向,即朝 `NF` 方向前进。并且由于我们在每次点击时都反转方向,因此现在方向的初始值必须为 `-1`,以便在第一次点击时反转为 `+1`。
所有上述内容的结果可以在 这个交互式 Pen 中看到(仅在 Firefox 53+ 使用标准的 `mask-composite` 和 Chrome 1.0+/ Safari 4.0+ 使用非标准的 `-webkit-mask-composite` 时有效)。
我们也可以对每个停止点使用不同的 alpha 变量,就像我们在射线情况下所做的那样。
$lyr1: repeating-conic-gradient(#000 0% .5*$pc, transparent 0% $pc);
$lyr0: repeating-radial-gradient(closest-side,
rgba(#000, var(--a0)), rgba(#000, var(--a1)) .5*$pr,
rgba(#000, var(--a2)) 0, rgba(#000, var(--a3)) $pr);
在 JavaScript 中,我们有 `ease-in` 和 `ease-out` 计时函数。
const TFN = {
'ease-in': function(k, e = 1.675) {
return Math.pow(k, e)
},
'ease-out': function(k, e = 1.675) {
return 1 - Math.pow(1 - k, e)
}
};
在 `update()` 函数中,与第一个动画演示唯一的区别是,我们不再改变单个 CSS 变量的值,现在我们要处理四个变量:`--a0`、`--a1`、`--a2`、`--a3`。我们在循环中执行此操作,对偶数索引处的变量使用 `ease-in` 函数,对其他变量使用 `ease-out` 函数。对于前两个,进度由 `f/NF` 给出,而对于后两个,进度由 `1 - f/NF` 给出。将所有这些公式合并在一起,我们得到:
(function update() {
f += dir;
for(var i = 0; i < 4; i++) {
let j = ~~(i/2);
document.body.style.setProperty(
`--a${i}`,
TFN[i%2 ? 'ease-out' : 'ease-in'](j + Math.pow(-1, j)*f/NF).toFixed(2)
)
}
if(!(f%NF)) dir *= -1;
requestAnimationFrame(update)
})();
结果如下所示。

与圆锥渐变一样,我们也可以动画掩盖径向渐变的透明部分和不透明部分之间的停止位置。为此,我们使用 CSS 变量 `--p` 来表示此停止位置的进度。
$lyr1: repeating-conic-gradient(#000 0% .5*$pc, transparent 0% $pc);
$lyr0: repeating-radial-gradient(closest-side,
#000, #000 calc(var(--p)*#{$pr}),
transparent 0, transparent $pr);
JavaScript 与第一个 alpha 动画的 JavaScript 几乎相同,只是我们没有更新 alpha `--a` 变量,而是更新停止进度 `--p` 变量,并且我们使用 `ease-in-out` 类型的函数。
/* same as before */
function easeInOut(k) {
return .5*(Math.sin((k - .5)*Math.PI) + 1)
};
(function update() {
f += dir;
document.body.style.setProperty('--p', easeInOut(f/NF).toFixed(2));
/* same as before */
})();

如果我们在不透明部分之前添加一个 `transparent` 条带,并同时动画化从这个 `transparent` 条带到不透明部分的停止位置 `--p0` 的进度,我们就可以使效果更加有趣。
$lyr1: repeating-conic-gradient(#000 0% .5*$pc, transparent 0% $pc);
$lyr0: repeating-radial-gradient(closest-side,
transparent, transparent calc(var(--p0)*#{$pr}),
#000, #000 calc(var(--p1)*#{$pr}),
transparent 0, transparent $pr);
在 JavaScript 中,我们现在需要动画化两个 CSS 变量:`--p0` 和 `--p1`。我们对第一个使用 `ease-in` 计时函数,对第二个使用 `ease-out` 计时函数。我们也不再反转动画方向。
const NF = 120,
TFN = {
'ease-in': function(k, e = 1.675) {
return Math.pow(k, e)
},
'ease-out': function(k, e = 1.675) {
return 1 - Math.pow(1 - k, e)
}
};
let f = 0;
(function update() {
f = (f + 1)%NF;
for(var i = 0; i < 2; i++)
document.body.style.setProperty(`--p${i}`, TFN[i ? 'ease-out' : 'ease-in'](f/NF);
requestAnimationFrame(update)
})();
这给了我们一个相当有趣的结果。

这太棒了,Ana!不过你的键盘点击声太大了。感谢你分享这篇内容!
太棒了!期待更多浏览器的支持。
在看到你的文章之前,我从未听说过这些属性,但它们提供的可能性非常令人兴奋和强大!我期待着随着对这些属性的支持不断改善,在 Web 上更多地使用和看到它们。
很棒,但是……
我的脑袋疼。
:-)
谢谢!正在将对 CSS HTML 验证器的支持添加到… 顺便说一下,“transparent 0 $p” 可以工作。
像往常一样,Ana,你又发表了一篇开创性的文章,充满了酷炫的示例和技术数据,将激励未来的 Web 设计创新者。在这些新兴 Web 技术的时代,你的工作和文章的重要性不可低估。真诚地感谢你。