新闻详情

新闻详情

首页 / 资讯中心 / 详情

从零手搓3D魔方:CSS 3D舞台搭建与JS旋转矩阵实战

发布时间:2026/9/29 18:26:35来源:尧图网络
从零手搓3D魔方:CSS 3D舞台搭建与JS旋转矩阵实战
简介这是一份面向前端初学者与网页游戏爱好者的3D魔方小游戏完整源码基于HTML、CSS与JavaScript三大核心技术实现适合用来练习CSS3 3D变换、鼠标交互与游戏状态管理。压缩包共4个文件包含1个html页面、1个css样式表、2个js脚本整体约150KB其中html负责搭建游戏区域与控制面板css借助perspective与rotateX/Y/Z等属性呈现立体旋转效果js则处理双击打乱、鼠标拖拽旋转、复位计时等核心逻辑。资源还引入three.min.js辅助3D渲染代码结构清晰便于对照理解矩阵运算与魔方状态数据结构。目前已有1914人学习下载读者可从中掌握3D旋转的精确控制思路、事件监听与时间戳计时方法并以此为基础扩展更复杂的Web交互应用。1. 从零手搓一个能转的 3D 魔方为什么我不建议你直接抄网上的源代码你可能在搜「htmlcssjs实现的3D魔方小游戏源代码」的时候翻到过一堆看起来能跑、下载下来却转不动的代码包。打开控制台一看要么是 CSS 的transform层级写反了要么是 JS 里判断旋转方向的逻辑用了硬编码的if-else堆到几百行。我自己第一次做这个的时候也翻过车——用纯 CSS 把 27 个小方块拼出来结果鼠标一拖整个魔方像被拧麻花一样散架了。后来才想明白3D 魔方的核心根本不是「画 27 个立方体」而是用一套统一的坐标变换去驱动每一块小方块。这篇笔记就按我实际落地的顺序把 HTML 结构、CSS 3D 舞台搭建、JS 旋转矩阵和交互判定这四块拆开讲。适合两类人想拿它当 WebGL 入门练手的前端新手以及需要一个小型 3D 交互案例塞进课程设计或作品集的开发者。你不需要会 Three.js但得能看懂transform: rotateY(60deg) translateZ(300px)这种写法到底把元素扔到了哪里。2. 先把 3D 舞台搭对CSS 透视与坐标系的三个必调参数2.1 为什么你的魔方看起来是「扁」的很多人写 3D 魔方第一步就错了给容器加了transform-style: preserve-3d却忘了在父级加perspective。结果就是所有小方块虽然按 3D 坐标摆好了但投影到屏幕上还是正交的看起来像一张被压扁的贴纸。perspective这个属性决定了观察者到 z0 平面的距离值越小透视畸变越夸张。我一般会把它设在舞台容器上而不是每个小方块上/* 舞台容器负责透视和整体旋转 */ .stage { width: 300px; height: 300px; perspective: 1200px; /* 关键参数模拟人眼到屏幕的距离 */ perspective-origin: 50% 50%; /* 默认就是中心一般不用改 */ } /* 魔方本体负责承载 27 个小方块 */ .cube { width: 100%; height: 100%; position: relative; transform-style: preserve-3d; /* 让子元素保留 3D 空间位置 */ transform: rotateX(-30deg) rotateY(-45deg); /* 初始视角方便看到三个面 */ }逻辑说明perspective写在.stage上.cube及其所有子元素共享同一个透视原点。如果你把perspective写到.cube上那么.cube自身的旋转不会影响透视效果但子元素的 3D 变换会各自以.cube为基准计算透视容易出现「近处方块大、远处方块小」的错乱感。参数上1200px是我试过比较舒服的值——小于 800px 时边缘方块拉伸明显大于 2000px 又接近正交投影失去立体感。2.2 小方块的定位translate3d 比 left/top 靠谱在哪27 个小方块如果用position: absolute加left/top去摆在 3D 旋转时会出现层叠顺序错乱因为z-index在 3D 空间里不生效。正确做法是全部用translate3d(x, y, z)把每个方块推到它该在的坐标上。假设每个小方块边长 100px间隙 2px那么坐标就是-102px、0、102px三档// 生成 27 个小方块的坐标并创建 DOM const CUBE_SIZE 100; const GAP 2; const STEP CUBE_SIZE GAP; // 102 const positions [-STEP, 0, STEP]; positions.forEach(x { positions.forEach(y { positions.forEach(z { const el document.createElement(div); el.className piece; // 关键用 translate3d 定位而不是 left/top el.style.transform translate3d(${x}px, ${y}px, ${z}px); cube.appendChild(el); }); }); });逻辑说明每个.piece是一个 100x100 的 div内部再用 6 个面拼成立方体。这里translate3d的 z 轴正值朝向观察者负值远离。参数上STEP必须等于CUBE_SIZE GAP否则方块之间会重叠或出现缝隙。我见过有人把GAP设成 0结果旋转到侧面时相邻面因为浮点数精度问题产生闪烁加 2px 间隙是最省事的解法。提示.piece自身也要加transform-style: preserve-3d否则它内部的 6 个面会被压平你只能看到最后一个面。2.3 六个面的贴图与背面隐藏每个小方块有 6 个面但魔方内部的面其实永远看不到。为了省性能我一般只给外露面加颜色内部面用background: #111或者直接backface-visibility: hidden隐藏。但注意backface-visibility只对旋转到背面的元素生效不能用来「挖空」内部。真正靠谱的做法是根据小方块的坐标判断它是否在表面只给表面的面加颜色类名。.piece .face { position: absolute; width: 100px; height: 100px; border: 1px solid #333; box-sizing: border-box; backface-visibility: hidden; /* 背面不可见减少渲染压力 */ } /* 六个面的旋转与位移注意 rotate 顺序不能反 */ .face.front { transform: rotateY(0deg) translateZ(50px); } .face.back { transform: rotateY(180deg) translateZ(50px); } .face.right { transform: rotateY(90deg) translateZ(50px); } .face.left { transform: rotateY(-90deg) translateZ(50px); } .face.top { transform: rotateX(90deg) translateZ(50px); } .face.bottom { transform: rotateX(-90deg) translateZ(50px); }逻辑说明translateZ(50px)中的 50 是CUBE_SIZE / 2把面推到立方体表面。旋转顺序必须是先rotate再translateZ如果反过来写面会沿着旋转后的局部坐标轴位移位置全错。参数上rotateY(90deg)和rotateY(-90deg)分别对应右面和左面这个正负判断规则是从旋转轴正方向看向原点逆时针为正。很多人在这里翻车把左右面写反导致魔方颜色错位。3. 让魔方转起来JS 旋转矩阵与动画队列的实现3.1 用四元数还是旋转矩阵我选矩阵的四个理由实现魔方旋转有两种主流方案一是用四元数做平滑插值二是用 3x3 旋转矩阵直接算坐标。四元数听起来高级但在魔方这个场景里每次旋转都是 90 度的整数倍根本不需要插值。用矩阵反而更直观——你只需要定义绕 X、Y、Z 轴旋转 90 度的三个矩阵然后根据用户操作选择对应的矩阵去乘每个小方块的坐标。// 绕 Y 轴旋转 90 度的矩阵右手坐标系 function rotateY90(point) { const { x, y, z } point; return { x: z, y: y, z: -x }; } // 绕 X 轴旋转 90 度 function rotateX90(point) { const { x, y, z } point; return { x: x, y: -z, z: y }; } // 绕 Z 轴旋转 90 度 function rotateZ90(point) { const { x, y, z } point; return { x: -y, y: x, z: z }; }逻辑说明这三个函数就是旋转矩阵的「手算版」。以rotateY90为例绕 Y 轴旋转 90 度后原来的 z 轴方向变成了新的 x 轴方向原来的 x 轴负方向变成了新的 z 轴方向。参数上传入的point是{x, y, z}对象返回值也是同样结构。注意这里用的是右手坐标系如果你的 CSS 里translate3d的 z 轴方向相反需要把正负号调过来。3.2 动画队列为什么 setTimeout 会翻车新手最容易踩的坑是用setTimeout去串行动画。比如「先转 90 度等 300ms再转下一层」结果用户快速点击时多个setTimeout叠加魔方直接转飞。正确做法是维护一个动画队列每次旋转完成后从队列里取下一个任务。const animationQueue []; let isAnimating false; function enqueueRotation(axis, layer, direction) { animationQueue.push({ axis, layer, direction }); if (!isAnimating) processQueue(); } function processQueue() { if (animationQueue.length 0) { isAnimating false; return; } isAnimating true; const task animationQueue.shift(); // 执行旋转动画动画结束后递归调用 processQueue animateRotation(task, () { processQueue(); }); }逻辑说明isAnimating是一个锁防止多个动画同时执行。enqueueRotation把用户操作推入队列如果当前没有动画在跑就立即启动队列处理。animateRotation内部用requestAnimationFrame做 300ms 的过渡结束后调用回调。参数上axis取x | y | zlayer是-1 | 0 | 1表示哪一层direction是1 | -1表示顺时针或逆时针。注意动画过程中要禁用用户输入否则队列会无限增长。我一般会在isAnimating为 true 时直接忽略新的点击或者只允许入队但限制队列长度不超过 3。3.3 旋转时的坐标更新与 DOM 同步动画只是视觉上的过渡真正重要的是动画结束后更新每个小方块的逻辑坐标。这里有个血泪经验不要直接改 DOM 的 transform 字符串而是维护一份 JS 里的坐标数据每次旋转后重新计算所有受影响方块的坐标再统一写回 DOM。// 每个小方块的逻辑数据 const pieces []; // { el, x, y, z } function applyRotation(axis, layer, direction) { pieces.forEach(p { // 只处理在指定层上的方块 if (p[axis] ! layer * STEP) return; // 根据轴和方向选择旋转函数 let newPos; if (axis y) { newPos direction 1 ? rotateY90(p) : rotateY90(rotateY90(rotateY90(p))); } // ... 其他轴同理 p.x newPos.x; p.y newPos.y; p.z newPos.z; // 同步到 DOM p.el.style.transform translate3d(${p.x}px, ${p.y}px, ${p.z}px); }); }逻辑说明layer * STEP用来判断方块是否在旋转层上比如layer 1时只处理y 102的方块。逆时针旋转 90 度等于顺时针转 270 度所以连续调用三次rotateY90。参数上direction为 1 表示顺时针从轴正方向看向原点-1 表示逆时针。每次旋转后必须立即更新 DOM否则下一次旋转会基于旧坐标计算导致错位。4. 交互判定鼠标拖拽怎么识别用户想转哪一层4.1 从鼠标位移到旋转轴的映射用户拖拽时你拿到的是屏幕上的deltaX和deltaY。但魔方有 6 个面每个面对应的拖拽方向都不一样。我的做法是先根据当前视角的旋转矩阵把屏幕位移反投影到魔方的局部坐标系再判断哪个轴的位移分量最大。function getRotationAxis(deltaX, deltaY, currentViewMatrix) { // 把屏幕位移转成魔方局部坐标下的向量 const localVec multiplyMatrixVector(currentViewMatrix, { x: deltaX, y: deltaY, z: 0 }); // 取绝对值最大的分量作为旋转轴 const absX Math.abs(localVec.x); const absY Math.abs(localVec.y); const absZ Math.abs(localVec.z); if (absX absY absX absZ) return x; if (absY absX absY absZ) return y; return z; }逻辑说明currentViewMatrix是当前魔方整体旋转的逆矩阵。把屏幕位移向量乘上这个逆矩阵就得到了在魔方局部坐标系下的位移方向。参数上deltaX和deltaY是mousemove事件里两次触发的差值一般取e.movementX和e.movementY。如果浏览器不支持movementX就用e.clientX - lastX手动算。4.2 判断哪一层射线检测的简化版确定旋转轴之后还要知道用户想转哪一层。完整做法是用射线检测Raycasting去算鼠标点到了哪个方块但那个实现太重。我一般用简化版根据鼠标按下时的屏幕坐标结合当前视角估算出它在旋转轴上的投影位置。function getLayerFromMouse(mouseX, mouseY, axis, viewMatrix) { // 把鼠标坐标归一化到 [-1, 1] const nx (mouseX / window.innerWidth) * 2 - 1; const ny -(mouseY / window.innerHeight) * 2 1; // 根据轴选择投影平面 let projected; if (axis y) { projected nx; // 水平拖拽时用 x 坐标判断层 } else if (axis x) { projected ny; // 垂直拖拽时用 y 坐标判断层 } else { projected nx; // z 轴旋转一般用 x 坐标 } // 映射到 -1, 0, 1 三层 if (projected -0.33) return -1; if (projected 0.33) return 1; return 0; }逻辑说明nx和ny是归一化设备坐标范围[-1, 1]。根据旋转轴选择用哪个分量来判断层。参数上阈值0.33是经验值对应屏幕宽度的三分之一。如果你觉得边缘层不好点可以调到0.25或0.4。这个简化版在视角旋转不超过 45 度时准确率很高超过之后需要引入更精确的投影计算。4.3 拖拽阈值与防误触用户可能只是轻微移动鼠标并不想旋转。我一般设一个 10px 的阈值超过才触发旋转判定。另外mousedown时要记录起始坐标mouseup时清空状态防止拖拽到一半松开后魔方还在转。let dragState null; stage.addEventListener(mousedown, e { dragState { startX: e.clientX, startY: e.clientY, moved: false }; }); stage.addEventListener(mousemove, e { if (!dragState) return; const dx e.clientX - dragState.startX; const dy e.clientY - dragState.startY; if (Math.abs(dx) 10 Math.abs(dy) 10) return; // 阈值 dragState.moved true; // 触发旋转判定... }); stage.addEventListener(mouseup, () { dragState null; });逻辑说明dragState.moved用来区分「点击」和「拖拽」如果用户只是点了一下没移动就不触发旋转。参数上阈值10可以根据设备调整触屏设备建议设到15以上因为手指抖动比鼠标大。5. 避坑与排查3D 魔方最容易翻车的五个地方5.1 旋转方向反了现象是顺时针转变成逆时针现象用户向右拖拽魔方却向左转。原因rotateY90函数里的正负号写反了或者direction的映射逻辑搞错了。解决在纸上画一个右手坐标系标出 x、y、z 轴正方向然后手动推导一次旋转后的坐标变化。我一般会写一个单元测试输入{x:1, y:0, z:0}看输出是不是{x:0, y:0, z:-1}。5.2 方块重叠闪烁z-fighting 的典型表现现象旋转到某些角度时相邻方块的面出现闪烁或条纹。原因两个面的 z 坐标完全相同GPU 不知道哪个在前。解决给每个小方块加 2px 的间隙或者给每个面加transform: translateZ(0.01px)微调。我一般用前者因为后者在旋转时会产生视觉上的「厚度」。5.3 动画卡顿每帧都在重排 DOM现象旋转动画不流畅帧率掉到 30 以下。原因在requestAnimationFrame里直接改style.transform字符串触发大量重排。解决用will-change: transform提前告诉浏览器这个元素要动或者用translate3d强制开启 GPU 加速。另外不要在动画过程中读取offsetWidth之类的布局属性。5.4 鼠标事件丢失快速拖拽时 mouseup 没触发现象用户快速拖拽后松开魔方还在转。原因mouseup事件绑定在stage上但鼠标移出了stage区域。解决把mouseup和mousemove绑定到document上而不是stage。或者用setPointerCapture把指针事件锁定到元素上。5.5 移动端适配touch 事件和 mouse 事件冲突现象在手机上拖拽魔方转两下就卡住。原因同时监听了touchmove和mousemove导致一次操作触发两次旋转。解决用pointerdown、pointermove、pointerup统一处理或者用e.preventDefault()阻止默认的鼠标事件模拟。参数上touch-action: none要加在stage上防止页面滚动干扰。6. 进阶技巧用 CSS 变量做主题切换与性能优化做到这里魔方已经能转了。但如果你想把它塞进作品集还得加两个东西主题切换和性能优化。主题切换我用 CSS 变量实现把六个面的颜色抽成变量JS 里只改变量值不用重新渲染 DOM。:root { --color-front: #ff6b6b; --color-back: #4ecdc4; --color-right: #ffe66d; --color-left: #1a535c; --color-top: #ff9f1c; --color-bottom: #2ec4b6; } .face.front { background: var(--color-front); } .face.back { background: var(--color-back); } /* ... 其他面同理 */function setTheme(theme) { const themes { classic: { front: #ff6b6b, back: #4ecdc4, /* ... */ }, dark: { front: #333, back: #555, /* ... */ } }; const colors themes[theme]; Object.entries(colors).forEach(([key, val]) { document.documentElement.style.setProperty(--color-${key}, val); }); }逻辑说明CSS 变量定义在:root上所有面通过var()引用。切换主题时只改变量值浏览器自动重绘比重新设置每个面的background快得多。参数上themes对象里每个主题只需要定义六个颜色值键名和 CSS 变量后缀对应。性能优化方面我一般会做三件事一是用transform: translate3d代替left/top做动画二是给.piece加contain: layout style paint限制重排范围三是在动画结束后把will-change去掉释放 GPU 内存。这三招下来中低端手机也能跑到 60fps。最后说个我自己的习惯每次改完旋转逻辑我都会在控制台里打印一份所有方块的坐标快照然后手动模拟一次旋转对比前后坐标是否一致。这个笨办法帮我省下了至少十次「看起来能转但颜色全错」的调试时间。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

M33/M55 调度器优化:TrustZone、PendSV 与上下文切换 2026/9/29 21:43:01

M33/M55 调度器优化:TrustZone、PendSV 与上下文切换

做嵌入式这一行,越往底层走你越会发现,调度这个概念的边界比想象中大得多。Cortex-M33和Cortex-M55现在已经大量出现在工业控制、AGV调度、农机作业控制、无人机协同等场景里,这些负载既有硬实时的确定性要求,又希望跑一点轻量AI或…

阅读更多 →
okbiye 一站式毕设平台|全方位解析,毕业论文全流程辅助神器 2026/9/29 21:43:01

okbiye 一站式毕设平台|全方位解析,毕业论文全流程辅助神器

毕业季写毕业论文,一连串难题接踵而至:开题找不到研究切入点、海量文献梳理耗费大量时间、正文提笔难、科研图表绘制麻烦、外文文献阅读吃力,定稿之后还要自查重复率与 AI 痕迹,最后还要花费大量时间调整 Word 格式、制作答辩 PPT…

阅读更多 →
AgentScope 2.0 与 Java 生态实战:多智能体协作与 RAG 落地避坑指南 2026/9/29 21:42:54

AgentScope 2.0 与 Java 生态实战:多智能体协作与 RAG 落地避坑指南

AgentScope 这个框架最近在开发者圈子里被讨论得挺多,尤其是围绕 2.0 版本和 Java 生态的落地实践。我前后花了几周时间,把它从官网文档、中文资料到实际跑通一个多智能体协作场景都过了一遍,中间踩了不少坑,也摸清了一些文档里不…

阅读更多 →
7个AI术语,让你秒懂2026年大模型,小白程序员必备(收藏版) 2026/9/29 21:42:47

7个AI术语,让你秒懂2026年大模型,小白程序员必备(收藏版)

2026年AI术语词典:给没时间背200个词的创作者和创始人。 大部分AI词典列了50多个词,你十分钟就忘了。 你不需要全记住。你只需要7个——它们能区分「真正在用AI的人」和「只会聊AI的人」。 这不是一本200词的AI词典。 这是2026年真正在跑的AI词汇&…

阅读更多 →
Orchard Core 模块开发模式速查:从 Content Part 到后台任务的完整实现范式 2026/9/29 21:42:47

Orchard Core 模块开发模式速查:从 Content Part 到后台任务的完整实现范式

CMS后端Web框架 【免费下载链接】OrchardCore Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework. 项目地址: https://gitcode.com…

阅读更多 →
写论文软件哪个好?别被“功能清单”骗了,先搞清楚你卡在哪一层 | 书匠策AI官网www.shujiangce.com 微信公众号搜一搜 书匠策AI 2026/9/29 21:42:47

写论文软件哪个好?别被“功能清单”骗了,先搞清楚你卡在哪一层 | 书匠策AI官网www.shujiangce.com 微信公众号搜一搜 书匠策AI

官网:www.shujiangce.com | 微信 公众号 :书匠策AI 一个反直觉的结论 我测评过七八款论文工具,发现一个规律:功能越多的工具,用起来越累。 你打开一个软件,左边是文献管理,右边是语法检查…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉