我用 CSS 写了个会自转的 3D 地球仪,原来球面构建藏着这么多细节

244 阅读5分钟

前端开发者们,今天我要分享一个令人兴奋的项目——完全使用CSS实现的3D地球仪效果!无需JavaScript,仅靠CSS的3D变换和动画特性,我们就能创建一个逼真旋转的地球模型。

为什么这个效果值得关注?

在大多数人的认知中,创建3D效果通常需要借助WebGL或Three.js这样的库。但CSS的transform-style: preserve-3d属性配合一些巧妙的技巧,同样能实现令人惊艳的3D效果。这个地球仪项目完美展示了CSS的强大能力。

从一个圆到一个球

最开始的想法很简单:用一个圆形 div,给它贴上地球纹理,再加点阴影不就完了?但实际写出来才发现,那顶多是个 "扁平的圆形图片",无论怎么调整阴影,都没有那种球面的立体感。

.earth {
  width: 400px;
  height: 400px;
  border-radius: 50%;
  background: url('https://raw.githubusercontent.com/d3/d3-geo-projection/master/img/earth.jpg');
  box-shadow: 0 0 20px rgba(0,0,0,0.3);
}

这代码运行起来就像个贴了地图的乒乓球,完全没有星球的厚重感。后来才想明白:球面的本质是无数个朝向不同的面组合,单靠一个 div 永远模拟不出这种空间感。就像切西瓜时每一刀都会露出新的切面,要做 3D 地球,就得用类似的思路 —— 用多个相互交错的环面拼出球体。

经线和纬线

解决思路其实很朴素:用两组垂直交叉的圆环(经线和纬线)来构建球体。经线绕 Y 轴旋转,纬线绕 X 轴旋转,当数量足够多时,这些环就能拼出一个近似球体的结构。

先试试手动写 6 条经线:

.meridian:nth-child(1) { transform: rotateY(0deg); }
.meridian:nth-child(2) { transform: rotateY(60deg); }
.meridian:nth-child(3) { transform: rotateY(120deg); }
.meridian:nth-child(4) { transform: rotateY(180deg); }
.meridian:nth-child(5) { transform: rotateY(240deg); }
.meridian:nth-child(6) { transform: rotateY(300deg); }

每个经线都是一个带圆角的 div,旋转不同角度后确实有了球形的雏形,但边缘太稀疏,像个六边形灯笼。这时候 JavaScript 就派上用场了 —— 动态生成 24 条经线,让它们均匀分布在 360 度空间里:

// 动态生成24条经线
for (let i = 0; i < 24; i++) {
  const meridian = document.createElement('div');
  meridian.className = 'meridian';
  // 计算每条经线的旋转角度
  meridian.style.transform = `rotateY(${(360 / 24) * i}deg)`;
  earth.appendChild(meridian);
}

纬线的处理更 tricky 一些。如果从 - 90 度到 90 度均匀分布,两极会挤在一起。后来发现一个规律:纬线应该像剥洋葱一样从赤道向两极递减密度,实际代码里用了 18 条纬线,避开了极点位置:

// 生成纬线(避免直接到达90/-90度极点)
for (let i = 0; i < 18; i++) {
  const angle = 90 - ((180 / (18 + 1)) * (i + 1));
  parallel.style.transform = `rotateX(${angle}deg)`;
}

当 24 条经线和 18 条纬线交叉时,一个由 864 个三角形组成的球面网格就形成了 —— 这已经足够欺骗人眼,让大脑认为这是一个连续的球体。

把平面图片 "包" 到球面上的

网格搭好了,但每个面怎么显示正确的地图部分?这才是整个项目最烧脑的地方。

最初直接把地球图片贴到每个面片上,结果像把世界地图随意剪开贴在灯笼上, continents 七零八落。问题出在球面展开到平面的投影方式—— 地球是球体,而图片是平面,需要计算每个面片对应地图的哪部分。

解决办法是利用 CSS 的background-position配合网格的角度:

/* 经线面片的贴图位置计算 */
.meridian:nth-child(n) .face {
  background-position: `${(100 / 24) * i}% 0%`;
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
}

每个经线面片被裁剪成菱形(用clip-path),再通过background-position定位到地图的对应经度。这里的关键是把地图图片横向拉伸 4 倍(background-size: 400% 200%),让 24 条经线刚好覆盖完整的 360 度经度。

调试时发现一个有趣的现象:当把鼠标放在不同经线上,能清晰看到非洲、亚洲、美洲的衔接过程,就像在玩谷歌地球的 "街景模式"。

让地球 "活" 起来:动画与交互的细节

静态的地球总感觉少了点什么。加个自转动画很简单,但要做出真实感需要注意两个细节:

  1. 转速控制:地球自转周期是 24 小时,动画时长设为 40 秒比较合适(animation: rotate 40s infinite linear),太快像玩具,太慢没效果
  2. 大气层效果:用一个半透明的渐变圆环模拟大气层,增加立体感:
.atmosphere {
  width: 105%;
  height: 105%;
  background: radial-gradient(circle at 30% 40%, 
            rgba(200, 230, 255, 0.9) 0%,
            rgba(80, 150, 255, 0.3) 40%,
            transparent 70%);
  filter: blur(12px);
}

交互方面,添加鼠标拖拽旋转会让体验提升一个档次。原理是记录鼠标移动的差值,转化为球体的旋转角度:

let isDragging = false;
let startX, startY;
let currentRotateX = 0, currentRotateY = 0;

earth.addEventListener('mousedown', (e) => {
  isDragging = true;
  startX = e.clientX;
  startY = e.clientY;
});

document.addEventListener('mousemove', (e) => {
  if (!isDragging) return;
  const deltaX = e.clientX - startX;
  const deltaY = e.clientY - startY;
  currentRotateY += deltaX * 0.5;
  currentRotateX -= deltaY * 0.5;
  earth.style.transform = `rotateX(${currentRotateX}deg) rotateY(${currentRotateY}deg)`;
  startX = e.clientX;
  startY = e.clientY;
});

这里有个坑:直接旋转地球容器会导致子元素的贴图位置错乱,后来发现需要给每个面片添加transform-style: preserve-3d,保证 3D 空间关系正确。

性能优化:从卡顿到丝滑的秘诀

用 36 条经线 + 24 条纬线时,地球旋转起来明显卡顿。问题出在过多的 DOM 元素和复杂的 clip-path 计算

问题出在三个地方:

  1. clip-path计算量大:每个菱形都要实时计算边缘,864 个面片叠加起来很耗资源
  2. 3D 变换触发重绘:旋转时浏览器要不断计算每个元素的位置
  3. 背面无需渲染:球体背面的面片其实看不到,却在一直消耗资源
.meridian, .parallel {
  will-change: transform; /* 告诉浏览器提前准备动画 */
  backface-visibility: hidden; /* 隐藏背面,减少50%渲染量 */
}

.face {
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); /* 用最简单的多边形 */
  transform: translateZ(0); /* 触发硬件加速 */
}

优化后转动时像拨动一个真实的地球仪,这种流畅感带来的满足感难以言表。

CSS 3D 的边界与可能性

回头看这个地球仪,它本质上是个 "视觉骗局"—— 用 2D 的 div 通过 CSS 3D 变换模拟出 3D 效果。但这个过程教会我最重要的不是技巧,而是突破对 CSS 能力的固有认知

  • 不要低估基础属性的组合威力:transform+clip-path+background-position就能做出复杂效果
  • 3D 效果的核心是 "欺骗眼睛":足够多的细节会让大脑自动补全空间感
  • 性能与效果需要平衡:不是元素越多越好,恰到好处才是关键

其实 CSS 能做的远不止于此,之前还见过用类似思路实现的 3D 太阳系模型。下次再有人说 "CSS 只是样式表",你可以把这个地球仪甩给他看 —— 毕竟,谁能拒绝一个能用鼠标拨弄的地球呢?