最近接到了一个新需求,需要把天地图颜色换成自定义颜色。思索了半天无从下手,毕竟搞地图我不是专业的。但是做好牛马只能硬着头皮干了。
前言
- 我有AI过,有百度过,查看了很多写法和别人总结的经验,发现效果都无法达到想要的。
试错的路上
我叫AI给我封装一个组件,修改主题色的,然后AI一直帮我使用css的filter去调颜色,其实弊端很大,能调的其实也不多,都是通过filter几个参数不停的调例如:filter:invert(100%) sepia(80%) hue-rotate(10deg) saturate(200%) brightness(80%);

- 你会发现路线颜色不满意,底色太暗不满意,调试效果不满意等。
走正轨
- 发现调试很久都达不到效果,于是找了一些组件库刚开始使用一些不知名的库,我就不说了因为没啥用。最后使用OpenLayers才慢慢开始走上正轨。
- 这个库很丰富,我都没时间去看文档,立马叫AI帮使用这个库去封装一个组件。
- 刚出来效果也是很一般。
底色
- 刚开始底色是对了,AI封装了一个颜色修改函数,
setMapTheme这个函数里包含了修改颜色所有逻辑。 - 发现给完底色后,我的二维建筑物线条都被模糊掉了。
- 于是让AI重新把相关的线条重新描绘出来。
const edgeThreshold = 70; const edgeDarkness = 0.8;这是控制描绘线条的粗细颜色参数。
设置道路和水相关变成黑色
- 描绘出来后发现效果还不错,但是UI图需要道路和水黑色。
- 于是叫AI帮忙加上,调了很久AI都没办法把道路和水设置黑色,老是调反了,于是我只能自己检查代码去做判断了。
- 最后通过条件判断把颜色加上去了。
结束
- 经过层层试错和调试,终于把UI想要的样式调出来了,颜色值可能有点点偏差,但是大体是对的。
- 以上就是我的解决思路和一些问题了。
- 最后完整代码献上,喜欢记得三连赞赞赞。。。
- 效果图如下:

vue
<template>
<div ref="mapContainer" class="map-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, defineProps } from "vue";
import "ol/ol.css";
import Map from "ol/Map";
import View from "ol/View";
import TileLayer from "ol/layer/Tile";
import XYZ from "ol/source/XYZ";
import { fromLonLat } from "ol/proj";
const props = defineProps({
tk: {
type: String,
required: true,
default: "你的密钥key",
},
themeColor: { type: String, default: "#0d3883" },
center: {
type: Array,
default: () => [113.280637, 23.125178],
},
contrast: { type: Number, default: 1.0 }, // 新增对比度参数,>1 增强清晰度
sharpen: { type: Number, default: 0 }, // 锐化强度 (0~1,0为无锐化)
});
const mapContainer = ref(null);
let map = null;
// 将十六进制颜色转为 RGB 数组
const hexToRgb = (hex) => {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b);
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16),
]
: [19, 44, 93]; // 默认 #132c5d
};
const sobelX = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1],
];
const sobelY = [
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1],
];
// 判断是否为道路(浅灰/白色,高亮度,低饱和度)
function isRoadPixel(r, g, b) {
const brightness = 0.299 * r + 0.587 * g + 0.114 * b;
if (brightness < 200) return false;
const maxC = Math.max(r, g, b);
const minC = Math.min(r, g, b);
const saturation = maxC - minC;
// 道路:亮度高、色差小(接近灰色)
return saturation < 40;
}
// 判断是否为河流(蓝色主导)
// function isRiverPixel(r, g, b) {
// // 蓝色明显高于红和绿,且蓝色本身有一定强度
// return b > r + 30 && b > g + 30 && b > 80;
// }
// 新增:判断是否为水体(河流、湖泊等)------ 类似 isRoadPixel 的结构
function isWaterPixel(r, g, b) {
// 水体特征:蓝色分量显著高于红色和绿色,且整体亮度适中
const blueDominant = b > r + 35 && b > g + 25;
const notTooDark = b > 70;
const notTooBright = b < 230; // 避免误判白色反光
// 可选:排除极低饱和度(纯灰)的情况,因为水体通常带蓝色调
const maxC = Math.max(r, g, b);
const minC = Math.min(r, g, b);
const saturation = maxC - minC;
const enoughSaturation = saturation > 15; // 有一定的蓝色饱和度
return blueDominant && notTooDark && notTooBright && enoughSaturation;
}
const setMapTheme = (event) => {
const context = event.context;
const canvas = context.canvas;
const width = canvas.width;
const height = canvas.height;
const imageData = context.getImageData(0, 0, width, height);
const data = imageData.data;
const [targetR, targetG, targetB] = hexToRgb(props.themeColor);
const contrastFactor = props.contrast;
const edgeThreshold = 70;
const edgeDarkness = 0.8;
// 1. 计算亮度(用于后续染色)
const lum = new Float32Array(width * height);
for (let i = 0; i < data.length; i += 4) {
if (data[i + 3] === 0) continue;
const idx = i / 4;
let l = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
l = (l - 128) * contrastFactor + 128;
lum[idx] = Math.min(255, Math.max(0, l));
}
// 2. 边缘检测(用于边框变暗)
const edge = new Float32Array(width * height);
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
const idx = y * width + x;
let gx = 0,
gy = 0;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const l = lum[(y + ky) * width + (x + kx)];
gx += l * sobelX[ky + 1][kx + 1];
gy += l * sobelY[ky + 1][kx + 1];
}
}
edge[idx] = Math.hypot(gx, gy);
}
}
// 3. 逐像素处理
for (let i = 0; i < data.length; i += 4) {
if (data[i + 3] === 0) continue;
const rOrig = data[i];
const gOrig = data[i + 1];
const bOrig = data[i + 2];
// 优先判断道路和河流 → 直接填充黑色
if (
!(isRoadPixel(rOrig, gOrig, bOrig) || isWaterPixel(rOrig, gOrig, bOrig))
) {
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
continue;
}
// 其他区域:主题色染色 + 边缘暗化
const idx = i / 4;
let factor = lum[idx] / 255;
if (edge[idx] > edgeThreshold) {
factor = factor * edgeDarkness;
}
data[i] = targetR * factor;
data[i + 1] = targetG * factor;
data[i + 2] = targetB * factor;
}
context.putImageData(imageData, 0, 0);
};
onMounted(() => {
// 矢量底图 (vec_w)
const baseLayer = new TileLayer({
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk=${props.tk}`,
crossOrigin: "anonymous", // 必须,否则 Canvas 无法操作
}),
});
// 矢量注记 (cva_w) - 用户要求不用管颜色,保持默认即可
const labelLayer = new TileLayer({
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=cva_w&x={x}&y={y}&l={z}&tk=${props.tk}`,
crossOrigin: "anonymous",
}),
});
// 只对底图图层应用主题色,不影响注记层
baseLayer.on("postrender", setMapTheme);
map = new Map({
target: mapContainer.value,
layers: [baseLayer, labelLayer],
view: new View({
center: fromLonLat(props.center),
zoom: 12,
minZoom: 1,
maxZoom: 18,
}),
controls: [],
});
});
onUnmounted(() => {
if (map) map.setTarget(null);
});
</script>
<style scoped>
.map-container {
position: relative;
width: 100%;
height: 100%;
/* 关键:容器背景色设为目标色,防止瓦片加载瞬间闪烁黄色 */
background-color: v-bind("props.themeColor");
:after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at center, transparent 0%, #0c162f 100%);
pointer-events: none; /* 确保不阻挡地图点击 */
mix-blend-mode: multiply; /* 乘法混合模式,让线条变黑,亮部染上蓝色 */
z-index: 1;
}
}
/* 移除 OpenLayers 默认的水印和旋转按钮 */
:deep(.ol-attribution),
:deep(.ol-zoom) {
display: none;
}
:deep(.ol-viewport) {
background-color: v-bind("props.themeColor") !important;
}
</style>