1. 一个旋转的地球,为什么值得单独写一篇
第一次在别人的数据看板上看到一个缓缓自转、表面还带着淡淡光晕的三维地球时,我盯着看了好一会儿。那种感觉挺奇妙的——明明只是一堆点线面在浏览器里转,但它就是比一张静态的世界地图更有"信息在流动"的暗示。后来翻源码才知道,那个效果背后站着的就是planetary.js这个库。
planetary.js 是一个基于D3.js和TopoJSON的 JavaScript 库,专门用来在网页里生成可交互、可自转的动态地球。它做的事情说起来很朴素:把一份地理边界数据(通常是世界地图的 TopoJSON 文件)投影到一个球面上,用 SVG 或 Canvas 渲染出来,然后让这个球慢慢转起来。但就是这么一个"朴素"的功能,在数据可视化、地理信息展示、大屏看板、教学演示这些场景里,出镜率高得惊人。
它解决的核心问题其实有两个。第一是门槛:自己从零用 Three.js 或者 WebGL 撸一个地球,光是球面投影、经纬度换算、光照贴图就够折腾好几天,而 planetary.js 把这一层全封装好了,你只需要几行配置就能跑起来。第二是体积与依赖:它不依赖任何重型 3D 引擎,纯靠 D3 的投影能力 + SVG/Canvas 就能出效果,对于那种"我只想要个能转的地球,别给我整一堆 WebGL 上下文"的需求来说,简直是量身定做。
这篇文章适合谁看?如果你做过数据大屏、做过地理相关的可视化、或者单纯想给自己的个人主页加一个有点意思的动态地球,那这篇内容你应该能直接抄作业。如果你是完全没碰过 D3 和地理数据的新手,我也会把 TopoJSON、投影、经纬度这些概念用生活化的方式讲清楚,保证你看完能自己动手跑出一个来。下面我会从整体设计思路、核心细节、完整实操、常见坑排查这几个角度,把这个库彻底拆一遍。
2. 整体设计思路:为什么是 D3 + TopoJSON 这套组合
2.1 从"画一个球"到"投影一份地图"的思路转变
很多人第一次接触动态地球,脑子里想的是"我要画一个球体"。这个思路会直接把你引向 Three.js、Babylon.js 这类 3D 引擎,然后你就会陷入球体网格、材质贴图、光照、相机控制的泥潭里。planetary.js 的作者显然想得更聪明一点:我不画球,我画的是"被投影到球面上的地图"。
这个思路的关键在于 D3 的**地理投影(geo projection)**能力。D3 内置了一堆投影方式,比如等距圆柱投影、墨卡托投影、正射投影(orthographic)等等。其中正射投影的效果就是"从一个很远的距离看一个球体",它会把地球背面的部分裁掉,只显示朝向你的那一面——这正是我们想要的那种"地球仪"效果。所以 planetary.js 本质上做的事情是:拿一份地理边界数据,用正射投影把它投到一个虚拟球面上,再用 SVG 的 path 把投影后的路径画出来。
这个思路的好处非常明显。首先,渲染层和 3D 计算层解耦了,你不需要维护一个 3D 场景,只需要在每一帧更新投影的旋转参数,然后重新生成 path 就行。其次,SVG 天然支持交互,你想给某个国家加 hover 效果、点击事件,直接绑在 path 上就行,比在 WebGL 里做拾取(picking)简单太多。最后,体积小,整个库压缩后也就几十 KB 级别,对页面性能友好。
2.2 TopoJSON 相比 GeoJSON 到底省在哪
地理数据这块,绕不开两个格式:GeoJSON 和 TopoJSON。planetary.js 默认吃的是 TopoJSON,这里有必要解释一下为什么。
GeoJSON 你可以理解成"每个国家/地区各自记一份自己的边界点坐标"。问题在于,相邻的两个国家共享的那条边界,在 GeoJSON 里会被重复存储两次——A 国记一遍,B 国再记一遍。世界地图那么多国家,这种重复累积起来,文件体积会膨胀得很厉害。
TopoJSON 的思路是"先把所有边界拆成弧段(arcs),每个弧段只存一次,然后每个国家记录自己由哪些弧段组成"。这样一来,共享边界只存一份,整个文件的体积通常能压到 GeoJSON 的20% 到 30%。对于要在网页里加载的世界地图数据来说,这个差距是决定性的——一个 1MB 的 GeoJSON 和一个 250KB 的 TopoJSON,在弱网环境下的加载体验完全是两回事。
提示:planetary.js 内部依赖 topojson 这个库来把 TopoJSON 转成可绘制的几何对象,所以你在引入 planetary.js 的时候,topojson 和 d3 都得一起引进来,顺序不能乱。
2.3 旋转动画的实现逻辑:不是"转球",是"转投影"
这是很多人容易误解的一点。planetary.js 让地球转起来,并不是真的在旋转一个 3D 球体,而是在每一帧里改变投影的旋转角度参数。
D3 的投影对象有一个.rotate([lambda, phi, gamma])方法,三个参数分别控制绕三个轴的旋转。planetary.js 在动画循环里,每一帧把 lambda(经度方向的旋转)增加一点点,然后重新调用投影生成所有 path 的d属性。因为正射投影会根据旋转角度重新计算"哪一面朝向观察者",所以视觉上就产生了地球在自转的效果。
这个实现方式有个很实际的好处:你控制旋转速度、方向、甚至暂停,都只是改一个数值的事,不需要去操作任何相机或者场景图。想让它转快点就调大增量,想让它反向转就把增量取负,想让它停在某个经度上就把增量设为零。这种"参数化动画"的思路,比 3D 引擎里的相机控制要直观得多。
3. 核心细节解析:投影、数据、渲染三件套
3.1 正射投影的参数怎么调才好看
正射投影(orthographic projection)是 planetary.js 的默认投影,也是做"地球仪"效果最自然的选择。它的核心参数有这么几个:
- scale(缩放):决定地球在画布上占多大。这个值不是随便给的,它和画布尺寸、以及你想要的留白有关系。经验上,如果你画布是 800x800,scale 给到 350 到 380 之间比较合适,留出一点边距让光晕或者阴影有地方放。
- translate(平移):地球中心在画布上的位置。通常设成画布宽高的一半,也就是
[width/2, height/2],这样地球居中。 - rotate(旋转):前面说过的三轴旋转,动画就是靠改这个。
- clipAngle(裁剪角):这个参数很关键,默认是 90 度,意思是"只显示朝向观察者 90 度范围内的部分"。如果你把它调大,比如 100 度,地球边缘会稍微"翻"过去一点,看起来更饱满;调小则边缘更"薄"。一般保持 90 就行,特殊审美需求再动它。
我实测下来,scale 这个值最容易出问题。给太小,地球缩在中间一小块,周围全是空白;给太大,地球边缘会被画布裁掉,看起来像被切了一刀。建议的做法是先用一个保守值跑起来,然后根据实际渲染效果微调,别一上来就追求"填满画布"。
3.2 地理数据的来源与预处理
planetary.js 官方示例里用的是一份世界地图的 TopoJSON 数据。实际项目中,你可以用几份不同的数据源:
| 数据内容 | 常见来源类型 | 体积量级 | 适用场景 |
|---|---|---|---|
| 世界国家边界 | 公开地理数据集 | 200KB - 1MB | 通用地球展示 |
| 简化版世界地图 | 低精度数据集 | 50KB - 150KB | 移动端、弱网 |
| 特定区域边界 | 区域地理数据集 | 视范围而定 | 聚焦某地区 |
| 经纬度点位 | 自定义 JSON | 极小 | 标记城市/事件 |
这里有个实操经验:如果你的地球只是背景装饰,不需要精确到每个小岛,那就用简化版数据。原始的世界地图数据里,光是各种小岛屿的边界点就能占掉一大半体积,而它们在直径几百像素的地球上根本看不清。用简化版能把加载时间砍掉一大截,视觉上几乎没差别。
数据预处理这块,如果你手上只有 GeoJSON,可以用命令行工具转成 TopoJSON。转换的时候有个--simplify参数可以控制简化程度,值越大简化越狠、体积越小,但边界也越粗糙。一般给个 0.1 到 0.5 之间的值比较平衡。
3.3 SVG 渲染 vs Canvas 渲染的取舍
planetary.js 支持两种渲染方式,这个选择会直接影响你的交互能力和性能表现。
SVG 渲染的特点是每个国家/地区都是一个独立的<path>元素。好处是交互极其方便——你想给某个国家加 hover 变色、点击弹窗,直接绑事件就行,浏览器原生支持。坏处是当元素数量多、或者动画帧率高的时候,DOM 操作会成为瓶颈,地球转起来可能会有点卡。
Canvas 渲染是把所有内容画在一张位图上。好处是性能好,元素再多也不怕,动画流畅。坏处是交互要自己算——你得自己实现"鼠标点在哪个国家上"的命中检测,通常要靠颜色拾取或者几何计算,麻烦不少。
我的建议是:如果你的地球需要精细的交互(比如点击国家显示数据),用 SVG;如果只是纯展示、或者元素特别多,用 Canvas。大部分展示型场景,SVG 完全够用,而且开发效率高得多。
4. 完整实操:从零跑出一个会转的地球
4.1 环境准备与依赖引入
先把三样东西准备好:D3、TopoJSON、planetary.js。引入顺序很重要,因为 planetary.js 依赖前两者。
<script src="https://d3js.org/d3.v3.min.js"></script> <script src="https://d3js.org/topojson.v1.min.js"></script> <script src="planetary.js"></script>注意:planetary.js 是比较早期的库,它依赖的是 D3 v3 版本,不是现在主流的 v7。如果你直接引最新的 D3,大概率会报错。这一点踩过坑的人不少,务必确认版本。
页面里准备一个容器:
<div id="globe"></div>然后给它一点基础样式,主要是定尺寸:
#globe { width: 800px; height: 800px; margin: 0 auto; }4.2 初始化地球与加载数据
核心的初始化代码大概长这样:
var globe = planetaryjs.planet(); globe.loadPlugin(planetaryjs.plugins.earth({ topojson: { file: 'world-110m.json' }, oceans: { fill: '#0a1a2f' }, land: { fill: '#2e6b4f' }, borders: { stroke: '#1a3a2a' } })); globe.loadPlugin(planetaryjs.plugins.autorotate({ rotateSpeed: 10 })); globe.draw(document.getElementById('globe'));逐段解释一下。planetaryjs.planet()创建一个地球实例。earth插件负责加载地理数据并渲染陆地和海洋,里面的topojson.file指向你的数据文件,oceans、land、borders分别控制海洋、陆地、边界的样式。autorotate插件负责自动旋转,rotateSpeed控制速度,数值越大转得越快。
globe.draw()把地球画到指定容器里。到这里,一个会转的地球就出来了。
4.3 旋转速度与方向的参数计算
rotateSpeed这个参数的单位是"度/秒"。地球转一圈是 360 度,所以如果你想让地球36 秒转一圈,那 rotateSpeed 就设成 10(360 / 36 = 10)。如果想 60 秒转一圈,就设成 6。
这个换算关系很实用,因为很多时候设计稿给的是"转一圈要多久",而不是"每秒转多少度"。记住rotateSpeed = 360 / 一圈秒数这个公式,调起来就心里有数了。
方向的话,默认是自西向东(和真实地球自转方向一致)。想反向转,把 rotateSpeed 设成负数就行。
4.4 添加交互:拖拽旋转与缩放
光会自己转还不够,用户通常希望能手动拖一下看看别的地方。planetary.js 提供了drag插件:
globe.loadPlugin(planetaryjs.plugins.drag({ onDragStart: function() { this.plugins.autorotate.pause(); }, onDragEnd: function() { this.plugins.autorotate.resume(); } }));这段代码的逻辑是:用户开始拖拽时暂停自动旋转,松手后恢复。这个细节很重要——如果不暂停,用户拖的时候地球还在自己转,手感会很别扭,像在跟一个不听话的陀螺较劲。
缩放的话,planetary.js 本身没有内置缩放插件,但你可以通过监听滚轮事件,动态修改投影的 scale 来实现。思路是维护一个 scale 变量,滚轮上滚就增大、下滚就减小,然后重新调用投影更新。
4.5 在地球上标记点位
这是 planetary.js 一个很实用的能力——往地球上打点。比如你想标记几个城市的位置:
globe.loadPlugin(planetaryjs.plugins.pings({ color: '#ffcc00', ttl: 3000, angle: 5 })); globe.on('draw', function() { globe.plugins.pings.add(116.4, 39.9); // 某个经纬度 globe.plugins.pings.add(-74.0, 40.7); });pings插件会在指定经纬度画一个扩散的圆环,像雷达扫描一样,很适合做"事件发生地"的视觉提示。ttl是圆环从出现到消失的毫秒数,angle控制圆环的大小。
提示:经纬度的顺序是"经度在前、纬度在后",而且经度范围是 -180 到 180,纬度是 -90 到 90。写反了的话点会跑到奇怪的地方去,这个错误新手特别容易犯。
5. 常见问题与排查技巧实录
5.1 地球不显示或者显示成一片空白
这是最高频的问题,原因通常有这么几个。第一是数据没加载成功,打开浏览器控制台看 Network 面板,确认那个 TopoJSON 文件是不是 404 了。第二是 D3 版本不对,前面强调过,planetary.js 要配 D3 v3,引了 v7 会静默失败。第三是容器没有尺寸,如果#globe的宽高是 0,地球画出来也是 0,什么都看不见。检查一下 CSS 有没有生效。
排查顺序建议是:先看控制台报错,再看网络请求,最后看容器尺寸。按这个顺序走,九成问题能定位到。
5.2 地球转起来卡顿、掉帧
卡顿一般和渲染方式、数据精度、动画帧率有关。如果你用的是 SVG 渲染,而数据又是高精度版本,那每个国家一个 path,几百个 path 每帧重绘,卡是正常的。解决办法有三个:换 Canvas 渲染、换简化版数据、降低旋转帧率。
降低帧率这个技巧比较隐蔽——planetary.js 默认是每帧都更新,你可以改成每隔几帧更新一次,视觉上几乎看不出差别,但性能能提升不少。具体做法是在 autorotate 的更新逻辑里加一个计数器,每 2 到 3 帧才真正重绘一次。
5.3 点位标记位置偏移
点位偏移几乎都是经纬度顺序或者数值范围的问题。再强调一遍:经度在前,纬度在后。另外注意有些数据源用的是 0 到 360 的经度范围,而 planetary.js 期望的是 -180 到 180,这种数据要先转换一下,把大于 180 的值减去 360。
还有一种情况是点位"跑到地球背面去了"。这是正常的——正射投影下,背面的点会被裁掉或者显示在边缘。如果你希望背面的点不显示,那默认行为就是对的;如果希望它显示,那得自己处理可见性判断。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 完全空白 | 数据 404 / D3 版本错 / 容器无尺寸 | 控制台、Network、CSS |
| 地球卡顿 | SVG + 高精度数据 / 帧率过高 | 换 Canvas、简化数据、降帧 |
| 点位偏移 | 经纬度顺序错 / 范围不对 | 检查参数顺序与数值 |
| 拖拽手感怪 | 未暂停自动旋转 | 在 drag 回调里 pause/resume |
| 边缘被裁 | scale 过大 | 调小 scale 值 |
| 颜色不生效 | 插件配置项名写错 | 对照文档检查 key |
5.5 几个我踩过的坑
第一个坑是数据文件的路径。planetary.js 加载 TopoJSON 是异步的,如果你在数据还没加载完就去调pings.add(),点会加不上去。正确做法是监听draw事件,等地球画完了再加。
第二个坑是多个地球实例共享状态。如果你一个页面里要放两个地球,得分别创建两个 planet 实例,不能复用。我一开始图省事复用了,结果两个地球的旋转角度互相干扰,转得乱七八糟。
第三个坑是移动端性能。手机上跑 SVG 渲染的地球,如果数据不够简化,发热和掉帧都很明显。移动端强烈建议用简化数据 + Canvas 渲染,或者干脆降低旋转帧率。
6. 进阶玩法与扩展思路
6.1 给地球加光晕和阴影提升质感
默认渲染出来的地球比较"平",加一点光晕和阴影会立刻高级起来。光晕可以用 SVG 的filter加高斯模糊,或者用径向渐变做一个外圈。阴影的话,可以在海洋的填充色上用径向渐变,让靠近边缘的地方暗一点,模拟球体的立体感。
具体做法是在地球外面套一层 div,用 CSS 的box-shadow或者radial-gradient做光晕。这个纯 CSS 就能搞定,不需要动 planetary.js 的渲染逻辑。
6.2 结合数据做动态可视化
planetary.js 最有价值的用法,其实是把它当成一个"地理坐标的画布"。你可以把任何带经纬度的数据映射到地球上——比如某个时间段内的事件分布、物流路径、用户地理分布等等。
思路是:数据里每条记录都有经纬度,你遍历这些记录,用pings.add()或者自己画弧线的方式把它们呈现出来。如果要做连线(比如两地之间的航线),可以用 D3 的geoPath配合大圆插值,在两个经纬度之间画一条弧线。
6.3 性能优化的几个实操手段
除了前面说的换 Canvas、简化数据、降帧率,还有几个手段值得一试。一是按需渲染,地球转到背面看不见的区域,其实可以不重绘,虽然实现起来复杂点,但收益明显。二是缓存 path,如果某些元素在旋转过程中形状变化不大,可以缓存它们的 path 字符串,减少重复计算。三是用 requestAnimationFrame 控制节奏,别用 setInterval,前者和浏览器刷新率同步,更省电也更流畅。
6.4 和其他可视化库的配合
planetary.js 不是孤立的,它可以和很多库配合。比如你想在地球旁边放一个数据面板,用 ECharts 或者 Chart.js 都行,地球负责"地理维度",图表负责"数据维度",两者联动。再比如你想做时间轴控制,可以用一个滑块控制地球旋转到某个特定经度,或者控制点位数据的时间范围。
这种"地球 + 图表 + 控件"的组合,在数据大屏里非常常见,也是 planetary.js 最能发挥价值的地方。它不抢戏,但作为背景和地理锚点,能让整个看板的空间感一下子立起来。
7. 我个人在实际操作中的体会
用 planetary.js 做了几个项目之后,我最大的感受是:它的价值不在于"炫技",而在于"恰到好处"。它不追求物理精确的光照、不追求极致的 3D 效果,它就是把"一个能转、能交互、能打点的地球"这件事做到了足够简单。对于绝大多数展示型需求来说,这种"够用就好"的定位反而让它比那些重型 3D 方案更实用。
如果你正准备用它,我的建议是先把官方示例跑通,然后拿一份简化数据替换进去,感受一下数据体积对加载速度的影响。等你对投影参数、旋转速度、点位坐标这几样东西有了手感,再往上叠交互和样式,会顺很多。别一上来就想着做多复杂的效果,先把那个"地球转起来"的瞬间拿到手,剩下的都是在这个基础上加东西。
最后分享一个小技巧:如果你觉得默认的旋转速度不好把握,就记住那个公式——rotateSpeed = 360 / 一圈秒数。想要 30 秒一圈就填 12,想要 45 秒一圈就填 8。这个换算能帮你省掉很多来回试参数的时间。