ARTICLE DETAIL

资讯详情

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

OpenLayers4地图遮罩实战:canvas绘制与矢量挖洞性能优化

OpenLayers4地图遮罩实战:canvas绘制与矢量挖洞性能优化 做GIS和前端地图开发的朋友对“地图遮罩”这个词应该不陌生核心目的就是把地图上非目标区域压暗让用户的视线集中到某个点、某个范围上产品里常见的聚光灯、暗角高亮、区域雷达圈基本都是这个思路。上一篇我用OpenLayers4后面统一叫OL4实现了最基础的矢量要素遮罩这篇接着往下写在OL4里把遮罩做得更灵活、更工程化重点放在canvas的postcompose绘制、带内环多边形的矢量挖洞、多区域遮罩组合、动画过渡和性能优化上。这篇内容适合已经跑通过OL4基础地图、想在地图上做区域高亮和交互效果的前端同学。不管你最后选哪条实现路线只要把下面几个核心机制吃透写出来的遮罩效果才不会一拖动地图就“露馅”。完整代码我会贴出来版本基于OL44.6.x其他4.x小版本通用。1. 先别急着写代码把遮罩的原理和路线选型搞清楚1.1 遮罩解决的问题和应用场景地图遮罩在不同业务里的叫法不太一样GIS平台里叫“掩膜”大屏项目里叫“聚光灯”安防项目里叫“重点区域高亮”但本质上都是同一件事让地图大部分区域变暗只保留若干区域保持原始亮度从而形成视觉聚焦。这个交互的前提是——地图本身不能只在一个静态屏幕里看用户会拖动、缩放遮罩区域必须始终咬合在地理坐标上而不是固定在屏幕某个像素点上。我整理过几个常见的落地场景数据中心展示机房的覆盖范围、园区系统高亮某栋楼的服务半径、疫情或灾害场景里标出风险区域、商业选址系统圈出候选商圈。这类需求通常还要配合点击交互、动画过渡比如点一个列表项地图上的高亮圆就平滑飞到对应位置并调整半径。所以遮罩不只是“画个半透明黑罩”它还承担着状态切换和视觉引导的任务实现方案必须兼顾功能扩展和性能。1.2 两条实现路线矢量挖洞与画布合成目前OL4里实现遮罩效果主流的路线有两条。第一条是“矢量几何挖洞”路线。思路是用一个覆盖全球大范围的多边形作为外环然后在内环位置放置一个圆形或者其他任意形状作为空洞形成一个带洞的多边形要素放进矢量图层用半透明填充样式渲染。地图亮着的区域就是那个洞暗掉的区域就是多边形填充区。这条路线的好处是它是正儿八经的地图要素天然支持命中检测、样式动态切换、与图层控制联动逻辑直观代码量少。第二条是“canvas画布合成”路线。思路是监听地图渲染管线里的postcompose事件在地图所有图层绘制完成之后拿到渲染用的CanvasContext手动在整个画布上铺一层半透明颜色再利用canvas的合成模式globalCompositeOperation把高亮区域“挖掉”。这条路线的好处是完全不依赖矢量要素可以做到像素级精细控制画渐变、加动画、做多个任意形状空洞都非常方便而且不受OL要素几何精度的限制。两条路线的本质区别在于矢量路线是“先有几何再被渲染器画出来”canvas路线是“渲染完成后在像素层面做后处理”。理解到这一层你后面遇到的绝大多数问题都能自己定位。1.3 选型判断标准我做过几个项目之后总结出一个相对实用的选型判断标准如果遮罩区域是静态的、数量不多、主要靠图层显隐控制优先选矢量挖洞方案。它更符合OL的数据模型后续如果要接点击事件、加弹窗、联动高亮改造成本最低。如果遮罩需要频繁变化比如跟随鼠标移动、动画缩放、多个区域动态切换优先选canvas postcompose方案。矢量方案在频繁修改几何时既要重建要素又要触发图层刷新性能瓶颈很容易出现。如果两种需求都有可以以canvas方案为主把静态区域缓存成离屏canvas减少每帧重复绘制成本。这个优化技巧后面我会单独讲。2. 搭建基础工程与地图画布2.1 初始化一个干净的OL4地图不管选哪条路线底层地图基础是一样的。先搭一个最简工程这里我直接通过CDN方式引入OL4不引框架方便你直接复制到HTML里跑。!DOCTYPE html html head meta charsetutf-8 / titleOpenLayers4 地图遮罩二/title link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/openlayers4.6.5/ol.css / style html, body, #map { margin: 0; width: 100%; height: 100%; } /style /head body div idmap/div script srchttps://cdn.jsdelivr.net/npm/openlayers4.6.5/dist/ol.js/script script 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([116.397428, 39.90923]), zoom: 10 }) }); /script /body /html这段代码本身没什么可说的重点在view的center坐标。OL4默认投影是EPSG:3857Web墨卡托单位是米所以如果用经纬度坐标必须通过ol.proj.fromLonLat做转换。这个习惯如果没养成后面所有坐标相关的遮罩位置都会偏到天上去这是遮罩项目里最高频的翻车点之一后面我会专门列一个排查清单。2.2 把遮罩参数做成可配置对象我建议从一开始就不要把遮罩的中心、半径、透明度、颜色这些值写死在绘制函数里而是抽成一个全局配置对象这样后续做动画、做交互、做多状态切换时只需要改配置然后触发重绘。var maskConfig { center: ol.proj.fromLonLat([116.397428, 39.90923]), radius: 1200, // 半径单位米投影单位 color: rgba(0, 0, 0, 0.65), strokeColor: #ffffff, strokeWidth: 3 };radius这里我特意用“米”而不是“像素”原因是遮罩要跟着地理坐标走当地图缩放时同样1200米在屏幕上占的像素数会变化这才能体现出“该区域实际覆盖范围”的语义。如果只是想要一个固定屏幕尺寸的圆那也可以直接把单位改成像素但你要知道这两者的区别不要混用。2.3 换算幕后的像素与坐标关系遮罩绘制最难理解的一环就是地理坐标、投影坐标、屏幕像素这三者的关系。OL4里有一个非常核心的转换函数map.getPixelFromCoordinate(coordinate)它能把地图投影坐标转成当前屏幕上的像素坐标返回值是[x, y]数组原点在画布左上角。你只要在绘制遮罩时拿到这个像素坐标再用canvas的arc画圆就行。但是有一个关键点必须注意postcompose事件触发时当前帧的视图状态已经更新完成这时候调用getPixelFromCoordinate得到的像素坐标才是准确的。如果你在其他时机调用比如地图还在动画过渡中、或者resize之后还没更新画布就可能拿到错位的坐标。我见过很多新手把坐标转换放在事件回调里算好存起来然后绘制时直接用结果地图一拖动就发现遮罩在“遛弯”原因就在这里——你存的是旧坐标不是新帧的坐标。3. 画布遮罩实操postcompose绘制高亮区域3.1 用globalCompositeOperation画圆形挖洞canvas路线最核心的一段代码就是在地图的postcompose事件里做事后绘制。OL4的地图对象支持监听postcompose事件对象里带一个context属性这就是地图渲染画布的2D上下文。监听它在图层合成完毕后再画我们的遮罩。绘制思路分两步先用fillRect把整个画布铺满半透明色再用globalCompositeOperation destination-out在高亮圆形的区域做“擦除”把那一块恢复成原始地图。代码如下map.on(postcompose, function (evt) { var ctx evt.context; var size map.getSize(); if (!size) return; // 遮罩中心地理坐标 - 像素坐标 var pixel map.getPixelFromCoordinate(maskConfig.center); // 遮罩半径将指定的地理半径米换算成像素半径 var eastPoint [maskConfig.center[0] maskConfig.radius, maskConfig.center[1]]; var eastPixel map.getPixelFromCoordinate(eastPoint); var radius Math.sqrt( Math.pow(eastPixel[0] - pixel[0], 2) Math.pow(eastPixel[1] - pixel[1], 2) ); ctx.save(); // 第一步全图覆盖半透明黑 ctx.fillStyle maskConfig.color; ctx.fillRect(0, 0, size[0], size[1]); // 第二步挖掉高亮区域 ctx.globalCompositeOperation destination-out; ctx.beginPath(); ctx.arc(pixel[0], pixel[1], radius, 0, Math.PI * 2); ctx.fill(); // 第三步给高亮圆加一圈描边 ctx.globalCompositeOperation source-over; ctx.beginPath(); ctx.lineWidth maskConfig.strokeWidth; ctx.strokeStyle maskConfig.strokeColor; ctx.arc(pixel[0], pixel[1], radius, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); });这里有三处容易出问题的地方。第一map.getSize()在地图还没初始化完成时可能返回空要加防御判断。第二radius的计算用了“中心点向东平移radius米后再转像素”的方式这在Web墨卡托投影下足够精确但如果你的投影不是以米为单位的比如EPSG:4326就需要先统一单位再算。第三save和restore一定要成对使用否则globalCompositeOperation的状态会污染后续的地图绘制。如果你不想用destination-out这种两步方式也可以一次性画出带洞的路径然后用evenodd填充规则ctx.beginPath(); ctx.rect(0, 0, size[0], size[1]); ctx.arc(pixel[0], pixel[1], radius, 0, Math.PI * 2); ctx.fillStyle maskConfig.color; ctx.fill(evenodd);evenodd规则会按照“射线穿越路径次数”判断内外天然支持挖洞不用关心路径绕向代码更简洁。不过evenodd是较新的canvas特性在老旧浏览器上有兼容性风险项目要求兼容老环境时还是用destination-out更稳。3.2 让遮罩紧贴地理坐标而不是屏幕像素上一节的代码里遮罩中心是通过map.getPixelFromCoordinate(maskConfig.center)实时换算的这句话是整个canvas方案不漂移的生命线。很多人会想当然地在地图初始化时算一次像素坐标然后一直用这个值去画结果地图一拖动遮罩就停留在原来的屏幕位置完全不跟地理走。原理其实很简单postcompose是每帧渲染完成后的回调地图每次移动、缩放、加载瓦片都会触发新一轮渲染。我们在回调里用最新的视图状态把地理坐标转成当前帧的像素坐标这一帧的像素位置就和当前视图完全对应。换句话说只要中心坐标是地理坐标、转换发生在回调内部遮罩就永远钉在地理位置上。如果你希望遮罩的半径也严格对应地理距离比如“周边两公里范围”一定要用3.1节里那种“同帧中心坐标换算半径”的方式。不要试图自己维护一个半径换算表因为不同纬度、不同缩放级别下单位的比例关系都在变化。我试过用一个固定的scale参数去乘结果在低纬度地区看起来还行一到高纬度地区就偏差得离谱因为Web墨卡托的纬向变形非常大。3.3 加一点细节渐变、半透明高亮基础遮罩做完后你会发现产品需求通常还要“好看一点”。遮罩区域内部加一个径向渐变高亮能让视觉中心更明显。做法是在挖洞之后用createRadialGradient在洞口画一层从透明到白或浅色的渐变。// 在挖洞之后洞内画一层弱化高亮 var gradient ctx.createRadialGradient( pixel[0], pixel[1], radius * 0.2, pixel[0], pixel[1], radius ); gradient.addColorStop(0, rgba(255, 255, 255, 0.25)); gradient.addColorStop(1, rgba(255, 255, 255, 0)); ctx.globalCompositeOperation source-over; ctx.fillStyle gradient; ctx.beginPath(); ctx.arc(pixel[0], pixel[1], radius, 0, Math.PI * 2); ctx.fill();注意这里渐变坐标用的是像素所以在回调里每帧重建渐变对象是正常的不用担心性能——canvas创建渐变对象的开销极小真正吃性能的是频繁绘制大区域这个后面讲优化时再展开。4. 矢量遮罩实操用带内环的多边形“抠洞”4.1 将圆形转为多边形并构造内环矢量路线的核心是构造一个“外环覆盖地图全局、内环是高亮区域”的多边形要素。OL4里圆形几何不能直接作为内环必须先通过ol.geom.Polygon.fromCircle把圆形转成多边形再取出它的第一条边环坐标作为多边形内环使用。这里有个必须避开的坑canvas绘制多边形的填充默认用的是非零环绕规则nonzero winding rule外环和内环的绕向必须相反空洞才能成立如果两个环绕向相同填充区域会覆盖掉空洞整个遮罩会变成一个实心色块。fromCircle生成的圆环绕向在不同版本里并不保证一致所以我写了一个确保内环绕向正确的辅助函数function ensureHoleRing(ring) { // 用鞋带公式计算有向面积 var area 0; for (var i 0, len ring.length - 1; i len; i) { var p1 ring[i]; var p2 ring[i 1]; area p1[0] * p2[1] - p2[0] * p1[1]; } // 内环需要有向面积为正逆时针否则反转 if (area 0) { ring.reverse(); } return ring; }然后构建带洞多边形加入矢量图层var maskLayer new ol.layer.Vector({ source: new ol.source.Vector() }); map.addLayer(maskLayer); function buildMaskFeature(center, radiusMeters) { var circle new ol.geom.Circle(center, radiusMeters); var circlePolygon ol.geom.Polygon.fromCircle(circle, 128); var innerRing ensureHoleRing(circlePolygon.getCoordinates()[0]); // 外环覆盖全球范围的矩形 var outerRing [ [-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90] ]; var geometry new ol.geom.Polygon([outerRing, innerRing]); return new ol.Feature({ geometry: geometry }); } var maskFeature buildMaskFeature( maskConfig.center, maskConfig.radius ); maskLayer.getSource().addFeature(maskFeature);这里fromCircle的第二个参数128是圆的逼近边数。默认是64但当地图放大到一定程度后64边形的高亮圆边缘会出现明显的“棱角感”改成128之后视觉上基本就是圆了。如果你要画的圆非常小边数太多反而增加顶点处理的负担64就够用如果半径大且对边缘要求高可以直接给256现代浏览器处理几万个顶点没什么压力。4.2 控制遮罩样式与鼠标交互矢量要素的好处是可以用样式函数做非常灵活的控制。比如让遮罩外圈的描边只在洞的边缘显示整体暗色填充保持不变。这个需求在样式里实现起来很直接maskFeature.setStyle(new ol.style.Style({ fill: new ol.style.Fill({ color: rgba(0, 0, 0, 0.65) }), stroke: new ol.style.Stroke({ color: #ffffff, width: 2 }) }));注意ol.style.Stroke会作用于多边形外环也作用于内环一般我们只需要洞的边缘有描边外环因为超出视野范围即使描边了也看不到所以可以直接这样用不用额外处理。矢量要素还可以直接挂事件。比如我们希望鼠标移动到遮罩区域时遮罩透明度降低一点给用户“这块是可以交互”的暗示就可以这样maskFeature.on(change, function () { // 当要素几何更新时可根据需要刷新样式 }); map.on(pointermove, function (evt) { var hit map.forEachFeatureAtPixel(evt.pixel, function (feature) { return feature maskFeature; }); if (hit) { map.getViewport().style.cursor pointer; } else { map.getViewport().style.cursor ; } });不过有一点要提醒遮罩多边形是一个覆盖全球的巨大几何体forEachFeatureAtPixel在命中检测时会遍历要素几何如果遮罩区域很大命中检测的耗时可能会比普通要素高。实测下来正常交互还好但如果你同时有几十个这样的巨型要素建议用canvas方案做交互检测或者把多边形外环缩小到当前视野范围。4.3 动态改变遮罩的位置和半径矢量方案要做动态变化本质是更新要素的几何。OL4里可以调用feature.setGeometry(newPolygon)整体替换也可以修改原几何的坐标后调用feature.changed()。后者的好处是不用重建对象但要注意坐标修改的细节。function updateMask(center, radiusMeters) { var circle new ol.geom.Circle(center, radiusMeters); var circlePolygon ol.geom.Polygon.fromCircle(circle, 128); var innerRing ensureHoleRing(circlePolygon.getCoordinates()[0]); var outerRing [ [-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90] ]; maskFeature.setGeometry(new ol.geom.Polygon([outerRing, innerRing])); } updateMask(map.getView().getCenter(), 2000); maskLayer.changed();调用setGeometry之后通常还需要手动调用一下maskLayer.changed()确保渲染管线知道这个图层需要重绘。我在实际项目里踩过这个坑输出了新的几何但图层没刷新屏幕上的遮罩纹丝不动排查了半天才发现是少了这个触发。实际上OL在要素geometry变化时会自动标记渲染需要更新但在某些OL4小版本里特别是要素已经加入图层之后再替换几何偶发不会自动触发手动changed()一次是最保险的。5. 进阶玩法多区域遮罩与动画过渡5.1 多个高亮区域做“聚光灯”效果业务上经常不止一个高亮区域比如同样一张城市地图上要同时高亮三个重点商圈。这件事在两条路线里实现方式不同我分别说一下。canvas路线最简单在同一帧的postcompose里把多个区域的路径都加到同一个path里然后用evenodd规则一次填充map.on(postcompose, function (evt) { var ctx evt.context; var size map.getSize(); if (!size) return; ctx.save(); ctx.fillStyle maskConfig.color; ctx.beginPath(); ctx.rect(0, 0, size[0], size[1]); regions.forEach(function (region) { var p map.getPixelFromCoordinate(region.center); var east [region.center[0] region.radius, region.center[1]]; var ep map.getPixelFromCoordinate(east); var r Math.sqrt(Math.pow(ep[0] - p[0], 2) Math.pow(ep[1] - p[1], 2)); ctx.moveTo(p[0] r, p[1]); ctx.arc(p[0], p[1], r, 0, Math.PI * 2); }); ctx.fill(evenodd); ctx.restore(); });矢量路线则是在同一个多边形几何里放多个内环这样整个遮罩仍然是一个要素方便整体控制var holes regions.map(function (region) { var poly ol.geom.Polygon.fromCircle( new ol.geom.Circle(region.center, region.radius), 128 ); return ensureHoleRing(poly.getCoordinates()[0]); }); var outerRing [ [-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90] ]; var multiHoleFeature new ol.Feature({ geometry: new ol.geom.Polygon([outerRing].concat(holes)) });两种方式都能实现多高亮区域但如果你想让每个高亮区域拥有不同的描边颜色canvas路线更好控制因为你可以对每个圆单独画描边矢量路线则需要对每个内环单独设置样式但OL4的样式中Stroke是作用于整个要素的做不到内环各自不同除非拆成多个要素。5.2 动画切换平滑过渡到目标区域遮罩动画是我认为canvas路线真正拉开差距的地方。矢量方案做动画需要逐帧替换几何绕向修正、顶点重建、图层刷新这些操作叠加起来在低端机上很容易掉帧。canvas方案只需要逐帧修改配置对象里的center和radius然后调用map.render()请求下一帧绘制逻辑完全不变动画就只是数据在变。function animateMaskTo(targetCenter, targetRadius, duration) { var startCenter maskConfig.center.slice(); var startRadius maskConfig.radius; var startTime Date.now(); function step() { var t Math.min(1, (Date.now() - startTime) / duration); // 缓动函数先慢后快再慢 var ease t 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t 2, 2) / 2; maskConfig.center [ startCenter[0] (targetCenter[0] - startCenter[0]) * ease, startCenter[1] (targetCenter[1] - startCenter[1]) * ease ]; maskConfig.radius startRadius (targetRadius - startRadius) * ease; map.render(); if (t 1) { requestAnimationFrame(step); } } step(); }使用方法也很直观比如点击列表中的某个园区让遮罩平滑飞到该园区并调整到指定半径animateMaskTo(ol.proj.fromLonLat([121.4737, 31.2304]), 800, 600);这里我用了requestAnimationFrame驱动每帧修改一次数据再触发一次map.render()。render()是异步请求渲染不会阻塞主线程所以动画过程中用户依然可以拖拽地图。实测下来在普通笔记本上跑这种动画帧率能稳定在50fps以上完全够用。5.3 地图平移缩放时遮罩的跟随处理这里要专门讲一个问题为什么canvas方案在地图平移、缩放时不需要额外监听事件因为OL4在每次视图变化时都会重新渲染所有帧postcompose每一帧都会触发我们在这个回调里读取的是当前帧的最新坐标所以遮罩天然跟随。这是canvas方案比矢量方案省心的地方——矢量方案如果要遮罩外环始终覆盖视野你还得监听moveend去更新外环几何。不过有一个例外如果你在postcompose里做了非常重的计算导致每一帧都超过16ms那么动画和拖拽都会卡顿。这时候的优化思路不是去减少map.render()的调用而是要优化postcompose回调本身。常见的手段是把不需要每帧重新计算的值缓存起来比如多个区域的像素坐标如果区域中心没变化就不需要每帧重新算getPixelFromCoordinate可以在地图moveend时更新一次缓存平时直接用缓存值。6. 高频踩坑与性能调优实录6.1 遮罩位置偏移最常见的原因这是我被问得最多的一个问题“为什么我的遮罩圆跑到别的地方去了”绝大多数情况是坐标系混用了。OL4默认投影是EPSG:3857而业务拿到的数据通常是经纬度EPSG:4326。如果你把一个经纬度数组直接当作center传给圆形或者直接作为几何坐标用遮罩位置就会偏到非洲西海岸附近经纬度0,0附近。所有坐标在进入OL4之前必须走ol.proj.fromLonLat或ol.proj.transform做转换。第二个常见原因是转换时机不对。上一节强调过getPixelFromCoordinate必须在postcompose回调内部调用不要在外部预先算好像素坐标存起来。地图每帧视图状态都在变像素坐标是瞬时值不是恒定值。第三个原因是地图容器尺寸变化后没有更新OL虽然监听window resize但在一些埋藏容器内比如tab切换、折叠面板容器尺寸变化时OL并不一定感知到需要手动调用map.updateSize()否则postcompose里拿到的map.getSize()和实际画布尺寸不一致遮罩也会跟着错位。6.2 地图拖动后遮罩没刷新的问题如果你发现遮罩在拖动后没有跟着走或者拖动了半天遮罩还在原地大概率是postcompose回调没有被触发。OL4的渲染是“按需”的地图静止时不会持续渲染只有发生交互、图层变化或主动调用map.render()时才渲染。如果遮罩参数变了而你没有触发渲染画面自然不会更新。canvas路线的修复方式很简单每次修改maskConfig后调用一次map.render()。我一般会封装一个更新函数把修改配置和触发渲染绑在一起避免遗漏。function setMask(config) { Object.assign(maskConfig, config); map.render(); }矢量路线则要确保要素几何变化后调用了maskLayer.changed()或map.render()。这两个调用会让渲染管线重新执行遮罩才会更新到新位置。6.3 Retina高分屏模糊、位移的修复高分屏devicePixelRatio大于1上OL4会自动把渲染画布放大对应的像素比以保证地图文字和矢量要素清晰。如果你在postcompose里直接按CSS像素坐标绘制理论上OL4的上下文已经做了坐标变换绘制出来的内容会根据当前变换自动映射到物理像素。但我实测发现不同浏览器里这个行为并不完全一致尤其是在自定义绘制较细的描边时会出现线条发虚、宽度不一致的问题。我的建议是如果项目明确要兼容高分屏不要用OL4内部的canvas上下文做精细绘制而是自己创建一层与地图同尺寸的canvas叠加在map容器上并手动处理设备像素比var overlayCanvas document.createElement(canvas); overlayCanvas.style.position absolute; overlayCanvas.style.left 0; overlayCanvas.style.top 0; map.getViewport().appendChild(overlayCanvas); function resizeOverlayCanvas() { var size map.getSize(); var dpr window.devicePixelRatio || 1; overlayCanvas.width size[0] * dpr; overlayCanvas.height size[1] * dpr; overlayCanvas.style.width size[0] px; overlayCanvas.style.height size[1] px; var ctx overlayCanvas.getContext(2d); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } map.on(moveend, resizeOverlayCanvas);这样你的绘制坐标始终是CSS像素清晰度由canvas.width和setTransform保证。缺点是需要自己监听moveend和postrender逻辑上比直接postcompose更重。我的习惯是简单效果直接用postcompose精细视觉效果用独立叠加层。6.4 性能优化从脏矩形到按需渲染最后聊性能。遮罩本身是一个全屏覆盖的操作最忌讳的就是在postcompose回调里做大量重复计算。我用的优化套路按优先级排是这样的第一减少每帧不必要的计算。把静态数据比如多个区域的中心坐标、提前换算好的像素位置缓存起来只在moveend或手动切换时更新。第二把渐变对象、离屏canvas等创建开销大的对象缓存起来不要每帧重建。第三如果遮罩区域和数量长时间不变可以考虑把遮罩整体绘制到一个离屏canvas上postcompose时直接drawImage过去这一步能把绘制成本从“多次canvas操作”降到“一次位图拷贝”。第四如果动画过程中还需要同时更新大量其他地图要素建议把动画帧率和地图渲染解耦用独立的requestAnimationFrame控制数据变化然后以较低的频率触发map.render()比如每两帧触发一次肉眼很难察觉区别。还有一个小技巧当遮罩中心超出当前视野范围较远时比如点击了视野外的一个点位这时候遮罩画在屏幕外绘制操作没有意义可以提前判断并跳过。判断方法是把中心坐标转成像素后看它是否落在[-radius, size[0] radius]和[-radius, size[1] radius]这个矩形范围外如果是就直接return。这个优化在高缩放级别下特别明显能省掉一整块全屏填充加挖洞的开销。我在实际项目里测试过一个包含4个动态遮罩区域、每帧还带渐变的场景用上述优化后地图拖动的帧率从30fps左右提升到稳定60fps。遮罩效果这类交互其实对性能极其敏感因为用户的视线焦点就在遮罩区域上一旦出现掉帧视觉体感会很糟糕。最后再分享一个小经验遮罩的透明度不要调得太死。rgba(0, 0, 0, 0.65)这个值在大多数地图底图下都合适既能压暗周边又能隐约看到暗区的路网和地名保持用户的空间方位感。透明度超过0.8的话暗区几乎看不清任何底图信息用户会有一种“地图被蒙住”的窒息感反而影响体验。遮罩不是把地图藏起来而是把用户的注意力引导到该看的地方这个度一定要拿捏好。
返回列表