threejs 3D元素周期表:CSS3D 与电子壳层动画
基于 Three.js CSS3DRenderer 构建的 3D 元素周期表:118 个元素卡片支持周期表 / 球形 / 螺旋 / 网格四种布局平滑变换,点击任意元素展开详情面板,内嵌 WebGL 实时渲染的电子壳层原子模型。
3D 元素周期表 (Periodic Table)
太阳系和宇宙尺度都讲”大”,这个模块反过来讲”小”——构成万物的 118 种化学元素。它和前两个模块最大的技术差异是:主体渲染不走 WebGL,而是 CSS3DRenderer——每个元素就是一张真正的 HTML 卡片,文字天然清晰、可以直接用 CSS 做悬浮高亮,同时仍能在三维空间里旋转、变换、纵深排布。
目录结构
1 | |
架构说明
1. CSS3D 周期表 (PeriodicTableScene.ts)
核心是一个 PeriodicTableScene 类:
- 渲染层:
CSS3DRenderer+CSS3DObject,每张元素卡片是一个div(序号、中文、符号、原子量),由 Three.js 只负责算出它的三维变换矩阵 - 布局目标:为四种视图各预计算一组
{x, y, z, rx, ry}目标位姿 - 布局变换:
transform(view, duration)用requestAnimationFrame手写缓动动画(easeInOutCubic),把 118 张卡片从当前位姿插值到目标位姿 - 控制器:
TrackballControls提供自由旋转、缩放、平移
2. 电子壳层模型 (ElectronShellScene.ts)
详情面板内嵌的原子模型,是另一个独立的小 WebGL 场景:
- 原子核:自发光的橙红色小球
- 电子层:每层一个
TorusGeometry轨道环(双层正交环增强纵深),半径按1.0 + i * 1.2递增,七种颜色循环 - 电子:每层最多渲染 12 个发光小球,沿各自倾斜的椭圆轨道独立运动,整层再叠加缓慢的绕轴旋转
选中元素变化时销毁旧场景、按新的 shells 数组重建,层数和电子数完全来自元素数据。
3. 数据层 (elementData.ts + elements.json)
118 个元素的完整数据集,每个元素包含:
- 基础信息:原子序数、符号、中英文名、原子量、电子构型、电子层分布
- 物理性质:熔点、沸点、密度、物态、导电性、磁性
- 原子性质:电负性、原子/共价/范德华半径
- 丰度数据:宇宙 / 地壳 / 海洋 / 人体中的含量
- 发现史与简介
10 个分类(碱金属、碱土金属、过渡金属、准金属、非金属、卤素、稀有气体、镧系、锕系等)各配一套配色方案,驱动卡片背景、边框、文字颜色和底部图例。
四种布局变换
| 视图 | 排布方式 |
|---|---|
| ⊞ 周期表 | 标准 18 列布局,镧系/锕系单独放置在第 8、9 行 |
| ◉ 球形 | 斐波那契球面均匀分布,半径 800 |
| ◎ 螺旋 | 沿纵轴盘旋上升的螺旋带,卡片朝向随切线旋转 |
| ▦ 网格 | 5×5×5 立方点阵 |
布局切换是 118 张卡片同时进行的位姿插值,1.2 秒内完成。周期表布局的行列号由 getElementPosition 统一计算,主表元素取 p(周期)/g(族),镧系(57-71)和锕系(89-103)映射到下方独立行。
一个实战问题:TrackballControls 劫持点击
开发中遇到一个典型问题:点击元素卡片偶尔失效。
原因是 TrackballControls 内部调用了 setPointerCapture,把后续的 pointerup 事件全部捕获到画布容器上,导致挂在卡片 div 上的 click 事件永远无法触发。
解决方案:自己区分”点击”与”拖拽”——
- 卡片上监听
mousedown/touchstart,记录候选元素、按下坐标和时间戳 - 在渲染容器的
pointerup上判定:移动距离 < 8px 且按压时长 < 300ms 才算点击 - 命中则回调
onClick选中该元素
这样拖拽旋转和点选元素两个手势就完全互不干扰了。
详情面板
点击元素后右侧滑出毛玻璃详情卡片(移动端从底部滑出):
- Hero 区:大号元素符号、序号、中英文名、分类徽章(随分类变色)
- 概述:原子量、电子构型、周期/族、分区、物态、氧化态
- 物理性质:熔点、沸点、密度、导电性、磁性
- 原子性质:电负性、三种半径
- 含量:宇宙、地壳、海洋、人体中的丰度(有数据才显示)
- 发现:发现史描述
- 电子层结构:分层电子数标签 + 实时 3D 原子模型
- 简介:元素科普
响应式交互
- 桌面端:面板靠右,可收起到屏幕边缘(只留当前元素符号的展开按钮)
- 移动端:面板占屏幕下方 55vh,支持下拉收起 / 上拉展开的手势判定(滑动位移 > 50px 触发)
技术要点
- CSS3D vs WebGL 的取舍:需要大量清晰文字时,CSS3DRenderer 是比”Canvas 贴文字纹理”更好的选择——渲染质量、可访问性、交互成本全面占优,代价是每个对象都是 DOM,数量要克制(118 张卡片刚好)
- 手写缓动动画:布局变换不引动画库,
t < 0.5 ? 4t³ : 1 - (-2t+2)³/2的 easeInOutCubic 十几行搞定 - 双渲染器共存:CSS3D 主场景 + WebGL 电子壳层,两个独立场景各管各的,销毁时分别清理
- 事件冒泡陷阱:使用带
setPointerCapture的控制器时,DOM 点击检测要自己兜底
Three.js 特性与实现
CSS3DRenderer:把 DOM 放进三维空间
本模块的核心特性选择是 **CSS3DRenderer + CSS3DObject**——Three.js 家族里的特殊渲染器:它不画 WebGL,而是为每个 CSS3DObject 计算三维变换矩阵,直接应用到对应 DOM 元素的 transform 上。
和 WebGL 方案对比:
| CSS3DRenderer | WebGLRenderer | |
|---|---|---|
| 文字渲染 | 原生 DOM,永远清晰 | Canvas 贴图,缩放会糊 |
| 交互 | 元素天然可绑事件 | 需要 Raycaster 拾取 |
| 样式 | CSS 全套能力(圆角/边框/阴影) | 需要着色器手写 |
| 性能 | 每对象一个 DOM,百级为上限 | 万级无压力 |
118 张元素卡片刚好在 CSS3D 的舒适区内:卡片就是 div,悬浮高亮用 mouseenter/boxShadow 两行 CSS 搞定,点击直接绑事件,符号、序号、原子量用绝对定位排版——这些如果走 WebGL,每一项都要多写不少代码。
四种布局:目标位姿 + 手写缓动插值
变换的实现没有用动画库,也没有用 Three.js 的 Tween:
- 启动时为四种视图各预计算一组目标位姿
{x, y, z, rx, ry}(周期表按行列号排布、球形用斐波那契球面分布phi = acos(-1 + 2i/n)、螺旋按索引递增的极角、网格 5×5×5 点阵) transform(view, duration)记录所有卡片当前位姿作起点,之后每帧算归一化进度t,套 easeInOutCubic(t < 0.5 ? 4t³ : 1 - (-2t+2)³/2),对 118 个对象做位置和旋转的线性插值- 用
isAnimating标志位防止动画中重复触发
纯 requestAnimationFrame + 数学,十几个字段的结构体数组就是全部状态。
TrackballControls 与 pointer capture 冲突
TrackballControls 提供”轨迹球”式的自由旋转缩放平移(比 OrbitControls 少了方位角/俯仰角限制,更适合全向浏览点阵)。但它在内部调用 setPointerCapture 把 pointerup 捕获到画布容器,导致卡片上的 click 事件失效——这是 CSS3D + TrackballControls 组合的经典冲突,解法是自判”短点击”(位移 < 8px 且时长 < 300ms),已在正文详述。
WebGL 子场景:电子壳层模型
详情面板里的原子模型是同一个页面里的第二个独立 Three.js 场景,回归传统 WebGL 渲染:
WebGLRenderer({ alpha: true })透明背景嵌在卡片内- 轨道用
TorusGeometry圆环(每层两个正交环增强纵深),电子是开了emissive自发光的小球 - 动画纯参数方程:电子沿各自倾斜轨道转,整层
Group再绕 y 轴慢旋,不同层转速不同(0.15 + i * 0.04)制造视差 - 每次切换元素
dispose()旧场景重建,几何体材质全部回收
一个页面、两个渲染器(CSS3D + WebGL)各管各的区域,互不干扰——这也是 Three.js 混合渲染的典型用法。
资源回收
两个场景都实现了完整 dispose():cancelAnimationFrame 停循环、removeEventListener 摘监听、renderer.dispose() 释放渲染上下文、从 DOM 移除画布。Vue 的 onUnmounted 里统一调用。
技术栈
- Vue 3: 组件框架(Composition API)
- Three.js: CSS3DRenderer(元素卡片)+ WebGLRenderer(电子壳层)
- TypeScript: 类型安全
- TrackballControls: 自由视角控制器