Mapbox GL JS 地球自转实现与性能优化

发布时间:2026/10/27 20:11:15
Mapbox GL JS 地球自转实现与性能优化

地球自转这个效果,放在 Mapbox 里其实是个"看起来简单、做起来一堆细节"的活儿。我最早是在一个数据大屏项目里被要求加这个效果的,产品经理的原话是"地球得转起来,不然太死板"。当时我想着不就是让经度一直加吗,半小时搞定。结果真上手才发现,速度不稳、拖拽冲突、掉帧、星空背景和大气层穿帮,一个接一个。这篇就把 Mapbox 地球自转这件事从头拆一遍:它到底适合用在哪、三种实现路径怎么选、核心循环怎么写、参数怎么算、性能怎么压,以及我踩过的那些坑。如果你手上正好有个大屏、官网首屏或者地理教学演示需要让地球动起来,不管你是刚接触 Mapbox GL JS 的新手,还是已经用过几年、想把这个效果做扎实的老手,下面的内容应该都能直接拿去用。

1. 先想清楚:这个自转到底为谁而转

动手写代码之前,有一件事必须先定下来——你要的是"视觉上的地球在转",还是"地理意义上的视角在移动"。这两个说法听起来一样,落到实现和后续维护上完全是两码事。

1.1 自转效果真正解决的问题

静态的地图截图最大问题是"没有生命感"。人的眼睛对运动物体天生敏感,一个缓慢旋转的球体能在不抢走主体信息的前提下,把观众的注意力牢牢锁在画面上。这就是为什么几乎所有地理信息大屏、卫星轨道演示、全球业务分布展示,首屏都会放一个转着的地球。

但它的价值不止于好看。自转带来的第二个好处是展示覆盖范围。一个正面朝向用户的半球,能看到的只有一半地球,背后的区域永远是盲区。缓慢自转相当于用一个很低的成本,把"背面"的信息轮流送到观众眼前。做全球节点分布、跨时区业务、卫星过境演示这类内容时,这一点比单纯的视觉装饰重要得多。

第三个好处是暗示可交互。一个静止的三维球体,很多人不确定能不能拖。一旦它开始缓慢转动,用户潜意识里就知道"这东西是活的,我能操作"。这是个很便宜的交互引导,比加一行"拖动查看"的提示文字有效得多。

所以我一般会先问清楚:这个自转是纯装饰,还是承担信息轮播的功能?答案不同,速度、暂停策略、是否需要无限循环,全都不一样。

1.2 三种最常见的落地场景

第一种是大屏可视化。特点是无人操作、长时间挂机、分辨率高(常见 4K 拼接屏)。这种场景下自转要慢,通常一圈 60 到 120 秒,全程不暂停,同时必须考虑长时间运行的稳定性和显存占用。挂机三天崩一次,运维就得找你。

第二种是产品官网或宣传页首屏。特点是有交互、停留时间短、需要快速出效果。这种场景自转可以稍微快一点,一圈 30 到 45 秒足够,用户一拖拽就停下来,松手后不必自动恢复——因为用户此刻的意图是"我要看某个地方",不是"我要看它转"。

第三种是教学或演示。比如讲时区、讲昼夜交替、讲经纬网。这种场景对准确性要求高,可能需要配合光照方向,甚至要能手动控制角速度。我做过一个讲时区的演示,自转速度必须严格对应真实地球的角速度(每小时 15 度),这样观众才能直观理解"为什么北京时间比伦敦早 8 小时"。

提示:先确定场景再定速度,别反过来。我见过太多项目先写了代码,最后发现一圈 10 秒转得人头晕,再回头改参数,牵一发动全身。

1.3 一个必须先澄清的认知:转的是相机,不是地球

Mapbox GL JS 渲染的是一个地理坐标系下的球体,地图数据本身是"钉"在球面上的。你没法真的去旋转这个球体对象——那样做会破坏坐标拾取、破坏图层对齐、破坏所有跟经纬度相关的逻辑。

我们实际做的是:让相机绕地轴转。具体说,是不断改变地图中心的经度值。相机绕着球心转一圈,视觉上就是地球自转了一圈。这两个在观感上完全等价,但在工程上一个安全、一个灾难。

我见过有人图省事,直接给 canvas 元素加 CSS 的transform: rotate()。第一次看效果确实转起来了,但接下来会连环爆炸:鼠标点 A 城市,拾取到的是 B 城市;图层的经纬度对齐全乱;缩放按钮的位置跟画面不同步;一旦窗口 resize,整个画面错位。这种方案只在"纯静态截图 + 完全禁交互"的极端场景下勉强能用,其他情况一律别碰。

理解了"转相机"这个前提,后面的所有实现就顺了:不管是每帧改中心点,还是用官方的 spin 插件,本质都是同一件事的不同封装。

2. 技术路径选型:三条路,各有各的代价

Mapbox 生态里做自转,能走的路其实就三条。我把它们摆在一起对比过很多次,结论是:绝大多数项目应该走第二条,但在特定条件下第一条和第三条也有存在价值。

2.1 三条路径的横向对比

路径核心做法优点缺点适用场景
官方 spin 插件引入插件后调用启用方法代码量最少,几行搞定可配置项少,速度控制粗糙,与复杂交互逻辑整合困难快速原型、演示 demo
逐帧改中心经度在渲染循环里累加经度并设置中心完全可控,速度、暂停、恢复、方向都能自定义需要自己处理时间基准和交互冲突生产项目,尤其是大屏和需要精细控制的场景
自由相机直接操作相机朝向矩阵可以做倾斜、俯仰等非标准视角学习成本高,容易跟内置交互打架需要复合运镜的特殊演示

先说官方插件这条路。它的定位很明确:让你用最少的代码看到效果。如果你的需求就是"页面上有个转着的地球",不涉及暂停恢复、不涉及跟其他动画协同,那用它完全没问题。但它的问题也明确——速度是固定的几档,想精确控制到"一圈 90 秒"这种粒度基本做不到,而且它内部也是逐帧改中心,跟你的业务逻辑抢控制权时会很别扭。

第三条自由相机路线,我个人的建议是:除非你确实需要让地球以某个倾斜角度旋转,比如模拟从黄道面斜上方俯视的效果,否则没必要碰。它绕过了地图的正常交互体系,后续维护的人看到那段代码会头疼。

2.2 为什么逐帧改中心经度是最稳的方案

我最终在项目里统一用的是这个方案,理由有三个。

第一,它走的是地图的正常 API。设置中心点是地图最基础的接口之一,所有内置逻辑——图层渲染、坐标转换、事件分发——都会正确处理。你不需要担心它跟别的功能冲突。

第二,控制粒度足够细。速度想调多慢就多慢,想反着转就反着转,想在某几个经度上停留一下也可以(比如转到某个业务区域时减速)。这种灵活性在真实项目里经常被需要。

第三,跟交互的耦合关系清晰。用户的拖拽本质上也是在改中心点,所以"暂停自转"和"恢复自转"就是在同一个维度上做开关,逻辑非常干净,不会出现"用户拖完了,自转又把画面拽回去"的诡异现象。

代价是你得自己写循环、自己算速度、自己处理边界。但这两百来行代码换来的是完全可控,我认为非常值。

2.3 关于 globe 投影的版本前提

自转效果在传统的墨卡托投影下也能做——你把经度一直加,地图会横向无限滚动。但那不是"地球在转",那是"一张无限长的纸在横向平移"。视觉上完全没有球体感,只是把地图从右往左推。

要做真正的球体自转,必须开 globe 投影。Mapbox GL JS 大约从 2.9 版本开始提供 globe 投影支持,后续大版本里对大气层、地形的处理逐步完善。所以第一步要确认你的依赖版本够新。

npm install mapbox-gl@latest

或者页面里直接引 CDN 的最新版。这里有个实际经验:别用太老的版本硬凑。globe 投影在早期版本里对高纬度区域的形变处理、对极点的渲染都有明显瑕疵,用到极地附近的场景(比如航线演示)时会露馅。至少上到 3.x 会省很多事。

3. 核心循环怎么写才不出岔子

这部分是整件事的心脏。很多人写出来的第一版能转,但会有一堆小毛病:速度不稳定、掉帧后变慢、长时间运行变卡。问题几乎都出在"怎么计算每一帧转多少"上。

3.1 初始化:一个最小的可运行骨架

先给一份能直接跑起来的最小结构。HTML 里放一个撑满视口的容器,初始化地图时两个关键参数:projection: 'globe'和合适的初始视角。

<div id="earth" style="position:absolute;inset:0;"></div>
mapboxgl.accessToken = '你的令牌'; const map = new mapboxgl.Map({ container: 'earth', style: 'mapbox://styles/mapbox/satellite-streets-v12', projection: 'globe', center: [105, 25], zoom: 1.2, pitch: 0, bearing: 0, attributionControl: false, antialias: false });

几个参数的选择理由值得说一下。zoom: 1.2是我反复试出来的甜点值:再小一点球体就会缩在画面中央,四周大片黑;再大一点又会被裁掉边缘,看不出是个球。pitch和bearing保持 0,是为了让地轴垂直,转起来最自然。antialias: false在自转场景下几乎看不出差别,但能省不少 GPU 开销——反正在动,人眼分辨不出锯齿。attributionControl: false是因为大屏上那行小字很碍眼,如果你需要合规保留,建议改成自定义位置而不是直接关掉。

地图加载完成后再启动循环,别在new Map()之后立刻跑,那时候样式还没加载完,中心点设置会被后续的初始化视角覆盖,表现为"启动瞬间跳一下"。

3.2 用时间戳而不是固定帧增量

这是最关键的一条经验。新手最常见的写法是:

function spin() { const c = map.getCenter(); map.setCenter([c.lng + 0.1, c.lat]); requestAnimationFrame(spin); }

看起来没问题:每帧加 0.1 度,60 帧就是 6 度/秒,一分钟一圈。但实际跑起来,在配置差一点的机器上你会发现它转得明显慢——因为帧率掉到 30 帧了,每帧还是加 0.1,速度直接腰斩。更糟的是不同机器上速度不一致,演示的时候甲方电脑和你自己的电脑转得不一样快,这很尴尬。

正确做法是基于真实流逝时间计算角度增量:

const SPEED_DEG_PER_SEC = 6; // 一圈 60 秒 let lastTs = null; let running = true; function spin(ts) { if (!running) { lastTs = null; requestAnimationFrame(spin); return; } if (lastTs === null) lastTs = ts; const dt = Math.min((ts - lastTs) / 1000, 0.1); // 秒,封顶防跳变 lastTs = ts; const c = map.getCenter(); let lng = c.lng + SPEED_DEG_PER_SEC * dt; if (lng > 180) lng -= 360; if (lng < -180) lng += 360; map.setCenter([lng, c.lat]); requestAnimationFrame(spin); } map.on('load', () => requestAnimationFrame(spin));

speed的单位换算很直白:度/秒 = 每帧增量 × 帧率,反过来一圈秒数 = 360 / 度每秒。想要 90 秒一圈,就是 360/90 = 4 度/秒;想要 45 秒一圈,就是 8 度/秒。我一般把它抽成配置项,方便不同场景直接改数字。

那个Math.min(dt, 0.1)的封顶很重要。浏览器切到后台标签页时,requestAnimationFrame会被节流甚至暂停,切回来那一瞬间dt可能是好几秒,不封顶的话地球会"嗖"地跳过去一大截,非常突兀。封顶到 0.1 秒,最坏情况就是慢慢挪一小步,观感上察觉不到。

3.3 经度归一化:一个容易被忽略的细节

上面代码里那两行if (lng > 180) lng -= 360是必须的。看起来setCenter对超出范围的经度也能处理,但长期累加下去,数值会越滚越大,浮点精度逐渐丢失,表现是转了几十分钟后开始出现轻微的顿挫感。

而且如果你把当前经度暴露给其他逻辑用(比如显示"当前朝向经度"),不做归一化的数值是没法读的。归一化到 -180 到 180 之间,一切就都干净了。

这里还有个小选择:用-180~180还是0~360。我习惯前者,因为这是地图 API 的原生范围,跟getCenter()返回值一致,省得来回转换。如果你要做"从 0 度开始顺时针转"这种叙事,那就自己加个偏移。

3.4 用渲染事件驱动还是 rAF 驱动

前面用的是requestAnimationFrame。还有一个选择是监听地图自己的render事件:

map.on('render', () => { // 每帧渲染后触发 });

两者区别在于:render事件跟地图的渲染节奏严格同步,地图不需要重绘时它不触发;rAF是浏览器的通用动画节拍,跟地图渲染解耦。

在自转这个场景里,因为我们每帧都在改中心点,地图必然要重绘,所以两者频率基本一致。但render有个隐患——你在render回调里改中心点会导致下一帧继续触发render,形成自驱动的死循环,一旦逻辑写错很容易把主线程卡死,而且很难调试。相比之下rAF更可控,出问题也容易定位。所以我统一用rAF。

注意:如果你在rAF里既改中心点又做其他重活(比如大量 DOM 操作),一定把它们分开。混在一起会让每帧耗时超标,帧率一掉,整个动画的顺滑感就没了。

4. 交互冲突:自转最容易翻车的地方

代码能转只是及格线。真正的难点在于:用户开始操作时,这个自转该怎么办。处理不好,用户体验会非常糟糕。

4.1 用户拖拽时必须停下来

用户按住地球拖动,说明他想看某个具体区域。这时候自转还在继续,就等于有人跟你抢方向盘——你往左拖,它往右拽,画面会抖得让人想砸鼠标。

解决方法是监听拖拽开始事件,把running置为false:

map.on('dragstart', () => { running = false; }); map.on('zoomstart', () => { running = false; });

注意zoomstart也要管。用户滚轮缩放的时候,如果自转还在跑,缩放中心会不断漂移,一松手发现放大的位置完全不是自己想看的地方。

然后就是恢复策略,这个必须分场景定:

  • 大屏挂机:压根不需要恢复逻辑,只要检测到任何交互就永久停掉,反正大屏没人操作。
  • 官网首屏:交互后永久停止,用户主动操作说明他有明确意图,别再自动转回去。
  • 教学演示:交互后停 5 到 10 秒自动恢复,因为演示者可能只是转一下看看,接着要继续讲。

如果要自动恢复,别用setTimeout直接开,容易被连续交互打乱节奏。建议用一个"最后交互时间戳"配合循环内的判断:

let lastInteract = 0; const IDLE_RESUME_MS = 6000; map.on('dragstart', () => { running = false; lastInteract = Date.now(); }); map.on('dragend', () => { lastInteract = Date.now(); }); // 在 spin 循环里 if (!running && Date.now() - lastInteract > IDLE_RESUME_MS) { running = true; lastTs = null; // 关键:重置时间基准,避免一次大跳 }

lastTs = null这一行是我踩过坑才加的。如果恢复时不清空,dt会是"暂停时长",被Math.min封顶后变成一次 0.1 秒的突跳,肉眼能看出来。清空之后,下一帧重新取基准,就顺滑了。

4.2 惯性滑动与自转的叠加问题

Mapbox 的拖拽带惯性——松手后画面会继续滑一段再停。这段惯性期间如果你立刻恢复自转,两者会叠加,滑动的方向被自转强行带偏,观感很怪。

我的处理是监听moveend而不是dragend,等惯性彻底结束再开始计时。moveend在地图完全静止后触发,是更准确的信号。

map.on('moveend', () => { lastInteract = Date.now(); });

另外还有一个容易漏的点:程序化调用也会触发moveend。你的自转循环里调setCenter,某些配置下也可能触发移动事件,导致自己把自己的计时器刷掉,表现为"永远不恢复"。如果你的自转是通过setCenter实现的,通常不会触发moveend(因为它不是一次"移动操作"而是一次瞬时设置),但如果你用的是easeTo,就一定会。这个区别要记住,用easeTo做自转的话事件处理逻辑要格外小心。

4.3 暂停时该不该保留当前位置

有个细节很多人不处理:用户拖完之后,自转从当前经度继续,还是回到原来的轨道?答案显然是前者——从当前经度继续,用户不会觉得画面"跳回去"。这一点上面代码天然满足,因为每帧都是读取getCenter()而不是维护一个独立的经度变量。

但这也带来一个副作用:如果你的自转需要跟某个动画时间轴对齐(比如背景音乐、解说词),就不能依赖getCenter(),得维护独立状态。这种情况极少,遇到再说。

5. 视觉打磨:让球体看起来像个球

技术跑通之后,剩下的是"好不好看"。自转效果对视觉细节特别挑剔,因为它在动,任何瑕疵都会被放大。

5.1 星空背景:最便宜的质感提升

globe 投影下,球体之外的区域默认是地图背景色(通常是深灰或黑)。纯黑太空看着空,加一层星空立刻就有宇宙感了。

最省事的做法是用 CSS 渐变叠在 canvas 下层:

#earth::before { content: ''; position: absolute; inset: 0; background: radial-gradient(1px 1px at 20% 30%, #fff, transparent), radial-gradient(1px 1px at 70% 60%, #ddd, transparent), radial-gradient(2px 2px at 45% 80%, #fff, transparent), radial-gradient(1px 1px at 85% 15%, #ccc, transparent), radial-gradient(circle at 50% 50%, #0b1020 0%, #05070f 100%); z-index: 0; }

要点是星星别太多、别太亮。我一开始堆了几十个点,结果背景像噪点图,抢了地球的注意力。后来减到十来个稀疏分布,再配合一个从中心到边缘的深蓝渐变,效果就对了。

注意z-index和地图 canvas 的层级关系。上面的伪元素是挂在容器上的,得确保地图 canvas 的层级在它之上,否则星空会盖住地球。简单做法是给地图容器设z-index: 0,伪元素设z-index: -1。

5.2 大气层:版本差异最大的部分

地球边缘那圈雾蒙蒙的蓝光,是质感的关键。Mapbox 在新版本里提供了大气效果配置,通常在样式加载完成后调用:

map.on('style.load', () => { map.setFog({ color: 'rgb(10, 20, 40)', // 近地面雾色 'high-color': 'rgb(30, 90, 160)', // 高空颜色 'horizon-blend': 0.08, // 地平线过渡柔和度 'space-color': 'rgb(5, 8, 18)', // 太空色 'star-intensity': 0.4 // 星空强度 }); });

这套参数我是这么调的:horizon-blend控制在 0.05 到 0.1 之间,太小边缘会硬得像贴图,太大整个球会糊。star-intensity给 0.3 到 0.5,超过 0.6 就喧宾夺主了。space-color一定要比你 CSS 背景的渐变末端稍亮一点点,否则球体边缘和背景之间会有个突兀的"断崖"。

如果你用的是老版本没有这套配置,替代方案是给球体外围加一层 CSS 的径向渐变光晕,但问题是球体会转、光晕不会转,静态光晕在球体转动时不会出错(因为球体轮廓不变),勉强能糊过去。只是效果明显不如原生的细腻。

5.3 昼夜分界线的取舍

真实地球有昼夜分界线,一半亮一半暗。Mapbox 的雾效配置里有相关能力可以让光照方向跟某个时刻对齐,但我一般不建议在这个场景里开。

原因很实际:一个一半黑的地球,暗的那半边的信息完全看不见。而你做自转本来就是为了展示全球信息,结果一半时间在展示黑色,逻辑上矛盾。而且昼夜分界线的效果在低分辨率屏幕上表现很差,暗部区域容易出现色带。

除非你的场景就是讲昼夜交替(比如教学),否则老老实实均匀打光,保证任何角度都看得清内容。

6. 性能与稳定性:挂机三天的考验

大屏项目跟普通网页最大的区别是运行时长。普通页面用户看两分钟就走了,大屏要连续跑几天。这时候任何微小的资源泄漏都会被放大成雪崩。

6.1 帧率不是越高越好

我见过有人把速度设成 60 度/秒,想让它飞起来。结果在集显机器上直接卡成 PPT。这里的原则是:自转这种匀速动画,30 帧已经完全够用,没必要追 60 帧。

控帧的办法很简单,在循环里加个时间闸门:

const FRAME_INTERVAL = 1000 / 30; let lastFrameTime = 0; function spin(ts) { requestAnimationFrame(spin); if (ts - lastFrameTime < FRAME_INTERVAL) return; // 后续逻辑用 ts 计算 dt lastFrameTime = ts; }

把帧率锁在 30,GPU 占用能降下来一大截,同时在慢速自转下肉眼完全看不出差别。这个改动在 4K 大屏上效果尤其明显——不锁帧的话风扇会一直狂转。

6.2 像素比和分辨率

高 DPI 屏幕下,Mapbox 默认按设备像素比渲染,4K 屏的像素比可能是 2 甚至 3,渲染量是普通屏的 4 到 9 倍。这个开销在自转场景完全不值。

const map = new mapboxgl.Map({ // ... pixelRatio: Math.min(window.devicePixelRatio, 2) });

限制在 2 是个折中。往下降画质会明显变糊,尤其球体边缘的锯齿会很扎眼;不限制又太耗。如果确定是大屏拼接且观看距离远,甚至可以降到 1.5,观感差异很小。

还有一个容易忽略的点:maxTileCacheSize。长时间运行 + 全球自转意味着要加载全世界的瓦片,缓存会一直涨。给它设个上限(比如 100 左右),让地图自己做 LRU 淘汰,能有效控制内存增长。

6.3 断网、切后台与自动恢复

大屏最常见的故障是网络抖动。瓦片加载失败时,球体会出现一块块的空白,转过去又转回来还是空的。Mapbox 有重试机制,但对于长期挂机的场景,建议加一层兜底:

监听error事件,如果是瓦片类错误,做一次轻量的map.triggerRepaint()或者延迟重设样式。别写得复杂,复杂了反而容易出二次问题。

另外,浏览器标签页切到后台时requestAnimationFrame会被暂停,切回来时dt会异常大——这个前面用Math.min封顶已经解决了。但如果你的场景需要"页面不可见时彻底停掉自转"来省资源,可以监听visibilitychange,切走时running = false,切回来时重置时间基准再恢复。

7. 常见问题速查

把我在项目里遇到的、以及帮同事排查过的问题整理成表,遇到问题先对号入座。

现象大概率原因处理方式
转起来一顿一顿的用的是固定帧增量,帧率波动导致速度不均改成基于时间戳计算 dt
不同电脑转速不一样同上同上
启动瞬间画面跳一下在样式加载完成前就开始改中心点放到load事件之后再启动
拖拽时画面抖自转没在拖拽开始时暂停监听dragstart/zoomstart置 false
拖完松手后画面被拽回用了easeTo做自转,与惯性叠加改用逐帧setCenter
转久了开始顿挫经度值无限累加导致浮点精度丢失每帧做-180~180归一化
切后台回来地球瞬移dt没有封顶Math.min(dt, 0.1)
长时间运行内存持续上涨瓦片缓存无上限设置maxTileCacheSize
球体之外的背景是纯灰没有设置雾效或 CSS 背景配置setFog或叠加渐变层
高纬度区域变形严重GL JS 版本过老,globe 支持不完善升级到 3.x
大屏上边缘有黑边容器没有撑满,或zoom过大裁切检查容器尺寸和初始 zoom
恢复自转时有一次突跳恢复时没重置时间基准lastTs = null

再补两个不在表里但很典型的。

一是地图容器尺寸为 0 导致白屏。在 Vue 或 React 里很常见:组件挂载时父容器还没算出高度,地图初始化拿到的是 0×0,之后再也不自愈。解决办法是在容器尺寸确定后再初始化,或者初始化后调一次map.resize()。这个问题跟自转无关,但自转项目里踩到的概率极高,因为大屏布局往往用了复杂的弹性布局。

二是多点触控下的手势冲突。移动端双指缩放时,如果自转还在跑,缩放中心会飘。处理方式跟拖拽一样,监听touchstart停掉即可。测试的时候一定要在真机上测,桌面端模拟触摸经常测不出问题。

8. 我自己的几个取舍

这个效果我前后改过四五版,最后稳定下来的配置是这样:速度 6 度/秒(60 秒一圈),帧率锁 30,像素比上限 2,交互后永久停止自转,雾效参数按上面那套给,星空用 CSS 稀疏点缀。

有几个当时纠结过、后来想明白的点,分享一下。

关于速度。我一开始设的 3 度/秒(120 秒一圈),觉得慢一点优雅。上线后反馈是"看不出在转"。人对缓慢匀速运动的感知是有阈值的,低于某个速度就跟静止没区别。最后定在 6 度/秒,是"能明显感到在动,但不至于让人头晕"的平衡点。如果你的屏幕特别大、观看距离远,可以再放慢到 4 度/秒,因为大屏上的角速度感知会不一样。

关于自动恢复。我原本很坚持"交互后 6 秒自动恢复",觉得这样才是完整的产品体验。后来在真实使用中发现,用户拖拽往往是有明确目的——他想看某个区域。这时候你过几秒把画面转走,是很冒犯的行为。所以现在默认不恢复,除非需求方明确要求演示场景。

关于要不要用官方插件。我现在的判断标准很简单:如果这个项目里自转是唯一的地图功能,用插件;如果地图上还有其他交互和图层,自己写循环。因为一旦有别的逻辑要跟自转协调(比如"转到某个区域时高亮某个图层"),插件的黑盒特性会让你非常难受。

最后提一句扩展方向:自转这套循环骨架其实可以直接复用做别的动画,比如沿着某个纬线缓慢平移、周期性切换视角、或者配合数据更新做"扫描式"的高亮。核心就是那个基于时间戳的帧循环,把它抽成一个独立的小模块,后面加什么效果都是往里塞回调的事,不用每次重写。