2025-06-15 Three.js魔方

学习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();
});

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容