ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

OpenLayers 4进阶:Canvas遮罩、圆角挖空与事件穿透实战

OpenLayers 4进阶:Canvas遮罩、圆角挖空与事件穿透实战 我写第一篇的时候只讲了利用Canvas绘制遮罩层、配合beforeRender事件实现暗色遮罩的基本思路。当时评论区就有朋友问能不能做圆角遮罩能不能在遮罩区域内正常触发地图点击能不能动态改变遮罩范围这些问题我在实际项目里也都踩过一遍所以这篇接着写把进阶版本的实现路径和排查思路完整捋一遍权当是对第一篇文章的补充和填坑。1. 为什么还要写第二篇基础版遮罩解决不了的问题先回顾一下第一篇文章里的基础实现通过ol.layer.Image加一个canvasFunction每次地图渲染前在Canvas上先画一层半透明黑色然后用clearRect挖出高亮区域。这个思路本身没问题也是OpenLayers 4框架下比较干净的遮罩做法。但它只解决了“画出来”的问题离“产品可用”还有一段距离。我在实际项目里遇到的第一个坎就是遮罩区域和地图交互是冲突的。Canvas覆盖层是独立于地图feature图层之上的它天然拦截鼠标事件。也就是说就算你把某个区域挖空了想让用户点击这个区域内的POI点或者地块要素地图根本收不到点击事件因为事件命中的是那个透明的Canvas层。这在地图上做“高亮选中地块”这种功能时特别致命。第二个坎是遮罩区域的外观限制。基础版用的是clearRect矩形挖空但业务方经常要求高亮区是椭圆、圆角矩形甚至是不规则多边形。Canvas的clearRect只支持矩形想扣出不规则形状必须换用globalCompositeOperation destination-over或evenodd填充规则这类方案处理起来要小心。第三个坎是动态更新遮罩范围。如果遮罩范围是一个根据用户操作实时变化的圆比如以鼠标为中心画一个高亮圆那每次地图渲染都得动态计算并重绘。如果直接在canvasFunction里读取外部变量必须处理好图层刷新触发的时机否则会面临“地图平移了但遮罩没跟着动”的尴尬。这篇文章我按照实际开发顺序来讲先补足基础原理中的关键盲点再讲圆角遮罩和事件穿透的实现最后说动态遮罩与地图联动、性能优化、多实例避坑这些我在生产环境里反复调过的点。2. 遮罩绘制的技术原理再挖掘2.1 三种可选实现路径的对比OpenLayers 4里做遮罩效果不止“Canvas Layer canvasFunction”一种方案。我梳理过三种主流路径各有利弊你在动手前可以对照自己的业务场景选一下。方案实现方式优点缺点A. Canvas Layer重绘new ol.layer.Image({canvasFunction})与渲染引擎解耦纯Canvas操作灵活性高需要自行管理图层刷新和重绘时机方案B. Custom Overlay覆盖DOM层在ol.Overlay里放一个半透明div遮罩区域用CSSmask或clip-path实现数据结构简单DOM操作直观Overlay跟随地图定位缩放平移时要同步更新坐标clip-path对边框样式支持有限方案C. 矢量图层 Feature样式用Polygon feature表示遮罩区域样式中填充半透明色和地图原生的点击交互兼容遮罩之外的区域需要额外画一个覆盖全图的“底图”多边形涉及复杂多边形挖洞洞中洞计算数据组织麻烦我实测下来的结论是方案A最适合大部分WebGIS遮罩场景。方案B在处理缩放动画时会有肉眼可见的“迟滞感”方案C在做不规则高亮区时多边形布尔运算的成本太高。2.2 方案A执行细节canvasFunction的参数问题方案A的核心API是ol.layer.Image的canvasFunction它接收extent、resolution、pixelRatio、size、projection五个参数大概长这样const canvasLayer new ol.layer.Image({ extent, canvasFunction: function(extent, resolution, pixelRatio, size, projection) { const canvas document.createElement(canvas); canvas.width size[0] * pixelRatio; canvas.height size[1] * pixelRatio; const ctx canvas.getContext(2d); ctx.scale(pixelRatio, pixelRatio); // 绘制遮罩 return canvas; } });这里有个非常容易踩的坑size不一定是整个地图画布的尺寸。OpenLayers 4会根据extent参数截取需要渲染的可见范围所以size是“当前帧需要渲染的像素尺寸”。如果直接把遮罩画成满幅黑色矩形地图旋转或缩放到了边界时遮罩会出现“缺边”现象。正确的做法是根据extent计算遮罩区域的屏幕坐标而不是简单地把整张Canvas涂满。2.3 把经纬度坐标换算成Canvas像素坐标在canvasFunction里你拿到的是extent和resolutionCanvas坐标系的坐标原点在左上角而地图投影坐标原点在左下角。换算公式如下// 假设遮罩中心的经纬度坐标为 centerLonLat const center ol.proj.fromLonLat(centerLonLat); const originX extent[0]; const originY extent[2]; // 注意这里用右上角Y不是左下角 // 屏幕像素坐标 const screenX (center[0] - extent[0]) / resolution; const screenY (extent[3] - center[1]) / resolution; // 考虑到pixelRatio实际Canvas像素需要乘上pixelRatio const deviceScreenX screenX * pixelRatio; const deviceScreenY screenY * pixelRatio;第一篇文章中没有明确强调过这个细节但我后来发现很多读者复现失败恰恰是这一步出了问题——错把ol.proj.fromLonLat的结果直接当成屏幕坐标用了。3. 圆角矩形遮罩的实现与两级抗锯齿处理3.1 为什么不能用clearRect直接实现圆角clearRect的特点是把矩形内部的所有像素“擦除”为透明它无法描述带圆角的区域。要实现圆角矩形遮罩需要在Canvas中构造一条带圆弧的路径然后用globalCompositeOperation destination-out来“挖空”路径内部的像素。这里要注意一个Canvas的底层原理destination-out的语义是“目标图像中与源图像重叠的区域变为透明”。也就是说先画完整遮罩目标图像再绘制一个圆角矩形路径源图像两者重叠的区域会被擦除从而得到圆角挖空效果。3.2 核心代码圆角矩形路径封装OpenLayers的Canvas环境中没有原生roundRect方法这个方法在现代浏览器里存在但在老版本兼容性和OpenLayers 4的渲染上下文中不建议依赖所以我们需要手写一个圆角路径函数。function roundRectPath(ctx, x, y, width, height, radius) { const r Math.min(radius, width / 2, height / 2); ctx.beginPath(); ctx.moveTo(x r, y); ctx.lineTo(x width - r, y); ctx.quadraticCurveTo(x width, y, x width, y r); ctx.lineTo(x width, y height - r); ctx.quadraticCurveTo(x width, y height, x width - r, y height); ctx.lineTo(x r, y height); ctx.quadraticCurveTo(x, y height, x, y height - r); ctx.lineTo(x, y r); ctx.quadraticCurveTo(x, y, x r, y); ctx.closePath(); }3.3 绘制流程和抗锯齿处理随后的绘制顺序如下// 1. 先绘制全图半透明遮罩 ctx.fillStyle rgba(0, 0, 0, 0.7); ctx.fillRect(0, 0, canvasWidth, canvasHeight); // 2. 通过 destination-out 挖出圆角矩形区域 ctx.globalCompositeOperation destination-out; roundRectPath(ctx, x, y, width, height, radius); ctx.fill(); // 重置合成模式 ctx.globalCompositeOperation source-over;但直接这样做边缘会明显有“锯齿”尤其在视网膜屏幕上更明显。这是因为destination-out直接把像素透明度置0不会做边缘过渡。解决办法是先离屏Canvas绘制高分辨率的遮罩再通过drawImage把离屏Canvas缩放到目标尺寸。// 离屏Canvas两倍尺寸绘制避免边缘锯齿 const offCanvas document.createElement(canvas); offCanvas.width canvasWidth * 2; offCanvas.height canvasHeight * 2; const offCtx offCanvas.getContext(2d); offCtx.fillStyle rgba(0, 0, 0, 0.7); offCtx.fillRect(0, 0, offCanvas.width, offCanvas.height); offCtx.globalCompositeOperation destination-out; roundRectPath(offCtx, x * 2, y * 2, width * 2, height * 2, radius * 2); offCtx.fill(); // 最后缩放回原尺寸 ctx.drawImage(offCanvas, 0, 0, offCanvas.width, offCanvas.height, 0, 0, canvasWidth, canvasHeight);这也是我在多次测试中找到的“最佳性价比”方案。两级Canvas处理可以显著平滑圆角边缘实测在2倍像素比设备上肉眼几乎看不出锯齿。3.4 给高亮区加轮廓线遮罩的另外一个需求是挖出的高亮区域要有一圈描边通常是白色或淡黄色用于视觉强调。描边不能直接画在挖空区域上否则会破坏遮罩的透明效果。我采用的技巧是用globalCompositeOperation source-over在挖空之后重新绘制圆角路径的描边。// 挖空后画描边 ctx.beginPath(); roundRectPath(ctx, x, y, width, height, radius); ctx.strokeStyle rgba(255, 255, 255, 0.9); ctx.lineWidth 2; ctx.stroke();这样描边只出现在高亮区的边缘遮罩其余部分保持不变。这里还可以通过调整lineWidth和strokeStyle让高亮区更显眼比如业务上常要求的“红色虚线框选高亮区域”。4. 让遮罩和地图交互共存事件穿透与命中区域判断4.1 问题本质Canvas Image Layer是地图的一个渲染层它不属于ol.interaction体系也不会自动把事件委托给底下的feature。当用户点击遮罩层时地图容器收到的是作用在Canvas上的事件但OpenLayers的命中检测事实上没有把该Canvas作为检测对象。在OpenLayers 4里有个相对省力的实现方式不要直接用Canvas Layer而是用ol.layer.Vector渲染遮罩区域再将遮罩区域内的feature标记为可选中。然而如果被高亮的区域本身是栅格图层或者需要和地图交互的是整个图层而非单个矢量feature矢量方案又不好使。4.2 最常用的方案在遮罩层之上叠加透明交互层我最终采用的通用方案是在遮罩层之上再添加一个“透明交互层”该层不承载任何可见内容但负责监听地图点击事件判断点击坐标是否命中遮罩范围。这个方案的底层逻辑是遮罩层拦截事件的问题无法通过“优先把事件传给下层”解决——Canvas的像素透明区在事件系统中仍然是“可命中的平面”。所以正确的思路是“分两层考虑”一层是遮罩层纯展示一层是透明的交互层只做事件命中逻辑。交互层的style设为透明填充OpenLayers会为其生成一个不可见但可命中的区域。// 交互层不可见但可命中 const interactionLayer new ol.layer.Vector({ source: new ol.source.Vector({ features: [highlightFeature] }), style: new ol.style.Style({ fill: new ol.style.Fill({ color: rgba(255, 255, 255, 0) }), stroke: new ol.style.Stroke({ color: rgba(255, 255, 255, 0) }) }) }); map.on(click, function(evt) { const feature map.forEachFeatureAtPixel(evt.pixel, function(f) { return f; }, { layerFilter: function(layer) { return layer interactionLayer; } }); if (feature) { // 点击发生在遮罩区域内部 handleInsideClick(evt); } else { // 点击发生在遮罩区域外部 handleOutsideClick(evt); } });这种方式干净利落而且不依赖任何浏览器私有API。有个小缺点是透明fill的矢量图层仍然会被map.forEachFeatureAtPixel命中所以在业务逻辑里需要额外判断一下feature的图层来源避免误判。4.3 纯数学方式判断点与遮罩区域的位置关系如果不方便加交互层也可以在map.on(click)事件里用纯数学方法判断我自己在性能敏感的场景中使用过这个方式。如果遮罩是圆形或矩形判断点是否在遮罩内很简单map.on(click, function(evt) { const centerCoord ol.proj.fromLonLat([116.4, 39.9]); const radiusMeters 1000; // 半径1公里 const distance ol.extent.getDistance(evt.coordinate, centerCoord); // 注意ol.extent.getDistance计算的是欧氏距离不是球面距离 // 在Web Mercator投影下误差可以接受但高纬度需谨慎 const isInside distance radiusMeters; });但对于不规则多边形遮罩就得用射线法Ray Casting判断点是否在多边形内部。射线法的原理是从该点向任意方向发出一条射线统计射线与多边形边的交点个数奇数个交点则在多边形内部偶数个则在外部。OpenLayers 4没有直接暴露这个API需要自己写一个几十行的函数。我在项目里基于Turf.js做了更稳妥的实现import turf from turf/turf; map.on(click, function(evt) { const point turf.point(evt.coordinate); const polygon turf.polygon([highlightPolygonCoords]); const isInside turf.booleanPointInPolygon(point, polygon); if (isInside) { // 高亮区域内部逻辑 } });Turf.js体积不小但可以按需引入turf/boolean-point-in-polygon单模块几KB而已性能足够。4.4 事件穿透后地图自身的平移缩放真正的大坑在于即使判断出了“点击在遮罩内部”用户拖动地图时仍希望地图能正常平移缩放。如果遮罩层完全覆盖了地图平移事件会被Canvas吞掉。我的处理方式是在地图容器上监听pointerdown和pointermove在地图交互层确认遮罩区域的坐标和像素范围前先检测按下时的坐标位置map.getViewport().addEventListener(pointerdown, function(evt) { const pixel map.getEventPixel(evt); const hit map.forEachFeatureAtPixel(pixel, function(feature) { return feature; }); if (!hit) { // 遮罩外部——地图原有交互可以正常触发 return; } // 遮罩内部——阻止地图平移的原生交互 map.getInteractions().forEach(function(interaction) { if (interaction instanceof ol.interaction.DragPan) { interaction.setActive(false); } }); }); map.getViewport().addEventListener(pointerup, function() { map.getInteractions().forEach(function(interaction) { if (interaction instanceof ol.interaction.DragPan) { interaction.setActive(true); } }); });这个方案的代价是每次点击时都要临时禁用DragPan交互。实际体验中短暂禁用会在快速拖拽时有卡顿感但相比“完全无法操作地图”还是好很多。如果是更复杂的产品建议在遮罩区域内部直接允许地图平移只拦截点击事件——这更符合用户习惯。5. 动态遮罩让高亮区域跟随地图移动或用户操作5.1 动态遮罩的现实场景静态遮罩应付“固定高亮某块区域”没问题但很多真实需求的遮罩范围是动态变化的以用户点击位置为中心高亮周围1公里范围鼠标移动时实时显示当前位置周边1公里范围搜索某个地标后遮罩区域自动移动到该地标并高亮周围区域。这些场景有一个共同的技术诉求每次遮罩范围变化后要让遮罩Canvas重新渲染而不是让用户看到旧遮罩残留。5.2 正确触发遮罩图层重绘OpenLayers 4的ol.layer.Image有一个特别容易忽略的方法layer.changed()。调用这个方法可以手动触发图层的重新渲染。因为canvasFunction本质上是一个渲染函数只有图层判定自己“脏了”时才会重新执行。function updateMask(centerLonLat) { maskCenter ol.proj.fromLonLat(centerLonLat); canvasLayer.changed(); // 触发重绘 }同时在canvasFunction中读取外部的中心坐标let maskCenter null; // 外部变量由业务逻辑更新 const canvasLayer new ol.layer.Image({ canvasFunction: function(extent, resolution, pixelRatio, size, projection) { const canvas document.createElement(canvas); canvas.width size[0] * pixelRatio; canvas.height size[1] * pixelRatio; const ctx canvas.getContext(2d); ctx.scale(pixelRatio, pixelRatio); ctx.fillStyle rgba(0, 0, 0, 0.7); ctx.fillRect(0, 0, size[0], size[1]); if (!maskCenter) return canvas; // 计算遮罩区域屏幕坐标 const screenX (maskCenter[0] - extent[0]) / resolution; const screenY (extent[3] - maskCenter[1]) / resolution; ctx.globalCompositeOperation destination-out; ctx.beginPath(); ctx.arc(screenX, screenY, 50, 0, Math.PI * 2); ctx.fill(); ctx.globalCompositeOperation source-over; return canvas; } });5.3 平移结束后的遮罩拖影问题动态遮罩最容易出现的现象是地图平移后遮罩没有马上跟着变画面出现“拖影带”。原因在于ol.layer.Image的canvasFunction不是每帧都重新生成的它只在图层渲染管线认为需要刷新时执行。我在OpenLayers 4的moveend事件中强制刷新可以解决大部分问题map.on(moveend, function() { canvasLayer.changed(); });但要注意moveend只在平移结束后触发。如果需要在动画过程中实时更新遮罩比如平移过程中始终显示中心点周围区域还得监听movestart或者postrender事件。一种代价更低的方案是遮罩坐标也存入一个外部变量然后在postrender的时候重新检查遮罩中心是否需要重绘。map.on(postrender, function() { canvasLayer.changed(); });这个方案虽然简单粗暴但会让Canvas在每次地图帧渲染后都重建一次性能压力较大。生产上我更推荐的做法是只在moveend或者遮罩范围真正变化时才changed()配合一个debounce函数效果和性能都更可控。5.4 动态遮罩与缩放级别联动另外一个常见的坑是动态遮罩的“可视半径”依赖当前缩放级别。比如高亮“当前视角下屏幕中心周围500像素范围”但500像素在地图放大到1:5000时可能是50米缩小到1:50000时可能就是500米。这种情况下如果只是把圆形半径设成固定像素视觉效果会飘。我的处理办法是结合当前分辨率计算实际半径function getCircleRadiusPixels(resolution, radiusMeters) { return radiusMeters / resolution; }然后在canvasFunction里使用这一动态半径这样无论如何缩放遮罩始终对应真实世界中的固定半径范围。这个逻辑虽然简单但要记得在moveend或change:resolution事件中触发重绘。6. OpenLayers 4实战中的额外坑与性能调优6.1 高DPIRetina屏的像素比适配在手机上测试时很多人会发现遮罩边缘有模糊或者错位。这是因为canvasFunction返回的Canvas尺寸和地图实际显示的CSS尺寸不一致。必须在创建Canvas时显式乘以pixelRatiocanvas.width size[0] * pixelRatio; canvas.height size[1] * pixelRatio; ctx.scale(pixelRatio, pixelRatio); // 缩放后所有绘制操作都以CSS像素为单位注意size是OpenLayers传给canvasFunction的参数它已经是CSS像素单位不是设备像素单位。漏掉ctx.scale的后果是画出来的圆边缘发虚、坐标错位且在Retina屏上高亮区域明显偏小。6.2 多地图实例下遮罩变量冲突在单页应用里如果同时实例化了多个地图每个地图都有自己的canvasFunction但假如把maskCenter定义成了模块级全局变量就会导致两个地图共用同一个遮罩中心——A地图移动遮罩位置B地图跟着变。一定要保证遮罩状态信息绑定在图层实例或地图实例上// 推荐用闭包保存状态 function createMaskLayer() { let maskCenter null; return new ol.layer.Image({ canvasFunction: function(extent, resolution, pixelRatio, size, projection) { // 在闭包中访问 maskCenter }, updateMask: function(center) { maskCenter center; this.changed(); } }); }通过闭包存储状态每个地图实例创建的遮罩层互不干扰这是我调试多地图联动功能时总结出的最实用的模式。6.3 离屏Canvas缓存与遮罩性能如果一个遮罩覆盖全图同时被频繁刷新尤其在地图旋转、连续平移的动画过程中每次canvasFunction里都重新画整图遮罩的成本较高。性能敏感的场景下可以做一层缓存只把遮罩本身黑色覆盖层 高亮区域绘制到一张离屏Canvas上地图每次需要遮罩时只需drawImage贴图而不必重新走路径绘制。function createMaskCanvas(size, pixelRatio, maskCenter) { // 离屏Canvas缓存 let cacheCanvas null; return function(extent, resolution, pixelRatio, size, projection) { if (!cacheCanvas) { cacheCanvas document.createElement(canvas); cacheCanvas.width size[0] * pixelRatio; cacheCanvas.height size[1] * pixelRatio; // 绘制一次遮罩 } return cacheCanvas; }; }这样当遮罩范围不变时canvasFunction直接返回缓存的Canvas不执行任何路径计算和绘制操作性能提高非常明显。不过要记住只要遮罩范围变化需要主动重置缓存Canvas的cacheCanvas null或重建cacheCanvas。6.4 避坑清单常见的OpenLayers 4遮罩误操作我在带团队做项目时总结过一张常见问题对照表这里直接放出来供你自查现象根因处理方案遮罩中心位置漂移未将经纬度转为投影坐标直接用了经纬度计算屏幕坐标用ol.proj.fromLonLat()转成投影坐标后再参与extent计算遮罩边缘锯齿严重未做离屏Canvas的超采样抗锯齿用两倍尺寸绘制后缩放或使用ctx.imageSmoothingEnabled遮罩拖动跟不上地图未监听moveend触发changed()map.on(moveend, () canvasLayer.changed());遮罩挖空区域有黑边destination-out绘制时Canvas合成模式未重置每次fill()后立即恢复globalCompositeOperation点击遮罩内部无法选中featureCanvas层拦截了鼠标事件通过透明交互层或forEachFeatureAtPixel做命中判断遮罩区域在旋转地图后错位未考虑地图旋转角仅用矩形坐标计算先获取当前视图旋转角度使用ctx.rotate()并重新计算坐标多地图实例遮罩串数据遮罩状态定义成了全局变量改用闭包或地图实例属性存储状态6.5 旋转视角下遮罩的处理如果你的地图允许旋转比如在某些室内地图场景那么遮罩的圆角矩形和中心点的绘制就需要额外处理。因为OpenLayers的Canvas坐标系是基于视口viewport的地图旋转后原来基于投影坐标计算的矩形宽高在屏幕空间里会倾斜。此时有两种处理方式方式一让遮罩也跟随地图旋转——在canvasFunction里先拿到视图的旋转角度const view map.getView(); const rotation view.getRotation(); if (rotation) { ctx.translate(canvas.width / 2, canvas.height / 2); ctx.rotate(rotation); ctx.translate(-canvas.width / 2, -canvas.height / 2); }方式二让遮罩始终固定为正立矩形——在绘制前将矩形四个角的投影坐标先转换成屏幕坐标然后根据四个角坐标画出四边形。这种方式不依赖旋转角度适配性更强。大多数业务场景下用户希望遮罩的圆角矩形是“贴在地图上”的也就是跟随地图旋转方式一因为视觉上更有“贴地感”。但如果是做简单的“当前区域高亮”往往固定正立的矩形更符合直觉。这个取舍要根据产品形态来定没有绝对正确。6.6 自测方法在真实数据源上跑通遮罩代码写完之后强烈建议用真实瓦片地图源做一次完整自测而不是局限在空白的ol.source.Vector上。我在测试中发现当加载的是离线瓦片或使用ol.source.XYZ的第三方瓦片时某些图层的renderMode设置会影响canvasFunction的执行时机偶尔会出现“首帧遮罩不显示”的问题。首帧遮罩不显示的处理方式很简单在地图初始化后立即手动执行一次canvasLayer.changed()同时在地图的rendercomplete事件里再兜底刷新一次。这样能够保证即使在瓦片加载异步渲染的情况下遮罩也不会出现空白帧。7. 项目实践中的经验与总结手写遮罩图层这个功能看起来简单真正落地的时候牵扯到的Canvas合成机制、地图事件系统、渲染生命周期其实比想象中要多。下面是我在多个地图项目中反复验证过的一些“复盘级”经验。第一遮罩图层一定要单独封装成一个类或函数不要在地图初始化回调里层层嵌套闭包。我在早期项目中直接把canvasFunction写进了地图初始化代码块结果后来需要做“遮罩区域动态修改”“遮罩与图层显隐联动”时代码已经变成一团乱麻。正确做法是把遮罩层封装成一个有update()/show()/hide()方法的独立模块这样地图代码和业务逻辑保持解耦。第二Canvas遮罩测试时务必用真实的数据源和真实的分辨率进行验证。很多开发者习惯在低分辨率屏幕的桌面浏览器调试代码看起来很完美一放到高DPI笔记本或手机浏览器上测试遮罩边缘立马露出马脚。我的经验是在开发阶段就把浏览器的Device Toolbar打开实测Retina屏和普通屏两种模式。第三遮罩和交互永远都要走“双重保险”。光靠canvasFunction绘制遮罩、不处理事件穿透是没办法满足真实业务对“遮罩内要素可点击”的需求的。我总结的最小可用方案是遮罩层画视觉效果地图的forEachFeatureAtPixel配合透明交互层做命中判定二者结合才能做到“看得见、点得着”。第四性能优化的优先级。动态遮罩的刷新频率应该严格受控不要简单地在postrender里无条件changed()。生产环境我推荐的做法是在地图movestart时检测遮罩中心位置是否变化如果变化则触发重绘如果不变则直接沿用缓存Canvas。这个优化虽然代码上多几行判断但能明显减少高分辨率下的掉帧率。第五不要忽略浏览器兼容性。OpenLayers 4本身面向的是老版浏览器生态比如IE11所以Canvas代码里尽量不要使用roundRect、CanvasRenderingContext2D.filter等新特性。用兼容性好一点的quadraticCurveTo替代arcTo的某些能力以及在ctx.scale(pixelRatio, pixelRatio)之后没事别去随意重置变换矩阵这些小的编码习惯能帮你在老环境中少踩很多坑。最后分享一个我在实际项目里“捡到”的小技巧如果遮罩区域需要显示文字提示比如“已选区域”或“点击此区域查看详情”不要直接绘制在遮罩Canvas上因为地图一旦重绘文字就会闪动。更好的方式是在遮罩层之上再放一个ol.Overlay定位到遮罩中心这样文字始终清晰也不会跟随地图频繁重建。这个小细节最初是我在做地块选中功能时无意中调整出来的效果比直接在Canvas上写字好得多后来一直沿用到了现在。遮罩功能本身不是OpenLayers框架的内置能力但只要理解了Canvas合成、地图渲染管线和事件机制这三者的关系实现一个稳定、流畅、可交互的遮罩效果并不复杂。希望这篇“第二篇”能帮你把上一篇的基础代码真正用起来少走一些我在生产环境里走弯路的坑。
返回列表