记录Canvas图案内阴影效果的实现

794 阅读4分钟

前言

在使用canvas制作项目时,需要绘制如下图这样的内阴影效果。

image.png

本以为强大的Canvas有Shadow外阴影的API,肯定也会有类似innerShadow之类的内阴影API,结果翻了一遍MDN竟然没有找到现成的API。搜了搜网上,也没找到什么太好的解决办法。经过一阵思索,我想到了一个曲线救国方案,在这里记录一下。

思路

内阴影其实说白了就是在图形边缘盖一层图形的投影,那既然是投影,肯定就得从外阴影入手。虽然canvas没有内阴影效果,但是说白了内阴影其实就是在一个大的矩形上挖一个当前图形的镂空,再给这个镂空矩形施加一个外阴影,最后在把镂空部分的阴影挖出来叠加在原图形上就可以实现了。

就比如一个红色的正方形图形,可以把内阴影效果看作是两个图层的叠加。 image.png

而内阴影边框效果,又可以看作是在一个更大的矩形上减去当前图形后添加一个外阴影效果,再取当前图形的交集。 image.png

镂空图形就很好办了,就是一个大矩形减去当前图形。 image.png 外阴影效果是有的,各种图层挖空跟叠加模式Canvas也是有的,先决条件都有了,那就按思路来开撸吧!

先实现一个镂空矩形

假设我们有一个500x500的canvas,里面有个随便什么形状,这里还是就以这个红红的方块为例。


<body>
    <div id="container">
        <canvas id="cvs" class="cvs" width="500" height="500"></canvas>
    </div>
    <script>
        const mainCvs = document.getElementById("cvs");
        const mainCtx = cvs.getContext("2d");
        mainCtx.fillStyle = "red";
        mainCtx.fillRect(100, 100, 300, 300);
    </script>
</body>

比如我们要给这个300x300的方块添加一个shadowBlur = 100的内阴影,内阴影的色值是shadowColor = #555,内阴影的透明度为opacity = 0.5。我们就需要创建一个新的整个画布尺寸的图形(其实也不一定是整个画布,如果不考虑偏移的话,稍微大一点就够了)。

const hollowCvs = document.createElement("canvas");
hollowCvs.height = mainCvs.height;
hollowCvs.width = mainCvs.width;
const hollowCtx = hollowCvs.getContext("2d");
hollowCtx.fillStyle = shadowColor;
hollowCtx.fillRect(0, 0, hollowCvs.width, hollowCvs.height);

然后在这个新图形上减去原图形的范围。

//这个模式是在源图形上去掉目标图形的范围
hollowCtx.globalCompositeOperation = "destination-out";
hollowCtx.drawImage(mainCvs, 0, 0);

这样我们就得到了一个镂空图形,再给这个镂空图形按设定好的参数添加一个外阴影。原本我是建了一个新层来绘制的,后来发现直接在镂空图层上再画一遍自己就好了,反正两个图形是重叠的。不过画之前要记得先把叠加模式改回默认,不然就啥都木有了。

hollowCtx.shadowColor = "#555";
hollowCtx.shadowBlur = 100;
hollowCtx.globalAlpha = 0.5;
hollowCtx.globalCompositeOperation = "source-over";
hollowCtx.drawImage(hollowCvs, 0, 0);

将内阴影部分跟原图形组合

我们已经获得了一个包含内阴影的镂空图形。那就很简单了,我们直接将镂空图形跟原图形组合,只绘制原图形的范围就好了。

//这个模式是在保留原图层的基础上,再在原图层和新图层重叠的地方绘制,新图层其他部分都会被删除
mainCtx.globalCompositeOperation = "source-atop";
mainCtx.drawImage(hollowCvs, 0, 0);

这样我们就获得了一个拥有内阴影的图案。

总结

理论上来说任何图形、文字或者是路径之类的都可以用这个方式去获得内阴影。我们可以将其封装成一个方法,这样只要传进去相关参数就可以进行绘制了。

/*
* @Params cvs 当前图案的canvas对象
* @Params options -blur 内阴影的尺寸
*		-shadowColor? 内阴影颜色
*		-opacity? 内阴影不透明度
*/
function drawInnerShadow(cvs, options) {
        const { blur, shadowColor, opacity } = options;
        const hollowCvs = document.createElement("canvas");
  			//这个尺寸可以保证哪怕原图形cvs是撑满整个尺寸的,也会有一定的距离。
        hollowCvs.height = cvs.height + blur * 2;
        hollowCvs.width = cvs.width + blur * 2;
        const hollowCtx = hollowCvs.getContext("2d");
        hollowCtx.fillStyle = shadowColor;
        hollowCtx.fillRect(0, 0, hollowCvs.width, hollowCvs.height);
        hollowCtx.globalCompositeOperation = "destination-out";
        hollowCtx.drawImage(cvs, blur, blur);

        hollowCtx.shadowColor = shadowColor ?? "black";
        hollowCtx.shadowBlur = blur;
        hollowCtx.globalAlpha = opacity ?? 1;
        hollowCtx.globalCompositeOperation = "source-over";
        hollowCtx.drawImage(hollowCvs, 0, 0);
  
        const ctx = cvs.getContext("2d");
        ctx.globalCompositeOperation = "source-atop";
        ctx.drawImage(hollowCvs, -blur, -blur);
      }

而且canvas的外阴影还有一些额外的参数可以用,比如offsetXoffsetY,可以用来调整内阴影的位置,还有叠加模式,比如PS里常用的正片叠底模式,可以先用一个中间层承载内阴影,然后再把原图层的模式设置成multiply模式去叠加中间层来实现。这些都可以封装到方法里,不过目前这些暂时满足我的需求了,等到以后有必要的时候再进行考虑吧。

PS: 本人是个前端小白,第一次发文章,权当是做个记录,写的可能有点枯燥,希望各位不喜勿喷。。如有更好的实现方式也欢迎留言讨论。