OpenStreetMap + Canvas 城市地图实践:EveryCityMap 的五个工程取舍
EveryCityMap 的起点其实很简单:我想用真实城市做一些游戏地图。
上海被黄浦江切开,纽约由岛屿、河流和密集路网塑形,巴黎有向外发散的街道,东京则像一张不断生长的网络。真实城市本身已经很像经过设计的游戏关卡:道路是路径,河流是边界,桥梁是天然的连接点,不同城区也有自己的节奏。
我当时甚至没有先决定它会是一款策略游戏、探索游戏,还是别的什么类型。我只是想先得到一张足够干净、可以控制、也能继续加工的真实城市地图。现成地图适合导航和查找地点,却不一定适合作为游戏画面或创作素材,于是我决定从 OpenStreetMap 数据开始,自己 DIY 一个地图生成工具。
这次 DIY 最后慢慢长成了 EveryCityMap:一个运行在浏览器里的城市地图工作室。目前准备了 500 座城市,可以切换五种主题、编辑图层与颜色、生成分享图,并且无需注册就能导出 1440px WebP。

从一张游戏地图到一个城市地图工作室
一开始,我以为只要把道路画出来就够了。真正开始做之后才发现,一张能看的截图和一套可持续使用的城市地图系统完全是两件事。
如果地图只是一次性素材,很多问题可以手工修补;但当城市数量从一座变成 500 座,主题从一种变成五种,同样的修补方式就会迅速失控。上海需要处理大片水域与海岸,纽约需要在极高的道路密度里保留层级,内陆城市又不应该凭空出现海洋效果。
EveryCityMap 因此没有继续沿着“一张图一套配置”的方向发展,而是逐渐形成了这条链路:
真实城市数据 → 城市语义图层 → 主题规则 → Canvas 预览 / 分享 / 导出这里面有很多实现细节,但真正影响产品形态的,是下面五个工程取舍。
取舍一:把城市当成关卡,而不是一个矩形
普通在线地图通常从一个矩形视窗开始:中心点在哪里、缩放级别是多少、屏幕能看到多大范围。做游戏地图和城市视觉作品时,我更关心的是另一件事:这座城市自己的形状是什么?
河流可能把城市分成两部分,岛屿可能决定交通连接,环线可能形成清晰的活动区域。随意截取一个矩形虽然简单,却会把这些特征切断,也让不同城市看起来只是换了名字的同一张底图。
所以 EveryCityMap 会尽量保留城市的自然构图,再把道路、水域、陆地和其他信息组织到同一个空间里。这样得到的地图不只适合导出成壁纸;如果以后继续做游戏,城市结构也能自然成为路线、区域和关卡边界的基础。
取舍二:一份城市几何,支持所有视觉主题
我不希望 Ink、Paper、Night、Metro 和 Atlas 各自保存一份城市地图。它们看到的应该是同一座上海或纽约,只是强调的内容不同。
因此,城市几何和视觉主题被分成两部分:前者描述“城市里有什么”,后者决定“应该怎样被看见”。切换主题时不需要重新准备一座城市,只需要让 Canvas 按新的视觉规则重绘。
这使五套主题不只是五组颜色:
| 主题 | 主要表达 | 更适合的画面 |
|---|---|---|
| Ink | 道路骨架与黑白对比 | 极简海报、游戏底图 |
| Paper | 温和的纸张质感 | 旅行记录、打印作品 |
| Night | 暗色空间中的城市网络 | 屏幕壁纸、夜间氛围 |
| Metro | 更强的路线层级 | 交通感、节奏感画面 |
| Atlas | 水系、地表与道路共同出现 | 完整城市表达与深度编辑 |
具体的颜色、线宽和组合参数仍然属于主题本身;但从工程上看,重要的是所有主题共享同一份城市事实,不会因为换了风格就变成另一座城市。
取舍三:先理解图层的意义,再决定它的颜色
如果所有线条都只是“线”,所有色块都只是“面”,地图很快会失去层次。主干道、普通道路、铁路和河流在画面上可能都表现为线,但它们对城市的意义完全不同。
EveryCityMap 先把城市整理为道路、水域、陆地、绿地、铁路等语义图层,再由主题决定显示哪些内容。Ink 可以只留下克制的道路网络;Atlas 则能展示更完整的自然和城市信息,并允许用户关闭图层、修改颜色。
这种设计最初是为了地图编辑,后来我发现它也保留了游戏化的可能:同一座城市可以强调水路、交通网络或某类区域,而不必重新处理底层几何。数据没有直接绑死在某个画面上,产品才有继续生长的空间。
取舍四:预览和导出必须是同一张图
地图编辑器最容易出现的体验问题,是屏幕上看到的效果和最终导出的图片不一致。字体、透明度、线宽和绘制顺序只要有一处走了不同路径,用户调好的地图就可能在下载后变样。
我最终让编辑器预览、分享图和 WebP 导出共同依赖同一套 Canvas2D 渲染逻辑。界面负责修改状态,渲染器负责把状态变成画面。这样做并不炫技,却能保证用户看到什么,导出的就是什么。
这部分也延续了我过去做 Canvas 数据可视化 时的经验:真正困难的通常不是把图形画出来,而是建立稳定的绘制顺序。EveryCityMap 会先处理水域与地表,再叠加自然特征、道路层级和必要文字。顺序一旦错了,道路会盖住河流,海岸会变成生硬边框,城市也会失去空间感。

取舍五:500 座城市不能依赖临时在线计算
只做一张地图时,可以等待在线服务返回数据;做成产品后,用户不会关心数据来自哪里,只会感受到页面是不是稳定、第二次打开是不是还要重新等待。
因此,EveryCityMap 会提前准备已经进入目录的城市数据,再通过 CDN 和浏览器本地缓存交付。主题切换只改变视觉表达,不重复下载城市几何;用户再次打开同一座城市时,也可以尽量复用已经存在的数据。
我没有把公开地图服务当成产品实时可用性的保证。它们更适合数据准备与失败重试,而不是让每一次用户访问都重新经历采集过程。这个选择增加了前期准备工作,却换来了更可预测的体验,也让 500 座城市真正成为产品内容,而不是 500 个随时可能失败的在线请求。
最费时间的不是功能,而是浏览器里的“看起来不对”
Atlas 是目前最难调的一套主题。它需要让海岸、岛屿、河流、植被和道路同时存在,又不能让任何一层抢走整张地图。
其中一个很具体的问题出现在 Safari:原本用于柔化海岸的 Canvas 模糊效果,会产生有规律的色带。继续增大模糊值并不能解决问题,反而让边缘更脏。最后我改为先在较低分辨率的透明画布上分别处理海岸效果,再合成回最终画面。
这里真正有价值的经验不是某一段代码,而是:Canvas 在不同浏览器中的视觉结果不能只靠参数推测。对地图、绘画和数据可视化产品来说,“数学上连续”不代表“人眼看起来连续”,最终仍然需要用真实城市反复检查。
回到最初:真实城市还能变成什么?
EveryCityMap 现在首先是一个城市地图工作室,但我仍然喜欢它最初的那个问题:如果把真实城市当成游戏世界,而不是导航背景,会发生什么?
一条河可以成为边界,一座桥可以成为争夺点,城市道路可以生成移动网络,不同主题也可能对应完全不同的游戏规则。即使这些想法最后没有全部变成游戏,它们仍然推动我做出了一个可以被其他人使用的地图工具。
产品目前还很早。我会继续调整城市构图、主题和编辑体验,也会观察大家究竟把地图用作壁纸、旅行纪念、设计素材,还是一些我没有想到的东西。
如果你愿意,可以选择自己的城市试一下。我也很好奇:如果把你的城市做成一张游戏地图,你会先保留什么,又会隐藏什么?
CURRENTLY BUILDING
EveryCityMap
Style, share, and export custom maps for 500 cities. Choose a theme, edit the layers, and download a 1440px WebP without signing up.