设计档案馆
中EN

从一张 3D 倾斜地图到一个离线工具

设计稿里的 3D 倾斜地图,透视强度交不给前端,因为地图 SDK 的视场角是写死的。我把一个一次性的倾斜出图需求做成了单文件离线工具,方向也反过来:先量出前端地图的透视,再让设计稿去迁就它。

L.14 / 14
CATEGORY
工具
YEAR
2026
STATUS
笔记
TOOLS
svg / geojson / mercator

entry记录

一块数据可视化大屏的主视觉是一张带 3D 倾斜效果的地图,地块有厚度,侧面有渐变,顶面贴着地表纹理;设计稿在矢量设计工具里做,前端用一款 WebGL 地图 SDK 实现。起点只有一个 JSON 文件,由某款设计工具的 3D 地图插件导出,我想知道能不能在设计工具里直接打开。拆开文件后,问题有四个:文件里只有平面轮廓加深度、相机位置、几盏点光源这组三维渲染参数,3D 效果是插件运行时实时算的,没有现成的 3D 矢量;矢量设计工具能把图形压扁来模拟倾斜,但做不出近大远小;前端地图 SDK 的视场角是写死的,设计稿里的透视调得再好看,前端也未必还原得出;侧面要能在设计工具里单独填渐变,厚度必须是一个独立、干净的图层。

最早只要求沿 X 轴旋转 30°,Python 脚本把 Y 坐标乘以 cos30°,即正交压缩。结果画面太平,没有空间感,于是改为透视。透视可以理解为把地图放到一个真实相机前面拍,离镜头越近的部分越大;相机离地图多远做成一个参数,用来控制近大远小的强弱。

同一个 45° 倾斜角:左边是正交,各处缩放一致(×1.00);右边是透视,下缘放大 1.42 倍,上缘缩小到 0.77 倍。

每调一次角度都要跑一遍脚本,太慢,于是做成单文件 HTML 工具,要求能导入 SVG,能调三轴旋转、投影模式和透视强度,有实时预览且预览能关掉以免大文件卡顿,能导出 SVG。第一版另外支持了贝塞尔曲线和圆弧,把嵌套的坐标变换属性(即 transform)算进坐标,保留继承的样式,导出不带 transform 的纯 path,所有步骤都在浏览器本地完成。同一个操作做第三次,就考虑做成工具;这次从脚本改成工具,是在第三次改参数重出图的时候。

处理流程:所有步骤都在浏览器本地完成。

设计稿的参数要交给前端:倾斜角对应前端的 tilt(俯仰角),Z 轴旋转对应 heading(旋转朝向),可以直接照抄;Y 轴旋转在地图里没有对应参数;透视强度传不过去,因为前端 SDK 的视场角固定,透视强弱只由缩放级别和容器高度决定。于是方向反过来,不让前端迁就设计稿,先量出前端地图的真实透视,再让设计稿迁就它。一段标定脚本在前端页面上量出同一段实地距离在画面下缘和上缘的像素宽度之比 R,再把 R 换算回工具里的相机距离;R 越接近 1,近大远小越不明显,算出的相机越远。先在模拟地图上验证,设定相机距离 1.2、地图朝向转 37°,脚本反算回来正好是 1.2000,并自动把朝向恢复原状,验证通过。设计稿和前端对齐,靠一份共享的参数和一套可执行的标定方法,不靠效果图做得像。参数存成参数方案 JSON,标定方法写进一份交接规范。

手动导出 SVG 再导入太绕,我想直接用行政区的 GeoJSON(一种带经纬度的地图数据格式)。为此评估了三种数据来源:手动导入文件、内置离线数据包、直接调用前端地图 SDK 自带的边界数据;实测一个公开数据网站的接口,除个别热门数据外,跨站调用基本被防盗链拦截,返回 403。因为需求只是设计稿和前端用同一份数据,另两种方案的成本和授权风险不值得,所以先只做手动导入。导入改用大多数 Web 地图底层采用的墨卡托投影,平面形状本身就和前端一致;工具自动算出前端需要的中心点和缩放级别,并支持国内常见的坐标加偏转换。

厚度在设计工具里必须是独立图层,而厚度图层的形状决定渐变好不好看。比较了三种做法后,选了「可见侧边带」:先把所有区域合并成外轮廓,使省与省之间不冒出侧壁;再生成侧壁,剔除背对镜头的部分;最后用多边形布尔运算减掉顶面,只留下真正看得见的那一圈。为此内嵌了一个开源的多边形裁剪库,工具仍是单文件、可离线运行;需要沿厚度方向做明暗过渡时,可以把厚度按深度切成多层,每层一个颜色。

厚度按深度切成 5 层,每层一个颜色。

最初用脚本裁出的一张卫星图只是平面图,放进设计工具后没法跟着地图倾斜,于是评估能不能把生成纹理和贴图都做进工具。地图顶面是一个平面,平面在透视下的变形是一次固定的映射(数学上叫单应变换),所以每个输出像素都能精确反算回地图上的经纬度,再到卫星图上取色。生成的纹理和矢量轮廓严丝合缝,数据源是一张内置在工具里的公共领域全球卫星影像。另一条要求是纹理里不要有边界线:纹理本身只含影像像素,边界线来自顶面的描边,所以把描边做成独立开关。

功能加多后,右侧面板成了一条要滚动两屏半的长清单,问题不在视觉,而在信息架构。按优先级做了调整:右侧分成「视图 / 外观 / 交付」三个标签页;导出入口收拢成一个弹窗;选中图层后右侧切换为该图层的属性,和常见设计工具一致;零散提示汇总到状态栏;画布上增加前端取景框;支持保存参数方案。

工具界面:左侧图层,中间画布,右侧「外观」标签页。
「交付」面板实时生成前端代码,虚线框是前端容器在这个缩放级别下实际能看到的范围。

最早的透视脚本在真实地图上画出了一个巨大的矩形。先怀疑渲染器,再逐个图层排查,发现原因是原始路径用了只带一个坐标的水平线、垂直线命令(即 H 和 V 命令),脚本把所有数字两两配对当成 x、y,从某个位置开始整条路径错位。修复后发现之前交付的两个文件的画布尺寸也是这个 bug 算错的,一并重新生成;正式做工具时,解析器支持了全部路径命令,包括压缩工具常用的紧凑写法。每一步都要在真实数据上跑,并且看图,不只看测试通过。

用样例数据复现的同一个问题:左边把所有数字两两配对,是错误解析;右边识别出 H / V 只带一个坐标,是正确解析。

某次单元测试里有一条断言其实失败了,当时却报告了「全部通过」,回顾时才发现。复查发现是测试本身写错了,代码是对的。此后看测试结果先看原始输出,不只看结论。

同一份地图拆成几块分别导入时,结果叠在了一起。原因是每个 GeoJSON 图层被单独缩放到「长边 800px」,经纬度信息在这一步丢了。解决办法是让所有地理图层共用一套墨卡托坐标系,使用同一个原点、同一个比例尺。

先导入一个区域,再加入范围更大的周边区域,主体明显变平、变小,这是我自己观察到的。原因是镜头的中心和相机距离按所有图层的总范围算,范围一大,主体只占视野中间一小块,近大远小自然变弱。办法是引入「镜头基准图层」,镜头只围绕基准图层计算,加入别的图层不影响它;厚度改为锁定真实距离,换基准或改尺寸时自动换算。

同一个区域 A:左边以 A 为基准,近 / 远缩放比 1.84;右边把基准换成范围更大的区域 B,A 变小,近 / 远比降到 1.63。修复前加入周边图层时就是右边这种效果。

预览里纹理变黑而导出正常的问题,修了两轮。第一轮在测试中复现出,只要有输入框处于聚焦状态,浏览器就可能一直推迟异步的图片编码,纹理迟迟不出现,改成同步编码后解决。第二轮又出现纯黑,但在开发环境里没能复现,推断是复杂图层的 SVG 图片裁剪在部分浏览器里渲染异常,预览改成「画布绘制 + 轮廓遮罩」,完全绕开这条渲染路径,导出格式不变。这次修复没有在出问题的那台浏览器上复现验证,所以记作「大概率已解决」,不是「已确认解决」。

为了实现方便,最初的图层列表是越靠下画得越靠上,和主流设计工具正好相反,只在说明文字里提了一句;我觉得这违反直觉,要求改掉。修改后列表顺序和设计工具一致,支持拖动排序,并加了一个「按远近自动排序」按钮,专门解决分块挤出时远处侧壁盖住近处的问题。

为这篇记录截图时,取景框显示的缩放级别是 NaN。原因是引入镜头基准时,一部分数据挪到了别的地方,取景框还在读旧位置。这个问题已在工具里修好;每次重构后,都要把所有入口完整走一遍。

从一个打不开的配置文件开始,经过二十多轮迭代,交付的是一个约 2.3 MB 的单文件 HTML,在浏览器里双击就能离线运行,覆盖从导入数据到交接前端的完整流程。每一版都在真实数据上跑过,由我验收。文中截图都来自工具的真实运行结果;出于数据授权和地图合规的考虑,演示用的是公共领域的东南亚样例区域,不是项目里的真实数据。

导出弹窗:图层 SVG、纹理 PNG 和参数方案 JSON,多个文件自动打包。

process对比

参数映射(设计稿 → 前端地图 SDK)

X 轴旋转
tilt,0 为正俯视,常见上限约 70°,以 SDK 实测为准
Z 轴旋转
heading,正负号因 SDK 而异,先用一行代码实测
Y 轴旋转
无对应参数,地图不支持滚转,设计稿必须为 0
相机距离
无对应参数,SDK 视场角固定,需反向标定
镜头基准图层的中心
center,由工具按经纬度算出
容器尺寸 + 基准图层范围
zoom,工具给出估算值,需真机微调
必须一起冻结
容器尺寸、zoom、center、tilt、heading

透视强度标定

text
1. 前端按上表设置好地图
2. 在页面控制台运行工具生成的标定脚本,
   量出同一段实地距离在画面下缘和上缘的像素宽度之比 R
3. 把 R 填回工具,相机距离自动换算:

   相机距离系数 = sin(倾斜角) × (R + 1) / (2 × (R − 1))

工具能力

导入
SVG:全部路径命令、class / style 样式、use 引用、嵌套 transform · GeoJSON:地图选区、搜索、坐标加偏转换
视图
三轴旋转 · 透视 / 正交 · 相机距离 · 视角预设 · 近端 / 远端缩放实时读数
外观
挤出厚度:向下 / 向上、1–8 层切片、侧面渐变 · 内置卫星纹理或自定义图片:调色、着色 · 边界线开关
图层
单图层样式覆盖 · 参与效果开关 · 拖动排序 · 按远近自动排序 · 镜头基准
交付
前端参数代码 · 取景框 · 透视标定脚本 · 参数方案 JSON
导出
分层 SVG(厚度 / 纹理 / 顶面)· 纹理 PNG · 方案 JSON · 多个文件自动打包

导出到设计工具的图层结构

厚度
侧面;单层时自带上→下渐变,可直接替换填充;多层时为「厚度-1 … 厚度-N」
纹理
地表影像(位图),已按顶面轮廓裁剪
顶面
各区域矢量,id 为区域名
坐标
已烘焙进 path,没有 transform 包裹;多个图层共用同一投影原点,叠放即对齐

result小结

结论
透视强度传不给前端(地图 SDK 的视场角固定),改为反向标定:量出同一段实地距离在画面下缘和上缘的像素宽度之比 R,换算回工具里的相机距离;在模拟地图上设定相机距离 1.2,反算回来正好是 1.2000。
参数
X 轴旋转 → tilt · Z 轴旋转 → heading · Y 轴旋转必须为 0 · 相机距离系数 = sin(倾斜角) × (R + 1) / (2 × (R − 1)) · 容器尺寸、zoom、center、tilt、heading 五项一起冻结
没做完
预览里纹理变黑的第二轮修复没有在出问题的那台浏览器上复现验证,只能记作「大概率已解决」;工具给出的 zoom 是估算值,需要真机微调。

log日志

2026-10
首次记录

相关实验

← LabL.14 / 14
1 / 1

100%