
网上讲 Openlayers 遮罩效果的文章其实不少但大部分都会停在最基础的那一版先画一个半透明黑色矩形盖住全图然后用 Canvas 的 globalCompositeOperation 挖出一个洞把高亮区域露出来。这个思路没问题第一篇文章里我也按这个路子把基础功能跑通了。但如果你真的拿它去做正式项目很快就会碰到三个绕不开的问题第一业务上要高亮的区域往往不止一个两个、五个、十几个点同时要“露出来”代码该怎么组织第二遮罩的范围是动态变化的用户点一下地图洞口就要跟着变总不能每次把整张地图重新渲染一遍吧第三遮罩画完以后地图拖动、缩放的过程中边界很容易出现残影、偏移这个问题在 OpenLayers 4 里尤为典型。这篇文章就是来把这些问题一次性解决的。我会先讲清楚 Canvas 遮罩的底层原理再给出一个适合多区域、可动态更新的完整实现最后把拖动残影、坐标转换精度这些坑一条条列给你看。不管你是刚把第一个遮罩demo跑通的新手还是已经写了好几个 OL 项目但没深挖过 Canvas 渲染细节的开发者这份内容都能直接抄到你的项目里用.1. 整体设计思路从单区域到多区域遮罩1.1 为什么第一篇的基础写法撑不住“多区域”需求先回顾一下第一篇最核心的代码形态。通常是在地图的 postcompose 事件里拿到 Canvas 上下文然后分两步操作第一步用 fillRect 把整个画布填成半透明黑色第二步把 globalCompositeOperation 改成 destination-out再把目标区域画一遍于是这个区域就变成了全透明下面的地图也就显示出来了。这个写法在单区域场景下非常清晰但一旦目标变成“同时高亮上海市所有核心商圈”问题就出来了。你会发现每个商圈都需要一组经纬度坐标还需要不同的半径或者形状如果还是每次都在事件回调里临时拼路径代码很快就变成一坨难以维护的 if-else。更麻烦的是只要地图有任何一次平移或者缩放postcompose 就会触发一次所有区域都得重新计算一遍性能压力会随着区域数量成倍增加。所以第二篇文章的核心思路就是从“一次绘制”升级成“数据驱动绘制”。把所有需要高亮的区域统一抽象成一张“区域配置表”每一个配置项里记录坐标、类型、半径、颜色、透明度这些参数。渲染函数只负责遍历这张表用统一的方式把每个区域画出来。1.2 一个合理的数据结构长什么样在实际项目里我建议直接把遮罩配置设计成一个数组每个元素代表一个高亮区域。这样做的好处有几个一是和后端返回的数据结构天然对齐后端接口返回一个 list前端直接塞给渲染函数就行二是便于动态新增和删除不用动渲染逻辑只改数组。下面是我在项目里用的一个简化版结构var highlightRegions [ { id: region_001, type: circle, // 区域类型circle 圆形polygon 多边形 center: [121.4737, 31.2304], // 中心点经纬度 radius: 3000, // 圆的半径单位米只对 circle 类型生效 fillColor: rgba(0, 0, 0, 0.6), // 遮罩填充色 holeColor: rgba(255, 255, 255, 1) // 挖洞区域画色实际用透明混合 }, { id: region_002, type: polygon, coordinates: [ // 多边形顶点经纬度数组 [121.40, 31.20], [121.50, 31.20], [121.50, 31.28], [121.40, 31.28] ], fillColor: rgba(0, 0, 0, 0.6) } ];你没看错我把“遮罩颜色”放在每个区域配置里而不是全局统一。因为实际业务里很可能需要不同区域使用不同的遮罩透明度比如核心区域遮罩淡一点周边区域遮罩浓一点用来体现等级差异。当然你也可以把 fillColor 抽到全局只有 coordinates 跟 type 放在区域配置里这个看具体项目。1.3 渲染流程拆成三个独立环节多区域遮罩的渲染流程我强烈建议拆成三个独立环节清理画布、绘制遮罩层、挖洞。三个阶段各干各的事互不干扰。这样拆开以后你后续做动态更新就非常轻松了只需要在数据变化时重新执行一次这三个环节即可。这里有一个容易被忽略的细节地图的 postcompose 事件在每一帧渲染时都会触发包括拖动、缩放、甚至只是鼠标滑过某些控件导致的重绘。所以这三个环节的执行频率其实很高。如果阶段二和阶段三里面用到的 Canvas 对象本身不做复用每次都 new 一遍性能损耗就会非常明显。正确的做法是提前创建一个离屏 Canvas把遮罩层和挖洞结果缓存成一张静态图片地图重绘时只需要把这张缓存图片贴上去。2. 核心实现原理Canvas“整面遮盖挖洞”到底怎么运作2.1 globalCompositeOperation 的两种关键模式要理解遮罩必须清楚 Canvas 的合成模式。这里只讲两个最关键的值其他的有兴趣自己翻文档。第一个是默认的 source-over。这个模式下新画的图形会覆盖在原有内容之上。如果你直接用 fillRect 画一个半透明黑色矩形它会盖住地图这不是我们要的效果吗确实是第一步要的效果但第二步就麻烦了如果用 source-over 去画高亮区域的圆它只会让那个圆看起来更黑而不会把遮罩层“擦掉”。第二个就是 destination-out这个模式的作用是“让新画的图形区域内的原有内容变成透明”。你可以把它理解成橡皮擦只不过橡皮擦的形状由你画的路径决定。我们要实现“高亮区域显示地图”本质就是先把整张地图用半透明黑色盖住再用 destination-out 模式把高亮区域那一块的黑色擦掉让下面的地图露出来。两步配合一个标准的遮罩效果就出来了。这里必须提醒你一点destination-out 擦除的是 Canvas 上已有的像素所以你不能在第一次 fillRect 之前就开启这个模式。如果顺序反了你会把原本清晰的地图底图给擦掉出现一个大白洞而且这个白洞会一直透到地图底层容器去。2.2 坐标转换是大多数坑的源头Canvas 的绘制坐标是像素而业务数据通常是经纬度。所以绘制之前必须把经纬度转换成屏幕像素坐标。OpenLayers 4 里最常用的方法是 map.getPixelFromCoordinate(coordinate)这个方法会返回一个 [x, y] 像素坐标x 相对于地图容器左上角y 往下为正。拿到像素坐标以后你才能调用 ctx.arc(x, y, radius, 0, Math.PI * 2) 去画圆形的“洞”。这里有个很容易踩的坑圆形的 radius 是像素半径不是地理半径。假设后端告诉你某个商圈要高亮周边 3000 米你不能直接把 3000 当成像素值传给 ctx.arc。因为这会导致在缩放级别不同的时候这个圆的实际地理范围完全不一样。在 Zoom 级别高的时候一个 3000 像素的圆可能覆盖大半个屏幕在 Zoom 级别低的时候它可能小得看不见。所以在绘制圆形遮罩之前你得先做一次单位换算。OpenLayers 提供了简单的方式可以取当前地图分辨率 map.getView().getResolution()这个值的单位是“米/像素”。你用 3000 除以它就得到对应像素半径。代码如下var view map.getView(); var resolution view.getResolution(); var pixelRadius 3000 / resolution;当然这个换算方式在 Web Mercator 投影下是比较准的因为 Web Mercator 在赤道附近近似等距纬度越高形变越大。如果你的精度要求很高或者业务区域跨越的纬度范围很大建议用 ol.proj 的接口做更严格的距离换算或者直接用 spherical 几何计算。考虑到大部分业务都在城市尺度3000 米这种半径用上面的公式完全够用。2.3 多区域的绘制顺序与重叠规则多区域遮罩尤其是两个区域靠得非常近时会出现洞口重叠。如果第二次画路径时忘了重置合成模式重叠部分可能反复被擦除最终结果看起来会是两个区域中心特别“亮白”边界地带却出现了半透明残留。这在视觉上非常突兀。解决方式有两个思路。思路一先在一个离屏 Canvas 上把所有的区域路径全部收集起来统一填充成一个完整的大路径然后再一次性交给 destination-out 去挖。思路二按照顺序依次绘制每个区域但每个区域绘制前都显式地重置一遍合成模式画完以后再切回 destination-out。第二个思路代码上会更简单但性能上不如第一个。因为每切换一次合成模式Canvas 引擎都可能要做一次内部状态刷新。我在实际项目里用的是“合并路径”的做法也就是用 ctx.beginPath() 开始然后循环遍历每个区域用 moveTo/lineTo 或者 arc 把路径都加进去最后统一调用一次 fill()。这样无论多少个区域挖洞的 fill 只执行一次效率高很多也彻底避免了重叠区域的二次擦除问题。3. 完整实操多区域动态遮罩的代码落地3.1 基础工程结构准备动手之前先确认你本地的 OpenLayers 是 4.6.x 这个系列版本。因为 OpenLayers 5 之后的 API 有调整postcompose 事件虽然还保留但部分内部实现改了这篇代码不能保证在 6.x、7.x 上原样跑通。如果你的地图对象是下面这种方式初始化的就可以继续往下看var map new ol.Map({ target: map, layers: [ new ol.layer.Tile({ source: new ol.source.OSM() }) ], view: new ol.View({ center: ol.proj.fromLonLat([121.4737, 31.2304]), zoom: 10 }) });如果你连这个初始化步骤都还没跑通建议先回第一篇补个基础或者直接用官方示例把地图显示出来。接下来所有代码都建立在 map 对象已经可以正常显示底图的基础上。3.2 定义并注册遮罩渲染函数OpenLayers 4 里实现 Canvas 叠加效果最标准的做法是监听 map 的 postcompose 事件。这个事件会在每次地图帧渲染完成后触发你可以在里面拿到 Canvas 渲染上下文然后往上画任何东西。先定义一个渲染遮罩的函数var highlightRegions []; // 全局区域配置动态更新 function renderMask(event) { if (!highlightRegions.length) { return; } var canvas event.context; var ctx canvas; var size map.getSize(); // 保持画布状态退出时恢复 ctx.save(); // 第一步全图半透明遮罩 ctx.globalCompositeOperation source-over; ctx.fillStyle rgba(0, 0, 0, 0.6); ctx.fillRect(0, 0, size[0], size[1]); // 第二步合并所有区域路径 ctx.beginPath(); for (var i 0; i highlightRegions.length; i) { var region highlightRegions[i]; if (region.type circle) { var centerPixel map.getPixelFromCoordinate(ol.proj.fromLonLat(region.center)); var resolution map.getView().getResolution(); var radiusPixel region.radius / resolution; ctx.moveTo(centerPixel[0] radiusPixel, centerPixel[1]); ctx.arc(centerPixel[0], centerPixel[1], radiusPixel, 0, Math.PI * 2); } else if (region.type polygon) { var pixelCoords region.coordinates.map(function(coord) { return map.getPixelFromCoordinate(ol.proj.fromLonLat(coord)); }); ctx.moveTo(pixelCoords[0][0], pixelCoords[0][1]); for (var j 1; j pixelCoords.length; j) { ctx.lineTo(pixelCoords[j][0], pixelCoords[j][1]); } ctx.closePath(); } } // 第三步挖洞 ctx.globalCompositeOperation destination-out; ctx.fillStyle #000; ctx.fill(); ctx.restore(); } map.on(postcompose, renderMask);这段代码的关键点有两个。第一个是 circle 分支里的 moveTo 调用很多人画完第一个圆后直接画第二个圆结果两个圆之间会无端多出一条连接线。原因就是路径没有断开Canvas 会把前一个图形的终点连到新图形起点。处理办法就是每个新图形开始前 moveTo 到当前图形边缘这里我取的是圆心右侧半径处的一点确保路径移动到圆上。第二个关键点是 polygon 分支里的竖线问题。如果多边形没画完直接进入下一个区域的 moveToOpenLayers 地图中间就会穿出一条对角线。所以每个多边形完成之后必须 closePath()让路径闭合。3.3 动态更新点击地图添加高亮区域现在讲整个“二”里最有价值的动态更新环节。业务场景是这样的用户点击地图上的某个位置地图上就高亮显示以这个点为中心、半径 2000 米的圆形区域再次点击别的位置高亮区域就更新到新的位置如果点击的是已经高亮的点则取消它的高亮。监听地图的单次点击事件map.on(singleclick, function(evt) { var lonLat ol.proj.toLonLat(evt.coordinate); var existed false; for (var i 0; i highlightRegions.length; i) { var region highlightRegions[i]; if (region.type circle region.radius 2000) { var dist Math.sqrt( Math.pow(region.center[0] - lonLat[0], 2) Math.pow(region.center[1] - lonLat[1], 2) ); if (dist 0.01) { highlightRegions.splice(i, 1); existed true; break; } } } if (!existed) { highlightRegions.push({ id: click_ Date.now(), type: circle, center: [lonLat[0], lonLat[1]], radius: 2000, fillColor: rgba(0, 0, 0, 0.6) }); } // 触发一次地图重绘 map.render(); });核心是最后一行的 map.render()。很多人改完 highlightRegions 数组后发现地图不刷新就是这个原因。OpenLayers 的 postcompose 事件是跟着渲染帧走的单纯改数组不会主动触发渲染你必须手动调用 map.render() 通知地图重新绘制一次。我记得第一次做的时候漏掉了这个 render调试了半小时才反应过来还以为是自己数组操作写错了。现在养成了条件反射任何 postcompose 里读取的数据只要它变化了就必须补一个 map.render()。3.4 完整效果验证跑起来以后你应该看到的效果是地图上所有未被高亮的区域都蒙上一层半透明黑色点击的任意位置会露出一个半径 2000 米的圆形亮区亮区内的地图清晰可见亮区外的地图被黑色蒙层盖住。重复点击同一个点亮区消失蒙层恢复完整。这就是一个标准的多区域动态遮罩效果。如果点完第一个点再点第二个点两个圆心之间如果有重叠地段这个地段应该保持干净透明不会出现重叠的半透明黑斑。这一点在早期版本里经常出问题原因就是路径没合并填了两次 destination-out 导致的用上面合并路径的写法就能彻底避免。4. 圆形高亮与事件联动把遮罩从“画图”变成“交互层”4.1 给遮罩区域绑定鼠标悬浮效果遮罩本身不是目的交互才是。一个很常见的需求是鼠标悬浮在哪个高亮区域上就给这个区域加一圈描边同时在旁边弹一个小提示框显示区域名称和覆盖范围。要实现这个效果需要先解决一个问题怎么判断鼠标当前在哪一个遮罩区域内部因为圆形区域最简单这里只讲圆形的判断。监听地图的 pointermove 事件var hoveredRegionId null; map.on(pointermove, function(evt) { if (evt.dragging) { return; } var coordinate ol.proj.toLonLat(evt.coordinate); var foundRegionId null; for (var i 0; i highlightRegions.length; i) { var region highlightRegions[i]; if (region.type ! circle) { continue; } var dx coordinate[0] - region.center[0]; var dy coordinate[1] - region.center[1]; var distanceInMeters Math.sqrt(dx * dx dy * dy) * 111320; // 粗略换算1度纬度约111.32km if (distanceInMeters region.radius) { foundRegionId region.id; break; } } if (foundRegionId ! hoveredRegionId) { hoveredRegionId foundRegionId; map.getTargetElement().style.cursor hoveredRegionId ? pointer : default; map.render(); } });这里有一个粗略的经纬度换算把经纬度差值乘 111320 得到一个近似米数。这个换算只在纬度不极端、且范围较小的情况下够用。如果你的区域跨度达到上百公里或者在高纬度地区建议用 ol.sphere.getDistance 做精确计算。这里的代码给的是一个简单直接可跑通的方案你要用它做更严谨的业务记得替换成精确算法。4.2 在遮罩层上绘制描边与悬浮提示上面判断出 hoveredRegionId 之后还需要在渲染函数里对这个区域做差异化绘制。思路是在挖洞之前先对 hovered 的区域单独画一个描边路径function renderMask(event) { // ... 前面的遮罩全图填充和路径收集保持不变 ... // 描边只在 hoveredRegionId 有值时执行 if (hoveredRegionId) { ctx.globalCompositeOperation source-over; ctx.strokeStyle #ffd700; ctx.lineWidth 3; ctx.setLineDash([8, 4]); for (var i 0; i highlightRegions.length; i) { var region highlightRegions[i]; if (region.id ! hoveredRegionId) { continue; } if (region.type circle) { var centerPixel map.getPixelFromCoordinate(ol.proj.fromLonLat(region.center)); var resolution map.getView().getResolution(); var radiusPixel region.radius / resolution; ctx.beginPath(); ctx.arc(centerPixel[0], centerPixel[1], radiusPixel, 0, Math.PI * 2); ctx.stroke(); } break; } ctx.setLineDash([]); } }这里我用的 hovered 描边是在 postcompose 里同步绘制的所以视觉上没有任何延迟鼠标移过区域边缘的瞬间虚线描边就会亮起来。悬浮提示这块建议不要用 Canvas 自己画气泡维护成本太高。直接用 OpenLayers 的 Overlay 组件挂一个 HTML 元素上去通过 pointermove 事件动态更新 Overlay 的位置和内容。代码如下var tooltip new ol.Overlay({ element: document.getElementById(tooltip), positioning: bottom-center, offset: [0, -10] }); map.addOverlay(tooltip); map.on(pointermove, function(evt) { var hoveredRegion getHoveredRegion(evt.coordinate); if (hoveredRegion) { tooltip.setPosition(evt.coordinate); document.getElementById(tooltip).innerHTML b hoveredRegion.name /bbr/覆盖半径 hoveredRegion.radius 米; tooltip.getElement().style.display block; } else { tooltip.getElement().style.display none; } });这个方案的优势是提示框可以是任意 HTML 结构可以带图片、可以加 CSS 动效、可以点击跳转比 Canvas 画文字好用太多。4.3 从高亮到业务动作点击区域触发详情既然鼠标悬浮都能判断了点击触发业务逻辑就顺手了。监听 singleclick 事件先判断点击位置是否落在任何高亮区域内。如果在就调用业务回调比如打开一个信息窗口如果不在任何区域内才执行“新增一个高亮区域”的逻辑。判断代码可以复用 pointermove 里的 getHoveredRegion 逻辑。这样做的好处是交互逻辑非常统一先判断点中谁再决定干什么。我封装了一个函数function getHoveredRegion(coordinate) { for (var i 0; i highlightRegions.length; i) { var region highlightRegions[i]; if (region.type circle) { var dx coordinate[0] - region.center[0]; var dy coordinate[1] - region.center[1]; var distanceInMeters Math.sqrt(dx * dx dy * dy) * 111320; if (distanceInMeters region.radius) { return region; } } } return null; }这样 singleclick 事件和 pointermove 事件都复用同一个判断函数不会出现“鼠标判断和点击判断对不上”的问题。5. 性能优化与常见问题排查实录5.1 拖动残影、闪烁、边界错位到底是怎么回事这是所有遮罩方案里最容易被吐槽的问题。表现是地图拖动过程中遮罩层跟着地图一起移动但遮罩里的“洞口”位置明显滞后或者洞口边缘出现毛边、残影。很多人在这个问题上卡了好几天最后实在受不了用了一个笨办法在 drag 开始时把遮罩隐藏drag 结束后再画出来。这样做确实没有残影了但体验非常差遮罩会在拖动时一闪一闪地消失。我先解释残影产生的根本原因。OpenLayers 4 的 postcompose 事件在每次渲染帧结束后触发。地图拖动时渲染帧是连续产生的每一帧里地图底图已经移动到了新位置但你基于上一帧的像素坐标画的遮罩洞口还没更新到最新位置两者之间就会出现一个错位的帧。视觉上就是洞口拖了一个尾巴。最有效的解决方案不是避免在 postcompose 里绘制而是使用离屏 Canvas 加缓存。思路是只有当 highlightRegions 数组发生变化时才重新绘制遮罩地图平移、缩放过程中不再实时计算每个区域的像素位置而是直接把上一次绘制好的遮罩结果当图片贴上去。但这里有个细节平移缩放会改变区域在图片里的位置所以缓存画布必须和地图容器同尺寸并且在地图移动时需要同步更新缓存的位置或重新计算偏移。简单说纯用 Canvas 缓存并不能完美解决拖动残影只是把绘制频率降低了。真正让拖动时遮罩和地图“贴住”的是要在地图的 moveend 事件里把缓存清掉让下一次 postcompose 重绘一次。但这又会导致 moveend 之前的一两帧内遮罩是偏的。后来我翻阅了 OpenLayers 社区的各种讨论发现一个实用的折衷方案把遮罩作为独立的 ImageLayer 或者使用自定义 Layer而不是在 map 的 postcompose 里画。用独立图层OpenLayers 的渲染引擎会帮你在移动时对图层做同步处理不会再出现明显的错位。不过这个方案改造量有点大不是每个项目都愿意。对于中小型项目我推荐的做法是保持 postcompose 绘制但在地图的 movestart 事件里把遮罩区域的透明度降到 0.3 或更低让它在拖动时视觉上不那么突兀地图 moveend 之后再恢复原来的透明度并重绘。这样既不会闪也看不出错位。5.2 性能优化离屏 Canvas 缓存提升重绘效率如果高亮区域有几十个每次 postcompose 都重新计算几十个区域的经纬度转像素然后再画几十个路径性能开销其实不小。更重要的是很多区域在多次渲染之间根本没有变化重复计算完全没必要。优化方案是引入离屏 Canvas。第一次或者区域数据变化时在离屏 Canvas 上绘制一次遮罩和挖洞结果之后每次 postcompose 直接把离屏 Canvas 的绘制结果通过 drawImage 画到主画布上。var maskCanvas document.createElement(canvas); var maskContext maskCanvas.getContext(2d); var needRedrawMask true; function buildMaskCanvas() { var size map.getSize(); maskCanvas.width size[0]; maskCanvas.height size[1]; // 清空 maskContext.clearRect(0, 0, size[0], size[1]); // 画遮罩 maskContext.globalCompositeOperation source-over; maskContext.fillStyle rgba(0, 0, 0, 0.6); maskContext.fillRect(0, 0, size[0], size[1]); // 挖洞 maskContext.beginPath(); for (var i 0; i highlightRegions.length; i) { // ... 与之前相同的路径收集逻辑 ... } maskContext.globalCompositeOperation destination-out; maskContext.fillStyle #000; maskContext.fill(); needRedrawMask false; } function renderMaskWithCache(event) { if (needRedrawMask) { buildMaskCanvas(); } event.context.drawImage(maskCanvas, 0, 0); }然后所有动态更新操作比如 highlighRegions.push 或者 splice 之后都要把 needRedrawMask 置为 true让下一次渲染重新生成缓存。这个方案能把多区域场景下的渲染开销从 O(N) 降到 O(1)N 再大都不怕。5.3 常见问题速查表这里整理我在实际集中开发碰到的几个高频问题你照着排查可以省下大量时间。场景一遮罩画出来了但洞没有透明地图还是被盖住。原因通常是第二部没有设置 destination-out或者 fillStyle 设置成了透明色。解决办法是确认 globalCompositeOperation 在调用 fill 之前已经切到 destination-out并且 fillStyle 随便设一个不透明的颜色就行因为它只负责擦除。场景二洞是透明的但整张地图也跟着变透明了。这是因为在 fillRect 之前就开启了 destination-out等于先把全图擦了一遍。确保第一步是 source-over第二步才切换为 destination-out。场景三鼠标点击添加高亮区域后地图没有任何反应。原因是忘了 map.render()。前面讲过postcompose 是被动触发的改数据不会自动引起重绘必须手动调一下 map.render()。场景四地图初始化时遮罩没有显示但点击一次后才出现。这通常是因为初始化时 highlightRegions 为空数组postcompose 直接 return 了。后面填充数组后虽然调用了 map.render()但如果目标区域用的是 ol.proj.fromLonLat而地图投影不是默认的 EPSG:3857坐标转换可能会出偏差。检查一下地图的 projection 配置把所有经纬度统一用 ol.proj.fromLonLat 转不要手动去改。5.4 从遮罩效果到完整产品功能的思考遮罩功能真正落到生产环境还需要考虑一些细节。比如区域配置是从接口异步拉取的拉回来之前地图上不能出现忽闪的遮罩比如遮罩的透明度在不同显示屏上要有统一观感不要在深色背景和浅色背景的屏幕上差异过大再比如移动端触摸操作singleclick 在触摸设备上是否会出现误触发这都需要做额外处理。这里我给一个建议把遮罩配置和渲染逻辑彻底分离。配置管理单独抽一个模块负责从接口拉数据、做增删改查、维护 highlightRegions 数组渲染模块只负责读数组、判断 needRedrawMask、执行绘制。这样后面换地图库比如从 OpenLayers 4 升级到 OpenLayers 7或者换成 Leaflet渲染模块重写就行业务逻辑完全不用动。还有个小技巧如果你想给不同区域配不同的遮罩颜色在 buildMaskCanvas 里的 fillRegionPath 函数中可以针对 region.fillColor 分别调用 fillRect 或者 fill而不是统一在最后 fill。注意这样会从一次 fill 变成多次 fill性能会略降但视觉表现力会强很多。我之前做过一个案例核心保护区用 0.7 透明度缓冲协调区用 0.3 透明度整个地图一眼就能看出等级的层次感效果非常好。6. 写在最后一些个人体会这套多区域遮罩方案前后在不下五个项目里用过从最开始简单的“全市核酸采样点分布高亮”到后来“多个片区地块叠加分析”核心逻辑都没怎么变过都是数据驱动加离屏缓存这套组合拳。我个人感受最深的一点是遮罩效果看似很简单但真正的复杂度从来不在“怎么画”上而在“怎么画得稳、画得快、画得能和用户交互”。如果你只是写 demo第一篇的代码完全够用但如果你要做成一个长期维护的功能模块从一开始就做好区域配置的数据结构、缓存重建的时机、事件判定的逻辑分离后面会省非常多的事情。另外在我自己的电脑上实测如果区域数量在 5 个以内用不用缓存方案性能差异并不明显但一旦超过 15 个并且同时有鼠标悬浮动态判断缓存方案带来的帧率提升立刻就能感知到。尤其是在低端笔记本上用 win10 跑 Chrome 测试时差别特别明显。建议你写完基础版以后直接把数据加到 50 个区域压测一下再决定要不要上离屏缓存。