学习three.js模块,制作了一个3D魔方在线玩的程序。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js 3D Axis Demo</title>
<style>
body {
margin: 0;
overflow: hidden;
}
canvas {
display: block;
}
.buttonContainer {
position: absolute;
top: 20px;
right: 20px;
}
.button {
padding: 10px 20px;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
transition: all 0.3s;
float: right;
margin-left: 10px;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
#resetBtn {
background: #4CAF50;
}
#resetBtn:hover {
background: #45a049;
}
#randomBtn {
background: #f44336;
}
#randomBtn:hover {
background: #d32f2f;
}
/* 新增操作指南样式 */
.operation-guide {
position: absolute;
left: 20px;
top: 20px;
background: rgba(255, 255, 255, 0.8);
padding: 10px;
border-radius: 5px;
width: 220px;
box-sizing: border-box;
}
.operation-guide h3 {
margin: 0 0 10px 0;
}
.operation-guide p {
margin: 0 0 5px 0;
}
/* 新增:魔方颜色选择面板样式 */
.color-panel {
position: absolute;
bottom: 20px;
left: 20px;
display: flex;
flex-direction: row;
gap: 15px;
padding: 15px;
background: rgba(255, 255, 255, 0.8);
border-radius: 10px;
}
.color-options {
display: flex;
gap: 15px;
}
/* 新增:编辑开关样式 */
.edit-toggle {
position: relative;
width: 60px;
height: 30px;
border-radius: 15px;
background: #888;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
padding: 0 5px;
}
.edit-toggle.off {
background: #f44336;
}
.edit-toggle.on {
background: #4CAF50;
}
.edit-toggle::before {
position: absolute;
left: 4px;
content: '';
width: 22px;
height: 22px;
border-radius: 11px;
background: white;
transition: all 0.3s;
}
.edit-toggle.on::before {
left: calc(100% - 26px);
}
.edit-toggle span {
color: white;
font-size: 12px;
width: 100%;
text-align: center;
z-index: 1;
pointer-events: none;
}
.color-option {
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.3s;
}
.color-option:hover {
transform: scale(1.1);
}
.color-option.selected {
box-shadow: 0 0 10px 5px rgba(70, 30, 10, 0.7);
transform: scale(1.2);
}
.color-option.white {
background: #ffffff;
}
.color-option.yellow {
background: #ffff00;
}
.color-option.blue {
background: #0000ff;
}
.color-option.green {
background: #00ff00;
}
.color-option.red {
background: #ff0000;
}
.color-option.orange {
background: #ff7b00;
}
/* 新增:魔方展开图样式 */
#unfoldContainer {
position: absolute;
right: 20px;
bottom: 20px;
width: 300px;
height: 400px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 5px;
padding: 10px;
}
.unfold-face {
position: relative;
border: 2px solid #000;
border-radius: 4px;
background: #888 !important;
display: grid;
grid-template-columns: repeat(3, 1fr);
/* 明确3列 */
grid-template-rows: repeat(3, 1fr);
/* 明确3行 */
gap: 2px;
padding: 2px;
margin: 2px 0;
}
.face-cell {
width: 100%;
height: 100%;
border-radius: 2px;
}
.face-cell.white {
background: #ffffff;
}
.face-cell.yellow {
background: #ffff00;
}
.face-cell.blue {
background: #0000ff;
}
.face-cell.green {
background: #00ff00;
}
.face-cell.red {
background: #ff0000;
}
.face-cell.orange {
background: #ff7b00;
}
/* 新增:公式列表样式 */
.formula-list {
position: absolute;
left: 20px;
top: 150px;
background: rgba(255, 255, 255, 0.8);
padding: 0px;
border-radius: 10px;
width: 220px;
box-sizing: border-box;
max-height: 60vh;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.formula-list h3 {
margin: 0 0 15px 0;
color: #333;
border-bottom: 1px solid #ddd;
padding-bottom: 10px;
margin: 15px;
}
.formula-list>div {
max-height: calc(60vh - 60px);
overflow-y: auto;
padding: 5px 5px 15px 5px;
box-sizing: border-box;
}
.formula-list ul {
list-style: none;
padding: 0;
margin: 0;
}
.formula-list li {
padding: 10px;
margin: 5px 0;
background: #f5f5f5;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
}
.formula-list li:hover {
background: #2196F3;
color: white;
transform: translateX(5px);
}
/* 新增:命令序列显示样式 */
.command-sequence {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: #fff;
opacity: 0.3;
color: rgb(0, 0, 0);
padding: 10px 20px;
border-radius: 10px;
font-family: Arial, sans-serif;
font-size: 16px;
min-width: 100px;
text-align: center;
display: none;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
transition: all 0.3s;
}
/* 新增:命令输入框样式 */
.command-input {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.9);
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 100;
display: none;
width: 300px;
}
.command-input input {
width: 100%;
padding: 12px 15px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 16px;
margin-bottom: 10px;
transition: border 0.3s;
box-sizing: border-box;
}
.command-input input:focus {
border-color: #2196F3;
outline: none;
}
.command-input p {
margin: 0 0 10px 0;
color: #666;
font-size: 14px;
}
</style>
</head>
<body>
<!-- 新增:命令序列显示容器 -->
<div class="command-sequence" id="commandSequence"></div>
<!-- 修改后的操作指南 -->
<div class="operation-guide">
<h3>操作指南</h3>
<p>←→↑↓XYZ: 魔方整体旋转</p>
<p>RLFBUD: 魔方操作</p>
</div>
<div class="buttonContainer">
<div class="button" id="randomBtn">随机打乱</div>
<div class="button" id="resetBtn">重置魔方</div>
</div>
<!-- 修改:魔方颜色选择面板 -->
<div class="color-panel">
<div class="color-options">
<div class="color-option white selected" data-color="white"></div>
<div class="color-option yellow" data-color="yellow"></div>
<div class="color-option blue" data-color="blue"></div>
<div class="color-option green" data-color="green"></div>
<div class="color-option red" data-color="red"></div>
<div class="color-option orange" data-color="orange"></div>
</div>
<div class="edit-toggle off">
<span>OFF</span>
</div>
</div>
<!-- 新增:魔方展开图容器 -->
<div id="unfoldContainer"></div>
<!-- 新增:公式列表 -->
<div class="formula-list">
<h3>常用公式</h3>
<div>
<ul>
<li data-formula="RUR'U'">右手公式 (RUR'U')</li>
<li data-formula="L'U'LU">左手公式 (L'U'LU)</li>
<li data-formula="R'U'R'U'R'URUR">右手二层公式</li>
<li data-formula="LULULU'L'U'L'">左手二层公式</li>
<li data-formula="RUUR'URUUR'UF'U'F">二层棱块交换公式</li>
<li data-formula="FRUR'U'F'">十字公式 (FRUR'U'F')</li>
<li data-formula="RUR'URU2R'">小鱼公式 (RUR'URU2R')</li>
<li data-formula="R'U'RU'R'U2R">顶面公式1(左下黄块)</li>
<li data-formula="RUR'URU2R'">顶面公式2(右下黄块)</li>
<li data-formula="RB'RF2R'BRF2R2">顶面角块</li>
<li data-formula="R'UR'U'R'U'R'URUR2">最后还原步骤</li>
</ul>
</div>
</div>
<!-- 新增:命令输入框 -->
<div class="command-input" id="commandInput">
<p>输入命令序列(Shift键取反):</p>
<input type="text" id="commandInputText" placeholder="例如:R U R' U'" autocomplete="off">
</div>
<script src="three.min.js"></script>
<script src="threejs-axis.js"></script>
</body>
</html>
// 全局配置
const CONFIG = {
camera: {
fov: 75,
near: 0.1,
far: 1000,
position: { x: 5, y: 4, z: 8 }
},
axes: {
length: 5,
arrowSize: 0.5,
colors: {
x: 0xff0000,
y: 0x00ff00,
z: 0x0000ff
}
},
cube: {
size: 1,
spacing: 0,
innerColor: 0x555555,
borderColor: 0x000000,
borderWidth: 0.05,
colors: [
0xff0000, // 红 (右)
0xff7b00, // 橙 (左)
0xffffff, // 白 (上)
0xffff00, // 黄 (下)
0x0000ff, // 蓝 (前)
0x00ff00 // 绿 (后)
]
},
unfold: {
containerId: 'unfoldContainer',
faces: {
back: { row: 1, col: 2, color: 'green' }, // 背面 (绿)
up: { row: 2, col: 2, color: 'white' }, // 顶面 (白)
left: { row: 3, col: 1, color: 'orange' }, // 左面 (橙)
front: { row: 3, col: 2, color: 'blue' }, // 前面 (蓝)
right: { row: 3, col: 3, color: 'red' }, // 右面 (红)
down: { row: 4, col: 2, color: 'yellow' } // 底面 (黄)
}
}
};
/**
* 表示立方体旋转状态的类(初始顺序:右, 左, 上, 下, 前, 后)
*/
class CubeRotation {
constructor() {
this.reset();
}
// 重置
reset() {
// 初始状态:每个面直接对应原始面
// 顺序:[右, 左, 上, 下, 前, 后]
// this.faces = [0, 1, 2, 3, 4, 5];
this.faces = ['right', 'left', 'up', 'down', 'front', 'back'];
this.right = 'right';
this.left = 'left';
this.up = 'up';
this.down = 'down';
this.front = 'front';
this.back = 'back';
}
/**
* 绕X轴旋转90度
* @param {boolean} clockwise 是否顺时针(默认true)
*/
rotateX(clockwise = true) {
const [right, left, up, down, front, back] = this.faces;
if (clockwise) {
// 顺时针旋转:
// 前面 -> 上面
// 上面 -> 后面
// 后面 -> 下面
// 下面 -> 前面
// 左右面不变
// s.faces = [right, left, up, down, front, back];
this.faces = [right, left, front, back, down, up];
} else {
// 逆时针旋转:
// 前面 -> 下面
// 下面 -> 后面
// 后面 -> 上面
// 上面 -> 前面
// 左右面不变
// s.faces = [right, left, up, down, front, back];
this.faces = [right, left, back, front, up, down];
}
this.right = this.faces[0];
this.left = this.faces[1];
this.up = this.faces[2];
this.down = this.faces[3];
this.front = this.faces[4];
this.back = this.faces[5];
return this;
}
/**
* 绕Y轴旋转90度
* @param {boolean} clockwise 是否顺时针(默认true)
*/
rotateY(clockwise = true) {
const [right, left, up, down, front, back] = this.faces;
if (clockwise) {
// 顺时针旋转:
// 前面 -> 右面
// 右面 -> 后面
// 后面 -> 左面
// 左面 -> 前面
// 上下不变
// s.faces = [right, left, up, down, front, back];
this.faces = [front, back, up, down, left, right];
} else {
// 逆时针旋转:
// 前面 -> 左面
// 左面 -> 后面
// 后面 -> 右面
// 右面 -> 前面
// 上下不变
// s.faces = [right, left, up, down, front, back];
this.faces = [back, front, up, down, right, left];
}
this.right = this.faces[0];
this.left = this.faces[1];
this.up = this.faces[2];
this.down = this.faces[3];
this.front = this.faces[4];
this.back = this.faces[5];
return this;
}
/**
* 绕Z轴旋转90度
* @param {boolean} clockwise 是否顺时针(默认true)
*/
rotateZ(clockwise = true) {
const [right, left, up, down, front, back] = this.faces;
if (clockwise) {
// 顺时针旋转:
// 上面 -> 右面
// 右面 -> 下面
// 下面 -> 左面
// 左面 -> 上面
// 前后不变
// s.faces = [right, left, up, down, front, back];
this.faces = [up, down, left, right, front, back];
} else {
// 逆时针旋转:
// 上面 -> 左面
// 左面 -> 下面
// 下面 -> 右面
// 右面 -> 上面
// 前后不变
// s.faces = [right, left, up, down, front, back];
this.faces = [down, up, right, left, front, back];
}
this.right = this.faces[0];
this.left = this.faces[1];
this.up = this.faces[2];
this.down = this.faces[3];
this.front = this.faces[4];
this.back = this.faces[5];
return this;
}
/**
* 应用旋转序列
* @param {Array} rotations 旋转序列,如 [{axis: 'x', clockwise: true}, ...]
* @returns {Object} 面对应关系
*/
applyRotations(rotations = []) {
rotations.forEach(({ axis, clockwise = true }) => {
switch (axis.toLowerCase()) {
case 'x': this.rotateX(clockwise); break;
case 'y': this.rotateY(clockwise); break;
case 'z': this.rotateZ(clockwise); break;
default: throw new Error(`Invalid rotation axis: ${axis}`);
}
});
return this;
}
}
// 新增:贝塞尔曲线序列生成函数
function generateBezierSequence(startValue, endValue, steps) {
const sequence = [];
for (let i = 0; i <= steps; i++) {
const t = i / steps;
// 二次贝塞尔曲线 (慢-快)
const value = startValue + (endValue - startValue) * t * t;
sequence.push(value);
}
return sequence;
}
// 全局应用对象
const APP = {
// 新增:选中的颜色
selectedColor: 'white',
// 新增:编辑模式状态
editMode: false,
// 新增:旋转速度
rotationSpeed: 0.05,
// 新增:初始旋转状态
initialRotation: { x: 0, y: 0, z: 0 },
// 新增:魔方组
cubeGroup: null,
cubes: [],
rotationGroup: null,
// 新增:每个面的中心cube
centerCubes: {
right: null, // 右 (红)
left: null, // 左 (橙)
up: null, // 上 (白)
down: null, // 下 (黄)
front: null, // 前 (蓝)
back: null // 后 (绿)
},
// 新增:按面分组的方块数组
facesGroups: {
right: [], // 右 (红)
left: [], // 左 (橙)
up: [], // 上 (白)
down: [], // 下 (黄)
front: [], // 前 (蓝)
back: [] // 后 (绿)
},
keySequence: [],
animate: {
running: false,
finish: null,
params: {
face: null,
direction: null,
clockwise: true,
},
step: 0,
},
bezier: null,
unfold: {
front: [],
back: [],
left: [],
right: [],
up: [],
down: [],
}
};
// 新增:重置旋转函数
function resetCubeRotation() {
if (APP.keySequence.length == 0) {
if (APP.cubeGroup) {
APP.cubeGroup.rotation.set(
APP.initialRotation.x,
APP.initialRotation.y,
APP.initialRotation.z
);
}
APP.cubes.forEach(cube => {
cube.rotation.set(0, 0, 0);
cube.position.copy(cube.userData.initialPosition);
cube.userData.faceMapping.reset();
cube.material.forEach((material, index) => {
material.color.setHex(cube.userData.materialColors[index]);
});
});
const reset = (a) => {
for (let i = 0; i < a.length; i++) {
for (let j = 0; j < a[i].length; j++) {
a[i][j].e.className = `face-cell ${a[i][j].color}`;
}
}
};
reset(APP.unfold.right);
reset(APP.unfold.left);
reset(APP.unfold.up);
reset(APP.unfold.down);
reset(APP.unfold.front);
reset(APP.unfold.back);
}
updateCommandSequenceDisplay(); // 新增:更新命令显示
}
// 新增:根据camera看到的角度进行前后左右上下分组
function groupCubesByFace() {
// 清空原有分组,为每个面初始化空数组
for (const face in APP.facesGroups) {
APP.facesGroups[face] = [];
}
const halfSize = (CONFIG.cube.size + CONFIG.cube.spacing);
// 遍历cubeGroup中的所有立方体,按位置分组到对应面
APP.cubes.forEach(cube => {
// 根据立方体位置判断属于哪个面
if (cube.position.x <= -halfSize) {
APP.facesGroups.left.push(cube); // 左侧面
}
if (cube.position.x >= halfSize) {
APP.facesGroups.right.push(cube); // 右侧面
}
if (cube.position.y <= -halfSize) {
APP.facesGroups.down.push(cube); // 下侧面
}
if (cube.position.y >= halfSize) {
APP.facesGroups.up.push(cube); // 上侧面
}
if (cube.position.z <= -halfSize) {
APP.facesGroups.back.push(cube); // 后侧面
}
if (cube.position.z >= halfSize) {
APP.facesGroups.front.push(cube); // 前侧面
}
});
}
function createRubiksCube(scene) {
const half = (CONFIG.cube.size + CONFIG.cube.spacing) * 1;
// 新增:创建旋转组
APP.rotationGroup = new THREE.Group();
// 修改:创建魔方组并添加到旋转组
APP.cubeGroup = new THREE.Group();
// 修改:将整个旋转组添加到场景
scene.add(APP.rotationGroup);
scene.add(APP.cubeGroup);
// 新增:创建边框材质
const borderMaterial = new THREE.LineBasicMaterial({
color: CONFIG.cube.borderColor,
linewidth: CONFIG.cube.borderWidth
});
for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
for (let z = -1; z <= 1; z++) {
const geometry = new THREE.BoxGeometry(
CONFIG.cube.size,
CONFIG.cube.size,
CONFIG.cube.size
);
// 创建6个面的材质数组
const materials = [];
// 默认每个面的颜色
const material_colors = [
CONFIG.cube.innerColor, // 右
CONFIG.cube.innerColor, // 左
CONFIG.cube.innerColor, // 上
CONFIG.cube.innerColor, // 下
CONFIG.cube.innerColor, // 前
CONFIG.cube.innerColor // 后
];
// 外层方块 - 6个面不同颜色
material_colors[0] = (x === 1) ? CONFIG.cube.colors[0] : CONFIG.cube.innerColor // 右 (红)
material_colors[1] = (x === -1) ? CONFIG.cube.colors[1] : CONFIG.cube.innerColor; // 左 (绿)
material_colors[2] = (y === 1) ? CONFIG.cube.colors[2] : CONFIG.cube.innerColor; // 上 (蓝)
material_colors[3] = (y === -1) ? CONFIG.cube.colors[3] : CONFIG.cube.innerColor; // 下 (黄)
material_colors[4] = (z === 1) ? CONFIG.cube.colors[4] : CONFIG.cube.innerColor; // 前 (紫)
material_colors[5] = (z === -1) ? CONFIG.cube.colors[5] : CONFIG.cube.innerColor; // 后 (青)
for (let i = 0; i < 6; i++) {
materials.push(new THREE.MeshBasicMaterial({ color: material_colors[i] }));
}
const cube = new THREE.Mesh(geometry, materials);
cube.position.set(
x * (CONFIG.cube.size + CONFIG.cube.spacing),
y * (CONFIG.cube.size + CONFIG.cube.spacing),
z * (CONFIG.cube.size + CONFIG.cube.spacing)
);
// 确保所有cube都设置了userData
cube.userData = {
initialPosition: cube.position.clone(),
initialRotation: cube.rotation.clone(),
initialScale: cube.scale.clone(),
faceMapping: new CubeRotation(),
materialColors: [
material_colors[0],
material_colors[1],
material_colors[2],
material_colors[3],
material_colors[4],
material_colors[5],
],
};
// 修改:将方块添加到组而不是场景
APP.cubeGroup.add(cube);
APP.cubes.push(cube);
// 新增:记录中心cube
if (x === 1 && y === 0 && z === 0) {
APP.centerCubes.right = cube;
}
if (x === -1 && y === 0 && z === 0) {
APP.centerCubes.left = cube;
}
if (y === 1 && x === 0 && z === 0) {
APP.centerCubes.up = cube;
}
if (y === -1 && x === 0 && z === 0) {
APP.centerCubes.down = cube;
}
if (z === 1 && x === 0 && y === 0) {
APP.centerCubes.front = cube;
}
if (z === -1 && x === 0 && y === 0) {
APP.centerCubes.back = cube;
}
// 新增:为每个方块添加边框
const edges = new THREE.EdgesGeometry(geometry);
const line = new THREE.LineSegments(edges, borderMaterial);
cube.add(line);
}
}
}
}
// 合并后的初始化函数
function initThreeJS(width, height) {
APP.scene = new THREE.Scene();
APP.camera = new THREE.PerspectiveCamera(
CONFIG.camera.fov,
width / height,
CONFIG.camera.near,
CONFIG.camera.far
);
APP.camera.position.set(
CONFIG.camera.position.x,
CONFIG.camera.position.y,
CONFIG.camera.position.z
);
APP.camera.lookAt(0, 0, 0);
APP.renderer = new THREE.WebGLRenderer({ antialias: true }); // 开启抗锯齿
APP.renderer.setSize(width, height);
document.body.appendChild(APP.renderer.domElement);
createAxes(APP.scene);
createRubiksCube(APP.scene);
createUnfoldContainer(); // 新增:创建展开图
APP.bezier = generateBezierSequence(0, Math.PI / 2, 15);
}
// 创建坐标轴函数
function createAxes(scene) {
const dirX = new THREE.Vector3(1, 0, 0);
const dirY = new THREE.Vector3(0, 1, 0);
const dirZ = new THREE.Vector3(0, 0, 1);
const origin = new THREE.Vector3(0, 0, 0);
const arrowX = new THREE.ArrowHelper(dirX, origin, CONFIG.axes.length, CONFIG.axes.colors.x, CONFIG.axes.arrowSize);
const arrowY = new THREE.ArrowHelper(dirY, origin, CONFIG.axes.length, CONFIG.axes.colors.y, CONFIG.axes.arrowSize);
const arrowZ = new THREE.ArrowHelper(dirZ, origin, CONFIG.axes.length, CONFIG.axes.colors.z, CONFIG.axes.arrowSize);
scene.add(arrowX);
scene.add(arrowY);
scene.add(arrowZ);
}
// 主程序
initThreeJS(window.innerWidth, window.innerHeight);
// 位置和旋转角度对齐
function alignCubePositionAndRotation(cube) {
const list = [-(CONFIG.cube.size + CONFIG.cube.spacing), 0, (CONFIG.cube.size + CONFIG.cube.spacing)];
const rotations = [-(Math.PI), -(Math.PI / 2), 0, Math.PI / 2, Math.PI];
list.forEach(v => {
if (cube.position.x >= (v - 0.01) && cube.position.x <= (v + 0.01))
cube.position.x = v;
if (cube.position.z >= (v - 0.01) && cube.position.z <= (v + 0.01))
cube.position.z = v;
if (cube.position.y >= (v - 0.01) && cube.position.y <= (v + 0.01))
cube.position.y = v;
});
rotations.forEach(v => {
if (cube.rotation.x >= (v - 0.01) && cube.rotation.x <= (v + 0.01))
cube.rotation.x = v;
if (cube.rotation.y >= (v - 0.01) && cube.rotation.y <= (v + 0.01))
cube.rotation.y = v;
if (cube.rotation.z >= (v - 0.01) && cube.rotation.z <= (v + 0.01))
cube.rotation.z = v;
});
}
// 移动立方体到新的组
function moveCubeBetweenGroup(cube, newParent) {
const oldParent = cube.parent;
const worldMatrix = cube.matrixWorld.clone();
if (oldParent) {
oldParent.updateMatrixWorld();
oldParent.remove(cube);
}
newParent.add(cube);
// 计算新父级的逆矩阵
const parentInverseMatrix = new THREE.Matrix4().copy(newParent.matrixWorld).invert();
const newLocalMatrix = worldMatrix.premultiply(parentInverseMatrix);
cube.matrix.copy(newLocalMatrix);
cube.matrix.decompose(cube.position, cube.quaternion, cube.scale);
// 可选
newParent.updateMatrixWorld();
}
// 魔方整体旋转
function rotationRubikCube(direction, clockwise) {
prepareRubikRotation();
APP.animate.params.face = 'all';
APP.animate.params.direction = direction;
APP.animate.params.clockwise = clockwise;
APP.animate.finish = function () {
const clockwise = (APP.animate.params.direction === 'y') ?
(APP.animate.params.clockwise) : (!APP.animate.params.clockwise);
// 将旋转的小方块,归还到cubeGroup中
APP.cubes.forEach(cube => {
// 将小方块归还到cubeGroup中
moveCubeBetweenGroup(cube, APP.cubeGroup);
// 旋转完成后,调整发生旋转的小方块的面的对应关系
cube.userData.faceMapping.applyRotations([{
axis: APP.animate.params.direction,
clockwise: clockwise,
}]);
// 调整小方块的位置和旋转,对齐到标准位置
alignCubePositionAndRotation(cube);
});
// 同步小方块到 unfoldCube 中
syncRubikFaceToUnfold(APP.cubes);
APP.animate.running = false;
};
}
// 按照面进行旋转
function rotationRubikFace(face, direction, clockwise) {
prepareFaceRotation(face);
APP.animate.params.face = face;
APP.animate.params.direction = direction;
APP.animate.params.clockwise = clockwise;
APP.animate.finish = function () {
const clockwise = (APP.animate.params.direction === 'y') ?
(APP.animate.params.clockwise) : (!APP.animate.params.clockwise);
const cubes = [];
// 将旋转的小方块,归还到cubeGroup中
APP.facesGroups[APP.animate.params.face].forEach(cube => {
// 将小方块归还到cubeGroup中
moveCubeBetweenGroup(cube, APP.cubeGroup);
// 旋转完成后,调整发生旋转的小方块的面的对应关系
cube.userData.faceMapping.applyRotations([{
axis: APP.animate.params.direction,
clockwise: clockwise,
}]);
// 调整小方块的位置和旋转,对齐到标准位置
alignCubePositionAndRotation(cube);
cubes.push(cube);
});
// 同步小方块到 unfoldCube 中
syncRubikFaceToUnfold(cubes);
APP.animate.running = false;
};
}
function prepareRubikRotation() {
APP.cubeGroup.rotation.set(0, 0, 0);
APP.cubeGroup.position.set(0, 0, 0);
APP.rotationGroup.rotation.set(0, 0, 0);
APP.rotationGroup.position.set(0, 0, 0);
APP.cubes.forEach(cube => {
APP.cubeGroup.remove(cube);
APP.rotationGroup.add(cube);
});
}
function prepareFaceRotation(face) {
APP.cubeGroup.rotation.set(0, 0, 0);
APP.cubeGroup.position.set(0, 0, 0);
APP.rotationGroup.rotation.set(0, 0, 0);
APP.rotationGroup.position.set(0, 0, 0);
// 新增:按面分组立方体
groupCubesByFace();
// 修改:遍历front组中的每个cube,然后设置其materials
APP.facesGroups[face].forEach(cube => {
APP.cubeGroup.remove(cube);
APP.rotationGroup.add(cube);
});
}
function randomizeCubeRotation() {
// 重置魔方到初始状态
resetCubeRotation();
const scrambles = [
// 1. WCA官方比赛级打乱
["D2", "U'", "R'", "D", "U2", "L'", "D", "B2", "R2", "U'", "D'", "L2", "D'", "R2", "U"],
// 2. 多层复合打乱
["U2", "F'", "B'", "U2", "D", "R", "D", "L'", "F'", "D", "R'", "B2", "U", "R'", "D"],
// 3. 角块优先打乱
["F'", "U2", "L2", "U'", "L'", "D2", "L2", "B'", "L", "R", "D", "B2", "L'", "U", "D'"],
// 4. 高阶随机化公式
["R", "L'", "F", "R'", "B'", "L2", "R2", "D2", "B2", "R", "B'", "R2", "B", "L", "D"],
// 5. 桥式解法专用打乱
["B'", "R2", "F2", "U'", "F'", "B", "U2", "L", "R2", "U", "R", "F2", "L", "B'", "R2"],
// 6. 长步数打乱(25步)
["U", "D2", "L", "R", "U2", "B", "D", "F", "L", "B2", "R", "L", "F", "U2", "R", "F", "L", "U2", "R", "B", "U", "R", "B", "F", "L2"],
// 7. CFOP法针对性打乱
["F2", "B", "R", "D2", "F", "B", "L", "U", "R2", "F", "B", "D", "L", "R", "D", "B", "U", "D", "L2", "F", "U", "B", "D", "R", "B"],
// 8. 非对称打乱
["L2", "D'", "R2", "D'", "F2", "L2", "U", "R2", "U2", "B2", "U'", "L'", "B", "L2", "D2", "F", "R'", "B", "F2", "D"],
// 9. 比赛实战打乱
["R2", "U'", "F2", "D'", "B2", "D2", "B2", "U'", "R2", "U2", "L'", "R'", "U'", "F'", "L'", "D'", "B", "U", "L2", "U", "R'"],
// 10. 花式造型打乱
["U'", "L'", "U'", "F'", "R2", "B'", "R", "F", "U", "B2", "U", "B'", "L", "U'", "F", "U", "R", "F'"]
];
const sequence = scrambles[Math.floor(Math.random() * scrambles.length)];
// 放入APP.keySequence
APP.keySequence = sequence;
updateCommandSequenceDisplay(); // 新增:更新命令显示
}
// 新增:键盘控制函数
function setupKeyboardControls() {
document.addEventListener('keydown', (event) => {
if (["Backspace", "Enter", "Delete"].find(key => key === event.key)) {
return;
}
if (event.ctrlKey) {
const key = event.key.toLowerCase();
if (key === 'c' || key === 'v' || key === 'x' || key === 'a' || key === 'z') {
return;
}
}
const keyActions = {
'ArrowUp': () => { APP.keySequence.push("X"); },
'ArrowDown': () => { APP.keySequence.push("X'"); },
'ArrowLeft': () => { APP.keySequence.push("Y"); },
'ArrowRight': () => { APP.keySequence.push("Y'"); },
'x': () => { APP.keySequence.push("X"); },
'X': () => { APP.keySequence.push("X'"); },
'y': () => { APP.keySequence.push("Y"); },
'Y': () => { APP.keySequence.push("Y'"); },
'z': () => { APP.keySequence.push("Z"); },
'Z': () => { APP.keySequence.push("Z'"); },
' ': () => { resetCubeRotation(); },
'i': () => { if (event.ctrlKey) showCommandInput(); },
'I': () => { if (event.ctrlKey) showCommandInput(); },
'Escape': () => { hideCommandInput(); },
'Enter': () => { },
"f": () => { APP.keySequence.push("F"); },
"F": () => { APP.keySequence.push("F'"); },
"r": () => { APP.keySequence.push("R"); },
"R": () => { APP.keySequence.push("R'"); },
"u": () => { APP.keySequence.push("U"); },
"U": () => { APP.keySequence.push("U'"); },
"l": () => { APP.keySequence.push("L"); },
"L": () => { APP.keySequence.push("L'"); },
"d": () => { APP.keySequence.push("D"); },
"D": () => { APP.keySequence.push("D'"); },
"b": () => { APP.keySequence.push("B"); },
"B": () => { APP.keySequence.push("B'"); },
};
if (keyActions[event.key]) {
keyActions[event.key]();
}
event.preventDefault();
event.stopPropagation();
});
}
// 添加动画循环
function animate() {
requestAnimationFrame(animate);
if (APP.animate.running) {
if (APP.animate.step < APP.bezier.length) {
const angle = APP.bezier[APP.animate.step++];
APP.rotationGroup.rotation[APP.animate.params.direction] = (APP.animate.params.clockwise) ? angle : (-angle);
} else {
APP.animate.finish();
}
} else if (APP.keySequence.length > 0) {
const keyActions = {
"X": () => { rotationRubikCube('x', false); },
"X'": () => { rotationRubikCube('x', true); },
"Y": () => { rotationRubikCube('y', false); },
"Y'": () => { rotationRubikCube('y', true); },
"Z": () => { rotationRubikCube('z', false); },
"Z'": () => { rotationRubikCube('z', true); },
"F": () => { rotationRubikFace('front', 'z', false); },
"F'": () => { rotationRubikFace('front', 'z', true); },
"B": () => { rotationRubikFace('back', 'z', true); },
"B'": () => { rotationRubikFace('back', 'z', false); },
"L": () => { rotationRubikFace('left', 'x', true); },
"L'": () => { rotationRubikFace('left', 'x', false); },
"R": () => { rotationRubikFace('right', 'x', false); },
"R'": () => { rotationRubikFace('right', 'x', true); },
"U": () => { rotationRubikFace('up', 'y', false); },
"U'": () => { rotationRubikFace('up', 'y', true); },
"D": () => { rotationRubikFace('down', 'y', true); },
"D'": () => { rotationRubikFace('down', 'y', false); },
};
let key = "";
if (APP.keySequence[0].length > 1) {
if (APP.keySequence[0][1] == '2') {
APP.keySequence[0] = APP.keySequence[0][0] + "";
key = APP.keySequence[0];
} else {
key = APP.keySequence.shift();
}
} else {
key = APP.keySequence.shift();
}
if (keyActions[key]) {
keyActions[key]();
APP.animate.running = true;
APP.animate.step = 0;
}
updateCommandSequenceDisplay(); // 新增:更新命令显示
} else {
// updateCommandSequenceDisplay(); // 新增:隐藏命令显示
}
APP.renderer.render(APP.scene, APP.camera);
}
animate();
// 新增:设置键盘控制
setupKeyboardControls();
// 新增:处理窗口大小变化
window.addEventListener('resize', () => {
APP.camera.aspect = window.innerWidth / window.innerHeight;
APP.camera.updateProjectionMatrix();
APP.renderer.setSize(window.innerWidth, window.innerHeight);
});
// 新增:按钮事件监听
document.getElementById('resetBtn').addEventListener('click', () => {
resetCubeRotation();
});
document.getElementById('randomBtn').addEventListener('click', () => {
randomizeCubeRotation();
});
// 新增:颜色选择交互逻辑
document.querySelectorAll('.color-option').forEach(option => {
option.addEventListener('click', function () {
// 先移除所有选项的selected类
document.querySelectorAll('.color-option').forEach(opt => {
opt.classList.remove('selected');
});
// 为当前点击的选项添加selected类
this.classList.add('selected');
// 记录选中的颜色 - 修改为使用data-color属性
APP.selectedColor = this.dataset.color;
});
});
// 新增:绑定编辑模式切换按钮事件
document.querySelector('.edit-toggle').addEventListener('click', () => {
APP.editMode = !APP.editMode;
const toggleBtn = document.querySelector('.edit-toggle');
toggleBtn.classList.toggle('on', APP.editMode);
toggleBtn.classList.toggle('off', !APP.editMode);
toggleBtn.querySelector('span').textContent = APP.editMode ? 'ON' : 'OFF';
});
// 新增:公式列表点击事件
document.querySelectorAll('.formula-list li').forEach(item => {
item.addEventListener('click', function () {
InputKeySequence(this.dataset.formula);
});
});
function InputKeySequence(formula) {
// 将公式分解为单个操作
const list = formula.split('');
list.push(' ');
for (let i = 0; i < list.length; i++) {
if (list[i] === "'" || list[i] === "2" || list[i] === ' ') {
continue;
}
if (list[i + 1] === "'" || list[i + 1] === "2") {
APP.keySequence.push(list[i] + list[i + 1]);
} else {
APP.keySequence.push(list[i]);
}
}
}
// 修改:face-cell点击事件
function createUnfoldContainer() {
const container = document.createElement('div');
container.id = CONFIG.unfold.containerId;
// 创建每个面的展开图
for (const [face, config] of Object.entries(CONFIG.unfold.faces)) {
const faceDiv = document.createElement('div');
faceDiv.className = 'unfold-face';
faceDiv.style.gridRow = config.row;
faceDiv.style.gridColumn = config.col;
// 创建3x3的面格子
for (let r = 0; r < 3; r++) {
APP.unfold[face].push([]);
for (let c = 0; c < 3; c++) {
const cell = document.createElement('div');
cell.classList.add('face-cell');
cell.classList.add(config.color);
cell.dataset.face = face;
cell.dataset.row = r;
cell.dataset.col = c;
// 添加点击事件
cell.addEventListener('click', function () {
if (APP.editMode) {
// 移除所有颜色类
// this.classList.remove('white', 'yellow', 'blue', 'green', 'red', 'orange');
// 添加选中的颜色类
// this.classList.add(APP.selectedColor);
// updateCubeMetrialColor({
// face: this.dataset.face,
// color: APP.selectedColor,
// row: this.dataset.row,
// col: this.dataset.col,
// });
}
});
APP.unfold[face][r].push({ e: cell, color: config.color, id: `${r}-${c}` });
faceDiv.appendChild(cell);
}
}
container.appendChild(faceDiv);
}
document.body.appendChild(container);
}
function updateCubeMetrialColor(args) {
const halfSize = (CONFIG.cube.size + CONFIG.cube.spacing);
const val = [-halfSize, 0, halfSize];
const pos = {
x: val[args.col],
y: val[args.row],
};
// 颜色名称到THREE.js颜色的映射
const colorMap = {
'white': 0xffffff,
'yellow': 0xffff00,
'blue': 0x0000ff,
'green': 0x00ff00,
'red': 0xff0000,
'orange': 0xff7b00
};
APP.cubes.forEach(cube => {
if (cube.position.x === pos.x && cube.position.y === pos.y && cube.position.z === halfSize) {
const faceMap = cube.userData.faceMapping;
for (let i = 0; i < 6; i++) {
if (faceMap.faces[i] === args.face) {
cube.material[i].color.setHex(colorMap[args.color]);
cube.userData.faceMapping.set(i, args.color);
return;
}
}
}
});
}
function syncRubikFaceToUnfold(cubes) {
// 清空原有分组,为每个面初始化空数组
const faces = {
right: [],
left: [],
front: [],
back: [],
up: [],
down: [],
};
const halfSize = (CONFIG.cube.size + CONFIG.cube.spacing);
// 遍历cubeGroup中的所有立方体,按位置分组到对应面
cubes.forEach(cube => {
// 根据立方体位置判断属于哪个面
if (cube.position.x <= -halfSize) {
// faces.left.push(cube); // 左侧面
const posMapY = (v) => {
return (v < 0) ? 2 : ((v > 0) ? 0 : 1);
}
const posMapZ = (v) => {
return (v < 0) ? 0 : ((v > 0) ? 2 : 1);
}
faces.left.push({
row: posMapY(cube.position.y),
col: posMapZ(cube.position.z),
faceMap: cube.userData.faceMapping,
});
}
if (cube.position.x >= halfSize) {
// faces.right.push(cube); // 右侧面
const posMap = (v) => {
return (v < 0) ? 2 : ((v > 0) ? 0 : 1);
}
faces.right.push({
row: posMap(cube.position.y),
col: posMap(cube.position.z),
faceMap: cube.userData.faceMapping,
});
}
if (cube.position.y <= -halfSize) {
// faces.down.push(cube); // 下侧面
const posMapX = (v) => {
return (v < 0) ? 0 : ((v > 0) ? 2 : 1);
}
const posMapZ = (v) => {
return (v < 0) ? 2 : ((v > 0) ? 0 : 1);
}
faces.down.push({
row: posMapZ(cube.position.z),
col: posMapX(cube.position.x),
faceMap: cube.userData.faceMapping,
});
}
if (cube.position.y >= halfSize) {
// faces.up.push(cube); // 上侧面
const posMap = (v) => {
return (v < 0) ? 0 : ((v > 0) ? 2 : 1);
}
faces.up.push({
row: posMap(cube.position.z),
col: posMap(cube.position.x),
faceMap: cube.userData.faceMapping,
});
}
if (cube.position.z <= -halfSize) {
// faces.back.push(cube); // 后侧面
const posMap = (v) => {
return (v < 0) ? 0 : ((v > 0) ? 2 : 1);
}
const val = {
row: posMap(cube.position.y),
col: posMap(cube.position.x),
faceMap: cube.userData.faceMapping,
};
faces.back.push(val);
// console.log(`(${cube.position.x},${cube.position.y})->(${val.row},${val.col})`);
}
if (cube.position.z >= halfSize) {
// faces.front.push(cube); // 前侧面
const posMapX = (v) => {
return (v < 0) ? 0 : ((v > 0) ? 2 : 1);
}
const posMapY = (v) => {
return (v < 0) ? 2 : ((v > 0) ? 0 : 1);
}
const val = {
row: posMapY(cube.position.y),
col: posMapX(cube.position.x),
faceMap: cube.userData.faceMapping,
};
faces.front.push(val);
// console.log(`(${cube.position.x},${cube.position.y})->(${val.row},${val.col})`);
}
});
const color = {
right: 'red', // 右 (红)
left: 'orange', // 左 (橙)
up: 'white', // 上 (白)
down: 'yellow', // 下 (黄)
front: 'blue', // 前 (蓝)
back: 'green' // 后 (绿)
};
faces.front.forEach((item) => {
APP.unfold.front[item.row][item.col].e.className = `face-cell ${color[item.faceMap.front]}`;
});
faces.back.forEach((item) => {
APP.unfold.back[item.row][item.col].e.className = `face-cell ${color[item.faceMap.back]}`;
});
faces.up.forEach((item) => {
APP.unfold.up[item.row][item.col].e.className = `face-cell ${item.faceMap.up} ${color[item.faceMap.up]}`;
});
faces.down.forEach((item) => {
APP.unfold.down[item.row][item.col].e.className = `face-cell ${color[item.faceMap.down]}`;
});
faces.right.forEach((item) => {
APP.unfold.right[item.row][item.col].e.className = `face-cell ${color[item.faceMap.right]}`;
});
faces.left.forEach((item) => {
APP.unfold.left[item.row][item.col].e.className = `face-cell ${color[item.faceMap.left]}`;
});
}
function updateCommandSequenceDisplay() {
const commandDisplay = document.getElementById('commandSequence');
if (APP.keySequence.length > 0) {
commandDisplay.textContent = APP.keySequence.join(' ');
commandDisplay.style.display = 'block';
} else {
commandDisplay.style.display = 'none';
}
}
// 新增:命令输入框相关函数
function showCommandInput() {
const inputBox = document.getElementById('commandInput');
inputBox.style.display = 'block';
document.getElementById('commandInputText').value = '';
document.getElementById('commandInputText').focus();
}
function hideCommandInput() {
document.getElementById('commandInput').style.display = 'none';
}
// 新增:输入框事件绑定
document.getElementById('commandInputText').addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
InputKeySequence(event.target.value);
hideCommandInput();
updateCommandSequenceDisplay();
} else if (event.key === 'Escape') {
hideCommandInput();
} else if (event.key === 'Backspace' || event.key === 'Delete') {
return;
} else if (event.ctrlKey) {
const key = event.key.toLowerCase();
if (key === 'c' || key === 'v' || key === 'x' || key === 'a' || key === 'z') {
return;
}
} else if (event.key === 'Shift') {
} else {
let key = event.key.toUpperCase();
if ("FBUDLR".includes(key)) {
if (event.shiftKey) {
key += "'";
}
document.getElementById('commandInputText').value += key;
}
}
event.preventDefault();
event.stopPropagation();
});