纯CSS实现动态条纹背景效果

2022-12-09 ⏳4.4分钟(1.8千字) g

CSS-TRICKS 最近发表了一篇文章1用纯CSS实现了动态条纹背景效果。内容很基础,却也是非常好的入门学习材料。今天整理出来分享给大家。虽然不是原创,但我也没有简单地翻译原文内容。而是跟小鸟按钮2一样,自己重新实现了一把。读者可以直接在当前页面体验实际效果。另外原文中的组件尺寸比较大,直接嵌入博客正文不太美观,我把中间的绝对尺寸提取出来改为变量。这也算一点微小的创新吧😂

在开始之前先让大家体验一下整体效果,把鼠标悬停到下面的方形区域会看到:

taoshu is a good guy :)

现在我们开始逐步实现整个效果。首先画一个正方形:

<div></div>
div {
  width: 200px;
  height: 200px;
  background: beige;
}

然后我们使用背景色绘制条纹。基本原理就是给元素指定多种背景图片。CSS不支持设定多种前景颜色,但可以指定多个背景图片。但我们不需要图片,只需要纯色背景,但是background-image不支持直接设定背景颜色。一个迂回的办法是使用linear-gradient设置渐变色,但是把前后的颜色值设成一样的就能获得纯色背景。另外浏览器默认会将背景图铺满整个元素,所以我们要为每种颜色指定开始位置和宽高尺寸。所以完整 CSS 如下:

div {
  background-image: 
    linear-gradient(#ff0000, #ff0000),
    linear-gradient(#00ff00, #00ff00),
    linear-gradient(#0000ff, #0000ff),
    linear-gradient(#ffff00, #ffff00),
    linear-gradient(#00ffff, #00ffff);
  background-position:
    top   0px right,
    top  40px right,
    top  80px right,
    top 120px right,
    top 160px right;
  background-size: 100% 40px;
  background-repeat: no-repeat;
}

这里使用background-position为每种背景颜色设置开始位置。因为各颜色的位置不同,所以需要分别设置。而指定背景大小则需要使用background-size。又因为各背景色大小相同,可以统一设置为100% 40px。其中100%表示宽度,40px表示高度。最后还需要使用background-repeat: no-repeat告诉浏览器不要重复绘制背景图。显示效果如下:

如果想实现「锯齿」形条纹,则需要为每一个条纹单独设置宽度,顺便把高度调小一点留出间隙:

div {
  background-size:
    60% 35px,
    90% 35px,
    70% 35px,
    40% 35px,
    10% 35px;
}

这样我们就把条纹的整体形状绘制出来了:

接下来我们实现动画效果。说白了是通过hover伪元素来动态调整条纹宽度:

div:hover {
  background-size:
    100% 35px,
    100% 35px,
    100% 35px,
    100% 35px,
    100% 35px;
}

这样鼠标移上去条纹会自动变长,移走又会缩回去:

但这种变化太生硬了,我们给它添加一种动画效果:

div {
  transition: background-size 1s;
}

这里通过transition给background-size变化添加了1s的渐变动画,效果如下:

现在我们开始调色。刚才为了方便演示,我给条纹添加了不同的颜色。但最终要显示的其实是种渐变色linear-gradient(to right, red, orange),从红色过渡到橙色。我们可以先看看展示效果:

div {
  background-image: 
    linear-gradient(to right, red, orange),
    linear-gradient(to right, red, orange),
    linear-gradient(to right, red, orange),
    linear-gradient(to right, red, orange),
    linear-gradient(to right, red, orange);
}

已经很接近最终效果了。但这种处理方式还是有点小瑕疵,即条纹在伸缩的时候,不同位置的色值会有变化。出现这个问题是因为渐变色的范围是条纹的宽度。鼠标移上去后条纹宽度会变长,而且不同条纹的初始长度又不一样,变化的幅度也不同,所以浏览器需要重新计算渐变色,从而导致颜色变化。

那如何解决这个问题呢?CSS-TRICKS 给出了mix-blend-mode方案。简单来说就是人为设置两子元素,让它们都充满父元素,然后一个设置为渐变色,另一个绘制条纹实现颜色遮罩功能。

这里需要把mix-blend-mode设置为screen模式。在该模式下,浏览器会尝试将两个子元素的颜色混合。如果一个子元素的颜色是黑色,则会显示另外一个子元素的颜色;如果一个子元素的颜色是白色(默认色),则显示白色。更多细节请参考 MDN3。

为了实现遮罩效果,我们需要重构 HTML 结构:

<section>
<div></div>
<div></div>
<strong></strong>
</section>

我们在<section>中设置三个子元素。第一个<div>用来显示渐变色,第二个用来绘制条纹遮罩,最后的<strong>用来显示文字。

先定义几个变量:

section {
  --size: 200px; /* 元素边长 */
  --n: calc(var(--size) / 5); /* 条纹高度 */
  --h: calc(var(--n) - 5px); /* 条纹实际高度 */
}

然后基于grid将三个子元素重叠起来:

section {
  width: var(--size);
  height: var(--size);
  display: grid;
  align-items: center;
  justify-items: center;
  cursor: pointer;
}
section > * {
  width: inherit;
  height: inherit;
  grid-area: 1 / 1;
  transition: background-size 1s;
}

父元素<section>的显示模式指定为grid,并指定上下左右居中。然后通过section > * 将所有子元素都显示到第一个位置grid-area: 1/1,这样子元素就会重叠起来。

我们使用:nth-child伪元素指定子元素。给第一个<div>设置渐变色:

section > div:nth-child(1) { 
  background: linear-gradient(to right, red, orange); 
}

为第二个<div>绘制动态条纹:

section > div:nth-child(2) {
  --gt: linear-gradient(black, black);
  background: 
    var(--gt) top right,
    var(--gt) top var(--n) right,
    var(--gt) top calc(var(--n) * 2) right,
    var(--gt) top calc(var(--n) * 3) right,
    var(--gt) top calc(var(--n) * 4) right, 
    beige;
  background-repeat: no-repeat;
  background-size: 60% var(--h), 90% var(--h), 70% var(--h), 40% var(--h), 10% var(--h);
  mix-blend-mode: screen;
}
section:hover > div:nth-child(2) {
  background-size: 100% var(--h);
}

这次所有颜色都改为黑色,也就是linear-gradient(black,black),可以单独设置一个变量--gt来简化 CSS 规则。这里还使用calc动态计算每个条纹在上方的偏移量,方便后续调整。最后需要注意的点是把元素的mix-blend-mode指定为screen。

最后说一下文字样式。CSS-TRICKS 原文并没有讲到这部分。我自己本来也想略过。但实践发现没有文字显示效果就会打折,而且换个字体都不行。于是我又把原文的字体搞了过来。

strong  {
  height: max-content;
  text-align: center;   
  isolation: isolate;
  line-height: 1.2em;
  font-family: 'Bungee Shade';
  font-size: calc(var(--size) / 5.5);
  color: maroon;
}

第一个关键点是isolation: isolate,它的意思是创建独立的stacking context。其实我也不太明白是什么意思,但从效果上来看,是让<strong>元素不受mix-blend-mode的意识,独立渲染。大家可以参考 MDN4。

第二个点是需要给文字设置line-height,不然多行文字会挤到一起。

每三个点是需要根据元素尺寸动态计算字体大小,我用的是calc(var(--size)/5.5),这个需要根据字体形状和个人偏好动态调整。如果不动态计算,个性--size后文字可能超出元素边界。

这里用到的字体是 BungeeShade,大家可以从网上下载。我从 Google Font 下的是 TTF 格式,可以直接用作 Web Font。但转成 woff2 格式体积会减少一半,我就转了一下。个人博客能省则省😄

最后再给大家呈现最终效果:

taoshu is a good guy :)