利用Canvas与Billboard实现Cesium动态标签高级样式定制

张开发
2026/4/10 20:07:22 15 分钟阅读

分享文章

利用Canvas与Billboard实现Cesium动态标签高级样式定制
1. 为什么需要自定义Cesium标签样式在三维地理信息系统中标签Label是展示动态数据的重要载体。Cesium原生的Label组件虽然功能完善但在实际项目中经常会遇到样式定制受限的问题。比如最近我在一个智慧城市车辆监控项目中客户要求在每个车辆模型上方显示包含多行信息的标签并且要有圆角背景、自定义字体和边距。原生的Label只能设置简单的字体颜色和大小完全无法满足这种设计需求。原生Label的主要限制体现在三个方面首先是背景样式单一无法实现圆角、阴影等现代UI效果其次是多行文本支持不足自动换行和行间距控制非常困难最后是动态数据绑定不够灵活当需要实时更新车辆速度、位置等信息时性能开销较大。这些问题在需要高美观度的项目中尤为明显。CanvasBillboard的组合恰好能解决这些痛点。Canvas提供了近乎无限的2D绘制能力我们可以自由设计标签的每一个像素而Billboard作为Cesium的广告牌实体能够将Canvas生成的图像完美地固定在三维场景中的指定位置。这种方案我在多个项目中实测下来既能保证视觉效果又能维持不错的性能。2. Canvas绘制高级标签的核心技巧2.1 基础绘制流程创建一个自定义标签首先需要初始化Canvas环境。我习惯封装一个可配置的绘制函数这样不同场景下都能复用。核心代码结构如下function createLabelCanvas(options) { const canvas document.createElement(canvas); canvas.width options.width || 200; canvas.height options.height || 80; const ctx canvas.getContext(2d); // 绘制背景 drawRoundedRect(ctx, options); // 绘制文本 drawText(ctx, options); return canvas.toDataURL(image/png); }这里有几个关键点需要注意Canvas的尺寸要根据内容动态计算我通常会预留20%的边距使用toDataURL将Canvas转为Base64图片这是Billboard能识别的格式。在实际项目中建议将图片缓存起来避免重复生成。2.2 圆角背景的实现原生Canvas没有直接绘制圆角矩形的方法需要自己实现。下面是我优化过的圆角矩形函数支持单独控制每个角的半径function drawRoundedRect(ctx, x, y, width, height, radius) { ctx.beginPath(); // 左上角 ctx.moveTo(x radius, y); // 右上角 ctx.lineTo(x width - radius, y); ctx.quadraticCurveTo(x width, y, x width, y radius); // 右下角 ctx.lineTo(x width, y height - radius); ctx.quadraticCurveTo(x width, y height, x width - radius, y height); // 左下角 ctx.lineTo(x radius, y height); ctx.quadraticCurveTo(x, y height, x, y height - radius); ctx.closePath(); ctx.fillStyle rgba(50, 50, 50, 0.8); ctx.fill(); }这个实现比网上常见的版本更灵活你可以通过修改radius参数控制圆角大小。如果追求更炫的效果还可以添加渐变填充或阴影// 添加阴影效果 ctx.shadowColor rgba(0, 0, 0, 0.5); ctx.shadowBlur 10; ctx.shadowOffsetY 3;2.3 多行文本处理Canvas的fillText方法不支持自动换行需要手动实现。我的做法是先按空格分割单词然后逐行计算function wrapText(ctx, text, x, y, maxWidth, lineHeight) { const words text.split( ); let line ; for(let n 0; n words.length; n) { const testLine line words[n] ; const metrics ctx.measureText(testLine); if (metrics.width maxWidth n 0) { ctx.fillText(line, x, y); line words[n] ; y lineHeight; } else { line testLine; } } ctx.fillText(line, x, y); }对于车辆监控这类场景我建议将不同信息用不同样式区分。比如标题用粗体数值用醒目颜色ctx.font bold 14px Arial; ctx.fillText(车辆编号:, 15, 25); ctx.font 14px Arial; ctx.fillStyle #FFA500; ctx.fillText(沪A12345, 80, 25);3. 与Cesium Billboard的集成3.1 基本集成方法将Canvas生成的图片应用到Billboard上很简单但有几个细节需要注意const entity viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(121.5, 31.2), billboard: { image: createLabelCanvas({ text: 车辆01\n速度: 60km/h, width: 180, height: 60 }), pixelOffset: new Cesium.Cartesian2(0, -50), disableDepthTestDistance: Number.POSITIVE_INFINITY } });pixelOffset用于微调标签位置通常需要根据模型高度动态计算。disableDepthTestDistance这个参数很重要它决定标签是否会被其他3D物体遮挡。设置为无限大表示标签始终可见这在大多数监控场景中是需要的。3.2 动态数据更新对于实时变化的车辆信息直接重新生成整个Canvas效率较低。我的优化方案是为每个实体维护一个Canvas缓存只当数据变化超过阈值时才重绘使用requestAnimationFrame节流更新function updateVehicleInfo(entity, newInfo) { if(!entity._labelCanvas) { entity._labelCanvas document.createElement(canvas); entity._labelContext entity._labelCanvas.getContext(2d); } // 检查数据变化是否达到更新阈值 if(Math.abs(newInfo.speed - entity._lastSpeed) 5) { redrawLabel(entity._labelContext, newInfo); entity.billboard.image entity._labelCanvas.toDataURL(); entity._lastSpeed newInfo.speed; } }3.3 性能优化技巧在大规模场景中标签过多会导致性能下降。我总结了几条优化经验距离衰减当相机远离时简化标签内容或完全隐藏viewer.scene.postRender.addEventListener(function() { const distance // 计算与相机的距离; if(distance 1000) { entity.billboard.show false; } });聚合显示当标签密集时合并显示统计信息缓存复用相同样式的标签共用Canvas图像WebWorker将Canvas绘制放到Worker线程执行4. 高级样式定制案例4.1 带图标的状态标签在物流追踪系统中我经常需要制作包含状态图标的标签。实现方法是在Canvas中绘制图标和文字function drawStatusLabel(ctx, status) { // 绘制背景 drawRoundedRect(ctx, 0, 0, 200, 40, 5); // 绘制状态图标 const img new Image(); img.src icons/${status}.png; img.onload function() { ctx.drawImage(img, 10, 10, 20, 20); // 绘制状态文本 ctx.fillStyle getStatusColor(status); ctx.fillText(getStatusText(status), 40, 25); }; }4.2 动画效果实现通过定期重绘Canvas可以实现闪烁、呼吸灯等动画效果function createBlinkingLabel() { let alpha 1; let direction -0.05; function animate() { alpha direction; if(alpha 0.3 || alpha 1) direction * -1; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.globalAlpha alpha; drawLabel(ctx); requestAnimationFrame(animate); } animate(); }4.3 响应式设计不同屏幕分辨率下标签大小也需要调整。我的解决方案是根据相机高度动态计算标签尺寸viewer.camera.changed.addEventListener(function() { const height viewer.camera.positionCartographic.height; const scale Cesium.Math.clamp(height / 1000, 0.5, 2); entities.values.forEach(entity { if(entity.billboard) { entity.billboard.scale scale; } }); });在实际项目中这种CanvasBillboard的方案已经被证明是可靠且灵活的。从简单的信息标签到复杂的动态指示器几乎任何二维可视化需求都能满足。刚开始使用时可能会遇到性能问题但只要遵循本文的优化建议即使在上万级别的实体场景中也能保持流畅。

更多文章