原文地址: https://css-tricks.com/creating-yin-yang-loaders-web/

背景

网上做 Loading 动效的教程和工具比比皆是,用八卦图的还是头一次听说 =,=

因为一直都比较喜欢这种小而精、并且能让人眼前一亮的玩法。万万没想到居然还是个外国程序媛(推特请戳)的作品。

果断翻译过来供大家参考。

正文

不久前我见到了这样的动画。这让我有了一个想法即:我要在不使用外部的库的情况下,用尽可能少的代码、使用多样化的方法(其中包括一些如今可以使用的新功能,如 CSS 变量)创建我自己的版本。

本文将引导你完成构建这些 Demo 的过程。

在介绍其他任何步骤之前,先给出我最终要实现的动画效果,如下:

期望的效果: 一个旋转的八卦图,伴随着两个 “叶片” 大小循环地增加和缩小

从何处开始?

无论我们选择使用哪种方式重新创作上述动画,我们总是会先从静态阴阳形状开始,如下所示:

静态的阴阳八卦图

该起始静态形状的结构如下图所示:


静态八卦图的结构

首先,我们有一个直径为 d 的大圆。在这个圆内,我们紧紧地嵌入两个较小的圆圈,每个圆圈的直径都是我们初始大圆直径的一半。 这意味着这两个较小圆的每一个的直径等于大圆的半径 r(或 0.5 * d)。在直径为 r 的这两个较小圆的内部,我们有一个更小的同心圆。

如果我们画出通过所有这些圆的所有中心点的大圆的直径 - 上图中的线段 AB,它与内圆之间的交点将其分成 6 个相等的较小的段。 这意味着其中一个最小圆的直径为 r/3(或 d/6),其半径为 r/6。

纯 HTML + CSS 版

在这种情况下,我们可以用一个元素和它的两个伪元素来实现。 以下动画说明了创建两个“叶片”的方式(因为整个面板将会旋转,所以切换对称轴无关紧要):

实际的元素是外层的大圆,它有一个从上到下的渐变,中间有一个尖锐的过渡。 伪元素是我们放置的较小圆。一个较小圆的直径是大圆的直径的一半。 两个较小的圆都与大圆垂直中心对齐。

开始编写代码!

首先,我们决定大圆圈的直径 $d。 我们使用 viewport 单位,以便在调整大小时可以很好地扩展。 我们将这个直径值设置为其 widthheight,使用 border-radius使元素圆形化,并给出一个从上到下的渐变背景,中间从到黑色到白色的过渡。

1
2
3
4
5
6
7
$d: 80vmin;
.☯ {
width: $d; height: $d;
border-radius: 50%;
background: linear-gradient(black 50%, white 0);
}

So far, so good:

现在我们来看看我们用伪元素创建的较小的圆。 我们给我们的元素显示:通过设置align-items:center,使它的孩子(或我们的例子中的伪元素)中间垂直对齐。 我们使这些伪元素具有其父元素的一半高度(50%),并且确保它们水平地覆盖大圆圈的一半。 最后,我们将它们与border-radius进行整合,给它们一个虚拟背景,并设置内容属性,以便我们可以看到它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
.☯ {
display: flex;
align-items: center;
/* same styles as before */
&:before, &:after {
flex: 1;
height: 50%;
border-radius: 50%;
background: #f90;
content: '';
}
}

接下来,我们需要给他们不同的背景:

1
2
3
4
5
6
7
8
9
10
.☯ {
/* same styles as before */
&:before, &:after {
/* same styles as before */
background: black;
}
&:after { background: white }
}

有点像那么回事儿了对吧!

在我们得到静态形状之前,要做的就是给这两个伪元素添加边框。 黑色应该是一个白色的边框,而白色的黑色边框应该是黑色的。 这些边界应该是伪元素直径的三分之一,这是大圆圈直径的三分之一,即 $d/6

1
2
3
4
5
6
7
8
9
10
11
12
13
.☯ {
/* same styles as before */
&:before, &:after {
/* same styles as before */
border: solid $d/6 white;
}
&:after {
/* same styles as before */
border-color: black;
}
}

但是,结果看起来并不正确:

这是因为在垂直方向上,边框被加到了height。 水平地,我们没有设置宽度,所以边框可以从 content 空间获得。 有两个修复方法:一个是在伪元素上设置border-size:border-box;第二个是将伪元素的高度更改为$d/6 - 我们将使用后者:

我们现在有了基本的八卦形状,所以让我们继续做动画!

这个动画从第一个伪元素缩小的状态开始,让我们设定原始大小为一半(这意味着缩放因子$f为 0.5),而第二个伪元素已经扩展到占用所有可用的空间即:大圆圈的直径(原始尺寸的两倍)减去第一个圆圈的直径(即其原始尺寸的$f); 然后状态改变到第二个伪元件的缩小到$f的状态,第一个伪元素已扩展到其原始大小的2 - $f

第一个伪元素圆相对于其最左点(因此我们需要设置0 50%的变换原点),而第二个相对于其最右点(100%50%)进行缩放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$f: .5;
$t: 1s;
.☯ {
/* same styles as before */
&:before, &:after {
/* same styles as before */
transform-origin: 0 50%;
transform: scale($f);
animation: s $t ease-in-out infinite alternate;
}
&:after {
/* same styles as before */
transform-origin: 100% 50%;
animation-delay: -$t;
}
}
@keyframes s { to { transform: scale(2 - $f) } }

我们现在有了形状变化的动画:

最后一步是使整个形状旋转起来:

1
2
3
4
5
6
7
$t: 1s;
.☯ {
/* same styles as before */
animation: r 2*$t linear infinite;
}
@keyframes r { to { transform: rotate(1turn) } }

我们得到了最后的结果

然而,还有一件事情我们可以做,使编译的 CSS 更有效率:消除冗余与 CSS 变量!

白色可以以 hsl(0,0%,100%)的形式写入 HSL 格式。 色调和饱和度无关紧要,任何 100% 亮度的值都是白色的,所以我们将它们设置为 0,使我们的代码执行效率更高。 类似地,黑色可以写成 hsl(0,0%,0%)。 再次,色调和饱和度无关紧要,任何亮度为 0% 的值都是黑色的。 鉴于此,我们的代码变成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.☯ {
/* same styles as before */
&:before, &:after {
/* same styles as before */
border: solid $d/6 hsl(0, 0%, 100% /* = 1*100% = (1 - 0)*100% */);
transform-origin: 0 /* = 0*100% */ 50%;
background: hsl(0, 0%, 0% /* 0*100% */);
animation: s $t ease-in-out infinite alternate;
animation-delay: 0 /* = 0*-$t */;
}
&:after {
/* same styles as before */
border-color: hsl(0, 0%, 0% /* = 0*100% = (1 - 1)*100% */);
transform-origin: 100% /* = 1*100% */ 50%;
background: hsl(0, 0%, 100% /* = 1*100% */);
animation-delay: -$t /* = 1*-$t */;
}
}

从上述结果可以看出:

  • 我们的转换起点的 x 分量是第一个伪元素的calc(0 * 100%),第二个是calc(1 * 100%)
  • 我们的边框颜色是第一个伪元素的 hsl(0,0%,calc((1 - 0)* 100%))和第二个 hsl(0,0%,calc((1 - 1)* 100%))
  • 我们的背景是第一个伪元素的hsl(0,0%,calc(0 * 100%))和第二个伪元素的hsl(0,0%,calc(1 * 100%))
  • 我们的动画延迟是第一个伪元素的calc(0 *#{ - $ t}),第二个的calc(1 *#{ - $ t})

这意味着我们可以使用一个用作开关的自定义属性 --i,第一个伪元素为 0,第二个为伪元素为 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.☯ {
/* same styles as before */
&:before, &:after {
/* same styles as before */
--i: 0;
border: solid $d/6 hsl(0, 0%, calc((1 - var(--i))*100%));
transform-origin: calc(var(--i)*100%) 50%;
background: hsl(0, 0%, calc(var(--i)*100%));
animation: s $t ease-in-out calc(var(--i)*#{-$t}) infinite alternate;
}
&:after { --i: 1 }
}

这样就消除了所有这些规则两次的编写:我们现在需要做的就是翻转开关!

可悲的是,现在只能在 WebKit 浏览器中使用,因为 Firefox 和 Edge 不支持使用calc()作为动画延迟值,Firefox 不支持在 hsl()当中中使用它。 =^=

Canvas + JavaScript

还有兴趣?请戳原文吧

SVG + JavaScript

还有兴趣?请戳原文吧

结语

代码中的奇淫技巧固然重要,但归根结底,好的 idea 才是技术进步的源泉。

如果觉得文章对你有帮助的话,去 Github Repo 给个 star 吧亲~