什么是遮罩?
一个简单的遮罩例子出现在《Disguise and Gals》及无数其他卡通片的结尾。这里,一个动画渐晕逐渐隐藏了Yogi的更多面部。遮罩后面的内容并未被擦除,而是被隐藏。
在CSS中,遮罩使用位图、矢量或渐变遮罩图像来控制可见性。当遮罩的填充像素覆盖元素时,其内容将可见;当它们透明时,内容将被隐藏。填充像素可以是任何颜色,但我总是使用亮粉色,以便清楚哪些区域将可见。
clip-path的功能类似于遮罩,但使用路径创建硬边裁剪区域。严格来说,遮罩和裁剪路径在技术上是不同的,但它们的使用目标通常相同。因此,在本文中,我将它们视为同一洞穴的两个入口,并统称为“遮罩”。
使用裁剪路径进行遮罩
在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>
|
我定义了一个关键帧动画,通过改变其平移值,将角色从右侧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;
}
|
为了让角色在插图边缘变得可见,我添加了第二个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);
}
|
提示:实现该代码后,你会注意到调整浏览器窗口大小时存在问题。虽然我的洞穴插图是灵活的,但clipPath保持固定宽度。
要使clipPath响应式,请将clipPathUnits=“objectBoundingBox"添加到开始标签,然后将两个缩放值应用于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-clip、mask-mode、mask-origin、mask-position、mask-repeat、mask-size和mask-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
|
/* 选项: repeat, repeat-x, repeat-y, round, space, no-repeat */
mask-repeat: no-repeat;
|
它将放置在左上角,除非我改变其位置:
1
2
|
/* 选项: 关键字, 单位, 百分比 */
mask-position: center;
|
另外,我可以像background-size一样指定mask-size:
1
2
|
/* 选项: 关键字 (auto, contain, cover), 单位, 百分比 */
mask-size: cover;
|
最后,我可以定义遮罩的开始位置:
1
2
3
|
mask-origin: content-box;
mask-origin: padding-box;
mask-origin: border-box;
|
使用遮罩图像
Mike的FAQ页面包括他的英雄站在十字路口的动画插图。我的目标是将形状与其内容分离,允许我在英雄的旅程中更改插图。因此,我创建了一个可扩展的mask-image,定义了可见区域,并将其应用于figure元素:
1
2
3
|
figure {
mask-image: url(mask.svg);
}
|
为确保遮罩匹配插图的尺寸,我还将mask-size设置为始终覆盖其内容:
1
2
3
|
figure {
mask-size: cover;
}
|
虽然“X”从不标记地点,但Mike Worth的评论页面插图看到他的猩猩吉祥物正在研究他的宝藏地图。我想通过使用椭圆形将某人的注意力集中在图像的中心部分。
1
2
3
|
figure {
clip-path: ellipse(45% 35% at 50% 50%);
}
|
然而,clip-path的硬边没有产生我旨在实现的效果。
我尝试通过将高斯模糊过滤器与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%);
}
|
这种方法使我能够微调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;
}
|
但我需要更精确地控制光线的位置,以创建它们来自台灯的效果。因此,我用软边位图图像替换了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;
}
|
动画遮罩
动画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%;
}
|
然而,尽管有无限滚动的背景和英雄颠簸行驶的运动,动画仍然感觉静态。因此我添加了一个微妙的动画,移动mask-position,首先创建关键帧:
1
2
3
4
5
6
7
|
@keyframes pan-mask {
0% { mask-position: 45% 45%; } /* 从左下开始 */
25% { mask-position: 55% 55%; } /* 移动到右上 */
50% { mask-position: 43% 52%; } /* 更戏剧性地移动 */
75% { mask-position: 57% 48%; } /* 更多变化 */
100% { mask-position: 45% 45%; } /* 循环回 */
}
|
然后,我将其与滚动背景动画一起应用于figure元素:
1
2
3
4
5
6
|
/* 同时运行两个动画 */
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>
// 选择figure元素。
const figure = document.querySelector('figure');
document.addEventListener('mousemove', (event) => {
// 获取光标位置。
const mouseX = event.clientX;
const mouseY = event.clientY;
// 标准化mask-position。
const maskX = (mouseX / window.innerWidth) * 100;
const maskY = (mouseY / window.innerHeight) * 100;`
// 动态设置mask-position。
figure.style.maskPosition =${maskX}% ${maskY}%;
});
</script>
|
这样,只剩下一个挑战来完成效果。用双筒望远镜聚焦远处的目标很少容易,当英雄的死对头的手有银背大猩猩那么大时,任务更加困难。我扩展了我的脚本,模糊通过双筒望远镜形状遮罩看到的可见内容,然后在某人按下键盘空格键或鼠标按钮时移除过滤器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<script>
// 当鼠标按钮按下时,移除模糊
document.addEventListener('mousedown', () => {
figure.style.filter = 'blur(0)';
});
// 当鼠标按钮释放时,重新应用模糊
document.addEventListener('mouseup', () => {
figure.style.filter = 'blur(5px)';
});
// 当空格键按下时,移除模糊
document.addEventListener('keydown', (event) => {
if (event.key === ' ') {
figure.style.filter = 'blur(0)';
}
});
// 当空格键释放时,重新应用模糊
document.addEventListener('keyup', (event) => {
if (event.key === ' ') {
figure.style.filter = 'blur(5px)';
}
});
</script>
|
更多遮罩动画
当使用Mike Worth网站的人走错路或转错弯时,他最终会沉入热熔岩中。
为了让人知道他们可能已经到达冒险的终点,我想模仿本文开始的缩放效果:
1
2
3
|
<figure>
<svg>…</svg>
</figure>
|
我创建了一个圆形clip-path,并将其默认大小设置为75%。然后,我定义了动画关键帧,将圆从75%调整到15%,然后将其附加到我的figure,持续一秒,延迟三秒:
1
2
3
4
5
6
7
8
9
|
@keyframes error {
0% { clip-path: circle(75%); }
100% { clip-path: circle(15%); }
}
figure {
clip-path: circle(75%);
animation: error 1s 3s ease-in forwards;
}
|
动画现在将某人的注意力集中在不幸的英雄身上,然后他越来越低地沉入冒泡的热熔岩中。
将所有内容带入生活
遮罩为网页动画增加了一个额外的维度,使故事更加吸引人,某人的体验更加引人入胜——同时保持动画高效轻量。无论你是揭示内容、引导焦点还是为设计添加更多深度,遮罩都提供了无尽的创意可能性。那么为什么不在你的下一个项目中尝试它们呢?你可能会发现一种全新的方式将你的动画带入生活。
结束。或者是吗?…
Mike Worth的网站将于2025年6月启动,但你现在可以在CodePen上看到本文的示例。