基于 Three.js 的交互式 hover 效果原型。鼠标悬停按钮时触发涟漪、倾斜与视差;点击 EXPLORE 进入沉浸式赛博空间隧道动画。
点击 EXPLORE 进入的沉浸式隧道场景
- React 19 — UI 层
- Vite 6 — 构建与开发服务器
- Three.js — WebGL 渲染(按钮 shader、隧道、粒子、漂浮物)
- GSAP — 过渡与 UI 动画
- SCSS — 样式(CSS Modules + 共享变量 / mixin)
- 按钮 hover:涟漪扩散、3D 倾斜、背景视差
- 点击沉浸:按钮扩展至全屏,进入隧道场景
- 键盘支持:
Escape退出沉浸模式 - 移动端适配:低功耗模式、触摸交互
- 按需渲染:空闲时停止 animation loop,节省 GPU
- Node.js 18+
- npm / pnpm / yarn
npm install
npm run dev浏览器访问终端输出的本地地址(默认 http://localhost:5173)。
npm run build
npm run preview产物输出至 dist/。
EnterCyberspace/
├── public/ # 静态资源(favicon 等)
├── src/
│ ├── main.jsx # React 入口
│ ├── App.jsx # 根组件
│ ├── components/
│ │ ├── Header/ # 页头
│ │ └── ExploreScene/ # WebGL 画布 + EXPLORE 按钮
│ ├── hooks/
│ │ └── useHoverEffect.js # React ↔ Three.js 桥接
│ ├── engine/ # 3D 引擎(与 React 解耦)
│ │ ├── HoverEffectEngine.js
│ │ ├── button/ # 按钮 mesh 与 shader
│ │ ├── core/ # 相机、渲染器
│ │ ├── space/ # 隧道、粒子、漂浮物
│ │ ├── config/ # 可调参数
│ │ ├── shaders/ # GLSL 源码
│ │ └── utils/
│ └── styles/
│ ├── _variables.scss
│ ├── _mixins.scss
│ └── global.scss
├── index.html
└── vite.config.js
React 负责 DOM 结构与样式;Three.js 逻辑封装在 HoverEffectEngine 中,通过 useHoverEffect hook 在组件挂载时初始化、卸载时释放 WebGL 资源。
ExploreScene (React)
└── useHoverEffect
└── HoverEffectEngine
├── ButtonManager → 按钮 shader / 涟漪
├── SpaceManager → 隧道场景 render target
├── SceneCamera
└── SceneRenderer
项目最初为纯 JavaScript + Vite 实现:HTML 中手写 DOM 结构,App 类通过 data-* 选择器绑定元素并驱动 Three.js。后续重构为 React 负责 UI、引擎层独立 的分层结构:
| 阶段 | 内容 |
|---|---|
| 原型 | Three.js 全屏 canvas + DOM 按钮叠层,class 驱动交互 |
| 引擎拆分 | 将按钮、空间、相机、渲染器拆为独立模块,配置集中至 config/ |
| React 化 | 用组件替代静态 HTML,通过 ref 注入 DOM,useHoverEffect 管理生命周期 |
| 样式 | CSS 迁移为 SCSS,提取 _variables.scss / _mixins.scss 复用断点与颜色 |
React 不直接操作 Three.js 对象,只提供挂载点;所有 WebGL 逻辑仍由 HoverEffectEngine 统一管理,避免 React 重渲染与 rAF 循环冲突。
核心视觉由 两次 render 组成:
┌─────────────────────────────────────────┐
│ 主场景 (Scene) │
│ └── ButtonManager.mesh (ShaderMaterial) │
│ └── 采样 uTexture ←───────────────┼── SpaceManager RenderTarget
└─────────────────────────────────────────┘
- SpaceManager(离屏) — 隧道、灯光、漂浮物、粒子渲染到
WebGLRenderTarget,输出纹理texture - ButtonManager(主场景) — 带圆角的 Plane + 自定义 fragment shader,将
texture作为uTexture采样,叠加玻璃质感、涟漪、冲击波等效果后输出到屏幕
仅当 uHover > 0.01 或 uProgress > 0.01(即 hover 或沉浸中)时才执行离屏渲染,空闲时不绘制隧道场景。
鼠标/触摸位置经 Raycaster 投射到按钮 mesh,得到 UV 坐标:
// HoverEffectEngine.pickUv
this.pointer.set(
(clientX / viewport.width) * 2 - 1,
-(clientY / viewport.height) * 2 + 1,
);
this.three.raycaster.setFromCamera(this.pointer, this.three.camera);
return raycaster.intersectObject(buttonManager.mesh)[0]?.uv;UV 用于:涟漪原点(uOrigin)、3D 倾斜方向、空间视差偏移。
ButtonManager 维护最多 6 个涟漪(Vector4 数组),通过 uniform uRipples 传入 fragment shader。shader 内用 SDF 圆环 + 噪声计算高度场,再做法线扰动实现折射感。
- 进入 hover:在指针位置生成一次强涟漪(
RIPPLE.enterStrength) - 移动中:指针移动距离超过
minDistance时按速度生成弱涟漪 - 点击沉浸:触发
triggerShock()冲击波动画
setHover(true) 时:
- GSAP 驱动
uHover、uRefractuniform 过渡 - 根据 UV 偏移设置 mesh 的
rotation(倾斜) SpaceManager.setParallax()更新相机微旋转,产生视差- DOM 按钮设置
data-hover="true"切换文字颜色;GSAP 控制阴影透明度
指针离开按钮区域(含 8px 容差)或 pointerleave 时调用 releaseHover() 复位。
点击按钮或 canvas 上命中 UV 时调用 setImmerse(true):
- 按钮 mesh 动画 — GSAP 将 scale / position 从 DOM 按钮的屏幕坐标映射值过渡到全屏尺寸(
playImmerse) - uniform
uProgress— 0→1 驱动 shader 内 portal 开门、背景显现 - SpaceManager —
setDive(progress)沿 Z 轴推进相机并放大 FOV;setReduced(true)降低粒子等层的开销 - RenderTarget 扩容 — 沉浸时按屏幕分辨率 resize(上限
RENDER.rtCap = 2048),退出后恢复默认尺寸 - DOM 按钮 — GSAP 淡出(
autoAlpha: 0),避免与 WebGL 层重叠
退出:Escape、再次点击、或 touch pointerup 触发 setImmerse(false),反向播放上述动画。
SpaceManager 按层组织子模块,均在 update(time) 中逐帧更新:
| 模块 | 职责 |
|---|---|
SpaceLights |
场景灯光 |
Tunnel |
隧道几何 + shader,营造纵深 |
Floaters |
漂浮装饰物 |
Particles |
粒子流(移动端可 setLowPower 降采样) |
相机视差:setParallax(x, y) 写入目标偏移,每帧 lerp 平滑后应用到 camera.rotation。
layoutButton() 是关键桥接步骤:
- 读取 DOM 按钮的
getBoundingClientRect() - 结合
SceneCamera.getViewSize()将像素坐标转换为 Three.js 世界空间中的 position / scale - 读取 CSS
border-radius换算为 shader 的uCorner(圆角 SDF)
窗口 resize 时重新 layout,保证 WebGL 按钮与 DOM 按钮视觉对齐;沉浸期间跳过 layout,由全屏动画接管。
- 按需 rAF:
renderer.setAnimationLoop仅在交互或动画进行中运行;isIdle()检测 hover、沉浸、涟漪、GSAP tween 全部结束后停止循环 - 像素比上限:移动端最高 1.5,桌面 2.0(
getPixelRatio()) - visibility API:标签页隐藏时停止渲染
- WebGL context 丢失:监听
webglcontextlost/restored暂停与恢复 - debounce resize:150ms 防抖后再更新 pixelRatio,避免拖拽窗口时频繁重建 buffer
// ExploreScene.jsx — 仅提供 ref,不参与 3D 状态
const canvasRef = useRef(null);
const buttonRef = useRef(null);
useHoverEffect(canvasRef, buttonRef);// useHoverEffect.js — 挂载/卸载对称
useEffect(() => {
const engine = new HoverEffectEngine({ canvasEl, buttonEl });
engine.load();
return () => engine.dispose(); // 移除 listener、dispose renderer
}, [canvasRef, buttonRef]);注意:data-hover、opacity 等由引擎通过 GSAP 直接写 DOM,不放入 React state,避免多余重渲染。
- 调参:优先修改
src/engine/config/button.js与space.js - 新增空间层:实现
update(time)/objects接口,注册到SpaceManager.layers - _shader 改动:编辑
src/engine/shaders/*.glsl,Vite?raw导入无需额外 loader
主要参数位于 src/engine/config/:
| 文件 | 内容 |
|---|---|
button.js |
涟漪、hover 倾斜、渲染上限 |
space.js |
隧道、粒子、漂浮物参数 |
media.js |
移动端断点 media query |
| 命令 | 说明 |
|---|---|
npm run dev |
启动开发服务器 |
npm run build |
生产构建 |
npm run preview |
本地预览构建结果 |
Private — 仅供学习与原型演示使用。
