1.效果
在项目开发过程中,有一个需求是有若干个需要展示的点,每个点icon不一样、对应的广告牌文字不一样、并且文字还需要有图片背景、每个文字背景也不同(抓狂)。这种需求只能编写canvas来绘制“icon+文字背景+文字”的image,并返回给billboard。
实测100个点渲染,我这小破电脑花费140ms,能够满足项目需求;作为一个新手切图崽,所编写的代码还有很多改进的地方,有请各位大佬在评论区批评指正。
2.代码
调用renderBillboard(options)方法,将返回一个billboard对象,返回值可以直接使用。也可以使用回调,renderBillboard(options,(result)=>{}),result包含billboard所需的image和pixelOffset。
需要注意的是,vue3+vite加载图片资源使用的是new URL("地址", import.meta.url).href
而vue2加载图片资源使用的是require,在代码中均有,可以根据需要使用。
import * as Cesium from "cesium";
let canvas, context;
/**
* @author: linmaoxin
* @date: 2024/12/14
* @description 渲染billboard,可选择 带背景的文字+图标icon、仅背景的文字 两种类型。1、返回Promise对象,异步获取billboard;2、也可以使用回调分别获取image和pixelOffset。
* @param {*} options
* @callback function
* @returns Promise<object>
* @example
* let options = {
type: 'text-icon', // 默认 'text-icon'类型(图标+文字)、 'text' 类型(仅文字)
paddingTo: 20, // 图标距离文字背景
textPadding: 20, // 文字左右间距
textBcgWidth: 200, // 文字背景宽度
textBcgHeight: 50, // 文字背景高度
iconWidth: 60, // 图标宽度
iconHeight: 70, // 图标高度
textBcgUrl: '../assets/label-bcg.png', // 文字背景图片地址
iconUrl: '../assets/positionIcon.png', // 图标地址
textUrl:'', // 字体地址
text: '测试文字测试文字', // 文字内容
textColor: '#DCD085', // 字体颜色
textFontSize: 16, // 字体大小
textFontWeight: 800, // 字体粗细
}
*/
export function renderBillboard(options, callback) {
let settings = {
type: options.type || 'text-icon', // 默认 'text-icon'类型(图标+文字)、 'text' 类型(仅文字)
paddingTo: options.paddingTo || 5, // 默认 图标距离文字背景
textPadding: options.textPadding || 20, // 默认 文字左右间距
textBcgWidth: options.textBcgWidth || 150, // 默认 文字背景宽度
textBcgHeight: options.textBcgHeight || 40, // 默认 文字背景高度
iconWidth: options.iconWidth || 40, // 默认 图标宽度
iconHeight: options.iconHeight || 50, // 默认 图标高度
textBcgUrl: options.textBcgUrl || console.error('请传入文字背景图片地址 textBcgUrl '), // 文字背景图片地址
iconUrl: options.iconUrl || console.error('请传入图标图片地址 iconUrl '), // 图标地址
textUrl: options.textUrl, // 文字地址
text: options.text || '', // 文字内容
textColor: options.textColor || '#ffffff', // 默认 文字颜色
textFontSize: options.textFontSize || 16, // 默认 文字大小
textFontWeight: options.textFontWeight || 500, //默认 文字粗细
}
if (!canvas) {
canvas = document.createElement('canvas');
}
if (!context) {
context = canvas.getContext('2d', { willReadFrequently: true });
}
// 字体初始化
context.font = `${Number(settings.textFontWeight)} ${Number(settings.textFontSize)}px Microsoft Yahei`;
context.fillStyle = settings.textColor;
context.textAlign = 'center';
context.textBaseline = 'middle';
context.canvas.willReadFrequently = true;
// 根据字数计算宽度 并加上左右间距
const textMetrics = context.measureText(settings.text);
settings.textBcgWidth = textMetrics.width + settings.textPadding;
// 画布大小
if (settings.type === 'text-icon') {
settings.textBcgWidth = settings.textBcgWidth > settings.iconWidth ? settings.textBcgWidth : settings.iconWidth;
canvas.width = settings.textBcgWidth;
canvas.height = settings.textBcgHeight + settings.paddingTo + settings.iconHeight;
} else if (settings.type === 'text') {
canvas.width = settings.textBcgWidth;
canvas.height = settings.textBcgHeight;
}
return new Promise((result) => {
// 加载图片资源
if (settings.type === 'text-icon') {
// 字体二次设置
context.font = `${Number(settings.textFontWeight)} ${Number(settings.textFontSize)}px Microsoft Yahei`;
context.fillStyle = settings.textColor;
context.textAlign = 'center';
context.textBaseline = 'middle';
const image1 = new Image();
const image2 = new Image();
//image1.src = require(settings.iconUrl); //图标地址 vue2
// image2.src = require(settings.textBcgUrl); //文字背景图片地址 vue2
image1.src = new URL(settings.iconUrl, import.meta.url).href // vite
image2.src = new URL(settings.textBcgUrl, import.meta.url).href // vite
image1.onload = () => {
context.drawImage(image1, 0, 0, canvas.width, canvas.height);
}
image2.onload = () => {
context.drawImage(image2, 0, 0, canvas.width, canvas.height);
}
Promise.all([
new Promise(resolve1 => { image1.onload = resolve1; }),
new Promise(resolve2 => { image2.onload = resolve2; })
]).then(() => {
// 清除画布
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
// 渲染
context.drawImage(image2, 0, 0, settings.textBcgWidth, settings.textBcgHeight);
context.drawImage(image1, (canvas.width / 2) - (settings.iconWidth / 2), (settings.textBcgHeight + settings.paddingTo), settings.iconWidth, settings.iconHeight);
context.fillText(settings.text, settings.textBcgWidth / 2, settings.textBcgHeight / 2);
// 判断callback是不是函数
if (typeof callback === 'function') {
callback({ 'image': canvas.toDataURL(), 'pixelOffsetY': Math.round(-canvas.height / 2) })
}
result({
image: canvas.toDataURL(),
pixelOffset: new Cesium.Cartesian2(0, Math.round(-canvas.height / 2))
})
// console.log('canvas', canvas.height)
});
} else if (settings.type === 'text') {
const image2 = new Image();
//image2.src = require(settings.textBcgUrl); //文字背景图片地址 vue2
image2.src = new URL(settings.textBcgUrl, import.meta.url).href // vite
image2.onload = () => {
// 清除画布
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
// 渲染
context.drawImage(image2, 0, 0, canvas.width, canvas.height);
context.fillText(settings.text, settings.textBcgWidth / 2, settings.textBcgHeight / 2);
// 判断callback是不是函数
if (typeof callback === 'function') {
callback({ 'image': canvas.toDataURL(), 'pixelOffsetY': Math.round(-canvas.height / 2), })
}
result({
image: canvas.toDataURL(),
pixelOffset: new Cesium.Cartesian2(0, Math.round(-canvas.height / 2))
})
}
}
})
}
3.结尾
(1)加载图片资源其实可以用 new Cesium.Resource.fetchImage(url).then(image => {}这个方法,这是Cesium的一个异步加载图片资源的方法,并不一定要使用 new Image。
(2)实现过程本文章不过多赘述,有不懂的地方可以评论区讨论、或者问问AI。
(PS:创造不易,如果文章对你有帮助,可以点个赞鼓励下博主哦!)