ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HTML5 Canvas阴影完全指南:从shadowBlur到内阴影实现

HTML5 Canvas阴影完全指南:从shadowBlur到内阴影实现 1. 先从最常用的阴影四件套说起1.1 shadowBlur阴影的“扩散范围”到底是什么很多人第一次在HTML5 Canvas里调阴影都是照着网上的代码抄抄完发现阴影要么没有、要么糊成一团。其实Canvas的阴影系统非常简单核心就四个属性shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY。只要把这四个参数吃透外阴影和内阴影基本就等于解决了一半。shadowBlur这个属性特别容易误解。我最早以为它是阴影的“不透明度”或者“大小”其实都不是。它的本质是高斯模糊的半径单位是像素数值越大阴影边缘就越柔和、扩散范围越广。注意一个关键点shadowBlur为0的时候如果你同时没有设置偏移量阴影几乎看不见因为完全不模糊、完全不偏的阴影会被原始图形完全盖住。很多新手说“我怎么设置了阴影还是不显示”十有八九就是栽在这里。还有一个更隐蔽的坑shadowBlur的值不是越大越好。数值设到50以上在低分辨率的Canvas里很容易出现明显的颗粒感或边缘锯齿尤其在文字或细线图形上特别明显。我一般会把shadowBlur控制在5到30之间做光晕或者特殊效果时才会上到50以上。另外shadowBlur本质上是一个卷积模糊运算值越大计算量越大如果一帧里绘制了大量带阴影的图形帧率会肉眼可见地往下掉这块后面我会专门说。1.2 偏移量、颜色与三者的配合shadowOffsetX和shadowOffsetY控制阴影相对原始图形的位移。正负方向很好记shadowOffsetX为正阴影往右偏为负往左偏。shadowOffsetY为正阴影往下偏为负往上偏。注意这里不是“光源方向”而是“阴影方向”——阴影往哪个方向跑光源就在相反方向。这个逻辑和很多图片处理软件里正好相反我见过不少刚上手的人把光源方向当成偏移方向结果做出来的投影怎么看怎么别扭。shadowColor就是阴影的颜色支持标准CSS颜色值也支持带透明度的写法比如rgba(0, 0, 0, 0.3)。这里有一个很多人忽略的细节如果你不设置shadowColor默认值是rgba(0,0,0,0)也就是完全透明的黑色所以阴影一样看不见。很多“为什么阴影不显示”的问题根源就在这儿。还有一点shadowColor的透明度会影响整个阴影的浓淡而不是模糊范围。想要阴影浅一点可以直接调整透明度的alpha值这比把shadowBlur调大更精准、更省性能。说句题外话有人拿富士相机拍照后抱怨“高光阴影是反着的”其实和Canvas里的道理一样——你做后期调整时高光和阴影的方向、范围如果和光源方向反了画面就会透着一种说不出的“假”。Canvas的阴影偏移方向选错了也会有类似的“光源错乱感”。所以调整阴影前先想清楚你的假想光源是在左上还是右上这个习惯能帮你省下大量微调时间。2. 外阴影的核心玩法把“图层投影”调出质感2.1 用参数组合模拟常规控件投影外阴影最常见的应用场景就是给按钮、卡片、弹窗这些UI元素做“浮起来”的效果。用一个简单的圆形按钮来演示最直观。const canvas document.getElementById(myCanvas); const ctx canvas.getContext(2d); // 开启外阴影 ctx.shadowOffsetX 4; ctx.shadowOffsetY 4; ctx.shadowBlur 12; ctx.shadowColor rgba(0, 0, 0, 0.3); // 绘制按钮底 ctx.fillStyle #4A90D9; ctx.beginPath(); ctx.arc(120, 120, 50, 0, Math.PI * 2); ctx.fill(); // 绘制完主体取消阴影 ctx.shadowColor rgba(0, 0, 0, 0);这段代码里的关键操作是绘制完主体后立刻把shadowColor设回透明否则后续所有绘制都会带上阴影。很多人习惯把阴影属性设置一次就放着不管结果画第二张图、第三张图时阴影一直跟着调了半天才发现是状态没重置。针对不同需求我整理了一套我实测过的参数组合可以直接抄效果类型shadowOffsetXshadowOffsetYshadowBlurshadowColor适用场景轻投影024rgba(0,0,0,0.15)卡片、小按钮标准投影3510rgba(0,0,0,0.25)弹窗、浮层按钮悬浮高投影0820rgba(0,0,0,0.3)强调视觉层级发光效果0018rgba(0,200,255,0.6)霓虹、光晕、选中态用户头像226rgba(0,0,0,0.2)圆形头像、贴纸这套组合的最大优点是shadowBlur和偏移量保持在一个合理范围既不会让阴影糊成一片也不会生硬得像贴了层黑纸。你在实际项目里可以先把颜色透明度降到0.15试起再逐步往上加直到你觉得“看得出浮起来”但又“不脏”为止。2.2 用软阴影给游戏场景加层次在游戏开发里Canvas阴影用得最多的其实是“软阴影”——也就是让物件的影子边缘不那么锐利看起来更像真实光照下的影子。做过HTML5马里奥、格斗游戏这类项目的人应该深有体会如果角色脚下没有阴影跳跃落地会显得特别“飘”如果阴影太硬又像是踩在了一块黑纸上。软阴影的实现方法就是利用shadowBlur的大数值配合低透明度的shadowColor。比如一个格斗游戏的人物我希望他在平台上有一个柔和的范围阴影可以这样写// 绘制人物本体之前先绘制一个软阴影 ctx.save(); ctx.shadowColor rgba(0, 0, 0, 0.4); ctx.shadowBlur 25; ctx.shadowOffsetX 0; ctx.shadowOffsetY 8; // 用椭圆近似绘制人物底部区域 ctx.fillStyle #333; ctx.beginPath(); ctx.ellipse(player.x, ground.y, player.width * 0.6, 8, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore();这里我用的不是给角色本身加阴影而是单独画一个椭圆阴影贴片。这样做的优势是可以在不增加主绘制负担的情况下自由控制阴影的位置和形态。实际项目里我还会让这个椭圆的大小和透明度随着角色高度动态变化跳得越高阴影越小、越淡地面感一下就出来了。还有一个性能相关的经验不要在动画循环里直接修改shadowBlur因为模糊算法的重算开销非常大。我在做一个HTML5小游戏时一开始每帧都更新shadowBlur实现呼吸式光晕结果真机测试只有20多帧。后面改成预渲染——先把带阴影的角色画到一个离屏Canvas上每帧只是drawImage帧率立刻回到60帧。3. 内阴影的实现思路三种方案对比如果说外阴影是“原生的福利”那么内阴影就是“自己动手的坑”。Canvas的原生阴影API只支持往外扩散并不支持往内收缩所以想要让图形内部出现阴影必须另想办法。我整理过三种常用方案各有优劣直接对比着看。3.1 方案一离屏Canvas globalCompositeOperation这个方案的核心思路是“先画内部投影再限制显示区域”。具体做法是把带阴影的图形画到一个临时Canvas上然后利用globalCompositeOperation的合成模式只保留原图形区域内的内容从而把“向外扩散的阴影”裁成“向内收的阴影”。function drawInnerShadow(shapeDrawFn, radius, color) { // 创建一个离屏canvas const offCanvas document.createElement(canvas); offCanvas.width canvas.width; offCanvas.height canvas.height; const offCtx offCanvas.getContext(2d); // 第一步在离屏canvas上画带阴影的形状 offCtx.shadowBlur radius; offCtx.shadowColor color; offCtx.shadowOffsetX 0; offCtx.shadowOffsetY 0; shapeDrawFn(offCtx); // 第二步把阴影区域“向内裁切” offCtx.globalCompositeOperation source-atop; offCtx.shadowColor rgba(0, 0, 0, 0); offCtx.shadowBlur 0; shapeDrawFn(offCtx); // 第三步把结果画回主canvas ctx.drawImage(offCanvas, 0, 0); }这段代码里最关键的是source-atop这个合成模式。它的作用是新绘制的图形只出现在已有图形的区域内区域外的部分全部丢弃。第一次调用shapeDrawFn时阴影在形状外部扩散第二次调用时新的实心形状把阴影区域挤压到边缘内侧同时外部的阴影被source-atop裁掉最终视觉上就成了内阴影。这个方案的好处是通用性强圆形、矩形、任意路径都都能用坏处是需要额外的Canvas内存占用会翻倍。如果项目里画的东西不多这个代价可以接受。3.2 方案二clip裁剪配合shadow绘制第二个方案适合固定形状的场景比如按钮内凹效果。思路更直接先用clip()把绘图区域限制在形状内部然后再画一个带阴影的偏移图形由于绘制范围被裁剪阴影只会在内部可见。ctx.save(); // 先定义形状路径并裁剪 ctx.beginPath(); ctx.rect(50, 50, 200, 80); ctx.clip(); // 在裁剪区域内绘制带阴影的图形 ctx.shadowColor rgba(0, 0, 0, 0.4); ctx.shadowBlur 8; ctx.shadowOffsetY -3; // 阴影向上偏移形成凹陷顶部阴影 ctx.fillStyle #ddd; ctx.fillRect(50, 50, 200, 80); ctx.restore();注意这里的shadowOffsetY设成了负数意味着真正的阴影在形状上方。但因为clip()限制了可见范围整个阴影只能出现在矩形内部看起来就是顶部内凹的阴影。这个方法代码量最少性能也比离屏Canvas方案高但只适合形状简单、方向固定的场景。你要做任意角度的内阴影这个方案就不太够用了。3.3 方案三不用阴影API的“硬调”做法最后一种方案在某些场景下出奇地好用直接用径向渐变或线性渐变来模拟内阴影。比如想让一个圆形变成“凹进去”的容器用径向渐变就可以轻松实现const gradient ctx.createRadialGradient(100, 100, 20, 100, 100, 60); gradient.addColorStop(0, rgba(0, 0, 0, 0)); gradient.addColorStop(0.8, rgba(0, 0, 0, 0.05)); gradient.addColorStop(1, rgba(0, 0, 0, 0.35)); ctx.fillStyle gradient; ctx.beginPath(); ctx.arc(100, 100, 60, 0, Math.PI * 2); ctx.fill();这样画出来的效果是一个边缘加深、中间通透的圆视觉上就是很经典的“内凹容器”阴影。这种方式的优点是性能极高不需要离屏Canvas也不需要clip()缺点是只能模拟出渐变的阴影形态无法做任意形状的内阴影。但如果你做仪表盘、进度环、圆形头像框这类场景它反而是最优解。我个人的建议是固定形状用方案三任意形状用方案一方案二作为前两者的补充。实际项目里通常不是只用一种比如做一个仪表盘的凹槽时用渐变做一个复杂徽章的内阴影时用离屏Canvas两者配合能把效果和性能都照顾到。4. 常见问题与排查技巧实录4.1 我踩过的那几个坑做Canvas阴影这几年我在群里被问得最多的一个问题就是“为什么我的阴影看不见”。这个问题排查起来其实就三步先看shadowColor是不是透明再看shadowOffsetX/Y是不是都为0最后看shadowBlur是不是太小。三个默认值合在一起就是“没有阴影”所以很多初学的代码看着逻辑没问题结果就是不显示。第二个常见问题是阴影“叠得乱七八糟”。这通常是因为没有及时重置阴影状态。Canvas的上下文中阴影属性是“粘性”的——一旦设置了后面所有绘制都会继承直到你改掉或save/restore。我的习惯是所有带阴影的绘制都写成独立函数函数开头ctx.save()结尾ctx.restore()中间随意折腾外面绝对不受影响。第三个坑比较隐蔽在高DPI屏幕上Canvas阴影会出现明显的模糊或锯齿。这其实不是阴影代码的问题而是Canvas本身的像素密度没适配。解决办法是设置canvas宽高为CSS尺寸乘以window.devicePixelRatio再通过ctx.scale(dpr, dpr)来绘制。我有一段时间没有做适配阴影在Retina屏上比普通屏糊了一大截排查半天才发现是这一步漏了。第四个坑是iOS Safari上的兼容问题。某些iOS版本对shadowBlur和globalCompositeOperation的组合处理不一致离屏Canvas方案在Android正常在iPhone上却可能出现阴影错位。我的兼容做法是先检测ctx.filter是否支持如果支持就优先用ctx.filter drop-shadow(...)来实现部分常见阴影效果不支持则退回离屏Canvas方案。实测下来兼容性稳定很多。4.2 常见问题速查表现象可能原因解决办法阴影完全看不到shadowColor默认透明或offset为0、blur过小设置shadowColor为rgba(0,0,0,0.3)offset至少为1或者blur调到5以上阴影很脏、边缘颗粒感重shadowBlur过大或Canvas分辨率不足降低shadowBlur或按devicePixelRatio放大Canvas后scale后续绘制的图形都带阴影没有重置shadowColor/blur绘制完成后设置shadowColor为rgba(0,0,0,0)或者使用save/restore内阴影裁切不对外部也有阴影globalCompositeOperation设置位置错误确认在绘制“内部覆盖形状”之前设置为source-atop动画帧率骤降每帧频繁修改shadowBlur或绘制大量带阴影图形用离屏Canvas预渲染把带阴影图形缓存为图像iOS上阴影位置偏移部分iOS版本合成模式兼容性问题使用ctx.filter的drop-shadow做降级方案阴影方向与光源相反offset方向理解反了阴影往哪偏光源就在相反方向想清楚假想光源位置再设offset再说一个关于“软阴影”和“硬阴影”的选择技巧做UI时阴影偏硬、偏移量稍大界面会更利落做游戏场景或3D感强的插画时阴影偏软、透明度偏低层次会更自然。不要一套参数走天下我见过很多网页设计作业里用了过大的shadowBlur把整个按钮搞成一团模糊反而失去了投影的意义。最后分享一个我常用的调试小技巧给阴影单独加一层可视化开关。开发时在页面上放一个checkbox勾选后把所有shadowColor临时改成半透明的红色。这样能一目了然地看到每个图形的阴影范围排查“阴影叠在一起发暗”“阴影覆盖了不该盖的区域”这种问题非常高效。等调试完再把颜色改回真实的黑色或深灰色。Canvas的阴影系统就是这么一套东西没什么高深的理论关键是把四个参数理解透再根据场景选择合适的内阴影方案。只要动手试上几次相信你也会发现它其实比CSS的box-shadow更自由、更可控。
返回列表