HTML5实现魔方旋转动画教程与交互开发指南
引言:魔方动画的技术挑战与HTML5解决方案
魔方(Rubik’s Cube)作为经典的三维逻辑游戏,其旋转动画在网页开发中是一个极具挑战性的项目。传统的实现方式依赖Flash或复杂的3D引擎,而现代HTML5技术栈(CSS3 3D变换 + JavaScript)提供了轻量级且高性能的解决方案。本教程将详细讲解如何使用HTML5技术实现一个可交互的3D魔方旋转动画,涵盖从基础3D变换原理到完整交互开发的全过程。
核心技术栈分析
- CSS3 3D Transforms: 提供基础的3D空间变换能力,包括rotateX/Y/Z、translateZ等
- CSS3 Transitions/Animations: 实现平滑的旋转过渡动画
- JavaScript (ES6+): 处理用户交互、状态管理和复杂动画序列
- WebGL (可选): 用于更复杂的光照和材质效果(本教程以CSS3为主)
一、HTML5 3D变换基础原理
1.1 3D空间坐标系与透视投影
在开始构建魔方之前,必须理解HTML5的3D变换坐标系。与2D坐标系不同,3D空间增加了Z轴(深度轴)。
/* 启用3D变换环境 */ .cube-container { perspective: 1000px; /* 视距:决定3D效果的透视强度 */ perspective-origin: 50% 50%; /* 视点位置 */ width: 300px; height: 300px; margin: 0 auto; } /* 创建3D变换上下文 */ .cube { transform-style: preserve-3d; /* 保持子元素的3D位置 */ position: relative; width: 100%; height: 100%; transition: transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1); } 关键参数说明:
perspective: 值越小(如500px),3D透视越强烈;值越大(如2000px),3D效果越平缓transform-style: preserve-3d: 必须设置,否则子元素会被压扁成2D平面transition: 定义变换的缓动函数,使旋转更自然
1.2 3D旋转与位移函数
CSS3提供了多个3D变换函数,魔方旋转主要依赖这些函数:
// 3D旋转函数示例 const rotations = { rotateX: (deg) => `rotateX(${deg}deg)`, rotateY: (deg) => `rotateY(${deg}deg)`, rotateZ: (deg) => `rotateZ(${deg}deg)`, translateZ: (px) => `translateZ(${px}px)`, scale3d: (x, y, z) => `scale3d(${x}, ${y}, ${z})` }; // 魔方单个面的3D定位 const facePositions = { front: 'translateZ(50px)', // 前表面 back: 'translateZ(-50px) rotateY(180deg)', // 后表面 right: 'translateZ(50px) rotateY(90deg)', // 右表面 left: 'translateZ(50px) rotateY(-90deg)', // 左表面 top: 'translateZ(50px) rotateX(90deg)', // 上表面 bottom: 'translateZ(50px) rotateX(-90deg)' // 下表面 }; 二、构建3D魔方结构
2.1 HTML结构设计
魔方由27个小立方体(3x3x3)组成,每个小立方体有6个面。我们需要为每个小立方体创建6个面,并正确设置其3D位置。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>3D魔方旋转动画</title> <style> /* 基础样式 */ body { margin: 0; padding: 20px; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); font-family: 'Segoe UI', sans-serif; color: #fff; min-height: 100vh; display: flex; flex-direction: column; align-items: center; } .container { display: flex; flex-direction: column; align-items: center; gap: 30px; } /* 3D舞台容器 */ .scene { width: 300px; height: 300px; perspective: 1000px; perspective-origin: 50% 50%; } /* 魔方主体 */ .cube { width: 100%; height: 100%; position: relative; transform-style: preserve-3d; transition: transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1); } /* 小立方体容器 */ .cubie { position: absolute; width: 33.33%; height: 33.33%; transform-style: preserve-3d; transition: transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1); } /* 立方体面的通用样式 */ .face { position: absolute; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.9); border: 1px solid rgba(0, 0, 0, 0.3); box-sizing: border-box; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 14px; color: #000; backface-visibility: hidden; /* 隐藏背面,优化性能 */ } /* 颜色定义 */ .face.front { background: #ff0000; } /* 红 */ .face.back { background: #ff8800; } /* 橙 */ .face.right { background: #00ff00; } /* 绿 */ .face.left { background: #0000ff; } /* 蓝 */ .face.top { background: #ffffff; } /* 白 */ .face.bottom { background: #ffff00; } /* 黄 */ /* 控制面板 */ .controls { background: rgba(255, 255, 255, 0.1); padding: 20px; border-radius: 10px; backdrop-filter: blur(10px); display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; max-width: 300px; } .btn { padding: 10px 15px; background: rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.3); color: white; border-radius: 5px; cursor: pointer; transition: all 0.2s; font-size: 12px; } .btn:hover { background: rgba(255, 255, 255, 0.3); transform: translateY(-2px); } .btn:active { transform: translateY(0); } .btn.axis-x { border-color: #ff6b6b; } .btn.axis-y { border-color: #4ecdc4; } .btn.axis-z { border-color: #ffe66d; } .info { text-align: center; font-size: 14px; opacity: 0.8; max-width: 300px; line-height: 1.6; } </style> </head> <body> <div class="container"> <h1>3D魔方旋转动画</h1> <div class="scene"> <div class="cube" id="cube"> <!-- 小立方体将通过JS动态生成 --> </div> </div> <div class="controls" id="controls"> <!-- 按钮将通过JS动态生成 --> </div> <div class="info"> <p>点击按钮旋转魔方,或使用键盘方向键控制整体旋转</p> <p>提示:按住Shift键点击可反向旋转</p> </div> </div> <script> // JavaScript代码将在后续部分详细实现 </script> </body> </html> 2.2 JavaScript动态生成魔方结构
由于手动编写27个小立方体的HTML代码过于繁琐,我们使用JavaScript动态生成。每个小立方体需要根据其在3x3x3网格中的位置进行定位。
// 魔方生成器类 class CubeGenerator { constructor(containerId) { this.container = document.getElementById(containerId); this.cubeSize = 3; // 3x3x3 this.cubieSize = 100 / this.cubeSize; // 每个小立方体占容器百分比 this.cubies = []; // 存储所有小立方体引用 } // 生成所有小立方体 generate() { for (let x = 0; x < this.cubeSize; x++) { for (let y = 0; y < this.cubeSize; y++) { for (let z = 0; z < this.cubeSize; z++) { // 跳过中心不可见的小立方体(可选) if (x === 1 && y === 1 && z === 1) continue; const cubie = this.createCubie(x, y, z); this.container.appendChild(cubie); this.cubies.push({ element: cubie, position: { x, y, z }, rotation: { x: 0, y: 0, z: 0 } }); } } } } // 创建单个小立方体 createCubie(x, y, z) { const cubie = document.createElement('div'); cubie.className = 'cubie'; cubie.dataset.x = x; cubie.dataset.y = y; cubie.dataset.z = z; // 设置初始位置 const posX = x * this.cubieSize; const posY = y * this.cubieSize; const posZ = z * this.cubieSize; cubie.style.left = `${posX}%`; cubie.style.top = `${posY}%`; cubie.style.transform = `translateZ(${posZ * 3}px)`; // Z轴偏移量需要放大 // 创建6个面 const faces = ['front', 'back', 'right', 'left', 'top', 'bottom']; faces.forEach(faceName => { const face = document.createElement('div'); face.className = `face ${faceName}`; face.textContent = this.getFaceLabel(x, y, z, faceName); cubie.appendChild(face); }); return cubie; } // 为每个面添加标签(便于调试) getFaceLabel(x, y, z, face) { const pos = `${x},${y},${z}`; const labels = { 'front': `Fn${pos}`, 'back': `Bn${pos}`, 'right': `Rn${pos}`, 'left': `Ln${pos}`, 'top': `Un${pos}`, 'bottom': `Dn${pos}` }; return labels[face] || ''; } } // 初始化魔方 const cubeGenerator = new CubeGenerator('cube'); cubeGenerator.generate(); 三、实现魔方旋转动画
3.1 旋转算法核心逻辑
魔方旋转的核心是坐标变换。当旋转某一层时,需要:
- 识别属于该层的所有小立方体
- 计算每个小立方体相对于旋转轴的新坐标
- 应用3D旋转变换
- 更新内部状态数据
// 魔方控制器类 class CubeController { constructor(cubeGenerator) { this.cubeGenerator = cubeGenerator; this.cubies = cubeGenerator.cubies; this.isAnimating = false; this.animationQueue = []; } /** * 旋转指定层 * @param {string} axis - 旋转轴: 'x', 'y', 'z' * @param {number} layer - 层索引: 0, 1, 2 (左/上/前, 中, 右/下/后) * @param {boolean} clockwise - 是否顺时针 * @param {number} duration - 动画时长(ms) */ rotateLayer(axis, layer, clockwise = true, duration = 600) { if (this.isAnimating) { // 如果正在动画,加入队列 this.animationQueue.push({ axis, layer, clockwise, duration }); return; } this.isAnimating = true; // 1. 获取该层的小立方体 const targetCubies = this.getLayerCubies(axis, layer); // 2. 计算旋转角度 const angle = clockwise ? 90 : -90; // 3. 应用旋转动画 this.animateRotation(targetCubies, axis, angle, duration).then(() => { // 4. 更新内部坐标 this.updateCubiePositions(targetCubies, axis, layer, clockwise); // 5. 处理队列中的下一个旋转 this.isAnimating = false; if (this.animationQueue.length > 0) { const next = this.animationQueue.shift(); this.rotateLayer(next.axis, next.layer, next.clockwise, next.duration); } }); } // 获取指定层的所有小立方体 getLayerCubies(axis, layer) { return this.cubies.filter(cubie => { const pos = cubie.position; switch (axis) { case 'x': return pos.x === layer; case 'y': return pos.y === layer; case 'z': return pos.z === layer; default: return false; } }); } // 执行旋转动画 animateRotation(cubies, axis, angle, duration) { return new Promise(resolve => { // 设置过渡时间 cubies.forEach(cubie => { cubie.element.style.transition = `transform ${duration}ms cubic-bezier(0.4, 0.0, 0.2, 1)`; }); // 应用旋转 const rotationAxis = `rotate${axis.toUpperCase()}`; const transformValue = `${rotationAxis}(${angle}deg)`; cubies.forEach(cubie => { // 累加旋转角度(注意:需要保留之前的旋转) const currentTransform = cubie.element.style.transform || ''; cubie.element.style.transform = `${currentTransform} ${transformValue}`; }); // 等待动画完成 setTimeout(resolve, duration); }); } // 更新内部坐标(关键步骤) updateCubiePositions(cubies, axis, layer, clockwise) { cubies.forEach(cubie => { const { x, y, z } = cubie.position; let newX = x, newY = y, newZ = z; // 坐标变换公式(绕轴旋转90度) const dir = clockwise ? 1 : -1; switch (axis) { case 'x': // 绕X轴旋转:Y和Z坐标互换 newY = dir * z + (1 - dir) * (2 - z); // 简化公式 newZ = dir * (2 - y) + (1 - dir) * y; break; case 'y': // 绕Y轴旋转:X和Z坐标互换 newX = dir * (2 - z) + (1 - dir) * z; newZ = dir * x + (1 - dir) * (2 - x); break; case 'z': // 绕Z轴旋转:X和Y坐标互换 newX = dir * y + (1 - dir) * (2 - y); newY = dir * (2 - x) + (1 - dir) * x; break; } // 更新数据 cubie.position = { x: newX, y: newY, z: newZ }; // 更新DOM数据属性(用于调试和后续选择) cubie.element.dataset.x = newX; cubie.element.dataset.y = newY; cubie.element.dataset.z = newZ; }); } } 3.2 坐标变换的数学原理
绕轴旋转的坐标变换公式(90度旋转):
绕X轴旋转(固定x坐标):
- 新Y = 旧Z
- 新Z = -旧Y
- 在3x3网格中(索引0-2):新Y = 2-旧Z,新Z = 1-旧Y(需要中心对齐)
绕Y轴旋转(固定y坐标):
- 新X = -旧Z
- 新Z = 旧X
- 在3x3网格中:新X = 2-旧Z,新Z = 旧X
绕Z轴旋转(固定z坐标):
- 新X = 旧Y
- 新Y = -旧X
- 在3x3网格中:新X = 旧Y,新Y = 2-旧X
四、交互开发与用户控制
4.1 按钮控制界面生成
动态生成控制按钮,支持单层旋转和整体旋转:
// 控制器UI生成器 class ControlUI { constructor(cubeController) { this.cubeController = cubeController; this.controlsContainer = document.getElementById('controls'); this.generateButtons(); this.setupKeyboardControls(); } generateButtons() { const axes = ['x', 'y', 'z']; const layers = [0, 1, 2]; const layerNames = { 0: '左/上', 1: '中', 2: '右/下' }; // 生成单层旋转按钮 axes.forEach(axis => { layers.forEach(layer => { // 顺时针 const btnCW = document.createElement('button'); btnCW.className = `btn axis-${axis}`; btnCW.textContent = `${axis.toUpperCase()}${layer} ↻`; btnCW.title = `绕${axis.toUpperCase()}轴旋转第${layer}层(顺时针)`; btnCW.onclick = () => { const reverse = event.shiftKey; // 按住Shift反向 this.cubeController.rotateLayer(axis, layer, !reverse); }; this.controlsContainer.appendChild(btnCW); // 逆时针 const btnCCW = document.createElement('button'); btnCCW.className = `btn axis-${axis}`; btnCCW.textContent = `${axis.toUpperCase()}${layer} ↺`; btnCCW.title = `绕${axis.toUpperCase()}轴旋转第${layer}层(逆时针)`; btnCCW.onclick = () => { const reverse = event.shiftKey; this.cubeController.rotateLayer(axis, layer, reverse); }; this.controlsContainer.appendChild(btnCCW); }); }); // 添加整体旋转按钮 const resetBtn = document.createElement('button'); resetBtn.className = 'btn'; resetBtn.textContent = '重置魔方'; resetBtn.style.gridColumn = 'span 3'; resetBtn.style.background = 'rgba(255, 100, 100, 0.3)'; resetBtn.onclick = () => this.resetCube(); this.controlsContainer.appendChild(resetBtn); } // 键盘控制 setupKeyboardControls() { document.addEventListener('keydown', (e) => { if (this.cubeController.isAnimating) return; const keyMap = { 'ArrowUp': { axis: 'y', layer: 1, clockwise: true }, 'ArrowDown': { axis: 'y', layer: 1, clockwise: false }, 'ArrowLeft': { axis: 'x', layer: 1, clockwise: false }, 'ArrowRight': { axis: 'x', layer: 1, clockwise: true }, 'w': { axis: 'y', layer: 0, clockwise: true }, 's': { axis: 'y', layer: 2, clockwise: false }, 'a': { axis: 'x', layer: 0, clockwise: false }, 'd': { axis: 'x', layer: 2, clockwise: true }, 'q': { axis: 'z', layer: 0, clockwise: false }, 'e': { axis: 'z', layer: 2, clockwise: true } }; const action = keyMap[e.key.toLowerCase()]; if (action) { this.cubeController.rotateLayer(action.axis, action.layer, action.clockwise); } }); } // 重置魔方 resetCube() { if (this.cubeController.isAnimating) return; // 简单的重置:重新生成DOM const cubeContainer = document.getElementById('cube'); cubeContainer.innerHTML = ''; this.cubeController.cubies = []; const generator = new CubeGenerator('cube'); generator.generate(); this.cubeController.cubeGenerator = generator; this.cubeController.cubies = generator.cubies; } } 4.2 高级交互:拖拽旋转
实现鼠标拖拽旋转整个魔方:
// 拖拽控制器 class DragController { constructor(cubeElement) { this.cubeElement = cubeElement; this.isDragging = false; this.lastX = 0; this.lastY = 0; this.rotationX = 0; this.rotationY = 0; this.sensitivity = 0.5; // 旋转灵敏度 this.setupEvents(); } setupEvents() { const scene = document.querySelector('.scene'); scene.addEventListener('mousedown', (e) => { this.isDragging = true; this.lastX = e.clientX; this.lastY = e.clientY; scene.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (e) => { if (!this.isDragging) return; const deltaX = e.clientX - this.lastX; const deltaY = e.clientY - this.lastY; this.rotationY += deltaX * this.sensitivity; this.rotationX -= deltaY * this.sensitivity; // 应用旋转 this.cubeElement.style.transform = `rotateX(${this.rotationX}deg) rotateY(${this.rotationY}deg)`; this.lastX = e.clientX; this.lastY = e.clientY; }); document.addEventListener('mouseup', () => { this.isDragging = false; scene.style.cursor = 'grab'; }); // 触摸支持 scene.addEventListener('touchstart', (e) => { this.isDragging = true; this.lastX = e.touches[0].clientX; this.lastY = e.touches[0].clientY; }); scene.addEventListener('touchmove', (e) => { if (!this.isDragging) return; e.preventDefault(); const deltaX = e.touches[0].clientX - this.lastX; const deltaY = e.touches[0].clientY - this.lastY; this.rotationY += deltaX * this.sensitivity; this.rotationX -= deltaY * this.sensitivity; this.cubeElement.style.transform = `rotateX(${this.rotationX}deg) rotateY(${this.rotationY}deg)`; this.lastX = e.touches[0].clientX; this.lastY = e.touches[0].clientY; }); scene.addEventListener('touchend', () => { this.isDragging = false; }); } } 4.3 完整初始化代码
将所有组件整合:
// 完整初始化 document.addEventListener('DOMContentLoaded', () => { // 1. 生成魔方 const generator = new CubeGenerator('cube'); generator.generate(); // 2. 创建控制器 const controller = new CubeController(generator); // 3. 创建UI const ui = new ControlUI(controller); // 4. 创建拖拽控制 const cubeElement = document.getElementById('cube'); new DragController(cubeElement); // 5. 添加全局动画队列处理器 setInterval(() => { if (controller.animationQueue.length > 0 && !controller.isAnimating) { const next = controller.animationQueue.shift(); controller.rotateLayer(next.axis, next.layer, next.clockwise, next.duration); } }, 100); }); 五、性能优化与高级技巧
5.1 性能优化策略
1. 使用transform3d强制GPU加速
/* 在关键元素上添加 */ .cubie { will-change: transform; /* 提示浏览器准备GPU层 */ transform: translateZ(0); /* 强制GPU加速 */ } 2. 减少重绘和回流
// 批量更新DOM function batchUpdate(cubies, updates) { // 先暂停动画 cubies.forEach(c => c.style.transition = 'none'); // 批量更新 updates.forEach(update => { // 应用更新 }); // 恢复动画 requestAnimationFrame(() => { cubies.forEach(c => c.style.transition = ''); }); } 3. 使用requestAnimationFrame优化动画
// 替代setTimeout的动画完成检测 animateRotation(cubies, axis, angle, duration) { return new Promise(resolve => { let startTime = null; const step = (timestamp) => { if (!startTime) startTime = timestamp; const progress = timestamp - startTime; if (progress < duration) { requestAnimationFrame(step); } else { resolve(); } }; requestAnimationFrame(step); }); } 5.2 高级功能:动画序列与打乱
// 魔方打乱器 class CubeScrambler { constructor(cubeController) { this.controller = cubeController; } // 生成随机打乱序列 generateScramble(length = 20) { const axes = ['x', 'y', 'z']; const layers = [0, 1, 2]; const scramble = []; for (let i = 0; i < length; i++) { const axis = axes[Math.floor(Math.random() * axes.length)]; const layer = layers[Math.floor(Math.random() * layers.length)]; const clockwise = Math.random() > 0.5; scramble.push({ axis, layer, clockwise }); } return scramble; } // 执行打乱 async scramble(length = 20) { const sequence = this.generateScramble(length); for (const move of sequence) { await new Promise(resolve => { this.controller.rotateLayer(move.axis, move.layer, move.clockwise, 300); setTimeout(resolve, 350); // 略长于动画时间 }); } } // 播放动画序列 async playSequence(sequence, speed = 600) { for (const move of sequence) { await new Promise(resolve => { this.controller.rotateLayer(move.axis, move.layer, move.clockwise, speed); setTimeout(resolve, speed + 50); }); } } } // 添加打乱按钮 function addScrambleButton(controller) { const scrambleBtn = document.createElement('button'); scrambleBtn.className = 'btn'; scrambleBtn.textContent = '随机打乱'; scrambleBtn.style.gridColumn = 'span 3'; scrambleBtn.style.background = 'rgba(100, 255, 100, 0.3)'; scrambleBtn.onclick = async () => { const scrambler = new CubeScrambler(controller); await scrambler.scramble(25); }; document.getElementById('controls').appendChild(scrambleBtn); } 六、完整可运行代码
将所有代码整合为一个完整的HTML文件:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>3D魔方旋转动画 - 完整版</title> <style> /* 完整CSS代码见上文 */ body { margin: 0; padding: 20px; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); font-family: 'Segoe UI', sans-serif; color: #fff; min-height: 100vh; display: flex; flex-direction: column; align-items: center; } .container { display: flex; flex-direction: column; align-items: center; gap: 30px; } .scene { width: 300px; height: 300px; perspective: 1000px; perspective-origin: 50% 50%; cursor: grab; } .scene:active { cursor: grabbing; } .cube { width: 100%; height: 100%; position: relative; transform-style: preserve-3d; transition: transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1); } .cubie { position: absolute; width: 33.33%; height: 33.33%; transform-style: preserve-3d; transition: transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1); will-change: transform; transform: translateZ(0); } .face { position: absolute; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.9); border: 1px solid rgba(0, 0, 0, 0.3); box-sizing: border-box; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 12px; color: #000; backface-visibility: hidden; } .face.front { background: #ff0000; } .face.back { background: #ff8800; } .face.right { background: #00ff00; } .face.left { background: #0000ff; } .face.top { background: #ffffff; } .face.bottom { background: #ffff00; } .controls { background: rgba(255, 255, 255, 0.1); padding: 20px; border-radius: 10px; backdrop-filter: blur(10px); display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; max-width: 320px; } .btn { padding: 10px 15px; background: rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.3); color: white; border-radius: 5px; cursor: pointer; transition: all 0.2s; font-size: 12px; } .btn:hover { background: rgba(255, 255, 255, 0.3); transform: translateY(-2px); } .btn:active { transform: translateY(0); } .btn.axis-x { border-color: #ff6b6b; } .btn.axis-y { border-color: #4ecdc4; } .btn.axis-z { border-color: #ffe66d; } .info { text-align: center; font-size: 14px; opacity: 0.8; max-width: 300px; line-height: 1.6; } h1 { margin: 0 0 10px 0; font-size: 24px; } </style> </head> <body> <div class="container"> <h1>3D魔方旋转动画</h1> <div class="scene"><div class="cube" id="cube"></div></div> <div class="controls" id="controls"></div> <div class="info"> <p>点击按钮旋转魔方,或使用键盘方向键控制整体旋转</p> <p>提示:按住Shift键点击可反向旋转</p> <p>鼠标拖拽可旋转整个魔方</p> </div> </div> <script> // 完整JavaScript代码(见上文各部分) class CubeGenerator { constructor(containerId) { this.container = document.getElementById(containerId); this.cubeSize = 3; this.cubieSize = 100 / this.cubeSize; this.cubies = []; } generate() { for (let x = 0; x < this.cubeSize; x++) { for (let y = 0; y < this.cubeSize; y++) { for (let z = 0; z < this.cubeSize; z++) { if (x === 1 && y === 1 && z === 1) continue; const cubie = this.createCubie(x, y, z); this.container.appendChild(cubie); this.cubies.push({ element: cubie, position: { x, y, z }, rotation: { x: 0, y: 0, z: 0 } }); } } } } createCubie(x, y, z) { const cubie = document.createElement('div'); cubie.className = 'cubie'; cubie.dataset.x = x; cubie.dataset.y = y; cubie.dataset.z = z; const posX = x * this.cubieSize; const posY = y * this.cubieSize; const posZ = z * this.cubieSize; cubie.style.left = `${posX}%`; cubie.style.top = `${posY}%`; cubie.style.transform = `translateZ(${posZ * 3}px)`; const faces = ['front', 'back', 'right', 'left', 'top', 'bottom']; faces.forEach(faceName => { const face = document.createElement('div'); face.className = `face ${faceName}`; face.textContent = this.getFaceLabel(x, y, z, faceName); cubie.appendChild(face); }); return cubie; } getFaceLabel(x, y, z, face) { const pos = `${x},${y},${z}`; const labels = { 'front': `Fn${pos}`, 'back': `Bn${pos}`, 'right': `Rn${pos}`, 'left': `Ln${pos}`, 'top': `Un${pos}`, 'bottom': `Dn${pos}` }; return labels[face] || ''; } } class CubeController { constructor(cubeGenerator) { this.cubeGenerator = cubeGenerator; this.cubies = cubeGenerator.cubies; this.isAnimating = false; this.animationQueue = []; } rotateLayer(axis, layer, clockwise = true, duration = 600) { if (this.isAnimating) { this.animationQueue.push({ axis, layer, clockwise, duration }); return; } this.isAnimating = true; const targetCubies = this.getLayerCubies(axis, layer); const angle = clockwise ? 90 : -90; this.animateRotation(targetCubies, axis, angle, duration).then(() => { this.updateCubiePositions(targetCubies, axis, layer, clockwise); this.isAnimating = false; if (this.animationQueue.length > 0) { const next = this.animationQueue.shift(); this.rotateLayer(next.axis, next.layer, next.clockwise, next.duration); } }); } getLayerCubies(axis, layer) { return this.cubies.filter(cubie => { const pos = cubie.position; switch (axis) { case 'x': return pos.x === layer; case 'y': return pos.y === layer; case 'z': return pos.z === layer; default: return false; } }); } animateRotation(cubies, axis, angle, duration) { return new Promise(resolve => { cubies.forEach(cubie => { cubie.element.style.transition = `transform ${duration}ms cubic-bezier(0.4, 0.0, 0.2, 1)`; }); const rotationAxis = `rotate${axis.toUpperCase()}`; const transformValue = `${rotationAxis}(${angle}deg)`; cubies.forEach(cubie => { const currentTransform = cubie.element.style.transform || ''; cubie.element.style.transform = `${currentTransform} ${transformValue}`; }); setTimeout(resolve, duration); }); } updateCubiePositions(cubies, axis, layer, clockwise) { cubies.forEach(cubie => { const { x, y, z } = cubie.position; let newX = x, newY = y, newZ = z; const dir = clockwise ? 1 : -1; switch (axis) { case 'x': newY = dir * z + (1 - dir) * (2 - z); newZ = dir * (2 - y) + (1 - dir) * y; break; case 'y': newX = dir * (2 - z) + (1 - dir) * z; newZ = dir * x + (1 - dir) * (2 - x); break; case 'z': newX = dir * y + (1 - dir) * (2 - y); newY = dir * (2 - x) + (1 - dir) * x; break; } cubie.position = { x: newX, y: newY, z: newZ }; cubie.element.dataset.x = newX; cubie.element.dataset.y = newY; cubie.element.dataset.z = newZ; }); } } class ControlUI { constructor(cubeController) { this.cubeController = cubeController; this.controlsContainer = document.getElementById('controls'); this.generateButtons(); this.setupKeyboardControls(); } generateButtons() { const axes = ['x', 'y', 'z']; const layers = [0, 1, 2]; axes.forEach(axis => { layers.forEach(layer => { const btnCW = document.createElement('button'); btnCW.className = `btn axis-${axis}`; btnCW.textContent = `${axis.toUpperCase()}${layer} ↻`; btnCW.onclick = () => { const reverse = event.shiftKey; this.cubeController.rotateLayer(axis, layer, !reverse); }; this.controlsContainer.appendChild(btnCW); const btnCCW = document.createElement('button'); btnCCW.className = `btn axis-${axis}`; btnCCW.textContent = `${axis.toUpperCase()}${layer} ↺`; btnCCW.onclick = () => { const reverse = event.shiftKey; this.cubeController.rotateLayer(axis, layer, reverse); }; this.controlsContainer.appendChild(btnCCW); }); }); const resetBtn = document.createElement('button'); resetBtn.className = 'btn'; resetBtn.textContent = '重置魔方'; resetBtn.style.gridColumn = 'span 3'; resetBtn.style.background = 'rgba(255, 100, 100, 0.3)'; resetBtn.onclick = () => this.resetCube(); this.controlsContainer.appendChild(resetBtn); const scrambleBtn = document.createElement('button'); scrambleBtn.className = 'btn'; scrambleBtn.textContent = '随机打乱'; scrambleBtn.style.gridColumn = 'span 3'; scrambleBtn.style.background = 'rgba(100, 255, 100, 0.3)'; scrambleBtn.onclick = async () => { const scrambler = new CubeScrambler(this.cubeController); await scrambler.scramble(25); }; this.controlsContainer.appendChild(scrambleBtn); } setupKeyboardControls() { document.addEventListener('keydown', (e) => { if (this.cubeController.isAnimating) return; const keyMap = { 'ArrowUp': { axis: 'y', layer: 1, clockwise: true }, 'ArrowDown': { axis: 'y', layer: 1, clockwise: false }, 'ArrowLeft': { axis: 'x', layer: 1, clockwise: false }, 'ArrowRight': { axis: 'x', layer: 1, clockwise: true }, 'w': { axis: 'y', layer: 0, clockwise: true }, 's': { axis: 'y', layer: 2, clockwise: false }, 'a': { axis: 'x', layer: 0, clockwise: false }, 'd': { axis: 'x', layer: 2, clockwise: true }, 'q': { axis: 'z', layer: 0, clockwise: false }, 'e': { axis: 'z', layer: 2, clockwise: true } }; const action = keyMap[e.key.toLowerCase()]; if (action) { this.cubeController.rotateLayer(action.axis, action.layer, action.clockwise); } }); } resetCube() { if (this.cubeController.isAnimating) return; const cubeContainer = document.getElementById('cube'); cubeContainer.innerHTML = ''; this.cubeController.cubies = []; const generator = new CubeGenerator('cube'); generator.generate(); this.cubeController.cubeGenerator = generator; this.cubeController.cubies = generator.cubies; } } class DragController { constructor(cubeElement) { this.cubeElement = cubeElement; this.isDragging = false; this.lastX = 0; this.lastY = 0; this.rotationX = 0; this.rotationY = 0; this.sensitivity = 0.5; this.setupEvents(); } setupEvents() { const scene = document.querySelector('.scene'); scene.addEventListener('mousedown', (e) => { this.isDragging = true; this.lastX = e.clientX; this.lastY = e.clientY; scene.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (e) => { if (!this.isDragging) return; const deltaX = e.clientX - this.lastX; const deltaY = e.clientY - this.lastY; this.rotationY += deltaX * this.sensitivity; this.rotationX -= deltaY * this.sensitivity; this.cubeElement.style.transform = `rotateX(${this.rotationX}deg) rotateY(${this.rotationY}deg)`; this.lastX = e.clientX; this.lastY = e.clientY; }); document.addEventListener('mouseup', () => { this.isDragging = false; scene.style.cursor = 'grab'; }); scene.addEventListener('touchstart', (e) => { this.isDragging = true; this.lastX = e.touches[0].clientX; this.lastY = e.touches[0].clientY; }); scene.addEventListener('touchmove', (e) => { if (!this.isDragging) return; e.preventDefault(); const deltaX = e.touches[0].clientX - this.lastX; const deltaY = e.touches[0].clientY - this.lastY; this.rotationY += deltaX * this.sensitivity; this.rotationX -= deltaY * this.sensitivity; this.cubeElement.style.transform = `rotateX(${this.rotationX}deg) rotateY(${this.rotationY}deg)`; this.lastX = e.touches[0].clientX; this.lastY = e.touches[0].clientY; }); scene.addEventListener('touchend', () => { this.isDragging = false; }); } } class CubeScrambler { constructor(cubeController) { this.controller = cubeController; } generateScramble(length = 20) { const axes = ['x', 'y', 'z']; const layers = [0, 1, 2]; const scramble = []; for (let i = 0; i < length; i++) { const axis = axes[Math.floor(Math.random() * axes.length)]; const layer = layers[Math.floor(Math.random() * layers.length)]; const clockwise = Math.random() > 0.5; scramble.push({ axis, layer, clockwise }); } return scramble; } async scramble(length = 20) { const sequence = this.generateScramble(length); for (const move of sequence) { await new Promise(resolve => { this.controller.rotateLayer(move.axis, move.layer, move.clockwise, 300); setTimeout(resolve, 350); }); } } } // 初始化 document.addEventListener('DOMContentLoaded', () => { const generator = new CubeGenerator('cube'); generator.generate(); const controller = new CubeController(generator); const ui = new ControlUI(controller); const cubeElement = document.getElementById('cube'); new DragController(cubeElement); }); </script> </body> </html> 七、调试与故障排除
常见问题及解决方案
1. 3D变换不显示
- 检查父元素是否设置了
transform-style: preserve-3d - 确保没有其他CSS覆盖了transform属性
- 使用浏览器开发者工具检查计算后的样式
2. 动画卡顿
- 添加
will-change: transform提示浏览器 - 减少同时动画的元素数量
- 使用
transform: translateZ(0)强制GPU加速
3. 坐标计算错误
- 打印每个小立方体的
dataset属性验证坐标 - 使用简单的单层旋转测试坐标变换逻辑
- 确保旋转方向与预期一致(顺时针/逆时针)
4. 事件冲突
- 拖拽与点击事件冲突时,使用
event.stopPropagation() - 移动端触摸事件需要
preventDefault()防止页面滚动
八、扩展方向
- WebGL集成:使用Three.js实现更真实的光照和材质
- 求解器算法:实现魔方自动求解功能
- 声音效果:添加旋转时的音效
- 虚拟现实:结合WebXR实现VR魔方
- 多人协作:使用WebSocket实现多人同时操作
通过本教程,您已经掌握了使用HTML5实现3D魔方动画的核心技术。从基础3D变换到复杂交互,这套方案可以扩展到任何3D界面开发中。建议从简单版本开始,逐步添加高级功能,确保每个部分都经过充分测试。
支付宝扫一扫
微信扫一扫