经纬志
首页 / MapLibre / 教程

图标与字体

先看结论

符号图层画两种东西:图标与文字。图标来自雪碧图(sprite)或运行时添加的图片, 文字来自字体切片(glyphs)。两者都是二进制资源,配错时图层往往静默为空。

最后验证 2026-11-03 · MapLibre GL JS 4.x · 更新 2026-10-29 · 引用本文

sprite 与 glyphs 的位置

样式顶层的 sprite 指向一个 JSON 索引,索引里记录了每张图标在 雪碧图里的位置与尺寸;glyphs 是 PBF 字体的地址模板, MapLibre 会按 {fontstack}/{range} 逐段取字。

{
  "version": 8,
  "sprite": "https://example.com/styles/basic/sprite",
  "glyphs": "https://example.com/fonts/{fontstack}/{range}.pbf",
  "sources": { /* ... */ },
  "layers": [
    {
      "id": "poi-label",
      "type": "symbol",
      "source": "poi",
      "layout": {
        "icon-image": ["get", "kind"],
        "icon-size": 0.8,
        "text-field": ["get", "name"],
        "text-font": ["Noto Sans Regular"],
        "text-size": 12,
        "text-offset": [0, 1.2]
      }
    }
  ]
}

sprite 不写扩展名,MapLibre 会自动拼出 .json 与 .png(或 @2x 版本)。手写 .json 后缀会导致 请求路径变成 sprite.json.json。

运行时添加图片

图标不在雪碧图里时,用 map.addImage() 直接塞进去。 图片可以是 ImageData、HTMLImageElement, 也可以是 {width, height, data} 形式的原始像素。

function makeDot(color) {
  var size = 48;
  var canvas = document.createElement('canvas');
  canvas.width = canvas.height = size;
  var ctx = canvas.getContext('2d');

  ctx.beginPath();
  ctx.arc(size / 2, size / 2, size / 2 - 4, 0, Math.PI * 2);
  ctx.fillStyle = color;
  ctx.fill();
  ctx.lineWidth = 4;
  ctx.strokeStyle = '#ffffff';
  ctx.stroke();

  return ctx.getImageData(0, 0, size, size);
}

map.on('load', function () {
  for (var i = 0; i < 5; i++) {
    map.addImage('dot-' + i, makeDot('#17694a'), { pixelRatio: 2 });
  }

  map.addLayer({
    id: 'dots',
    type: 'symbol',
    source: 'sites',
    layout: {
      'icon-image': ['concat', 'dot-', ['to-string', ['get', 'level']]],
      'icon-size': 0.5,
      'icon-allow-overlap': true
    }
  });
});

pixelRatio: 2 表示图片是 2 倍图。设了之后, icon-size: 0.5 的实际显示尺寸等于设计稿尺寸; 不设 pixelRatio 时图片会按原始像素渲染,在高分屏上显得偏大。

可染色的 SDF 图标

普通图片的颜色被锁死在像素里,改 icon-color 没有效果。 用 sdf: true 添加的图标保存的是距离场,可以被 icon-color 与 icon-halo-color 染色。

map.addImage('pin-sdf', imageData, { sdf: true, pixelRatio: 2 });

map.addLayer({
  id: 'pins',
  type: 'symbol',
  source: 'sites',
  layout: { 'icon-image': 'pin-sdf', 'icon-size': 0.6 },
  paint: {
    'icon-color': ['match', ['get', 'status'],
      'ok',    '#17694a',
      'warn',  '#c2410c',
      '#6b7280'
    ],
    'icon-halo-color': '#ffffff',
    'icon-halo-width': 1
  }
});

文字的四个关键属性

属性作用常见错误
text-field文字内容,通常是 ['get', 'name']属性为数字,未用 to-string
text-font字体栈,必须与 glyphs 服务一致写成 CSS 字体名,文字全空
text-size字号,可随缩放插值单位写成 px,实际是无单位数值
text-halo-width描边宽度,压在深色底图上必需大于 2 后文字糊成一团

text-font 是数组,表示字体栈。服务里存在 Noto Sans Regular 这一组,就直接写这个名字; 写 Noto Sans(缺少字重)会取不到字体。

避让与重叠

符号图层默认参与碰撞检测:文字互相避让,被挤掉的要素不绘制。 调试时把 text-allow-overlap 打开,能立刻看出是数据没进来, 还是被避让逻辑吃掉了。

map.addLayer({
  id: 'station-labels',
  type: 'symbol',
  source: 'stations',
  minzoom: 12,
  layout: {
    'text-field': ['get', 'name'],
    'text-font': ['Noto Sans Regular'],
    'text-size': ['interpolate', ['linear'], ['zoom'], 12, 11, 18, 15],
    'text-padding': 4,
    'text-max-width': 8,
    'text-allow-overlap': false,
    'text-ignore-placement': false
  },
  paint: {
    'text-color': '#12202c',
    'text-halo-color': 'rgba(255,255,255,0.9)',
    'text-halo-width': 1.5
  }
});

注意事项

addImage 的图片在换样式后消失

addImage 只作用于当前地图实例的图片表,不写进样式 JSON。 setStyle 之后需要重新 addImage, 否则 icon-image 找不到图片,整个符号图层不绘制。

图标名大小写与拼写

雪碧图里的图标名区分大小写。用 ['get', 'icon'] 直接把数据字段 当图标名,数据里一个拼写不一致的图标就静默缺失。上线前跑一遍 map.hasImage(name) 校验。

字体切片缺失中文

多数公共字体服务只提供拉丁字符。中文注记要么自建 glyphs(用 fontnik 切分 Noto Sans SC),要么改用 HTML 标记(Marker)承载文字。 前者体积大,后者会失去随地图旋转的能力。

引用本文

经纬志(2026)。《图标与字体》。https://www.jingweizhi.com/maplibre/tutorial/sprite-glyphs.html(访问日期 2026-11-03)。