1. 项目概述与核心思路
1.1 一个让人眼前一亮的 Loading,真的有必要用 Three.js 吗?
先说说这个项目的起因。公司有个数据大屏项目,需要在首屏加载时展示一个有质感的 Loading 动画。产品经理给的需求就一句话:“要炫,但别耽误加载。”我第一反应是上 Three.js,毕竟 WebGL 做粒子系统太成熟了,网上随便找找就是一堆星空隧道的案例。
但问题来了——这个 Loading 只是整个项目里的一个配角,为了它引入一个 60 多 KB 的 Three.js 核心库,还要考虑 WebGL 上下文初始化、shader 编译、兼容性兜底,一套流程下来光调试就得小半天。而且我们这项目还有不少老机器,WebGL 出问题的时候 Loading 本身就先“崩”了,这不是本末倒置吗?
于是我把目光转向了 CSS 3D。不用引入任何外部依赖,浏览器原生支持,渲染压力完全在 GPU 的合成器上——理论上只要能用 CSS 表达出来的效果,它渲染起来比 WebGL 更轻量。关键是,CSS 的transform-style: preserve-3d配合perspective,完全可以模拟出“摄像机在隧道中穿行”的视觉效果。
事实证明这条路走得通。最终实现出来的效果,是一段由很多个发光圆环组成的星空隧道,视角沿着 Z 轴不停向前推进,圆环呈波浪形排列,周围点缀着大小不一的“星星”。整体跑起来稳定在 60fps,代码量也就三百行左右,没有任何 JavaScript 动画库参与。这篇文章就把整个实现过程拆开讲一遍,包括几个卡了我两天的细节问题,希望你可以直接照着抄。
1.2 这个项目适合谁来参考?
如果你是刚接触 CSS 动画的前端新手,这个项目能帮你把transform、perspective、animation这几个概念串起来,理解“浏览器如何把平面元素变成 3D 世界”的基本原理。
如果你是已经有几年经验的前端工程师,正在找一个纯 CSS 实现复杂视觉效果的案例,这篇文章里的圆环分布计算、动画时序控制和性能优化技巧,可以直接搬到你的项目里用。
如果你只是恰好看到了这个效果,觉得“挺好看”,想实现一个同款但不想碰 Three.js——那你来对地方了,我保证你跟着文章做,一个小时之内就能跑出效果。
2. 为什么选择纯 CSS 3D 方案
2.1 先聊聊 Three.js 和 CSS 3D 各自的定位
很多前端同学对 CSS 3D 的印象停留在“旋转卡片”“立方体翻转”这类效果上,觉得它就是个玩具。但事实上,CSS 3D 的能力边界远比想象中要宽。
Three.js 的优势在于它是一个完整的 3D 引擎,有场景图、材质系统、光照模型、后处理管线,你可以用它做任意复杂度的 3D 场景。但代价是:库体积大、学习曲线陡、运行时需要 WebGL 支持。而 CSS 3D 本质上是浏览器渲染引擎对 2D 元素施加 3D 变换,它没有真正的“光照”和“材质”,但它有几个不可替代的优点:
- 零依赖:不需要下载任何库,不存在版本冲突问题。
- 声明式:样式即代码,动效逻辑直接写在 CSS 里,团队协作时心智负担小。
- GPU 合成:
transform和opacity的动画走的是合成器线程,不阻塞主线程的 JS 执行,对首屏加载场景友好好。 - 降级自然:即使浏览器不支持
preserve-3d(比如 IE),页面也只是退回平面加载动画,不会直接白屏。
尤其对于 Loading 这种场景,它存在的意义就是“等待期间不让用户觉得无聊”,这时候任何额外的 JS 逻辑都是在跟加载过程抢主线程资源。用 CSS 3D 做,反而能把主线程完全让给真正的页面初始化逻辑。
2.2 这个场景的物理模型:一辆驶入隧道的车
星空隧道这个效果,拆开来看其实就是一个非常朴素的物理模型——摄像机不动,场景中的所有物体沿着视线方向朝摄像机运动。或者说,等效于你站在一个隧道口,一辆一辆的圆环火车从深处向你驶来,再穿过你身体远去。
这个模型在 Three.js 里通常是这样实现的:创建多个圆环几何体,沿 Z 轴排布,然后每一帧更新它们的位置。但在 CSS 3D 中,我们完全没有“每一帧更新位置”的概念。CSS 动画是通过animation属性声明关键帧,浏览器负责在中间插值,我们只需要告诉元素“从哪到哪,花多长时间”。
于是核心问题变成了:如何用有限数量的 DOM 元素,表达出无限延伸的隧道感?
我的答案是“循环复用”。设计 N 个圆环,让它们在 Z 轴上均匀排布,然后让整组圆环沿着 Z 轴往前运动。当一组圆环完全穿过摄像机(也就是视口)之后,把它们的位移重置回原点。换句话说,我让一组圆环永远在 Z 轴上循环移动,而不是创建几万几十万个圆环。这样既保证了渲染性能,又实现了视觉上的无限隧道。
这个思路和 CSS 不可能三角博弈的答案类似:用空间换时间,用数量换动态。先说清楚这个底层的循环逻辑,后面所有代码都好理解了。
3. CSS 3D 透视与坐标系,到底是怎么一回事
3.1 透视距离给视觉带来的变化
CSS 3D 的透视效果是由perspective属性控制的,它决定了“摄像机离屏幕的距离”。这个值越小,近大远小的效果越强烈;越大,物体的大小变化越平缓。
理解这些之前,先明确一个坐标概念:在 CSS 3D 变换中,translateZ正值表示元素朝用户方向移动,负值表示远离用户。当perspective属性加在父容器上时,子元素的translateZ是相对于这个父容器的透视中心的。
我项目里设置的透视值是800px。为什么是 800?因为对于一个全屏 Loading 来说,如果透视值太小,圆环靠近时会有强烈的畸变,看起来像鱼眼镜头;如果太大,圆环靠近时大小变化不明显,隧道感就弱了。实测下来,600 到 1000 之间是比较舒服的范围,大家可以根据自己页面的大小微调。
透视的另一个重要特性是,元素的translateZ值一旦接近甚至大于透视值,它就会“越过”摄像机,这时候元素在屏幕上是消失的——因为它在你的脑后了。这个特性后来成了我做通道循环的关键技巧之一。
3.2 preserve-3d 是如何让子元素拥有空间感的?
父容器设置了perspective只是让子元素有了透视投影,但子元素之间如果想要有真正的空间关系(比如一个元素在前面遮挡另一个元素,或者在后面被遮挡),那么父容器还必须设置transform-style: preserve-3d。
这里有一个容易踩坑的点:preserve-3d和overflow: hidden不能共存。一旦父元素设置了overflow: hidden,子元素的 3D 空间感就会被“拍扁”——浏览器被迫降级为平面合成模式。这个特性让很多想做“3D 空间里的遮罩效果”的人抓狂。我最初的版本想在容器边缘做一个渐隐的遮罩,结果一加overflow: hidden,隧道瞬间变成了一堆平面圆环重叠,3D 感全没了。
后来的解决方案是,不用overflow做遮罩,而是用一个覆盖在隧道上面的radial-gradient背景层,用 CSS mask 或者直接叠加半透明渐变实现边缘模糊效果。这个思路大家也可以记一下:在 3D 场景需要遮罩的时候,优先考虑加一层 2D 的叠加元素,而不是去裁剪 3D 容器。
3.3 让元素“抬头低头”:rotateX 和 rotateY 的组合
隧道中的圆环默认是垂直于 Z 轴的。如果只是让圆环往前飞,视觉上会非常单调,所以我在排列圆环时给每一环加了一些旋转角度。旋转的方式是让每个圆环绕自身 X 轴旋转一定角度,同时保持整体隧道沿 Z 轴排列。
这里需要补充一个常识:元素的变换顺序不是随意排列的,transform属性中的函数是从右往左应用的。比如:
transform: rotateX(15deg) translateZ(-100px);这个写法表示先将元素沿 Z 轴移动 -100px,再绕 X 轴旋转 15 度,旋转的中心是元素自身的原点。如果我颠倒顺序写成:
transform: translateZ(-100px) rotateX(15deg);效果就会完全不同——元素先旋转再移动,旋转后的 Z 轴方向已经变了,结果可能导致圆环飞向奇怪的方向。这个顺序问题在写循环排列时非常重要,后面我还会再强调。
4. 星空隧道核心实现:从零写出完整效果
4.1 搭建最基础的 HTML 骨架
用 CSS 3D 实现这个效果,HTML 结构非常简单,就三个层级:
<div class="loading-scene"> <div class="tunnel" id="tunnel"> <!-- 圆环们 --> </div> </div>.loading-scene是透视容器,负责建立 3D 上下文;.tunnel是所有圆环的父层,我给它设置transform-style: preserve-3d,让它成为一个可以承载 3D 子元素的空间容器。圆环则直接作为.tunnel的子元素。
在这个结构里,为了让隧道无限循环,我实际上会创建两组圆环:一组在当前可见区域排列,另一组作为“替补”排在更远处,当第一组全部运动到摄像机身后之后,第二组无缝跟上。这个方案的实现方式是在 JS 中动态生成圆环节点,并给它们设置交错排列的动画延迟。
为什么不把所有圆环写在 HTML 里?因为圆环数量较多,每个节点的样式虽然有规律,但手动写 40 个<div>加 40 段 CSS 样式会很啰嗦,用 JS 动态生成并统一管理显然是更好的方案。这并不违背“没有 Three.js 之外的库”的原则——这里的 JS 只负责生成 DOM 结构和初始样式,动画本身完全由 CSS 驱动,运行时没有任何 JS 介入。
4.2 动态生成圆环的 JS 逻辑
下面是生成圆环的简化代码,我加了一些注释:
const tunnel = document.getElementById('tunnel'); const ringCount = 40; const ringDistance = 180; // 每个圆环沿 Z 轴的间距,单位 px for (let i = 0; i < ringCount; i++) { const ring = document.createElement('div'); ring.className = 'ring'; ring.style.width = '400px'; ring.style.height = '400px'; // 计算每个圆环的位置和旋转,形成一个波浪形排列 const z = -i * ringDistance; // 从深处向外排列 const rotateY = i * 18; const rotateX = Math.sin(i * 0.5) * 10; ring.style.transform = `translateZ(${z}px) rotateY(${rotateY}deg) rotateX(${rotateX}deg)`; // 为每个圆环设置动画延迟,让波浪看起来是连贯的 const delay = (i * 0.12).toFixed(2); ring.style.animationDelay = `${delay}s`; tunnel.appendChild(ring); }圆环的尺寸我用了固定像素值,实际项目里也可以用百分比或者视口单位,取决于你的布局容器。关键在于transform的赋值顺序——先移动,后旋转。
如果顺序写反了,圆环会绕自身中心旋转后再沿世界 Z 轴移动,导致排列方式乱套。很多新手在一开始容易搞混的点就在这里。
圆环自身的基础样式:
.ring { position: absolute; top: 50%; left: 50%; margin-top: -200px; /* 宽度/高度的负一半 */ margin-left: -200px; border-radius: 50%; border: 2px solid rgba(180, 220, 255, 0.6); box-shadow: 0 0 10px rgba(100, 200, 255, 0.3), inset 0 0 10px rgba(100, 200, 255, 0.3); transform-style: preserve-3d; backface-visibility: visible; animation: flow 6s linear infinite; }这里有个细节:margin-top和margin-left设置为宽度和高度的一半负数,是为了让每个圆环的中心对齐到舞台中心。如果不设置这个偏移,圆环的定位点是左上角,所有圆环都会挤在舞台的右下角。
4.3 让圆环动起来的关键帧动画
动效的核心是这段关键帧:
@keyframes flow { 0% { transform: translateZ(1200px) rotateY(0deg) rotateX(0deg); opacity: 0; } 10% { opacity: 1; } 90% { opacity: 1; } 100% { transform: translateZ(-1400px) rotateY(360deg) rotateX(0deg); opacity: 0; } }我在每个圆环的基础静态 transform 之外,又叠加了一段动画变化。这段动画做了三件事:
- 把圆环从 Z 轴正方向(用户面前很远处)移动到 Z 轴负方向(用户身后远处),实现在隧道中穿行的效果。
- 在移动过程中,让圆环绕 Y 轴旋转一整圈,增加了动态感。
- 控制透明度:刚出现时淡入,快离开视野时淡出,避免在身后突然消失的突兀感。
注意最终动画的 transform 和静态 transform 之间的关系。因为 CSS 动画在播放时会覆盖元素原本的transform属性,所以我需要把静态位置的 transform 也完整地写进动画关键帧中。换句话说,0% 和 100% 的关键帧 transform 值,必须是在原有静态 transform 的基础上叠加动画位移后的结果。
这是整个实现里最容易搞混的地方。比如某个圆环静态时的 transform 是translateZ(-180px) rotateY(18deg),那么 0% 关键帧就应该是translateZ(1200px - 180px) rotateY(0deg + 18deg),100% 关键帧则是translateZ(-1400px - 180px) rotateY(360deg + 18deg)。为了方便控制,我实际是在 JS 生成时直接用最终值来拼装动画,而不是让静态 transform 和动画 transform 分开存在。
const targetZ = -i * ringDistance; const baseRotateY = i * 18; const baseRotateX = Math.sin(i * 0.5) * 10; ring.style.animation = `flow 6s linear ${delay}s infinite`; // 在 CSS 自定义属性中存基础值,方便关键帧引用 ring.style.setProperty('--base-z', targetZ + 'px'); ring.style.setProperty('--base-ry', baseRotateY + 'deg'); ring.style.setProperty('--base-rx', baseRotateX + 'deg');然后在关键帧中使用 CSS 变量:
@keyframes flow { 0% { transform: translateZ(1200px) translateZ(var(--base-z)) rotateY(0deg) rotateY(var(--base-ry)) rotateX(var(--base-rx)); opacity: 0; } 10% { opacity: 1; } 90% { opacity: 1; } 100% { transform: translateZ(-1400px) translateZ(var(--base-z)) rotateY(360deg) rotateY(var(--base-ry)) rotateX(var(--base-rx)); opacity: 0; } }这里用了多个相邻的translateZ是因为 CSS transform 列表中的多个平移会累加,rotateY(0deg) rotateY(var(--base-ry))等效于rotateY(var(--base-ry)),这样写的好处是不需要为每个圆环重新拼一整套动画样式。
其实更推荐直接在 JS 中为每个圆环生成独立的@keyframes并通过style标签注入,但在代码量不多的情况下,用 CSS 变量更简洁,维护起来也不容易出错。
4.4 星星:用 box-shadow 模拟满天星
圆环是隧道的主体,但一个只有圆环的隧道略显空旷。要有“星空”的感觉,最简单的方案是往舞台上撒一些“星星”。
我的做法是创建一定数量的小圆点元素,让它们随机分布在 3D 空间里。随机分布怎么生成?如果用 JS 给每个星星写死top、left、translateZ,代码量会很大而且不好看。所以我换了一种思路——用一个元素复制多份 box-shadow 来生成。
具体操作:先创建 8 个“星星种子”元素,每个元素本身是一个很小的圆形,然后给它们每各设置多个box-shadow点,再让它们整体参与隧道的流动动画。因为box-shadow不会受到transform-style: preserve-3d的影响,我在另一个容器中单独铺满整个屏幕靠纯 2D 方式来运动,这样反而更容易控制星星的疏密。
最终星星的实现是:使用一个覆盖全屏的 2D 容器,背景使用多个radial-gradient模拟星空点,然后让这个星空层以比隧道更慢的速度平缓移动,制造出视觉上的“景深差”。这其实是用视差手法来偷懒,但在视觉上效果非常自然。
5. 关键细节打磨:让效果“高级感”拉满
5.1 颜色方案与发光质感
隧道圆环的默认颜色我用了蓝白色系。背景色是深蓝到黑的渐变,圆环的边框是淡青色带有半透明,并且通过box-shadow模拟发光。
发光效果的参数需要反复调:
box-shadow: 0 0 8px rgba(120, 220, 255, 0.55), 0 0 20px rgba(120, 220, 255, 0.25), 0 0 40px rgba(80, 160, 255, 0.15);小半径的阴影决定了边缘的锐利度,大半径的阴影补充了辉光。三层叠加的效果比单层大阴影自然得多。如果浏览器性能较差,可以只保留前两层,减少合成时的模糊计算量。
整个页面的背景,我用了一个 CSS 渐变模拟星光闪烁:
background: radial-gradient(1px 1px at 20% 30%, rgba(255,255,255,0.8), transparent), radial-gradient(1px 1px at 40% 70%, rgba(255,255,255,0.6), transparent), radial-gradient(1.5px 1.5px at 60% 20%, rgba(255,255,255,0.7), transparent), radial-gradient(1px 1px at 80% 50%, rgba(255,255,255,0.5), transparent), linear-gradient(180deg, #020516 0%, #0b1133 50%, #020516 100%);这些radial-gradient位置都是写死的星星点,加上背景渐变的底色,让整个场景有了一个基本的“深夜星空”氛围。
5.2 渐入渐出与加载文案的时机配合
Loading 页面除了动画本身,还需要文字提示,一般显示“正在加载中…”。文案的出现和动画的循环之间会有一个节奏感的问题。
我的处理方式是把文字放在隧道动画的视觉中心——也就是舞台的正中央,给文字设置一个独立的动画周期,让它和隧道的流动保持同一个持续时间,也就是 6 秒。这样每次隧道从深处涌出新圆环时,文字恰好也会完成一次“呼吸”。
文字的呼吸动画:
.loading-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: rgba(220, 240, 255, 0.85); font-size: 16px; letter-spacing: 6px; text-transform: uppercase; animation: breathe 6s ease-in-out infinite; z-index: 10; } @keyframes breathe { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }文字必须在隧道动画之上显示,所以z-index: 10必不可少。另外注意文字本身没有 3D 变换,它是覆盖在隧道视口上的 2D 层,这样才能始终保持清晰。
5.3 让隧道“动起来”的波浪排列
前面 JS 里用到了Math.sin(i * 0.5) * 10来给每个圆环设置一个绕 X 轴的旋转角。这个波浪排列正是隧道看起来不那么死板的关键。
如果不加这一行,所有圆环都以同样的角度整齐向前飞行,视觉上就是一条笔直的管状隧道,很规整但不够惊艳。增加波浪后,圆环之间有了错落的倾斜感,看起来像一条“呼吸”的蛇在隧道中穿行。如果你想让波动更明显,把振幅从 10 度改成 20 度甚至 30 度即可。但要注意,振幅太大时圆环会发生严重的透视变形,某些角度的圆环看起来会非常扁,失去原本的圆环轮廓,所以要适度。
6. 性能优化与避坑指南
6.1 为什么变形顺序会影响性能
CSS 动画的性能问题和transform的写法有关。浏览器在合成每一帧时,会将元素的transform变化提交给 GPU。这里有一个常见的认知误区:translateZ和rotateY虽然都是 3D 变换,但如果元素本身没有其他 3D 上下文,浏览器在处理时依然会走 2D 合成通道,性能会更差。给动画元素加上transform-style: preserve-3d或者至少给父容器声明perspective,可以强制浏览器把合成器切换到高性能的 3D 路径。
实践中我遇到过一种情况:某个圆环动画偶尔会有肉眼可见的掉帧,排查半天发现是因为我忘了给圆环的父元素设置perspective,导致动画退化为平面合成,浏览器每次都要执行更多的 CPU 重绘。加上perspective之后立刻恢复了流畅。这个坑值得记录下来。
6.2 动画的数量与浏览器的承受能力
我在最终版本中保留了 40 个圆环、8 个星星层。在普通笔记本上测试,CPU 占用率大约在 5% 到 8%,GPU 使用率稳定。如果把圆环数量加到 80 个,帧率会有轻微波动,在低端手机上尤其明显。
关于数量,我的建议是:
- 桌面端全屏 Loading:40 个圆环以内,星星层不超过 10 个。
- 移动端全屏 Loading:20 个圆环左右,星星层 4 个以内。
- Loading 区域很小(比如按钮内):8 个圆环就够,太多反而看不清。
关键是在视觉效果和性能之间找到平衡点。我的 40 个圆环和 6 秒循环时间,正好能在任何一个瞬间让屏幕上出现约 10 到 12 个清晰的圆环,视觉密度刚刚好。
6.3 降级方案
并不是所有浏览器都支持preserve-3d,比如很老的工具内嵌浏览器就有可能忽略这个属性。这种情况下的表现是:3D 圆环全部被压平,变成同心圆平铺在屏幕上,动画依然会执行,但所有元素重叠在一起,观感很差。
为了优雅降级,我加了一个简单的特性检测:
const supports3D = (() => { const el = document.createElement('div'); el.style.transformStyle = 'preserve-3d'; return el.style.transformStyle === 'preserve-3d'; })(); if (!supports3D) { document.body.classList.add('no-3d'); }在.no-3d的样式下,我直接把隧道容器隐藏,显示一张静态的星空背景图,文字提示保留。这样即使 3D 不可用,Loading 也不会是一团乱麻。
7. 常见问题与排查思路
7.1 圆环不显示,只看到一堆线条
这种问题多半是容器的perspective没设置正确,或者父元素的高度塌陷了。.loading-scene必须有一个明确的宽高,子元素的translateZ才能有可用的投影空间。
排查思路:先打开浏览器开发者工具的“3D 视图”(Chrome 的 Layers 面板),看看容器的高度是否正常。如果容器高度为 0,圆环自然看不到。另外确认一下perspective是作用在父元素上,而不是圆环自己上。作用在自身上的perspective只影响该元素自己的子元素,不会影响它兄弟节点的 3D 投影。
7.2 圆环动画是停的,一点不动
先检查是不是 CSS 动画被其他样式覆盖了。比如animation属性的简写在部分浏览器中如果写法不完整,会被忽略。建议把动画相关的属性完整写出:
animation-name: flow; animation-duration: 6s; animation-timing-function: linear; animation-iteration-count: infinite; animation-delay: 0s;如果这样还是不转,检查浏览器开发者工具里元素的计算样式,看animation-name和@keyframes是否被正确识别,是否存在拼写错误。
7.3 圆环在远处突然冒出来,穿帮明显
这是因为 0% 关键帧的opacity: 0设得太晚。如果圆环在translateZ(1200px)的位置时才突然显示,用户在屏幕中央能明显看到一个圆环从无到有的瞬间。
解决方式:把关键帧改成0% { opacity: 0; } 8% { opacity: 1; },让圆环在刚出现在屏幕外沿时就淡入完成。远端的透明度渐变区域要足够长,才不会被人眼捕捉到。
7.4 为什么有的机器上圆环闪烁
闪烁一般是backface-visibility导致的。元素在 3D 变换中,如果旋转到了背面,默认是显示背面内容的。当圆环旋转到某个角度,浏览器对背面的渲染可能和部分 GPU 驱动的 bug 叠加,产生闪烁。
我的做法是在圆环上显式加上:
backface-visibility: hidden;绝大多数情况下能解决问题。但如果你的圆环本身要展示正反两面不同效果,那就需要另想办法,比如用两张半圆拼接代替整圆环。
我在实际项目中还遇到过一种低频闪烁:圆环的box-shadow比较大,某些老显卡在blur半径大的阴影上合成会出错,闪烁表现为“圆环外圈有一圈白色噪点”。这个只能通过调小阴影半径或者减少阴影层数来规避。
8. 项目总结与进阶方向
8.1 从这次实践中得到的核心体会
做一个纯 CSS 3D 的 Loading 动画,比我想象中需要更多对浏览器渲染机制的理解。比如transform-style的降级陷阱、backface-visibility的冷知识、动画延迟产生流动感的计算方式,每一个细节都在考验对 CSS 规范的理解程度,而不是单纯堆标签和样式。
如果你本来就在考虑要不要为了一个 Loading 引入 Three.js,我的建议是:如果你的视觉模型只需要圆环、圆柱面、平面贴图这类基本几何形状,且运动方式是匀速前进或旋转,那么纯 CSS 3D 是完全够用的。等你的效果开始需要真实的纹理映射、逐顶点动画、复杂光照时,再考虑 Three.js 不迟。
8.2 后续可以怎么扩展
这个星空隧道 Loading 的效果,底层的动画循环和 3D 排列逻辑可以非常方便地扩展到其他场景。比如:
- 改成粒子雨效果:把圆环替换成更小的 5px 圆形,排列方式从圆环循环改成随机分布,然后沿着 Z 轴方向飞。
- 配合文字:在隧道中间放置一段 3D 文本,让文本和圆环一起流动,可以做成品牌展示页的开场动画。
- 交互化改造:监听鼠标位置,让透视点跟随鼠标轻微移动,制造出一种“你在俯视隧道”的错觉。
根据我个人经验,这类纯 CSS 3D 效果最大的价值不是替代 Three.js,而是提供了一种更亲民的可能性——让没有太重 WebGL 需求的普通前端项目,也能低成本地享受 3D 视觉带来的产品质感提升。最后再分享一个小技巧:代码里所有动画的时长和延迟我都习惯用 CSS 变量统一管理,这样后期调数值特别方便,你可以在自己的实现里提前规划好这一点,会省去很多改样式的麻烦。