
简介这是纯JavaScript实现的3D魔方模拟程序面向想在浏览器中学习交互编程、算法逻辑或魔方还原机制的开发者。作者从Google搜索到原始代码后做了精简和去冗余处理使核心逻辑更加直观它既可直接当在线魔方模拟器也可作为JavaScript进阶练手项目。压缩包为RAR格式共21个文件含15个JS脚本、4个CSS样式表外加HTML入口页与PNG静态图整体仅255KBJS脚本覆盖旋转动画、事件监听、魔方状态数组与求解算法CSS负责3D场景及界面样式。目前已有779人学习下载。资源亮点在于把复杂魔方逻辑拆成多个模块便于逐一理解DOM操作、事件监听、状态数组、递归/回溯与逐帧动画等知识点对想深入魔方算法、网页3D效果或前端工程化的人来说是一份很有参考价值的源码范例。 玩魔方的人里十个有九个想过“这玩意儿代码怎么写”。我这次就用原生 HTML CSS JavaScript不依赖 Three.js 这类 3D 库把三阶魔方从零撸了一遍。整个“魔方代码 纯JS版”项目做下来核心收获不是“能转了”而是真正理解了三维空间里坐标旋转、贴片朝向、动画与状态同步这些前端硬核知识点。这篇文章就把完整的实现思路、关键代码和踩过的坑都摆出来想自己动手写一个纯 JS 魔方的开发者可以直接照着抄作业。1. 项目拆解纯JS魔方到底在做什么1.1 需求与现实为什么不用现成3D库一开始有人可能会问Three.js 渲染一个魔方也就几十行代码为什么要自讨苦吃我当时的想法很简单魔方是个有限状态的三维模型26 个可见块、54 个贴片状态完全可控。如果用 Three.js交互和渲染确实省事但核心的旋转算法、坐标变换还是得自己写而且很多细节会被库封装掉遇到问题反而更难看穿。“纯 JS 版”的含义我的边界是不引入任何 3D 渲染库渲染用浏览器自带的 CSS 3D transform逻辑层用原生 JavaScript 完成。这样代码体积小打开网页秒加载也方便后续塞进各种页面里作为一个小组件。做这个项目之前我建议你先评估一下自己的目标如果只是想快速做个能动的魔方 Demo用 Three.js 是对的。如果想理解魔方的数学本质、3D 变换原理、以及前端动画状态的协调从零写纯 JS 版会收获大得多。我属于后者所以选择了“纯 JS CSS 3D”这条路线。1.2 技术方案选型对比CSS 3D 与 Canvas 投影在我把“纯 JS”定为底线之后渲染层其实还有两个主流选择CSS 3D transform 和 Canvas 手动投影。我把两者的关键差异列出来对比了一下方案实现复杂度动画实现浏览器兼容性适合场景CSS 3D transform较低依赖 CSS transition流畅现代浏览器都支持块数不多、方体类模型Canvas 手写投影较高需要自己写渲染循环全兼容需要大量粒子、复杂模型对魔方来说贴片数量固定 54 个是极少数“CSS 3D 性能完全够用”的场景所以最终选了 CSS 3D。另外 CSS 3D 还有一个好处每一层旋转的动画可以用transition轻松完成不需要维护 requestAnimationFrame 循环代码瞬间少了一大截。听起来很轻松别急CSS 3D 的坑在“朝向”和“嵌套层级的坐标系”上这个后面细说。1.3 整体模块划分写代码前先把职责拆清楚这也是项目能顺利推进的关键状态层维护 54 个贴片的当前位置、法线方向朝向和颜色。逻辑层实现 12 种旋转操作6 个面 × 2 个方向以及打乱、复位、还原判定。渲染层根据状态层的贴片数据生成对应的 HTML 元素并同步 CSS transform。交互层鼠标/触控拖拽旋转视角点击按钮或按键触发单层旋转。模块之间靠数据单向流动交互触发逻辑层逻辑层更新状态层状态层变化通知渲染层同步 DOM。这个思路和现在前端框架的“状态管理 视图同步”是相通的。2. 核心算法魔方的旋转与状态管理2.1 数据建模贴片、法向量与颜色魔方由 26 个小块组成每个小块上至少有 1 个贴片中心块、最多 3 个贴片角块。我选择以“贴片”为最小的数据单位而不是以小块为单位这样渲染和旋转计算都更直接。每一个贴片用一个对象表示{ id: 0, // 位置坐标每个分量取值 -1/0/1 pos: { x: 1, y: 0, z: 1 }, // 法线方向表示贴片朝外是哪一面 normal: { x: 0, y: 0, z: 1 }, // 颜色按初始位置计算 color: #FFD500 }坐标系统约定X 轴右为正方向对应 R 面。Y 轴上为正方向对应 U 面。Z 轴面对用户为正方向对应 F 面。初始生成贴片时从魔方中心点 (0,0,0) 出发按 ±1 的坐标枚举 27 个位置去掉中心点。然后根据位置和法线方向分配颜色。每个面的颜色按标准配色来上黄下白、前绿后蓝、左橙右红。注意中心块虽然只有一个贴片但它同样有位置坐标和法线方向。旋转时中心块位置不变但法线会变所以也必须参与状态更新。2.2 层旋转的状态更新与坐标变换这是整个项目最核心的数学部分。每个贴片的状态位置和法线是一个三维向量旋转一层就是对这一层里所有贴片做一次“绕某个坐标轴旋转 90 度”的变换。绕 X 轴顺时针旋转 90 度时坐标变换公式为y -z z y x x同理绕 Y 轴z -x x z y y绕 Z 轴x -y y x z z这里的关键点是“顺时针”是从坐标轴正方向往原点看去的方向。我在编码时统一约定为面向坐标轴正方向按顺时针旋转。这样 12 种旋转操作可以统一封装成一个函数function rotateVec(vec, axis, dir) { const { x, y, z } vec; switch (axis) { case x: return { x, y: dir 1 ? -z : z, z: dir 1 ? y : -y }; case y: return { x: dir 1 ? z : -z, y, z: dir 1 ? -x : x }; case z: return { x: dir 1 ? -y : y, y: dir 1 ? x : -x, z }; } }执行一层旋转时先判断哪些贴片属于这一层。例如旋转 U 层就是筛选所有pos.y 1的贴片然后把每个贴片的pos和normal都做一次绕 Y 轴的旋转。整个操作时间复杂度是 O(54)完全可以忽略不计。这里有一个我一开始没注意、后来被坑了很久的细节贴片的 normal 也必须参与旋转。如果不转 normal旋转某个面之后贴片虽然移动到了新位置但颜色朝向还是旧的渲染出来就是一个“颜色错乱”的魔方看起来很诡异。2.3 还原判定与状态校验还原判定逻辑并不复杂遍历 6 个面每个面上 9 个贴片的颜色都一致。但要注意“面”的判定不是看贴片位置而是看贴片 normal 是否相同。我可以按照 normal 等于 (1,0,0)、(-1,0,0)、(0,1,0)、(0,-1,0)、(0,0,1)、(0,0,-1) 分成 6 组对每组检查颜色是否唯一。状态校验的意思是在开发过程中每次旋转后可以快速打印一份“展开图”来验证数据是否正确。我把 6 个面按 normal 分组把颜色映射成字符输出到控制台这样一眼就能看出旋转结果是不是和真实魔方一致。这个习惯帮我发现了不少算法 bug。3. 渲染与交互从状态到3D画面3.1 贴片的 CSS 3D 布局与渲染状态搞定了接下来是把 54 个贴片画出来。每个贴片对应一个 DOM 元素我用一个固定大小的 div 表示比如 56px × 56px稍微留一点间隙。先创建一个 3D 空间容器.viewport { perspective: 1000px; } .cube { width: 0; height: 0; position: relative; transform-style: preserve-3d; }然后把每个贴片 div 塞进.cube容器。贴片的 CSS transform 由两部分组成根据 normal 把 div 旋转到正确的朝向。根据 pos 沿着旋转后的朝向平移到对应的面上。更简单的做法是把贴片的 transform 写成从“坐标 法线”直接生成。实现思路如下设贴片边长为size位置为(x, y, z)那么 transform 是translate3d(x * size, y * size, z * size) rotateX(根据法线计算的角度) rotateY(...) rotateZ(...)但法线方向转成欧拉角比较绕而且容易遇到万向锁。我采用更直接的方式用 3×3 旋转矩阵生成 CSS 的matrix3d。给定法线向量n构造一个让 Z 轴旋转到 n 方向的旋转矩阵把这个矩阵展开成matrix3d的 16 个元素就可以了。核心代码类似function buildTransform(pos, normal, size) { const z normal; let x { x: 1, y: 0, z: 0 }; if (Math.abs(z.x) 0.99) { x { x: 0, y: 1, z: 0 }; } // 构造正交基 let xAxis cross(x, z); let yAxis cross(z, xAxis); // 拼装 4x4 矩阵 ... const m [ xAxis.x, xAxis.y, xAxis.z, 0, yAxis.x, yAxis.y, yAxis.z, 0, z.x, z.y, z.z, 0, pos.x * size, pos.y * size, pos.z * size, 1 ]; return matrix3d(${m.join(,)}); }注意matrix3d是按列主序排列的这也是一个很容易搞错的地方。每个贴片 div 内部再放一个同样大小的色块这样贴片之间的黑边间隙会产生“拼接感”接近真实魔方的观感。完成后在开发者工具里旋转一下容器就能看到一个完整的 3D 魔方出现了。3.2 视角拖拽中鼠标/触控的旋转实现只看到正面没意思用户肯定想拖拽旋转整个视角。我的方案是给.cube容器叠加两层的旋转状态用一个groupRotX记录整体绕 X 轴的旋转角度。用一个groupRotY记录整体绕 Y 轴的旋转角度。拖拽时监听鼠标按下、移动和松开事件通过位移量更新这两个角度再赋给.cube的transform。这一层旋转只影响视觉上的视角不影响贴片的逻辑状态。注意一点如果直接用rotateX(groupRotX) rotateY(groupRotY)这种写法长时间的连续拖拽在极端角度下可能会出现“摇晃”或者“突然跳变”原因是两个欧拉角之间存在耦合。要处理得更稳可以改成用四元数累积旋转或者用矩阵乘法维护一个累计旋转矩阵。四元数方案代码稍多一点但对体验的提升很明显。我的做法是维护一个旋转矩阵每次鼠标拖拽产生一个小的增量旋转矩阵左乘到累计矩阵上最后把这个累计矩阵还原成matrix3d赋给容器。手机端的话建议直接用 Pointer Events 统一处理鼠标和触控避免分别绑定 mouse 和 touch 事件带来的重复代码和兼容问题。3.3 层旋转的用户交互与动画过渡操作某一层的旋转视觉上最直观的做法是让这一层整层做 90 度旋转动画动画结束后再更新内部状态。初版实现里我给每个贴片 div 设了统一的 CSStransition: transform 0.3s ease。旋转层的逻辑是这样的找到该层所有贴片对应的 DOM 元素。把这些 DOM 元素临时包进一个层容器或者直接用当前的父容器。对层容器统一追加一次 90 度旋转的 transformCSS transition 会自动补间动画。动画结束后更新贴片的逻辑状态然后移除临时容器的旋转并把每个贴片的状态同步成新的坐标和法线。这里最需要小心的一点是动画期间要对用户输入做加锁。如果用户连点按钮上一次动画还没结束逻辑状态和视觉状态就会错位最终导致魔方不可控。我加了一个isAnimating标志动画进行中忽略新的旋转指令或者至少等动画结束后再排队执行。手势操作也可以做比如用户拖拽某个面识别出拖拽方向后自动判断是哪个层、哪个旋转方向。这个可以作为扩展功能但按钮和键盘控制已经足够作为核心闭环。4. 踩坑记录与经验补全4.1 坑坐标变换方向搞反魔方颜色全乱第一次完成旋转逻辑后我点了下“旋转 U 层”画面直接崩了——顶面的黄色贴片有三分之一跑到了侧面而且颜色朝向全乱。排查过程很有意思我打印展开图发现 U 层旋转后某些贴片的 pos 变了但 normal 没变导致出现“贴片在 F 面却是黄色”的现象。原因是旋转函数里我只更新了 pos忘了同时更新 normal。补上之后颜色错乱的问题解决了但方向又反了 90 度。最终靠“展开图 手工魔方对照”把所有 12 个旋转操作逐一核对了一遍才确定顺时针方向约定以及各轴变换公式的正确组合。4.2 坑动画与状态不同步导致错位给旋转加上动画之后我遇到一个搞笑的问题连续旋转 4 次之后魔方看起来是对的但状态和视觉差了 90 度。原因是我在动画开始时就动了逻辑状态动画结束时又同步了一次 DOM导致“二次旋转”。解决方法是逻辑状态更新放在动画结束后的回调里动画期间只改变视觉层的临时 transform。等回调触发一次性把贴片的坐标、法线、DOM transform 全部同步到位。这里有一个实用的小技巧给 DOM 操作设置一个较短的 transition 时长比如 200ms即使动画回调有轻微延迟肉眼也不容易察觉代码却会简单很多。4.3 坑matrix3d 列主序导致渲染方向偏移我大概写 CSS transform 写了太多次rotate/translate的组合第一次手写matrix3d时把矩阵按行主序填进去了结果魔方整体被镜像翻转看半天都没反应过来。记住CSS 的matrix3d是列主序的。也就是前四个值是第一列之后四个是第二列依此类推。拼矩阵时最好在旁边注释清楚不然过一个月回来看代码照样看不懂。4.4 常见错误速查表现象可能原因解决办法魔方颜色分布与实际不符normal 未随旋转更新旋转时同步更新 pos 和 normal某一层旋转后其他层被“带歪”旋转层容器选择错误或 transform 叠加检查旋转层容器是否独立动画结束及时清理临时 transform连点按钮后状态错乱动画期间逻辑状态被多次修改加 isAnimating 锁动画期间忽略新指令魔方整体镜像matrix3d 矩阵顺序写成行主序验证列主序逐列填充拖拽视角时魔方突然翻转 180 度欧拉角旋转耦合改用累计旋转矩阵或四元数我的经验是开发这类 3D 小项目一定要把状态和渲染分离并且不管是调试还是写博客都要习惯打印一份展开图来验证逻辑正确性。这个习惯帮我省掉的排查时间非常可观。最后再分享一个可以继续扩展的方向如果你不甘心只做一个“能动”的魔方可以再加一个打乱步骤的序列记录、一个手动复原计时器甚至接一个魔方求解算法比如 Kociemba 的 Two-Phase 算法。纯 JS 版魔方到这里只是第一步后面的空间大得很。我做完这个项目最大的体会是很多当时觉得复杂的 3D 数学拆成一个个小函数之后并不比业务逻辑难多少真正需要的是静下心来把坐标变换和动画时序梳理清楚。本文还有配套的精品资源点击获取