利用mask实现手电筒照亮效果

你看到一个效果,是不是会想着自己去实现一下。我就是这样。之前看到过一个视频,一个人拿着火把在一个迷宫里走路,火把只能照亮周围的空间。我在想,如果用前端来实现这个功能,要怎么去实现?一直没有想到合适的思路。
最近看到一个css的属性叫mask
。感觉利用这个mask
可以实现想要的效果。试了一下,果然实现了。
这篇文章,就讲解一下实现过程。
mask
我们先来看一下css中的mask
属性。mask
属性允许使用者通过遮罩或者裁切特定区域的图片的方式来隐藏一个元素的部分或者全部可见区域。
也就是说,我们利用mask
,可以对DOM元素显示的部分进行裁剪。
举个例子,我们想斜着只想显示右半边的图。可以这么写:
1.img-box-test {
2 width: 665px;
3 height: 400px;
4 background: url(./gqq.jpeg) no-repeat;
5 background-size: contain;
6 mask: linear-gradient(225deg, #000 50%, transparent 50%);
7 -webkit-mask: linear-gradient(225deg, #000 50%, transparent 50%);
8}

代码中linear-gradient
表示裁剪一个线, 225deg
表示渐变是从右上角往左下角变化。#000 50%
表示右上角(开始的位置)到50%进度时是#000
,这里是有透明度有效,你改成其他颜色效果一样。transparent 50%
表示从50%位置到左下角(结束位置),都是透明的,所以效果就是如图,右上角显示图片本身的效果,左下角为透明,显示背景颜色。
中间是圆圈
那如果要显示一个圈呢,只显示中间的图片内容。
1.img-box-test {
2 mask: radial-gradient(
3 circle,
4 transparent 0%,
5 rgba(0,0,0,0.2) 10%,
6 rgba(0,0,0,1) 20%,
7 #000 100%
8 );
9 -webkit-mask: radial-gradient(
10 circle,
11 #000 0%,
12 #000 10%,
13 transparent 40%,
14 transparent 100%
15 );
16}

说下代码,radial-gradient
表示要画一个圆形。可以是正圆形,也可以是椭圆形。circle
表示画一个正圆。从圆形(0%位置)开始到10%位置,画#000
,这里也是只有透明度生效,也就是图片本身的内容。10%
到40%
,走渐变,从不透明渐变成全透明。 最后从40%
到100%
,全部透明,显示背景颜色。
遮罩显示部分图片
好了,到这一步,我们就可以开始实现效果了。这里有两个思路:
图片在一个父节点div中,图片做上面的裁剪显示,父节点div显示黑色背景。
图片节点做一个
::after
伪元素,伪元素覆盖到图片上,对伪元素做裁剪。
我这里用的是第二个思路。为什么选第二个,单纯因为我喜欢。。。大家如果想自己写着试试,可以用第一个思路去实现下。实现了可以在评论下面回复我,让我看看你们的实现效果。
思路2的实现代码:
1.img-box {
2 position: relative;
3 width: 665px;
4 height: 400px;
5 background: url(./gqq.jpeg) no-repeat;
6 background-size: contain;
7}
8.img-box::after {
9 content: '';
10 position: absolute;
11 left: 0;
12 top: 0;
13 width: 665px;
14 height: 400px;
15 z-index: 1;
16 background-color: #000;
17
18 mask: radial-gradient(
19 circle,
20 transparent 0%,
21 rgba(0,0,0,0.2) 20%,
22 rgba(0,0,0,1) 40%,
23 #000 100%
24 );
25 -webkit-mask: radial-gradient(
26 circle,
27 transparent 0%,
28 rgba(0,0,0,0.2) 20%,
29 rgba(0,0,0,1) 40%,
30 #000 100%
31 );
32}

裁剪的放大缩小
为了实现光圈可以移动,以及光圈可以放大缩小。我这里需要使用到CSS变量。我分别设置了3个变量:
--radius
: 表示光圈的半径--x
: 表示圆形的x轴距离--y
: 表示圆形的y轴距离
然后在::after
中设置这几个变量:
这里,为了让光圈大小变化,我直接用了transition
来做。后面通过其他CSS去改变--radius
的值,就能自动实现光圈大小的缩放动画。
1.img-box::after {
2 --radius: 20%;
3 --x: 330px;
4 --y: 200px;
5 content: '';
6 position: absolute;
7 left: 0;
8 top: 0;
9 width: 665px;
10 height: 400px;
11 z-index: 1;
12 background-color: #000;
13 transition: --radius 300ms ease-in;
14
15 mask: radial-gradient(
16 circle at var(--x) var(--y),
17 transparent 0%,
18 rgba(0,0,0,0.2) var(--radius),
19 rgba(0,0,0,1) calc(var(--radius) + 15%),
20 #000 100%
21 );
22 -webkit-mask: radial-gradient(
23 circle at var(--x) var(--y),
24 transparent 0%,
25 rgba(0,0,0,0.2) var(--radius),
26 rgba(0,0,0,1) calc(var(--radius) + 15%),
27 #000 100%
28 );
29}
30.img-box-big::after {
31 --radius: 15%;
32}
33.img-box-small::after {
34 --radius: 5%;
35 transition-duration: 100ms;
36}
光圈跟随随便移动
好了,终于到最后一步了。到这里,就要开始用JS来监听鼠标移动了。
先把基础结构写好,在图片节点上,监听鼠标移动事件,如果有鼠标一动就会执行方法。
1const imgBox = document.getElementById('imgBox');
2
3function moveLightRing(x, y) {
4 // 跟随移动
5}
6
7imgBox.addEventListener('mouseenter', (e) => {
8 moveLightRing(e.offsetX, e.offsetY);
9});
10imgBox.addEventListener('mousemove', (e) => {
11 moveLightRing(e.offsetX, e.offsetY);
12});
13imgBox.addEventListener('mouseout', (e) => {
14 moveLightRing(e.offsetX, e.offsetY);
15});
在moveLightRing
方法中,要考虑几个事情:
因为鼠标不移动,光圈会变大,鼠标移动,光圈会变小。所以少量的移动,不能算作移动。
移动和停止移动的时候都要添加一个样式用于改变光圈的半径。
所以moveLightRing
的代码如下:
1function moveLightRing(x, y) {
2 // 短时间内的短距离移动,不算移动
3 const newTime = new Date().getTime();
4 if (lastMove.time + 10 > newTime
5 && lastMove.x + 10 > x
6 && lastMove.y + 10 > y
7 ) {
8 return;
9 }
10 // 记录上一次的移动情况
11 lastMove.time = newTime;
12 lastMove.x = x;
13 lastMove.y = y;
14
15 // 移动,设置样式
16 if (imgBox.className.indexOf('img-box-small') === -1) {
17 imgBox.className = 'img-box img-box-small';
18 }
19 // TODO: 修改after伪元素样式的--x, --y的值
20
21 // 持续移动,不设置。不移动了,100ms后,设置光圈变大。
22 clearTimeout(st);
23 st = setTimeout(() => {
24 imgBox.className = 'img-box img-box-big';
25 }, 100);
26}
这里有一个问题:我没法对::after
伪元素进行样式设置。
解决对伪元素样式设置的问题
现在还有一个问题,我没法对::after
伪元素进行样式设置。网上查了资料,都是说设置一个class
,然后通过class对伪元素进行设置。 但是我这个场景是需要不断修改属性值的。这种方案肯定不适用。
然后我突然想到,能不能利用属性继承。我们都知道,CSS中,父元素的某些属性,是可以被子元素继承。那么这里的CSS自定义属性是否可以被继承呢。我查了下,能被继承,这样我们的问题就解决了。

修改后代码如下:
1function moveLightRing(x, y) {
2 // 修改父节点的自定义样式的值为当前位置
3 imgBox.style.setProperty('--x', x + 'px');
4 imgBox.style.setProperty('--y', y + 'px');
5}1.img-box {
2 --x: 330px;
3 --y: 200px;
4}
5.img-box::after {
6 --radius: 20%;
7 --x: inherit;
8 --y: inherit;
9}
功能完成,最后看下最终效果:

结束
好了,本文到此结束,希望本文对你有所帮助 :-)
最近新弄了一个🌏号:写代码的浩,求关注 😄。后面会逐步把掌握的前端知识以及职场知识沉淀下来。
如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。