玩转OpenLayers主题色修改,打造独一无二的个性化地图

最近接到了一个新需求,需要把天地图颜色换成自定义颜色。思索了半天无从下手,毕竟搞地图我不是专业的。但是做好牛马只能硬着头皮干了。

前言

  • 我有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>
相关推荐
yuanpan2 小时前
Python 开发一个简单演示网站:用 Flask 把脚本能力扩展成 Web 应用
前端·python·flask
IT_陈寒2 小时前
Python的GIL把我CPU跑满时我才明白并发不是这样玩的
前端·人工智能·后端
小江的记录本2 小时前
【分布式】分布式系统核心知识体系:CAP定理、BASE理论与核心挑战
java·前端·网络·分布式·后端·python·安全
freewlt2 小时前
企业级前端性能监控体系:从Core Web Vitals到实时大盘实战
前端
研☆香2 小时前
聊聊什么是AJAX
前端·ajax·okhttp
Freak嵌入式2 小时前
无硬件学LVGL:基于Web模拟器+MiroPython速通GUI开发—布局与空间管理篇
前端
Southern Wind2 小时前
我在 Vue3 项目里接入 AI 后,发现前端完全变了
前端·人工智能·状态模式
Bigger2 小时前
我手搓了一个开源版 Claude Code (mini-cc)
前端·ai编程·claude
qq4356947012 小时前
JavaWeb03
前端·css·html