Leaflet + MapLibre 双引擎,这个开源工具把任意城市变成装饰级地图海报
map-to-poster 是开发者 Dimar Tarmizi 开源的一个地图海报生成工具,670 Star。核心功能是把任意地理位置转成可打印的海报,但它的主题系统走的是偏艺术的方向——除了常规的 Minimal White 和 Midnight Dark,还有 Arctic Frost、Aurora Glow、Cyber Glitch 这类有明确视觉风格的主题。
在线 demo:https://maptoposter.tarmizi.id (opens in a new tab)

双引擎渲染与视口同步
项目同时集成了两套地图渲染引擎:Leaflet 和 MapLibre GL。
Leaflet 基于图片瓦片渲染,处理标准地图和卫星图——数据已经在服务端渲染好,以图片形式分块传输。MapLibre GL 基于矢量瓦片,把原始地理数据传到浏览器里,由 GPU 实时渲染,样式完全由代码控制,是艺术主题的技术基础。
两套引擎同时运行,需要保持视口同步——切换主题时,地图的位置和缩放级别不能变。项目用双向事件监听实现这一点,并用 isSyncing 标志防止两个引擎互相触发对方的 moveend 事件造成死循环:
map.on('moveend', () => {
if (isSyncing) return;
isSyncing = true;
const center = map.getCenter();
const zoom = map.getZoom();
// Leaflet 坐标是 [lat, lng],MapLibre 是 [lng, lat],需要转换
artisticMap.jumpTo({ center: [center.lng, center.lat], zoom: zoom - 1 });
isSyncing = false;
});
artisticMap.on('moveend', () => {
if (isSyncing) return;
isSyncing = true;
const center = artisticMap.getCenter();
const zoom = artisticMap.getZoom();
map.setView([center.lat, center.lng], zoom + 1);
isSyncing = false;
});注意缩放级别的 ±1 差值——两个引擎对同一区域的默认缩放比例不同,通过这个偏移量补偿对齐。
样式切换队列
MapLibre 切换主题是异步的,加载新样式需要时间。如果用户快速连续切换主题,前一个样式还没加载完就来了新请求,会产生冲突。项目用一个队列机制处理这个问题:
if (styleChangeInProgress) {
// 缓存最新的待切换样式,丢弃中间状态
pendingArtisticStyle = style;
pendingArtisticThemeName = theme.name;
return;
}
// 样式加载完成后,检查队列里是否还有待处理的样式
artisticMap.on('style.load', () => {
if (pendingArtisticStyle) {
const next = pendingArtisticStyle;
pendingArtisticStyle = null;
artisticMap.setStyle(next);
}
});主题与自定义
内置主题分两类:
标准主题:Minimal White(简洁白底)、Midnight Dark(深色)、Satellite View(卫星图),适合常规城市地图海报。
艺术主题:Arctic Frost(冷色系冰雪感)、Aurora Glow(渐变发光)、Cyber Glitch(故障艺术风格)等,视觉风格更强烈,适合做装饰性海报或创意设计素材。
如果内置主题不够用,可以直接编辑项目里的 artistic-themes.js 文件添加自定义主题,每个主题本质上是一套 MapLibre 样式配置。
排版方面,提供了多种相框样式和留白(Mat)选项,可以选择不同字体和文字内容,组合出画廊风格的海报版式。
路线与标记点
除了静态地图,项目支持在地图上添加可视化路线和拖拽标记点——比如标出一段旅行的轨迹,或者在特定地点放置标记,导出带有这些元素的海报。
这让海报从"某个地方的地图"变成"某段经历的记录",适合做旅行纪念、活动路线图等个性化内容。
高分辨率导出:多层 Canvas 合成
导出格式是 PNG,分辨率最高 50,000px,所有处理在浏览器本地完成。
导出不是简单地截一张屏幕图,而是把地图层和 UI 层分开处理再合成。地图层(Leaflet 或 MapLibre)直接从引擎的 canvas 读取像素,UI 层(相框、标题、坐标文字)用 html2canvas 单独渲染。两层合成到最终 canvas 上输出:
// 忽略地图元素,只捕获 UI 覆盖层
const overlayCanvas = await html2canvas(element, {
useCORS: true,
scale: scale,
ignoreElements: (el) => el.id === 'map-preview' || el.id === 'artistic-map'
});
// MapLibre 地图层:等待渲染完成再读取 canvas
await new Promise(resolve => {
const timer = setTimeout(() => {
mapDataURL = artisticMap.getCanvas().toDataURL();
resolve();
}, 1500);
// 优先监听 idle 事件,地图空闲时立即读取
artisticMap.once('idle', () => {
clearTimeout(timer);
mapDataURL = artisticMap.getCanvas().toDataURL();
resolve();
});
});高分辨率通过 scale 参数控制——scale 越大,canvas 尺寸越大,图片越清晰。同时针对 iOS 的 canvas 像素上限(16,777,216px)做了兜底处理,超出限制时等比缩小:
if (canvasWidth * canvasHeight > IOS_MAX_CANVAS_PIXELS) {
const ratio = Math.sqrt(IOS_MAX_CANVAS_PIXELS / (canvasWidth * canvasHeight));
canvasWidth = Math.floor(canvasWidth * ratio);
canvasHeight = Math.floor(canvasHeight * ratio);
}状态管理:观察者模式
项目没有用 Redux 或 Pinia,而是自己实现了一个轻量的观察者模式。subscribe 订阅状态变化,updateState 触发所有订阅者更新,同时把需要持久化的字段写入 localStorage:
export function subscribe(callback) {
observers.push(callback);
callback(state); // 立即执行一次,同步初始状态
}
export function updateState(partialState) {
Object.assign(state, partialState);
saveSettings(); // 持久化到 localStorage
notifyObservers(); // 通知所有订阅者
}这套机制让地图、主题、导出三个模块各自订阅自己关心的状态变化,互不耦合。
技术栈
| 技术 | 版本 | 用途 |
|---|---|---|
| Vanilla JavaScript | - | 核心逻辑,无框架依赖 |
| Vite | 5 | 构建工具 |
| Leaflet | - | 光栅瓦片地图渲染(标准/卫星图) |
| MapLibre GL | - | 矢量瓦片地图渲染(艺术主题) |
| Tailwind CSS | 3 | 样式 |
| html2canvas | - | 将页面渲染结果导出为 PNG |
| Nominatim | - | OpenStreetMap 地理编码,支持地名搜索 |
几个值得注意的选型:
Vanilla JS 而非框架:整个项目没有使用 React 或 Vue,状态管理用观察者模式自行实现,设置通过 localStorage 持久化。对于想学习不依赖框架构建复杂 Web 应用的开发者,这份代码值得参考。
html2canvas 导出:导出 PNG 的方式是把当前 DOM 渲染结果转成 canvas,再导出图片。这种方案的优点是实现简单,缺点是导出结果受浏览器渲染影响,与 PDF 矢量导出的思路不同。50,000px 的超高分辨率通过缩放 canvas 尺寸实现。
双引擎同步:Leaflet 和 MapLibre GL 分别维护自己的渲染上下文,项目在两者之间同步视口状态(中心坐标、缩放级别),确保切换引擎时画面位置不跳变。
安装与运行
需要提前安装 Node.js 18 及以上版本。
git clone https://github.com/dimartarmizi/map-to-poster.git
cd map-to-poster
npm install
npm run dev生产构建:
npm run build写在最后
map-to-poster 和同类工具的主要差异在于两点:一是双引擎架构让卫星图和矢量艺术主题在同一套界面里共存;二是主题系统覆盖范围更广,Cyber Glitch 这类风格在同类工具里比较少见。
项目是 MIT 协议,允许商业使用和二次开发。如果你想做一个有特定视觉风格的地图海报应用,它的双引擎架构和主题系统是值得参考的起点。
GitHub 地址:https://github.com/dimartarmizi/map-to-poster (opens in a new tab)