最近,我在CodePen上看到了 一个Twitter心形点赞动画 。一般,如果我有时间就会研究案例代码,看是否可以进行利用或更改。在这个案例中,我很惊讶的看到演示使用的是Sprites图片。后来我学习了 Twitter对其实现的原理 。当然,这可以不使用图片就可以实现,不是吗?
我决定自己动手试一试。当然我不决定使用JavaScript,因为这是一个 关于复选框的hack ,可以结合CSS,通过对表单元素进行简单切换就可以实现。
实现的结果如下:
现在就让我们看一下如何实现这个动画效果。
如上图所示,它有 29
帧—— 一个用来计算时就会出现问题的数字。因为 29
是一个大的质数,我不能用一个小的数字对其整除,如: 2
、 4
或 5
。但是,这又似乎是一个很好的数字,因为 29
接近于 28
,也就是 4
的倍数( 4 * 7=28
),同时,它也接近于 30
也就是 5
的倍数( 5 * 6=30
)。所以我们可以将 29
当做 28
或者 30
进行使用。
我们会注意到雪碧图主要有三部分组成:
这就意味着可以只使用一个元素和两个伪元素就可以实现。心形就是那个元素,心形背后的气泡图形就是 ::before
伪元素,而心形周围的颗粒物图形就是 ::after
伪元素。
整颗心形和其周围部分就是复选框的 <label>
标签。点击标签将切换复选框,使我们能够对 两种状态进行处理 。在这种情况下,HTML的结构如下,一个复选框和一个包含Unicode心的标签。
<input id="toggle-heart" type="checkbox" /> <label for="toggle-heart">❤</label>
让复选框消失在我们的视野之中:
[id='toggle-heart'] { position: absolute; left: -100vw; }
根据复选框是否被选中对心形进行颜色设置。我们使用拾色器获取了雪碧图真实的颜色值。
[for='toggle-heart'] { color: #aab8c2; } [id='toggle-heart']:checked + label { color: #e2264d; }
我们还在标签上设置了 cursor:pointer
,并增加了字体的大小,不然我们的心看起来实在太小了。
[for='toggle-heart'] { font-size: 2em; cursor: pointer; }
之后我们将整颗心进行居中,这里使用的是flexbox。
body { display: flex; justify-content: center; margin: 0; height: 100vh; } [for='toggle-heart'] { align-self: center; }
现在我们就制作了一颗在复选框为未选中状态时为灰色,选中状态时为深红色的心:
观看雪碧图,心从 2
帧、 6
帧缩放为。 6
帧之后它开始变大,之后的某一点它又缩小了一点点。这种增长效果是使用 easeOutBack
定时功能的完美用例。我们将开始的增长设置为 17.5%
,因为这是一个很好的数字,看起来近似于我们的总帧数。接下来就要思考如何实现缩放。我们不可以使用 scale()
变换,因为其会对后代元素以及对我们的伪元素产生影响。我们不想让心一下子缩放为0,所以我们决定使用 font-size
。
@keyframes heart { 0%, 17.5% { font-size: 0; } } [id='toggle-heart']:checked + label { will-change: font-size; animation: heart 1s cubic-bezier(.17, .89, .32, 1.49); }
上述我们的代码所实现的效果为:
如果我们不包含 0%
或 100%
帧,它们就会自动获取元素所设置的值(案例中的 font-size:2em;
),或者,如果我们没有进行值设置就会获取默认值(案例中为 1em
的字体大小)。
现在我们转向伪元素进行心形周围的泡沫图形(以及下面将要讲述的颗粒图形)的制作。我们将心形标签设置为 position: relative
,所以我们可以对其进行绝对定位。我们使用 z-index: -1
实现其在心的下方。将 top
以及 left
位置设置为 50%
使其居中显示。泡沫图形以及颗粒图形均为圆形,所以我们设置 border-radius: 50%;
。这里我们使用了SCSS的语法,需要进行一些计算。
[for='toggle-heart'] { position: relative; &::before, &::after { position: absolute; z-index: -1; top: 50%; left: 50%; border-radius: 50%; content: ''; } }
观看雪碧图,最大的气泡图形是心形的两倍大小多一点,这里我们将它的直径设置为 4.5rem
。使用 rem
而不是 em
是因为我们使用 font-size
改变的心形的大小。设置伪元素的大小并将 ::before
伪元素居中。这里我们添加了一个背景色进行测试伪元素是否正确的显示(稍后就会对其删除)。
$bubble-d: 4.5rem; $bubble-r: .5 * $bubble-d; [for='toggle-heart']::before { margin: -$bubble-r; width: $bubble-d; height: $bubble-d; background: gold; }
目前为止,效果如下:
从 2
帧到 5
帧,气泡图形从无增长到完整大小,颜色也由深红变为紫色。之后,从 9
帧之后在中间出现了一个孔间隙,它逐渐变大直到和气泡图形一般大小。这个增长的大小效果就如动画的 scale()
转变实现的效果一样。我们可以对 border-width
进行动画设置,从 $bubble-r
到 0
,实现孔间隙增长的动画效果。需要注意的是我们需要在 ::before
设置 box-sizing:border-box;
使其工作。
[for='toggle-heart']::before { box-sizing: border-box; border: solid $bubble-r #e2264d; transform: scale(0); } @keyframes bubble { 15% { border-color: #cc8ef5; border-width: $bubble-r; transform: scale(1); } 30%, 100% { border-color: #cc8ef5; border-width: 0; transform: scale(1); } }
这里可以使用一个Mixin书写这个关键帧:
@mixin bubble($ext) { border-color: #cc8ef5; border-width: $ext; transform: scale(1); } @keyframes bubble { 15% { @include bubble($bubble-r); } 30%, 100% { @include bubble(0); } }
使伪元素继承心形的动画效果,切换 easeOutCubic
函数并改变动画的名称:
[id='toggle-heart']:checked + label { &::before, &::after { animation: inherit; animation-timing-function: cubic-bezier(.21, .61, .35, 1); } &::before { will-change: transform, border-color, border-width; animation-name: bubble; } &::after { animation-name: particles; } }
目前的效果如下所示:
观看雪碧图可以发现心形周围有七组两个圆形的颗粒物图形,并且分布在一个圆上。
它们的不同之处在于 opacity
、位置以及大小。我们使用多个框的阴影(每一个阴影用于一个颗粒物图形),之后对伪元素的 opacity
以及框阴影的偏移量进行动画处理。
我们要做得第一件事情是决定颗粒物图形的尺寸,之后设置 ::after
伪元素的大小以及位置。
$particle-d: 0.375rem; $particle-r: 0.5 * $particle-d; [for='toggle-heart']:after { margin: -$particle-r; width: $particle-d; height: $particle-d; }
我们将七组颗粒物图形分布在圆周上,如下所示我们有一个 360°
的圆圈:
我们将 360°
尽可能多的进行分组以满足我们的需要。下面的演示中,多边形的每一个顶点标志着一个群组。
我们按照顺时针,在 x
轴(钟表的 3
时刻)的 +
开始。如果我们从 y
轴(钟表的 12
时刻)的 -
开始,对应每一组的位置就需要减去 90°
的视角。
现在看看我们的编码成果,于顶部开始(钟表的 12
时刻),在初始化半径为 $bubble-r
的圆周上分布着我们的泡沫图形群组。将每一群组的组分视为一个粒子,所需代码如下:
$shadow-list: (); $n-groups: 7; $group-base-angle: 360deg/$n-groups; $group-distr-r: $bubble-r; @for $i from 0 to $n-groups { $group-curr-angle: $i*$group-base-angle - 90deg; $xg: $group-distr-r*cos($group-curr-angle); $yg: $group-distr-r*sin($group-curr-angle); $shadow-list: $shadow-list, $xg $yg; }
在 ::after
伪元素上设置 box-shadow: $shadow-list
:
现在处理每一个群组中的两个粒子。
将每一个群组中的中间点定位在一个圆周(半径为: ::after
伪元素直径—— $particle-d
)上。
接下来考虑起始角度。因为想要从顶部开始,在每一个群组中起始角度为 -90°
。对于单个粒子,起始角度为组加上所有粒子相对于心形相同的偏移角度。美观起见,我们选取这个角度为 60°
。
如上所述的代码实现如下:
$shadow-list: (); $n-groups: 7; $group-base-angle: 360deg/$n-groups; $group-distr-r: $bubble-r; $n-particles: 2; $particle-base-angle: 360deg/$n-particles; $particle-off-angle: 60deg; @for $i from 0 to $n-groups { $group-curr-angle: $i*$group-base-angle - 90deg; $xg: $group-distr-r*cos($group-curr-angle); $yg: $group-distr-r*sin($group-curr-angle); @for $j from 0 to $n-particles { $particle-curr-angle: $group-curr-angle + $particle-off-angle + $j*$particle-base-angle; $xs: $xg + $particle-d*cos($particle-curr-angle); $ys: $yg + $particle-d*sin($particle-curr-angle); $shadow-list: $shadow-list, $xs $ys; } }
现在的效果实现如下所示:
现在的位置效果看起来还不错,只不过所有的颗粒颜色均为所设置的心形 color
值。根据颗粒在 ($i)
中的索引以及 ($j)
中的索引,设置 hsl()
我们可以实现彩虹效果。
$shadow-list: $shadow-list, $xs $ys hsl(($i + $j) * $group-base-angle, 100%, 75%);
简单的改变就可以实现漂亮的彩虹颗粒效果:
这里也可以实现随机色相的选取,获取更加令人满意的效果。
给颗粒添加动画的时候,我们想要其起始位置在初始设置的位置稍稍向外一点,也就是半径为 $bubble-r
的圆周上向外一点。比方说在半径为 1.25 * $bubble-r
的圆周上。意味着我们需要更改 $group-distr-r
变量。
同时我们希望它们会从当前的尺寸收缩为 0
。收缩框的阴影没有失去立刻失去焦点,意味着需要设置一个绝对值至少等于元素或者它们所在伪元素的最小尺寸的一半的一个负的半径扩展值。 ::after
伪元素的尺寸应该为 $particle-d
(颗粒直径),所以我们的传播半径为 -$particle-r
(颗粒半径)。
回顾一下,状态 0
,在半径为 $bubble-r
的圆周上散布着颗粒群组并且传播半径为 0
。状态 1
,群组散布于一个半径为 1.25 * $bubble-r
的圆周上并且传播半径为 -$particle-r
。
如果使用 $k
变量,代码如下:
$group-distr-r: (1 + $k * 0.25) * $bubble-r; $spread-r: -$k * $particle-r;
我们就需要创建一个Mixin,这样就不再需要两次 @for
循环:
@mixin particles($k) { $shadow-list: (); $n-groups: 7; $group-base-angle: 360deg / $n-groups; $group-distr-r: (1 + $k * 0.25)*$bubble-r; $n-particles: 2; $particle-base-angle: 360deg / $n-particles; $particle-off-angle: 60deg; // offset angle from radius $spread-r: -$k * $particle-r; @for $i from 0 to $n-groups { $group-curr-angle: $i * $group-base-angle - 90deg; $xg: $group-distr-r * cos($group-curr-angle); $yg: $group-distr-r * sin($group-curr-angle); @for $j from 0 to $n-particles { $particle-curr-angle: $group-curr-angle + $particle-off-angle + $j * $particle-base-angle; $xs: $xg + $particle-d * cos($particle-curr-angle); $ys: $yg + $particle-d * sin($particle-curr-angle); $shadow-list: $shadow-list, $xs $ys 0 $spread-r hsl(($i + $j) * $group-base-angle, 100%, 75%); } } box-shadow: $shadow-list; }
这个时刻让我们再看一遍雪碧图。颗粒直到第 7
帧才会出现。 7
为 28
(已经很接近我们实际的 29
帧)的四分之一。这意味着我们的基本动画看起来如下所示:
@keyframes particles { 0%, 20% { opacity: 0; } 25% { opacity: 1; @include particles(0); } } [for='toggle-heart']:after { @include particles(1); }
实现的效果所下所示:
除了在Edge/IE浏览器中,其余浏览器中效果均不错。在Edge/IE中颗粒几乎看不到收缩因为其太小,几乎看不到。一个解决方案为一点点增加它们的扩展半径的绝对值:
$spread-r: -$k * 1.1 * $particle-r;
存在的另外一个问题是某些操作系统会将unicode心转换为emoji。我找到了一个防止其发生的方案,它看起来很丑陋并且最终被证实不可用。所以最后我决定当复选框没有被选中以及被选中删除的时候应用 grayscale(1)
的一个 filter
。
更多的调整就是设置一个好看的 background
, font
以及防止心形被选中:
还有一个可访问性问题: 当使用键盘导航时,没有出现心形切换的视觉效果(此时已经看不见复选框)。出现在脑海的第一个解决方案为,当心形处于焦点状态时,添加一个 text-shadow
效果。白色似乎是最佳选择:
[id='toggle-heart']:focus + label { text-shadow: 0 0 3px #fff, 0 1px 1px #fff, 0 -1px 1px #fff, 1px 0 1px #fff, -1px 0 1px #fff; }
使用初始的灰色好像没有足够的色彩对比度,所以决定修改为比雪碧图深一点的灰色。
更新: 在标签上添加了 arial-label = 'like'
。
本文作者根据Codepen上的案例进行分析,并且不通过JavaScript代码实现了Twitter心形点赞动画效果。而且将整个制作过程记录在案,大家根据文章的讲解,可以很好的掌握其实现过程和原理。
其实早在这篇文章之前 @Nicolas Escoffier 写了一篇有关于 Twitter心形点赞的动效 。其实他们制作原理是一样的,如果你对此感兴趣的话,可以阅读早前翻译的一篇文章《使用CSS制作Heart动画》。
本文根据 @ANA TUDOR 的《 Recreating the Twitter Heart Animation 》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处: https://css-tricks.com/recreating-the-twitter-heart-animation/ 。
在校学生,本科计算机专业。一个积极进取、爱笑的女生,热爱前端,喜欢与人交流分享。想要通过自己的努力做到心中的那个自己。微博:@静-如秋叶