CSS遮罩动画进阶:打造立体动态效果

本文深入探讨如何利用CSS遮罩和裁剪路径技术为网页动画增添立体感和电影级视觉效果,包含具体代码示例和实战技巧,帮助开发者提升动画设计的创意与实现水平。

Smashing Animations Part 2: How CSS Masking Can Add An Extra Dimension

Andy Clarke
May 14, 2025
0 comments
15 min read
CSS, Animation, Design

如果你能让CSS动画超越简单的淡入淡出和滑动效果,增添立体感和老派动画魔力呢?在这篇文章中,先驱作者和网页设计师Andy Clarke将向你展示遮罩如何为CSS动画解锁新的创意可能性,使其感觉更流畅、分层和电影化。

尽管有关键帧和滚动驱动事件,CSS动画仍然相对基础。正如我在第一部分中写到的,它们让我想起我在电视上观看的1960年代Hanna-Barbera动画系列,如《Dastardly and Muttley in Their Flying Machines》、《Scooby-Doo》、《The Perils of Penelope Pitstop》、《Wacky Races》以及当然的《Yogi Bear》。

Mike喜爱90年代动画——尤其是迪士尼的《Duck Tales》。因此,这是整个设计中应用的美学。

设计由Andy Clarke, Stuff & Nonsense完成。Mike Worth的网站将于2025年6月推出,但你可以在CodePen上看到本文中的示例。

我全程使用动画,并最近通过遮罩为它们增添了立体感。因此,为了解释这个动画时代如何与CSS中的遮罩相关,我选择了《The Yogi Bear Show》的一集“Disguise and Gals”,该集于1961年5月首次播出。在这个故事中,两个银行抢劫犯伪装成小老太太,将他们的战利品藏在Yogi和Boo-Boo洞穴中的一个“pic-a-nic”篮子里!

可能会出什么问题呢?

什么是遮罩?

一个简单的遮罩示例出现在“Disguise and Gals”和无数其他卡通片的结尾。在这里,一个动画小插图逐渐隐藏了Yogi的更多脸部。遮罩后面的内容没有被擦除;它被隐藏了。

在CSS中,遮罩使用位图、矢量或渐变遮罩图像控制可见性。当遮罩的填充像素覆盖一个元素时,其内容将可见。当它们透明时,内容将被隐藏,这很合理。填充像素可以是任何颜色,但我总是让我的像素为热粉色,这样我就能清楚哪些区域将可见。

clip-path的功能类似于遮罩,但使用路径创建硬边裁剪区域。如果你想要挑剔,遮罩和裁剪路径在技术上是不同的,但使用它们的目标通常是相同的。因此,在本文中,我将它们称为同一洞穴的两个入口,并将使用任一称为“遮罩”。

在“Disguise and Gals”的这个序列中,其中一个抢劫犯将装有他们战利品的野餐篮子冲进Yogi的洞穴。遮罩定义了可见区域,创造了抢劫犯进入洞穴的幻觉。

我如何选择何时使用clip-path和何时选择遮罩?我将在每个示例中解释我的理由。

当Mike Worth和我讨论合作时,我们知道我们既没有预算也没有时间为他的网站创建一个简短的动画卡通。然而,我们热衷于探索动画如何将原本是静态图像的内容赋予生命。

Mike Worth的网站将于2025年6月推出,但你可以在CodePen上看到本文中的示例。

使用裁剪路径进行遮罩

在Mike的传记页面上,他的角色也进入了一个洞穴。我创建的SVG插图包含两个组,一个用于背景,另一个用于前景中的猩猩:

1
2
3
4
5
6
<figure>
  <svg viewBox="0 0 1400 960" id="cave">
    <g class="background"></g>
    <g class="foreground"></g>
  </svg>
</figure>

我定义了一个关键帧动画,通过改变其translate值,将角色从右侧2000px移动到帧中心的自然位置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@keyframes foreground {
  0% { 
    opacity: .75; 
    translate: 2000px 0;
  }
  60% { 
    opacity: 1;
    translate: 0 0;
  }
  80% {
    opacity: 1; 
    translate: 50px 0;
  }
  100% {
    opacity: 1;
    translate: 0 0;
  }
}

然后,我将该动画应用到前景组:

1
2
3
4
.foreground {
  opacity: 0;
  animation: foreground 5s 2s ease-in-out infinite;
}

自己尝试:
参见Pen Mike Worth的关于页面(无clip-path)[forked] by Andy Clarke。

虽然50px的弹跳为他的移动增添了一丝真实感,但我不满意角色从视口边缘开始进入的方式。

我希望他在插图的边缘变得可见。由于洞穴墙壁的边缘是硬的,我选择了clip-path

在CSS中定义clip-path有几种方式。我可以使用像矩形这样的基本形状,其中前四个值指定其角位置。round关键字和随后的值定义任何圆角:

1
clip-path: rect(0px 150px 150px 0px round 5px);

或者使用xywh(x, y, width, height)值,我发现这更容易阅读:

1
clip-path: xywh(0 0 150px 150px round 5px);

我可以使用圆形:

1
clip-path: circle(60px at center);

或椭圆:

1
clip-path: ellipse(50% 40% at 50% 50%);

我可以使用多边形形状:

1
clip-path: polygon(...);

甚至使用我在像Sketch这样的图形应用程序中创建的路径的点:

1
clip-path: path("M ...");

最后——也是我这个示例的选择——我可能使用一个使用SVG文件中的路径定义的遮罩:

1
clip-path: url(#mask-cave);

为了使角色从插图边缘可见,我添加了第二个SVG。为了防止浏览器显示它,将其尺寸设置为零:

1
2
3
4
<figure>
  <svg viewBox="0 0 1400 960" id="cave">...</svg>
  <svg height="0" width="0" id="mask">...</svg>
</figure>

这包含一个单独的SVG clipPath。通过将其放在defs元素中,这个路径不会被渲染,但它将可用于创建我的CSS clip-path

1
2
3
4
5
<svg height="0" width="0" id="mask">
  <defs>
    <clipPath id="mask-cave">...</clipPath>
  </defs>
</svg>

我将clipPath URL应用到我的插图,现在Mike的吉祥物只有在他进入洞穴时才变得可见:

1
2
3
#cave {
  clip-path: url(#mask-cave);
}

自己尝试:
参见Pen Mike Worth的关于页面(带clip-path)(无clip-path)[forked] by Andy Clarke。

提示:实现该代码,你会注意到在调整浏览器窗口大小时有一个问题。虽然我的洞穴插图是灵活的,但clipPath保持固定宽度。

为了使clipPath响应,添加clipPathUnits="objectBoundingBox"到 opening tag,然后对transform属性应用两个缩放值。要计算这些值,首先将1除以SVG的宽度,然后除以其高度。我的SVG宽度是1400px,产生第一个缩放值,0.0007142857143。

1
2
3
4
5
<clipPath id="mask-cave"
  clipPathUnits="objectBoundingBox"
  transform="scale(0.0007142857143, 0.001041666667)">
  ...
</clipPath>

裁剪不规则形状

我经常需要更改或修改插图——可能是通过叠加颜色或应用混合模式——但我想保留它们的整体形状。

虽然clipPath会给我我想要的结果,但这些路径的复杂性和大小有时会对性能产生负面影响。那时我选择CSS遮罩,因为它的属性自2023年以来已成为基线且高度可用。

mask属性是一个简写,可以包括mask-clipmask-modemask-originmask-positionmask-repeatmask-sizemask-type的值。我发现最好单独学习这些属性,以更轻松地掌握遮罩的概念。

遮罩使用位图、矢量或渐变遮罩图像控制可见性。同样,当遮罩的填充像素覆盖一个元素时,其内容将可见。当它们透明时,内容将被隐藏。当遮罩的部分半透明时,一些内容将显示出来。我可以使用包含alpha通道的位图格式,如PNG或WebP:

1
mask-image: url(mask.webp);

我可以使用矢量图形应用遮罩:

1
mask-image: url(mask.svg);

或使用圆锥、线性或径向渐变生成图像:

1
mask-image: linear-gradient(#000, transparent); 

…或:

1
mask-image: radial-gradient(circle, #ff104c 0%, transparent 100%);

我可能对元素应用多个遮罩,并使用应该熟悉的语法混合几种图像类型:

1
2
3
mask-image:
  image(url(mask.webp)),
  linear-gradient(#000, transparent);

mask与CSS背景共享相同的语法,这使得记住其属性容易得多。要应用background-image,添加其URL值:

1
background-image: url("background.webp");

要应用遮罩,将background-image属性替换为mask-image

1
mask-image: url("mask.webp");

mask属性也与CSS背景共享相同的浏览器样式,因此默认情况下,遮罩将水平和垂直重复,除非我另有指定:

1
2
/* Options: repeat, repeat-x, repeat-y, round, space, no-repeat */
mask-repeat: no-repeat;

它将放在左上角,除非我改变其位置:

1
2
/* Options: Keywords, units, percentages */
mask-position: center;

另外,我可以以与background-size相同的方式指定mask-size

1
2
/* Options: Keywords (auto, contain, cover), units, percentages */
mask-size: cover;

最后,我可以定义遮罩开始的位置:

1
2
3
mask-origin: content-box;
mask-origin: padding-box;
mask-origin: border-box;

使用遮罩图像

Mike的常见问题解答页面包括他的英雄站在十字路口的动画插图。我的目标是将形状与其内容分离,允许我在英雄的旅程中更改插图。因此,我创建了一个可缩放的mask-image,它定义了可见区域,并将其应用到figure元素:

1
2
3
figure {
  mask-image: url(mask.svg);
}

为了确保遮罩匹配插图的尺寸,我还将mask-size设置为始终覆盖其内容:

1
2
3
figure {
  mask-size: cover;
}

自己尝试:
参见Pen Mike Worth的常见问题解答(mask-image)[forked] by Andy Clarke。

虽然“X”从未标记地点,但Mike Worth的评论页面插图看到他的猩猩吉祥物在研究他的宝藏地图。我想通过使用椭圆形状将某人的注意力集中在图像的中心部分。

1
2
3
figure {
  clip-path: ellipse(45% 35% at 50% 50%);
}

然而,clip-path的硬边没有创造我旨在实现的效果:

自己尝试:
参见Pen Mike Worth的评论页面(椭圆)[forked] by Andy Clarke。

我通过将高斯模糊过滤器与SVG中定义的遮罩结合进行了实验,首先创建过滤器,然后将其应用到遮罩的椭圆:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<defs>
  <filter id="mask-blur" x="-50%" y="-50%" width="200%" height="200%">
  <feGaussianBlur in="SourceGraphic" stdDeviation="10" /></filter>
</defs>

<defs>
  <mask id="mask">
    <ellipse cx="700" cy="480" fill="#FFF" rx="450" ry="320" filter="url(#mask-blur)"/>
  </mask>
</defs>

虽然这实现了我要找的结果,但实现感觉对于毕竟是一个简单效果来说过度设计了。到目前为止,最有效、优雅和响应的实现使用了radial-gradient,实现了我希望的效果,没有位图或矢量图像,只有一个CSS属性:

1
2
3
figure {
  mask-image: radial-gradient(ellipse farthest-corner at center center, #000 0%, transparent 75%);
}

自己尝试:
参见Pen Mike Worth的评论页面(径向渐变遮罩)[forked] by Andy Clarke。

这种方法使我能够微调我的mask-image大小,甚至在有人与其内容交互时动画其颜色停止、位置和大小。

分层多个遮罩

照明在Mike Worth的评论页面上尤其重要,他的猩猩英雄需要它来研究他的宝藏地图。像背景图像一样,我可以应用多个遮罩图像来创建我需要的照明。

我可以组合两个遮罩图像:一个圆形、半透明的radial-gradient提供氛围,加上一个45度角的linear-gradient用于光线。我将它们都应用到figure元素:

1
2
3
4
5
6
figure {
  mask-image:
  radial-gradient(circle, rgba(255,16,76,.5) 45%, transparent 55%),
  linear-gradient(45deg, transparent 40%, #ff104c 50%, #ff104c 50%, transparent 60%);
  mask-repeat: no-repeat;
}

我单独定位遮罩,将radial-gradient大小设置为80%,将linear-gradient光线设置为覆盖整个图像:

1
2
3
4
figure {
  mask-position: 50% 50%, 0 0;
  mask-size: 80%, cover;
}

自己尝试:
参见Pen Mike Worth的评论页面(分层多个遮罩)[forked] by Andy Clarke。

但是,我需要更精确地控制光线的位置,以创造它们来自台灯的效果。因此,我用软边位图图像替换了linear-gradient

1
2
3
4
5
6
figure {
  mask-image:
    radial-gradient(circle, rgba(255,16,76,.5) 45%, transparent 55%),
  url(mask.webp);
  mask-size: 90%, cover;
}

最后,为了增添一丝真实感,我添加了一个关键帧动画——改变mask-size并创造灯光明灭的效果——并将其应用到figure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@keyframes lamp-flicker {
  0%, 19.9%, 22%, 62.9%, 64%, 64.9%, 70%, 100% { 
    mask-size: 90%, auto;
  }

  20%, 21.9%, 63%, 63.9%, 65%, 69.9% { 
    mask-size: 90%, 0px;
  }
}

figure {
  animation: lamp-flicker 3s 3s linear infinite;
}

自己尝试:
参见Pen Mike Worth的评论页面(多个mask-images)[forked] by Andy Clarke。

动画遮罩

动画CSS遮罩创造了令人兴奋的揭示和场景之间的过渡,并帮助某人关注关键内容。它还可以将故事赋予生命,使一个人的体验更具吸引力和沉浸感。

在我为他网站设计的这个删除场景中,可以看到我为Mike Worth创建的猩猩冒险家吉祥物驾驶穿越景观。为了帮助讲述他被他的死对头从远处监视的故事,我想添加一个双筒望远镜形状的遮罩。

我首先创建了双筒望远镜形状,包括一些取景器标记。

然后,我将该图像应用为遮罩,设置其位置、重复和大小值以将其放在figure元素的中心:

1
2
3
4
5
6
figure {
  mask-image: url(mask.svg);
  mask-position: 50% 50%;
  mask-repeat: no-repeat;
  mask-size: 85%;
}

自己尝试:
参见Pen Mike Worth的旅程动画与双筒望远镜形状遮罩[forked] by Andy Clarke。

然而,尽管有无限滚动的背景和英雄颠簸行驶的运动,动画仍然感觉静态。因此我添加了一个微妙的动画,移动mask-position,首先创建关键帧:

1
2
3
4
5
6
7
@keyframes pan-mask {
  0% { mask-position: 45% 45%; } /* Start lower-left */
  25% { mask-position: 55% 55%; } /* Move to upper-right */
  50% { mask-position: 43% 52%; } /* Shift more dramatically */
  75% { mask-position: 57% 48%; } /* More variation */
  100% { mask-position: 45% 45%; } /* Loop back */
}

然后,我将其与滚动背景动画一起应用到figure元素:

1
2
3
4
5
6
/* Run both animations simultaneously */
figure {
  animation:
  background-scroll 5s linear infinite,
  pan-mask 6s ease-in-out infinite alternate;
}

在看到这些结果后,我想知道我是否可以通过将mask-position链接到某人光标的移动来将他们与故事连接起来。我添加了一个脚本,选择figure元素,获取光标相对于视口的位置,并动态改变mask-position

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<script>
  // Select the figure element.
  const figure = document.querySelector('figure');
  document.addEventListener('mousemove', (event) => {
  
  // Get the cursor position.
  const mouseX = event.clientX;
  const mouseY = event.clientY;
  
  // Normalise the mask-position.
  const maskX = (mouseX / window.innerWidth) * 100;
  const maskY = (mouseY / window.innerHeight) * 100;`
  
  // Dynamically set the mask-position.
  figure.style.maskPosition =${maskX}% ${maskY}%;
  });
</script>

自己尝试:
参见Pen Mike Worth的旅程动画与双筒望远镜跟随光标[forked] by Andy Clarke。

有了这个,只剩下一个挑战来完成效果。用双筒望远镜聚焦远处的目标很少容易,当英雄的死对头有银背大猩猩大小的手时,任务更加困难。我扩展了我的脚本,模糊通过双筒望远镜形状遮罩看到的可见内容,然后在某人按下键盘的空格键或按下鼠标按钮时移除过滤器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<script>
  // When mouse button pressed, remove blur
  document.addEventListener('mousedown', () => {
    figure.style.filter = 'blur(0)';
  });
  
  // When mouse button released, reapply blur
  document.addEventListener('mouseup', () => {
    figure.style.filter = 'blur(5px)';
  });
  
  // When spacebar pressed, remove blur
  document.addEventListener('keydown', (event) => {
    if (event
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计