1. 项目缘起:为什么给轨迹线加一条渐变色
先说说我为什么折腾这个。之前帮朋友做一个运动数据展示页面,需要在百度地图上把骑行轨迹画出来。最开始就是最简单的折线,用Polyline拉一条线出来,加点宽度和颜色,能看就行。
但实际跑起来之后发现问题了:轨迹很长,整条线就一种颜色,完全看不出速度变化、海拔起伏,也不知道哪里是起点、哪里是终点。尤其是不熟悉路线的观众,盯着屏幕看半天,脑子里只有一条“蓝色面条”,没有任何信息量。
后来我想能不能把轨迹线做成渐变色,让线条从起点到终点,颜色从绿逐渐过渡到红,或者根据实时速度动态调色。这样一眼扫过去,就能直观判断整条路线的状态分布,用户也愿意多看一眼。而且说实话,渐变轨迹线在视觉上确实比单一线条高级不少,地图渲染出来的效果也更贴近真实导航App的观感。
这个需求听起来不难,但真正落到百度地图 JavaScript API 里,你会发现官方Polyline并不支持直接传入渐变颜色。有人可能想,那我把它拆成很多小段,每段单独画一条线,每段设不同颜色,不就行了?
没错,思路就是这个思路。但真正的坑在于:怎么拆、拆多少段、段与段之间的颜色怎么算、性能会不会崩、线段拼接处会不会出现缝隙。这些问题不实际做一遍,看文档是想不到的。这篇文章就把我踩过的这些坑、最终稳定的实现方案,一步步拆给你们看。
2. 方案选型:为什么绕开官方API,自己拆线段
2.1 官方Polyline的能力边界
先聊清楚百度地图 JavaScript API 里Polyline到底能干什么。
BMapGL.Polyline支持你传入一组经纬度点数组,然后指定strokeColor、strokeWeight、strokeOpacity这些样式。画出来的就是一条统一颜色的折线,整个路径从头到尾一个样。
如果你用的的是BMapGL.Symbol或BMapGL.Polyline的某些扩展,最多也只能做到沿线加图标动画,依然做不到“一条线分段着色”。官方文档也没有任何strokeGradient之类的选项。所以想在百度地图里实现渐变轨迹,几乎没有官方捷径,必须自己处理。
2.2 三条技术路线对比
我试了三种思路,简单说下各自的优劣:
方案A:整条线用Canvas覆盖层渲染。把地图Canvas拿过来,自己计算坐标投影,自己绘制渐变路径。优点是效果好、可控性极强,甚至可以做成贝塞尔曲线平滑过渡;缺点是代码量一下子膨胀,要处理地图缩放、平移时的重绘逻辑,还有覆盖层层级问题。如果只是想加个渐变轨迹,这么做有点“杀鸡用牛刀”。
方案B:把轨迹线拆成N段,每段画一条
Polyline。核心思路是把长轨迹切分成几十段,比如每段对应10公里或20个坐标点,然后根据每段的属性(比如速度、里程进度)计算出一个颜色,逐段绘制。这是三种方案里性价比最高的,性能可接受,视觉效果不错,代码也不复杂。方案C:利用
BMapGL.Overlay自定义覆盖物绘制渐变线。理论上也可以绕开常规Polyline,在地图的覆盖层协议里自己实现绘制。但百度地图的覆盖物体系有较多限制,交互事件和刷新时机处理起来比较麻烦,而且缩放层级变化时线条会出现明显的失帧或重影,我做完测试就放弃了。
最终我选择了方案B。原因很简单:它和官方Polyline完全兼容,地图缩放、拖拽时的渲染都由地图引擎接管,不用我操心。我只需要解决两件事:怎么切分轨迹;怎么给每一段算颜色。
2.3 为什么切分线段是靠谱的“渐进思路”
有人会担心,拆成几十条线会不会性能很差?
其实地图 API 的数据结构里,Polyline本质是一个矢量对象,每一条线的开销并不只取决于线段本身,还取决于你要更新它的频次。如果只是初始化时画一次,之后不动,那么一百条左右的Polyline在绝大多数地图场景下是毫无压力的。
如果你的轨迹特别长,比如上千公里的跨省路线,点数量特别密集,那才需要考虑每100个点合并一段。我在实际项目里的做法是:每段包含10到20个原始点。这样一条几百公里的轨迹大概几十段,渲染完全平滑。
然后再看“拆线”会不会暴露拼接缝隙。只要你每段线的起终点是相邻的,且在相同像素坐标系下,缝隙几乎可以忽略。但如果你把颜色差异拉得太极端,比如从纯黑直接跳到纯白,视觉上就会看到明显“色斑”。所以颜色过渡尽量在相邻段之间保持平滑,这也是后面要对颜色做插值计算的原因。
3. 核心原理:渐变色轨迹的数学拆解
3.1 轨迹切分:按距离还是按点?
这是整个方案的第一个关键决策。按点切分和按距离切分,结果差别非常大。
按原始坐标点切分,操作最简单:你把polyline的点数组从中间切几刀,每段分配固定数量的点。问题是:地图上的轨迹点密度不均匀。城市道路点位密集,高速公路点位稀疏。如果你按点数切,城市路段可能只覆盖几公里,高速路段一段可能就覆盖几百公里,色带分布会在视觉上严重失真。
按距离切分是更合理的办法。做法是遍历整条轨迹,计算相邻点之间的距离(通常使用球面距离公式,或者调用百度地图的BMapLib.GeoUtils.getDistance),把点数组按“累计里程”切割。这样每一段的实际地理长度基本一致,渐变效果才均匀。
我当时写了一个切分函数,大概逻辑是:首先计算所有相邻点的距离;然后设定一个目标段长度,比如每段10公里;接着从头遍历,每累加到目标长度就生成一个分段边界;最后把边界之间的坐标点打包给每一段。这样得到的每一段路径长度约等于预设值,最后一段可能略短,没关系。
分段的粒度会影响渐变细腻程度。一般来说,段数在30到60之间效果比较好。少于20段,颜色跳变会非常突兀;多于200段,虽然颜色细腻,但地图上叠加的矢量对象太多,交互时会有轻微卡顿。所以10公里一段,适配大多数城市级轨迹。
3.2 颜色的线性插值:如何从绿色平滑过渡到红色?
渐变色轨迹线的美学核心,是颜色插值。如果轨迹要从起点到终点由绿变红,那本质上就是要把RGB(0, 255, 0)过渡到RGB(255, 0, 0)。
最简单的做法是在RGB空间做线性插值:
已知起点颜色c1 = (r1, g1, b1),终点颜色c2 = (r2, g2, b2),当前进度t(取值范围0到1),那么当前颜色就是:
r = r1 + (r2 - r1) * t g = g1 + (g2 - g1) * t b = b1 + (b2 - b1) * t这个公式计算出来的颜色,如果路径是中性的,比如蓝到黄,效果还算自然。但如果是红到绿,你会发现中间会经过一段暗褐色区域,这就是RGB插值的天生局限。所以在做骑行轨迹这类场景时,我建议不要直接红绿互转,而是用一组“锚点颜色”形成连续色带。
比如我常配的渐变色带是:
- 起点:绿色
#00C853 - 前段:黄绿色
#AEEA00 - 中段:黄色
#FFD600 - 后段:橙色
#FF6D00 - 终点:红色
#DD2C00
这样视觉上是从“健康绿”到“警示红”,中间经过黄、橙,观感比单纯的RGB线性插值顺眼得多。而且这种多锚点插值其实代码也不难,只需把整条轨迹分成五段区间,每一段内做一次线性插值即可。
如果你有额外的数据维度,比如每个点的海拔或速度,那么可以把t从单纯的里程占比,换成“速度归一化值”或“海拔归一化值”。这样渐变色就有实际含义了,信息表达力会更强。
3.3 透明度和线宽的配合处理
渐变色不只包含颜色,还应该考虑透明度和线宽变化。轨迹的端点如果是“淡入淡出”效果,会让整条线显得非常高级。
做法是:第一段线设置strokeOpacity为0.3,然后逐步递增到1;最后一段再渐降到0.5。或者你也可以让线宽从起点较细(比如6px)、中段最粗(10px)、终点再变细(7px),这样视觉上像一条有生命的“蜿蜒的河流”。
不过要小心:地图上的矢量线条在有背景瓦片时,线宽太大容易遮盖道路信息。一般建议线宽在5到10px之间,透明度不低于0.5。如果你做的是大屏展示,可以稍微加粗到12px,但那时最好同时降低地图瓦片的饱和度,让轨迹线突出。
4. 实操过程:从零实现百度地图渐变轨迹线
4.1 前置准备:引入百度地图API并初始化
这一步不啰嗦,基础代码直接给出来。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>百度地图渐变轨迹线实现</title> <style> html, body, #map { width: 100%; height: 100%; margin: 0; padding: 0; } </style> <script type="text/javascript" src="https://api.map.baidu.com/api?v=1.0&&type=webgl&ak=你的AK"></script> </head> <body> <div id="map"></div> <script> // 初始化地图,选择适合的经纬度和缩放级别 const map = new BMapGL.Map('map'); const initPoint = new BMapGL.Point(116.404, 39.915); map.centerAndZoom(initPoint, 15); map.enableScrollWheelZoom(true); </script> </body> </html>注意百度地图现在默认推荐使用BMapGL(WebGL版),老的BMap也是可以跑的,但部分新特性不兼容。我的做法是统一用BMapGL。需要提醒的是:v=1.0&&type=webgl里那个&&是文档原样提供的,直接复制即可,不要自作聪明改成&。
4.2 准备轨迹坐标点数据
真实项目里轨迹点一般来自运动App导出、GPS设备上报或后台数据库。这里为了演示,我模拟一条环绕城市公园的路线。
const trackPoints = [ new BMapGL.Point(116.404, 39.915), new BMapGL.Point(116.408, 39.913), new BMapGL.Point(116.412, 39.912), new BMapGL.Point(116.418, 39.914), new BMapGL.Point(116.420, 39.918), new BMapGL.Point(116.417, 39.922), new BMapGL.Point(116.411, 39.924), new BMapGL.Point(116.405, 39.923), new BMapGL.Point(116.402, 39.919), // 实际项目中可能有上百上千个点 ];这些点要注意一个原则:相邻点之间的距离不要过于稀,否则画出来的折线会显得很生硬。GPS设备默认上报频率如果是每1秒一个点,跑步大概每2-3米一个点,画轨迹没问题;但如果你是每5分钟上报一次,两点之间隔了几百米,那么建议做“抽稀”和“插值”处理,否则折线会像锯齿一样难看。
4.3 计算每段里程,并按距离切分轨迹
切分函数我直接用BMapLib.GeoUtils.getDistance,如果没有引入这个工具库,也可以自己写球面距离公式,但没必要,官方库稳定而且方便。
下面这段代码是把轨迹点按“每段目标距离”切成若干组点数组:
// 引入GeoUtils工具库 // 若已安装百度地图工具类,直接用即可 function splitTrackByDistance(points, segmentDistance) { if (!points || points.length < 2) return []; const segments = []; // 存放最终的分段点数组 let curSegment = [points[0]]; // 当前分段, 先放入起点 let accumulatedDistance = 0; // 当前分段内累计里程 for (let i = 1; i < points.length; i++) { const prevPoint = points[i - 1]; const curPoint = points[i]; // 计算两点之间的球面距离, 单位: 米 const dist = BMapLib.GeoUtils.getDistance(prevPoint, curPoint); accumulatedDistance += dist; curSegment.push(curPoint); // 当前分段累计里程达到目标值,则封段 if (accumulatedDistance >= segmentDistance) { segments.push(curSegment); // 下一段的起点是当前段的最后一个点,保证无缝连接 curSegment = [curPoint]; accumulatedDistance = 0; } } // 处理最后不足一段距离的残余点 if (curSegment.length > 1) { segments.push(curSegment); } return segments; } // 使用示例:每段大约800米 const segments = splitTrackByDistance(trackPoints, 800);这里要强调几个细节:
- 每段起点取上一段的末尾点,是为了保证首尾相接,视觉上不出现断裂。
- 决策阈值设多少合适?我一般考虑两个因素:轨迹总长度和目标段数。比如总长度20公里,想要40段,那
segmentDistance就是500米。如果是总长100公里的骑行路线,段长可以设在2公里左右,段数仍然保持在50以内。 - 最后残余的一小段如果不足一个目标段长,也应该画出来,不要丢弃。丢弃的话轨迹终点会缺一小截,非常明显。
4.4 定义多锚点渐变色带
接下来定义一个颜色类,负责从一组颜色锚点中,根据进度t计算对应的颜色值。
class GradientColor { constructor(colorStops) { // colorStops: [{ pos: 0, color: [0, 200, 83] }, { pos: 0.5, color: [255, 214, 0] }, ...] this.colorStops = colorStops; } // 根据 t (0-1) 返回对应的颜色字符串 getColor(t) { if (t <= 0) return this.rgbToStr(this.colorStops[0].color); if (t >= 1) return this.rgbToStr(this.colorStops[this.colorStops.length - 1].color); for (let i = 0; i < this.colorStops.length - 1; i++) { const stop1 = this.colorStops[i]; const stop2 = this.colorStops[i + 1]; if (t >= stop1.pos && t <= stop2.pos) { const localT = (t - stop1.pos) / (stop2.pos - stop1.pos); const color = [ Math.round(stop1.color[0] + (stop2.color[0] - stop1.color[0]) * localT), Math.round(stop1.color[1] + (stop2.color[1] - stop1.color[1]) * localT), Math.round(stop1.color[2] + (stop2.color[2] - stop1.color[2]) * localT) ]; return this.rgbToStr(color); } } return this.rgbToStr(this.colorStops[this.colorStops.length - 1].color); } rgbToStr(rgb) { const r = Math.min(255, Math.max(0, rgb[0])); const g = Math.min(255, Math.max(0, rgb[1])); const b = Math.min(255, Math.max(0, rgb[2])); return `rgb(${r}, ${g}, ${b})`; } } // 定义一个绿 -> 黄 -> 红的渐变色带 const gradient = new GradientColor([ { pos: 0, color: [0, 200, 83] }, // 绿色 { pos: 0.35, color: [174, 234, 0] }, // 黄绿 { pos: 0.6, color: [255, 214, 0] }, // 黄色 { pos: 0.8, color: [255, 109, 0] }, // 橙色 { pos: 1, color: [221, 44, 0] } // 红色 ]);这段实现里面我特意把RGB值作为数组处理,而不是直接拿来算字符串,是因为插值计算需要逐通道做数值运算,这样可以避免频繁做字符串解析。
4.5 计算每段的进度t,并绘制多条Polyline
现在到了最关键的一步。我们要遍历所有切分好的段,计算每一段“应该处于整条轨迹的百分之多少”的位置,然后取对应颜色,画到地图上。
这里有一个很容易踩的坑:如果你对每一段单独计算它自己的起点在整条轨迹上的里程占比,那么后段颜色会依赖于你整条轨迹的总长度。这个没问题。但如果你错误地用了段的索引除以总段数,那么当轨迹点密度不均匀时,颜色分布就一定不均匀。正确做法是:累计每一段开头点的里程,然后除以整条轨迹的总里程。
所以绘制前还是需要先算出每段起始点的progress。
// 先算整条轨迹总距离 let totalDistance = 0; for (let i = 1; i < trackPoints.length; i++) { totalDistance += BMapLib.GeoUtils.getDistance(trackPoints[i - 1], trackPoints[i]); } // 遍历各段,计算进度并画线 let passedDistance = 0; segments.forEach((seg, index) => { // 起点进度(本段开头已走过的比例) const startProgress = totalDistance === 0 ? 0 : passedDistance / totalDistance; // 终点进度(本段结尾已走过的比例) passedDistance += BMapLib.GeoUtils.getDistance(seg[seg.length - 1], segments[index + 1] && segments[index + 1][0] || seg[seg.length - 1]); // 更稳妥的写法是累加当前seg内首尾点距离,这里为了清晰我给简写 const endProgress = totalDistance === 0 ? 1 : passedDistance / totalDistance; // 取段中间位置的进度作为整段颜色 const midProgress = (startProgress + endProgress) / 2; const colorStr = gradient.getColor(midProgress); const polyline = new BMapGL.Polyline(seg, { strokeColor: colorStr, strokeWeight: 8, strokeOpacity: 0.9, enableClicking: false }); map.addOverlay(polyline); });上面这个示例里的passedDistance累加逻辑写得比较粗,你在实际项目中建议更明确地计算:
- 遍历每条分段,把分段内所有相邻点的距离累加到
passedDistance。 startProgress = (passedDistance - 当前段距离) / totalDistance。endProgress = passedDistance / totalDistance。
这样不容易出错。或者更简洁的方案:直接在主循环外先算出所有段的起始里程数组,再统一计算颜色。
4.6 完整可运行示例(核心绘制函数)
最后我整理了一个更清爽的完整绘制函数,直接复制到项目里改一改就能用:
function drawGradientTrack(map, trackPoints, segmentDistance) { if (!trackPoints || trackPoints.length < 2) return; const segments = splitTrackByDistance(trackPoints, segmentDistance); // 计算每段起始点占总里程的比例 const totalDistance = calcTotalDistance(trackPoints); let curDistance = 0; const segmentInfos = segments.map((seg, idx) => { const segDistance = calcSegmentDistance(seg); const startRatio = curDistance / totalDistance; curDistance += segDistance; const endRatio = curDistance / totalDistance; return { seg, startRatio, endRatio }; }); const gradient = new GradientColor([...]); // 配置你的色带 segmentInfos.forEach(({ seg, startRatio, endRatio }) => { const midRatio = (startRatio + endRatio) / 2; const polyline = new BMapGL.Polyline(seg, { strokeColor: gradient.getColor(midRatio), strokeWeight: 8, strokeOpacity: 0.85, enableClicking: false }); map.addOverlay(polyline); }); } function calcTotalDistance(points) { let total = 0; for (let i = 1; i < points.length; i++) { total += BMapLib.GeoUtils.getDistance(points[i - 1], points[i]); } return total; } function calcSegmentDistance(segment) { return calcTotalDistance(segment); }这段代码我用在项目里实测下来,渲染一整条20多公里的线路,分段大概25条左右,地图交互依然非常流畅。
5. 进阶玩法:让颜色反映速度、海拔和拥堵状态
5.1 基于速度的动态渐变
前面说的渐变是按“里程进度”变化的,也就是固定从起点到终点渐变。但很多场景下,你想让线条颜色代表“实时速度”,这样用户能看到哪一段骑得快、哪一段在堵车。
实现方式就是把颜色插值的参数t从“里程比例”换成“速度比例”。比如你采集的轨迹点里带有时间戳,可以计算两点之间的速度:
const speed = dist / (timeDelta / 3600); // km/h const maxSpeed = 40; // 设置一个最大速度用于归一化 const t = Math.min(1, speed / maxSpeed);然后gradient.getColor(t)就是当前线段的速度颜色映射。这时候渐变色带可以设成:
- 速度为0附近 -> 灰色或蓝色(表示停滞)
- 低速 -> 绿色
- 中速 -> 黄色
- 高速 -> 红色
这样用户一眼就能识别出整个行车的节奏,红绿灯路口也会自然地变成灰色或蓝色。这是我个人觉得最实用的扩展版本。
5.2 基于海拔的剖面色彩
如果你在户外徒步场景,海拔变化比速度更有表达力。轨迹点里带上altitude字段,然后做海拔归一化:
const minAlt = Math.min(...points.map(p => p.altitude)); const maxAlt = Math.max(...points.map(p => p.altitude)); const t = (point.altitude - minAlt) / (maxAlt - minAlt);颜色可以用“冷色代表低海拔、暖色代表高海拔”,效果很像等高线图。不过要注意:如果海拔数据噪声比较大,建议先做滑动平均滤波,不然颜色会频繁抖动,看起来非常杂乱。
5.3 分段粒度与颜色平滑度的权衡
速度和海拔渐变场景下,建议把分段距离调小一点,比如200米一段。因为如果段长太大,一个段内可能包含高速+低速混合路况,取中间值会丢失细节。但如果段长太短,比如20米一段,地图上可能会叠加两三百条线,性能就开始出现问题了,略显卡顿。
我的经验是:城市道路200到500米一段,高速公路可以放宽到1公里。具体的段长你可以根据总轨迹长度除目标段数来定,保持在30到80段以内是安全的。
6. 常见问题与排查技巧实录
6.1 线段之间有白缝或黑色描边
这是最常见的坑。如果你把每段Polyline画在地图上,可以看到线段交界处偶尔有非常细微的白边,尤其是地图缩放过程中。出现原因有两个:
- 相邻两条线段的坐标不是首尾完全相等,中间差了一点点浮点误差。
- 地图对相邻矢量对象做了抗锯齿渲染,边缘叠加会出现半透明白边。
解决办法很简单:每段在切分时,后面一段的第一个点必须完全复用前一段的最后一个点,不要重新new BMapGL.Point,直接用同一个对象引用。如果还有白边,可以给每条线设置strokeOpacity: 1,同时把相邻两条线重叠一个点,比如后段从当前段的倒数第二个点开始,这样缝合处就不会透底了。
6.2 渐变颜色出现明显的“斑马纹”
斑马纹指的是颜色不是平滑过渡,而是一段一段明显的色阶。原因基本是你的分段太粗,或者颜色锚点之间插值步长太大。
解决思路:增加分段数,观察30段左右的效果。如果你不想增加线段对象数量,可以改用“逐点绘制”的方式:每个点对之间画一条极短的Polyline,颜色直接按照当前点的插值算。这样颜色过渡最细腻,但代价就是对象数量大。实际项目中我用过一个折中方案——先按点数画,然后定时器在空闲时合并相邻同色系线段,逻辑稍复杂,但效果最佳。
6.3 地图缩放后渐变线变粗或者变模糊
地图矢量线条在WebGL渲染下,缩放时如果不设置属性,线宽可能不会随缩放级别等比变化,导致大比例尺下线条看起来过粗。解决方式:在map.addEventListener('zoomend', ...)里重新遍历所有线段,根据需要动态调整strokeWeight。
另外,如果你使用 BMapGL 的enableAutoDisplay或一些性能优化配置,可能会影响线条渲染清晰度。我一般会显式设置map.setMapMaxLevel(19)或使用自定义瓦片,避免缩放时线条重绘异常。
6.4 内存泄漏:反复更新轨迹线导致页面卡死
有些场景需要实时更新轨迹,比如运动App跟随用户移动画线。如果每次更新都是先map.clearOverlays()再重新添加几十条线段,就会触发频繁的内存分配,长时间运行很容易卡死。
优化思路:
- 复用已有的
Polyline对象实例。 - 每次更新只需要修改其中某一段的坐标和颜色,而不是删除所有对象。
- 如果轨迹增长很快,可以只新增最后一段,前面的线段不更新颜色。
我用过一种模式:维护一个数组polylines,计算出新的segments之后,和旧数组做diff,如果某一段存在,则调用polyline.setPath(newPath);如果不存在则新建;如果多余则移除。这样性能会好很多。
6.5 渐变色与地图主题不搭
地图底图如果是深色风格,你原来的绿黄红渐变可能不够醒目。建议在深色底图上换一套霓虹色带,比如青色到紫红色:
- 起点
[0, 229, 255](青) - 中点
[124, 77, 255](紫) - 终点
[255, 45, 85](红粉)
同时增加线条的发光效果。百度地图的Polyline不支持阴影或发光,但你可以叠加同路径的白色半透明粗线作为外发光。代码上就是对每一条线段同时画两次:一次是strokeWeight: 12透明度0.3的外圈,一次是strokeWeight: 6透明度1的内圈,视觉上就有光晕效果了。
6.6 切分后最后一段颜色不完整
如果你的轨迹总里程不是段长的整数倍,最后一段会明显偏短,颜色取中间值会造成整条轨迹末端的颜色跳跃很大。
我的做法是:对最后一段不按中间进度取色,而是直接取endRatio对应的颜色,也就是终点色。这样收尾会更自然。另一种做法是调整第一段的长度,把多余的距离均匀分配到每一段上,比如目标段长改为totalDistance / targetSegmentCount,这样所有段等长,最后一段也不会太短。推荐第二种,代码上只需把segmentDistance参数动态设置为totalDistance / targetCount即可。
7. 性能调优与展示效果增强
7.1 线段数量控制的艺术
从前面的内容可以总结出一个性能公式:
- 段数小于20:颜色过渡不自然,视觉打折。
- 段数在25到60之间:性能和观感最佳,绝大多数场景推荐。
- 段数大于150:画线操作开销上升,尤其在地图旋转和倾斜时掉帧明显。
如果你的轨迹点数非常多,切分之前建议先做simplify抽稀。一种简单的抽稀方法是“垂距法”:计算每个点到前后两点连线的垂直距离,如果小于阈值,就剔除该点。百度地图的BMapLib没有内置抽稀函数,你可以自己写一个,大概十几行代码就够了。
7.2 用飞机视角/旋转视角展示时,线条不能穿模
如果你做的是大屏3D地图,也就是开启map.setTilt(60)和map.setHeading(30)这类操作时,线条的层次感很重要。建议把渐变轨迹线放在一个单独的BMapGL.CustomOverlay或新建一个map.addOverlay层级,并设置zIndex高于普通道路标签。
BMapGL 对覆盖物的zIndex支持程度还可以,但不同版本有细微差异。我的经验是给每条Polyline设置zIndex: 100,并把它们统一放到map.addOverlay(polyline, 100)里调用。如果你发现线条被路名标注遮挡,可以临时把地图的标注图层关闭,或者把线条提升到100以上。
7.3 视觉增强:沿线动画指针
渐变轨迹线做好后,如果再加一个沿线移动的圆点,整个地图展示效果会更生动。这属于额外的动画,作为加分项可以这样做:
- 用
BMapGL.Marker创建一个圆点图标。 - 利用轨迹点的累计里程和
requestAnimationFrame,逐帧更新 marker 的位置。 - 当 marker 移入某一段时,可以让该段线条的
strokeOpacity提高,其他段降低,形成“扫光”效果。
不过这个动画涉及地图重绘性能,建议只在需要演示的时候开启,不要常驻。我实测在段数40左右时动画保持流畅;段数达到100以上后,动画帧率下降明显,需要用“降低帧率+插值移动”的方式优化。
7.4 数据更新时如何平滑过渡
如果你的轨迹是持续更新的,比如竞速类场景,希望每一秒都能看到轨迹线在生长,不建议整条重绘,建议只更新最后一段。
做法是:保存当前还没达到目标段长的“末段”对象,每来一个新点,就往这个末段里追加,并更新其路径。当末段长度超过目标段长时,就把末段封存,新建一条末段继续扩展。这样地图上同时维持的活跃线段永远只有一条,其他都是已完成的静态线段,性能开销小到可以忽略。
8. 从“能画出来”到“画得好看”的几点心得
做了几个项目之后,我对渐变色轨迹线的理解已经不只是“加个颜色”这么简单。这里面最花时间的地方其实在数据处理阶段,比如轨迹点乱序、重复、漂移的问题。如果你不先清洗数据,渐变线的颜色会来回乱跳,看起来像故障一样。
我的数据处理流程一般是:
- 去掉明显漂移点(速度超过合理阈值的点直接剔除)。
- 对轨迹做滑动平均滤波,让坐标更平滑。
- 按距离抽稀,保证点密度基本一致。
- 再做分段和着色。
这四步做完,基本不需要操心效果问题。
另外一个容易被忽略的点是:渐变线的业务含义要明确。单纯为了渐变而渐变,不如不做。如果你只是想好看,那用一条固定金色线就足够;如果你想让用户感知速度、里程、情绪,那么渐变才有价值。所以我在设计色带时,都会先问一句:观众看到这个颜色,能联想到什么?速度越快越红已经是直觉;海拔越高越红也是直觉;但你如果非要用绿代表快、红代表慢,那用户会天然觉得反了,交互体验就是失败的。
如果你打算把这段逻辑封装成通用工具,建议把颜色锚点、段长、数据维度抽象成配置项。你可以提供一套默认参数,同时暴露自定义配置接口。这样后续不同的业务模块可以直接复用,不用每次重新写插值和切分逻辑。
最后再分享一个小技巧:把渐变色的计算和数据切分放到Web Worker里做。当轨迹点有几千个、且需要实时更新时,主线程计算距离和颜色会造成地图拖动卡顿。放到 Worker 里算好之后,再在主线程直接绘制,体感差别非常明显。百度地图的 API 对象本身不能在 Worker 里操作,但纯数据的计算完全没问题。这也是我后面继续优化的方向,你可以直接照抄这个思路。