使用CSS变量简化Apple Watch呼吸应用动画

当我看到有关如何重新创建动画的原始文章时,我首先想到的是,可以通过使用预处理器和特定CSS变量来简化动画。 因此,让我们深入了解它,看看如何!

动画的gif。显示我们想要得到的结果:六个初始重合的圆从屏幕中间移出,而整个装配体按比例放大并旋转。
我们要复制的结果。

结构

我们保持完全相同的结构。

为了避免多次编写相同的内容,我选择使用预处理器。

我对预处理器的选择始终取决于我要执行的操作,因为在很多情况下,诸如Pug之类的东西提供了更大的灵活性,但是有时,Haml或Slim允许我编写最少的代码,而不必引入代码无论如何,我以后都不需要一个循环变量。

直到最近,在这种情况下,我可能仍会使用Haml。 但是,我目前偏爱另一种技术 ,它使我避免在HTML和CSS预处理程序代码中同时设置项数,这意味着如果需要在某个时候使用其他值,则不必在这两种情况下都进行修改。

为了更好地理解我的意思,请考虑以下Haml和Sass:

- 6.times do
  .item
$n: 6; // number of items

/* set styles depending on $n */

在上面的示例中,如果我更改了Haml代码中的项目数,那么我还需要在Sass代码中进行更改,否则事情会中断。 从某种程度上来说,结果不再是预期的结果。

因此,我们可以通过将圈数设置为稍后在Sass代码中使用CSS变量的值来解决此问题。 而且,在这种情况下,使用Pug会更好:

- var nc = 6; // number of circles

.watch-face(style=`--nc: ${nc}`)
  - for(var i = 0; i < nc; i++)
    .circle(style=`--i: ${i}`)

我们还以类似的方式为每个.circle元素设置了索引。

基本风格

我们在车body保持完全相同的样式,没有变化。

就像结构一样,我们使用预处理程序来避免多次编写几乎相同的东西。 我的选择是Sass,因为这是我最满意的,但是对于像本演示这样的简单事情,Sass并没有什么使其成为最佳选择– LESS或Stylus也能胜任。 对我来说,编写Sass代码只是更快。

但是我们使用预处理器做什么呢?

好吧,首先,我们使用变量$d作为圆的直径,这样,如果我们想使它们变大或变小,并控制它们在动画过程中走多远,我们只需要更改的值即可。这个变量。

如果有人想知道为什么不在这里使用CSS变量,那是因为我更喜欢只在需要动态变量时才采用此路径。 直径不是这种情况,那么为什么还要编写更多内容,然后甚至不得不想出可能遇到CSS变量错误的解决方法?

$d: 8em;

.circle {
  width: $d; height: $d;
}

请注意,我们没有在包装上设置任何尺寸( .watch-face )。 我们不需要。

通常,如果元素的目的只是作为放置绝对位置的元素的容器,那么该容器上将对其进行组转换(是否经过动画处理),并且该容器没有可见的文本内容,没有背景,没有边框,没有框阴影...那么就不需要在其上设置显式尺寸了。

这方面的一个副作用是,为了保持我们的圈在中间,我们需要给他们一个负margin减去该半径(即直径的一半)。

$d: 8em;
$r: .5*$d;

.circle {
  margin: -$r;
  width: $d; height: $d;
}

我们还为他们提供了与原始文章相同的border-radiusmix-blend-modebackground ,并且得到以下结果:

Chrome屏幕截图。显示应用这些属性后在这一点上获得的预期结果。
到目前为止的预期结果( 实时演示 )。

嗯,我们在WebKit浏览器和Firefox中获得了上述优势,因为Edge尚不支持mix-blend-mode (尽管您可以投票支持实现 ,但是如果您希望看到它的支持,请这样做,因为您的投票确实很重要),所以它向我们展示了一些丑陋的东西:

边缘截图。没有混合混合模式支持意味着重叠区域与非重叠区域看起来没有什么不同,结果很丑陋。
Edge的结果看起来不太好。

为了解决这个问题,我们使用@supports

.circle {
  /* same styles as before */
  
  @supports not (mix-blend-mode: screen) {
    opacity: .75
  }
}

不完美,但更好:

边缘截图。显示当我们使用部分透明性来获得更类似于其他浏览器中的mix-blend-mode模式的结果时所得到的结果。
使用@supportsopacity可以解决Edge( live demo )中缺少mix-blend-mode支持的问题。

现在让我们看一下我们想要得到的结果:

带有注释的所需圆形分布的屏幕快照。整个内容通过垂直中线分为两半(左半部分和右半部分)。前三个圆圈在右半部分中,具有蓝色绿色背景,而六个圆圈中的最后三个在左半部中,具有黄色绿色背景。圆圈从右半边的最上面的圆圈开始编号,然后顺时针旋转。
理想的结果。

我们总共有六个圈子,其中三个在左半边,另外三个在右半边。 它们的background都是某种绿色,左半部分偏向黄色,右半部分偏向蓝色。

如果我们从右半边的最上面的一个圆圈开始编号,然后按顺时针方向进行编号,则前三个圆圈在右半边并具有蓝色绿色background ,后三个圆圈在左半边并带有黄色绿色background

至此,我们将所有圆圈的background设置为淡蓝色。 这意味着我们需要在六个圆圈的前半部分覆盖它。 由于我们无法在选择器中使用CSS变量,因此我们可以从Pug代码执行此操作:

- var nc = 6; // number of circles

style .circle:nth-child(-n + #{.5*nc}) { background: #529ca0 }
.watch-face(style=`--nc: ${nc}`)
  - for(var i = 0; i < nc; i++)
    .circle(style=`--i: ${i}`)

如果需要对此进行刷新, :nth-child(-n + a)选择n ≥ 0整数值得到的有效索引处的项目。 在我们的例子中, a = .5*nc = .5*6 = 3 ,所以我们的选择器是:nth-child(-n + 3)

如果将n替换为0 ,则得到3 ,这是一个有效的索引,因此我们的选择器匹配第三个圆。

如果将n替换为1 ,则得到2 ,也是一个有效的索引,因此我们的选择器匹配第二个圆。

如果将n替换为2 ,则得到1 ,再次有效,因此我们的选择器匹配第一个圆。

如果将n替换为3 ,则会得到0 ,这不是有效的索引,因为此处的索引不是基于0的。 在这一点上,我们停止了,因为很明显,如果继续下去,我们将不会获得任何其他正值。

下面的Pen演示了它是如何工作的-一般规则是:nth-child(-n + a)选择第一个a项目:

由thebabydino( @thebabydino )上CodePen

回到循环分布,到目前为止的结果如下:

由thebabydino( @thebabydino )上CodePen

定位

首先,我们使包装器相对定位,并使其.circle子代绝对定位。 现在它们全部重叠在中间。

由thebabydino( @thebabydino )上CodePen

为了理解下一步需要做什么,让我们看一下下图:

SVG插图。显示圆的初始位置(中间重叠死点)和最终位置(最右边的圆圈在其最终位置突出显示)。该圆在初始位置和最终位置的中心点之间的线段是长度等于圆半径的水平线段。
最右边的圆圈从其初始位置到最终位置( live )。

初始位置中的圆的中心点在同一水平线上,并且半径与最右边的圆相距。 这意味着我们可以通过沿x轴平移半径$r来到达此最终位置。

但是其他圈子呢? 它们在最终位置的中心点也仅沿着其他直线偏离其初始位置的半径。

SVG插图。显示圆的初始位置(中间重叠死点)和最终位置。将连接其中心点的初始位置(均位于中间的死点)和相同点的最终位置的线段突出显示。它们都是长度等于圆半径的线段。
所有圆:初始位置(中间的死点)是每个最后一个圆( 活着 )的半径。

这意味着,如果我们首先旋转它们的坐标系,直到它们的x轴与中心点的初始位置和最终位置之间的线重合,然后平移它们的半径,我们就可以使它们全部位于正确的最终位置。非常相似的方式。

由thebabydino( @thebabydino )上CodePen

好的,但是将它们每个旋转一个角度?

好吧,我们从这样一个事实开始:围绕一个点在圆周上有360°

由thebabydino( @thebabydino )上CodePen

我们有六个均匀分布的圆,因此任何两个连续的圆之间的旋转差为360°/6 = 60° 。 由于我们不需要旋转最右边的.circle (第二个.circle ),因此该.circle ,这会将前面的第一个.circle (第一个)置于-60° ,将后面的.circle (第二个)置于60°等等。

由thebabydino( @thebabydino )上CodePen

请注意, -60°300° = 360° - 60° -60°在圆上占据相同的位置,因此我们是通过顺时针(正)旋转300°还是以60°绕圆的另一种方式到达那里(给我们减号)没关系。 我们将在代码中使用-60°选项,因为在我们的案例中,它可以更轻松地找到方便的图案。

因此,我们的转换如下所示:

.circle {
  &:nth-child(1 /* = 0 + 1 */) {
    transform: rotate(-60deg /* -1·60° = (0 - 1)·360°/6 */) translate($r);
  }
  &:nth-child(2 /* = 1 + 1 */) {
    transform: rotate(  0deg /*  0·60° = (1 - 1)·360°/6 */) translate($r);
  }
  &:nth-child(3 /* = 2 + 1 */) {
    transform: rotate( 60deg /*  1·60° = (2 - 1)·360°/6 */) translate($r);
  }
  &:nth-child(4 /* = 3 + 1 */) {
    transform: rotate(120deg /*  2·60° = (3 - 1)·360°/6 */) translate($r);
  }
  &:nth-child(5 /* = 4 + 1 */) {
    transform: rotate(180deg /*  3·60° = (4 - 1)·360°/6 */) translate($r);
  }
  &:nth-child(6 /* = 5 + 1 */) {
    transform: rotate(240deg /*  4·60° = (5 - 1)·360°/6 */) translate($r);
  }
}

这为我们提供了我们一直追求的分布:

由thebabydino( @thebabydino )上CodePen

但是,它是非常重复的代码,可以很容易地压缩。 对于它们中的任何一个,旋转角度都可以写为当前索引和项总数的函数:

.circle {
  /* previous styles */
  
  transform: rotate(calc((var(--i) - 1)*360deg/var(--nc))) translate($r);
}

在WebKit浏览器和Firefox 57+中有效 ,但在Edge和较旧的Firefox浏览器中失败,因为缺少对在rotate()函数中使用calc()的支持。

幸运的是,在这种情况下,我们可以选择在Pug代码中计算和设置各个旋转角度,然后在Sass代码中使用它们:

- var nc = 6, ba = 360/nc;

style .circle:nth-child(-n + #{.5*nc}) { background: #529ca0 }
.watch-face
  - for(var i = 0; i < nc; i++)
    .circle(style=`--ca: ${(i - 1)*ba}deg`)
.circle {
  /* previous styles */
  
  transform: rotate(var(--ca)) translate($r);
}

在这种情况下,我们实际上并不需要其他任何以前的自定义属性,因此我们摆脱了它们。

现在,我们拥有一个紧凑的代码,跨浏览器的发行版:

由thebabydino( @thebabydino )上CodePen

好,这意味着我们已经完成了最重要的部分! 现在为绒毛...

整理起来

我们将transform声明从类中取出,并将其放入一组@keyframes 。 在该类中,我们将其替换为无翻译用例:

.circle {
  /* same as before */
  
  transform: rotate(var(--ca))
}

@keyframes circle {
  to { transform: rotate(var(--ca)) translate($r) }
}

我们还将在.watch-face元素上添加为脉冲动画设置的@keyframes集。

@keyframes pulse {
  0% { transform: scale(.15) rotate(.5turn) }
}

请注意,我们既不需要0%from )关键帧,也不需要100%to )关键帧。 每当缺少这些属性时,它们的动画属性值(在我们的例子中只是transform属性)是从我们在没有animation情况下对animation元素拥有的值生成的。

circle动画的情况下,这就是rotate(var(--ca)) 。 在pulse动画情况下, scale(1)给我们的矩阵与none相同,这是transform的默认值,因此我们甚至不需要在.watch-face元素上进行设置。

我们将animation-duration设为Sass变量,这样,如果我们想对其进行更改,则只需要在一个位置进行更改即可。 最后,我们在.watch-face元素和.circle元素上都设置了animation属性。

$t: 4s;

.watch-face {
  position: relative;
  animation: pulse $t cubic-bezier(.5, 0, .5, 1) infinite alternate
}

.circle {
  /* same as before */
  
  animation: circle $t infinite alternate
}

请注意,我们没有为circle动画设置计时功能。 在原始演示中这很ease ,我们没有明确设置它,因为它是默认值。

就是这样–我们得到了动画效果

我们还可以调整平移距离,使其不完全是$r ,而是稍小的值(例如.95*$r )。 这也可以使mix-blend-mode效果更加有趣:

由thebabydino( @thebabydino )上CodePen

奖金:一般情况!

以上特别是针对六个.circle花瓣。 现在,我们将了解如何对其进行调整,使其适用于任意数量的花瓣。 等等,我们是否需要做更多的事情,而不只是更改Pug代码中的圆形元素数量?

好吧,让我们看看如果这样做的话会发生什么:

屏幕截图。它们显示了nc等于6、8和9时得到的结果。当nc为6时,我们遇到了前一种情况:将整个物体分成两条垂直线的两半,我们得到前三个圆(蓝绿色)右半部分和左半部分的最后三个(黄绿色)圆圈。当nc为8时,线的一侧也有圆的前半部分(前四个,蓝绿色),将装配件分成两个几何对称的两半,而线的另一侧则有后四个圆(黄绿色)。同一行。但是,这条线不再垂直。在nc = 9的情况下,所有圆圈均为黄绿色。
nc的结果等于6 (左), 8 (中)和9 (右)。

结果看起来不错,但它们并未完全遵循相同的模式-圆的上半部分(带蓝色的绿色)位于垂直对称线的右侧,下半部分(带黄色的绿色)位于左侧。

nc = 8情况下,我们非常接近,但是对称线不是垂直的。 但是,在nc = 9情况下,我们所有的圆圈都有淡黄色的绿色background

因此,让我们看看为什么会发生这些事情以及如何获得我们真正想要的结果。

使:nth-child()为我们工作

首先,请记住,使用以下代码,我们使一半的圆具有蓝绿色background

.circle:nth-child(-n + #{.5*nc}) { background: #529ca0 }

但是在nc = 9情况下,我们有.5*nc = .5*9 = 4.5 ,这使我们的选择器为:nth-child(-n + 4.5) 。 由于4.5不是整数,因此选择器无效,并且不会应用background 。 因此,我们在这里要做的第一件事是设置.5*nc值的下限:

style .circle:nth-child(-n + #{~~(.5*nc)}) { background: #529ca0 }

更好的是,对于nc值为9 ,我们得到的选择器是.circle:nth-child(-n + 4) ,这使我们获得前4在其上应用带蓝色绿色background项目:

由thebabydino( @thebabydino )上CodePen

但是,如果nc为奇数,我们仍然没有相同数量的蓝绿色和黄绿色绿色圆圈。 为了解决这个问题,我们使中间的圆(从第一个到最后一个)具有渐变background

所谓“中间的圆”是指距起点和终点相等数量的圆。 以下交互式演示说明了这一点,以及以下事实:当圆圈总数为偶数时,我们没有中间圆圈。

由thebabydino( @thebabydino )上CodePen

好吧,我们如何得到这个圈子?

从数学上讲,这是包含第一个ceil(.5*nc)项的集合与包含除第一floor(.5*nc)项之外的所有项的集合之间的交集。 如果nc为偶数,则floor(.5*nc)ceil(.5*nc)相等,并且我们的交集为空集 。 下面的钢笔对此进行了说明:

由thebabydino( @thebabydino )上CodePen

我们使用:nth-child(-n + #{Math.ceil(.5*nc)})获得第一个ceil(.5*nc)项,但是另一组呢?

通常, :nth-child(n + a)选择除第a - 1项之外的所有项:

由thebabydino( @thebabydino )上CodePen

因此,为了获得除第一floor(.5*nc)项目,我们使用:nth-child(n + #{~~(.5*nc) + 1})

这意味着我们为中间圆具有以下选择器:

:nth-child(n + #{~~(.5*nc) + 1}):nth-child(-n + #{Math.ceil(.5*nc)})

让我们看看这能给我们带来什么。

  • 如果我们有3项目,则选择器为:nth-child(n + 2):nth-child(-n + 2) ,它使我们获得第二个项目( {2, 3, 4, ...}{2, 1}集)
  • 如果我们有4项目,则选择器为:nth-child(n + 3):nth-child(-n + 2) ,它什么也没捕获( {3, 4, 5, ...}{2, 1}集是空集
  • 如果我们有5项目,则选择器为:nth-child(n + 3):nth-child(-n + 3) ,它使我们获得第三个项目( {3, 4, 5, ...}{3, 2, 1}集)
  • 如果我们有6项目,那么我们的选择器是:nth-child(n + 4):nth-child(-n + 3) ,它什么都不会捕获( {4, 5, 6, ...}{3, 2, 1} {3, 2, 1}集是空集
  • 如果我们有7项目,则选择器为:nth-child(n + 4):nth-child(-n + 4) ,它使我们获得第四个项目( {4, 5, 6, ...}{4, 3, 2, 1}集)
  • 如果我们有8项目,那么我们的选择器是:nth-child(n + 5):nth-child(-n + 4) ,它什么都不会捕获( {5, 6, 7, ...}{4, 3, 2, 1} {4, 3, 2, 1}集为空集
  • 如果我们有9项目,则选择器为:nth-child(n + 5):nth-child(-n + 5) ,它使我们获得第五个项目( {5, 6, 7, ...}{5, 4, 3, 2, 1}集)

现在,当我们总共有奇数个项目时,我们可以在中间选择该项目,让我们给它一个渐变background

- var nc = 6, ba = 360/nc;

style .circle:nth-child(-n + #{~~(.5*nc)}) { background: var(--c0) }
  | .circle:nth-child(n + #{~~(.5*nc) + 1}):nth-child(-n + #{Math.ceil(.5*nc)}) {
  |   background: linear-gradient(var(--c0), var(--c1))
  | }
.watch-face(style=`--c0: #529ca0; --c1: #61bea2`)
  - for(var i = 0; i < nc; i++)
    .circle(style=`--ca: ${(i - 1)*ba}deg`)

我们使用从上到下的渐变的原因是,最终,我们希望该项目位于底部,并通过装配体的垂直对称线分成两半。 这意味着我们首先需要旋转它直到其x轴指向下,然后沿着其x轴的新方向向下平移。 在此位置,项目的顶部在组件的右半部分,而项目的底部在组件的左半部分。 因此,如果我们想要从组件的右侧到组件的左侧的渐变,则这是该实际.circle元素上的从上到下的渐变。

由thebabydino( @thebabydino )上CodePen

使用这种技术,我们现在已经解决了一般情况的背景问题:

由thebabydino( @thebabydino )上CodePen

现在剩下要做的就是使对称轴垂直。

驯服角度

为了查看我们在这里需要做的事情,让我们集中在顶部的期望位置。 在那里,我们始终要有两个圆(相对于垂直轴对称放置)(第一个圆环在DOM顺序中,第一个圆环在DOM顺序中在左边),这两个圆将我们的装配体分成两个相互镜像的两半。

由thebabydino( @thebabydino )上CodePen

它们是对称的事实意味着垂直轴将它们之间的角距离ba360°除以总圆nc )分为两个相等的一半。

SVG插图。突出显示围绕中心点分布的圆,其中两个圆位于顶部(相对于垂直轴对称,该垂直轴将整个装配体分成两个镜像的两半)。该中轴将这两个圆的中心点之间的角距离分成两个相等的一半。
垂直对称线和径向线与顶角的中心点所形成的角度都等于底角( live )的一半。

因此,两者都是远离垂直对称轴的底角的一半(底角ba360°除以圆的总数nc ),一个是顺时针方向,另一个是相反方向。

对称轴的上半部分为-90° (等于270° )。

SVG插图。以90度为单位显示围绕圆的度数。我们从右边(3点钟)开始。这是0°,通常是360°角的任意倍数。顺时针方向,我们向下90°(在6点),在左侧180°(在9点),在270°在顶部(12点)。从0°开始反方向,顶部(12点钟)为-90°,左侧(9点钟)为-180°,依此类推。
围绕圆的度数值( 实时 )。

因此,为了到达DOM顺序的第一个圆(在右侧顶部的圆),我们从开始,在负方向上90° ,然后在正方向上旋转一个底角(顺时针)。 这会将第一个圆置于.5*ba - 90度。

SVG插图。以图形方式显示如何获取第一个圆的角位置。从0°(3点钟)开始,我们沿负方向旋转90°(在12点钟位置)。然后,我们沿正方向返回半个底角。
如何获取第一个圆在( live )处的角度。

之后,每隔一个圆与前一个圆的角度加一个底角。 这样,我们有:

  • 第一个圆(索引0 ,选择器:nth-child(1) )位于ca₀ = .5*ba - 90
  • 第二个圆(索引1 ,选择器:nth-child(2) )处于ca₁ = ca₀ + ba = ca₀ + 1*ba
  • 第三个圆(索引2 ,选择器:nth-child(3) u)处于ca₂ = ca₁ + ba = ca₀ + ba + ba = ca₀ + 2*ba
  • 通常,索引k的圆为caₖ = caₖ₋₁ + ba = ca₀ + k*ba

因此,当前在索引i处的.5*ba - 90 + i*ba = (i + .5)*ba - 90.5*ba - 90 + i*ba = (i + .5)*ba - 90度:

- var nc = 6, ba = 360/nc;

//- same as before
.watch-face(style=`--c0: #529ca0; --c1: #61bea2`)
  - for(var i = 0; i < nc; i++)
    .circle(style=`--ca: ${(i + .5)*ba - 90}deg`)

这给出了最终的Pen,在这里我们只需要从Pu​​g代码中更改nc即可更改结果:

由thebabydino( @thebabydino )上CodePen

翻译自: https://css-tricks.com/simplifying-apple-watch-breathe-app-animation-css-variables/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值