Skip to content
LAM
Read Home Blog
Make Projects HTML Tools Games
Touch grass Notes Resume Links
Home Blog HTML Projects
Tools Games Notes Resume Links
Back Interactive Rubik's Cube (N x N x N) Fun
Download Open
Show description 108 chars · Fun

Interactive Rubik's Cube (N x N x N)

Interactive Rubik's Cube (N x N x N)





Cube Size (N):

x N x N


Generate Cube
Scramble
Solve

Interactive Rubik's Cube (N x N x N)

20,856 bytes · HTML source
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interactive Rubik's Cube (N x N x N)</title>
    <style>
        body { 
            margin: 0; 
            font-family: Arial, sans-serif; 
            display: flex; 
            flex-direction: column; 
            align-items: center; 
            background-color: #333;
            color: #fff;
        }
        #controls-container { 
            margin: 10px; 
            padding: 15px; 
            border: 1px solid #555; 
            background-color: #444; 
            border-radius: 8px;
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            gap: 10px;
        }
        label, input, button { 
            margin: 5px; 
            padding: 8px 12px;
            border-radius: 5px;
            border: 1px solid #666;
            background-color: #555;
            color: #fff;
        }
        input[type="number"] {
            width: 60px;
        }
        button {
            cursor: pointer;
            transition: background-color 0.2s;
        }
        button:hover {
            background-color: #6a6a6a;
        }
        button:active {
            background-color: #777;
        }
        canvas { 
            display: block; 
            width: 100%;
            max-width: 1000px; /* Max width for the canvas */
            height: auto; 
            aspect-ratio: 16 / 9; /* Maintain aspect ratio */
            border-radius: 8px;
        }
        #buttons-container { 
            margin-top: 15px; 
            padding: 10px;
            border: 1px solid #555;
            background-color: #444;
            border-radius: 8px;
            display: flex;
            flex-direction: column;
            gap: 8px;
            max-height: 200px; /* Limit height and make scrollable */
            overflow-y: auto; /* Add scroll for many buttons */
            width: 90%;
            max-width: 600px;
        }
        .face-buttons {
            display: flex;
            align-items: center;
            gap: 5px;
            padding: 5px;
            border-bottom: 1px solid #505050;
        }
        .face-buttons:last-child {
            border-bottom: none;
        }
        .face-buttons span {
            min-width: 120px; /* Adjust for consistent label width */
            font-weight: bold;
        }
        .face-buttons button { 
            margin: 2px; 
            padding: 6px 10px; 
            font-size: 0.9em;
        }
    </style>
</head>
<body>
    <div id="controls-container">
        <div>
            <label for="cubeSize">Cube Size (N):</label>
            <input type="number" id="cubeSize" value="3" min="2" max="20">
            <span>x N x N</span>
        </div>
        <button id="generateCube">Generate Cube</button>
        <button id="scrambleCube">Scramble</button>
        <button id="solveCube">Solve</button>
    </div>
    
    <div id="buttons-container">
        </div>

    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.164.1/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
            }
        }
    </script>
    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

        let scene, camera, renderer, controls;
        let rubiksCubeGroup; 
        let cubies = []; 
        let N = 3; 

        const CUBIE_SIZE = 1;
        const CUBIE_SPACING = 0.05; 
        const EFFECTIVE_CUBIE_SIZE = CUBIE_SIZE + CUBIE_SPACING;

        const COLORS = {
            R: 0xdc3545, // Red (Right)
            L: 0xfd7e14, // Orange (Left)
            U: 0xffffff, // White (Up)
            D: 0xffc107, // Yellow (Down)
            F: 0x28a745, // Green (Front)
            B: 0x007bff, // Blue (Back)
            INNER: 0x303030 
        };

        let animationQueue = [];
        let isAnimating = false;
        let moveHistory = [];

        const sizeInput = document.getElementById('cubeSize');
        const generateButton = document.getElementById('generateCube');
        const scrambleButton = document.getElementById('scrambleCube');
        const solveButton = document.getElementById('solveCube');
        const buttonsContainer = document.getElementById('buttons-container');
        let canvasElement;

        init();
        generateRubiksCube(N);
        animate();

        function init() {
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x333333);

            const containerWidth = Math.min(window.innerWidth * 0.9, 1000);
            const containerHeight = containerWidth / (16/9);

            camera = new THREE.PerspectiveCamera(60, containerWidth / containerHeight, 0.1, 1000);
            
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(containerWidth, containerHeight);
            renderer.setPixelRatio(window.devicePixelRatio);
            canvasElement = renderer.domElement;
            document.body.appendChild(canvasElement);


            const ambientLight = new THREE.AmbientLight(0xffffff, 1.2);
            scene.add(ambientLight);
            const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
            directionalLight.position.set(5, 10, 7.5).normalize();
            scene.add(directionalLight);

            controls = new OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true;
            controls.dampingFactor = 0.05;
            controls.minDistance = 3;
            controls.maxDistance = 50;

            generateButton.addEventListener('click', () => {
                const newSize = parseInt(sizeInput.value);
                if (newSize >= 2 && newSize <= 20) {
                    if (isAnimating) {
                        animationQueue = []; // Clear queue if new cube is generated mid-animation
                        isAnimating = false;
                    }
                    N = newSize;
                    clearCube();
                    generateRubiksCube(N);
                    moveHistory = [];
                } else {
                    alert("Please enter a size between 2 and 20.");
                }
            });

            scrambleButton.addEventListener('click', scramble);
            solveButton.addEventListener('click', solve);

            window.addEventListener('resize', onWindowResize, false);
            onWindowResize(); // Call once to set initial size
        }

        function onWindowResize() {
            const containerWidth = Math.min(window.innerWidth * 0.9, 1000);
            const containerHeight = containerWidth / (16/9);
            
            camera.aspect = containerWidth / containerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(containerWidth, containerHeight);
        }
        
        function clearCube() {
            if (rubiksCubeGroup) {
                scene.remove(rubiksCubeGroup);
                 // Dispose of geometries and materials
                rubiksCubeGroup.traverse(object => {
                    if (object.isMesh) {
                        if (object.geometry) object.geometry.dispose();
                        if (object.material) {
                            if (Array.isArray(object.material)) {
                                object.material.forEach(material => material.dispose());
                            } else {
                                object.material.dispose();
                            }
                        }
                    }
                });
            }
            cubies = [];
        }

        function createCubie(x, y, z, sizeN) {
            const geometry = new THREE.BoxGeometry(CUBIE_SIZE, CUBIE_SIZE, CUBIE_SIZE);
            const materials = [];
            const halfN = (sizeN - 1) / 2;

            materials.push(new THREE.MeshStandardMaterial({ color: (Math.abs(x - halfN) < 0.01) ? COLORS.R : COLORS.INNER })); // Right (+X)
            materials.push(new THREE.MeshStandardMaterial({ color: (Math.abs(x + halfN) < 0.01) ? COLORS.L : COLORS.INNER })); // Left (-X)
            materials.push(new THREE.MeshStandardMaterial({ color: (Math.abs(y - halfN) < 0.01) ? COLORS.U : COLORS.INNER })); // Top (+Y)
            materials.push(new THREE.MeshStandardMaterial({ color: (Math.abs(y + halfN) < 0.01) ? COLORS.D : COLORS.INNER })); // Bottom (-Y)
            materials.push(new THREE.MeshStandardMaterial({ color: (Math.abs(z - halfN) < 0.01) ? COLORS.F : COLORS.INNER })); // Front (+Z)
            materials.push(new THREE.MeshStandardMaterial({ color: (Math.abs(z + halfN) < 0.01) ? COLORS.B : COLORS.INNER })); // Back (-Z)
            
            const cubieMesh = new THREE.Mesh(geometry, materials);
            cubieMesh.position.set(
                x * EFFECTIVE_CUBIE_SIZE,
                y * EFFECTIVE_CUBIE_SIZE,
                z * EFFECTIVE_CUBIE_SIZE
            );

            const cubieData = {
                mesh: cubieMesh,
                logicalPos: new THREE.Vector3(x, y, z),
            };
            cubieMesh.userData = cubieData;
            return cubieData;
        }

        function generateRubiksCube(sizeN) {
            clearCube(); // Ensure previous cube is properly cleared
            rubiksCubeGroup = new THREE.Group();
            const offset = (sizeN - 1) / 2;

            for (let i = 0; i < sizeN; i++) {
                for (let j = 0; j < sizeN; j++) {
                    for (let k = 0; k < sizeN; k++) {
                        const x = i - offset;
                        const y = j - offset;
                        const z = k - offset;
                        
                        // Only create cubies that form the shell of the Rubik's cube
                        // if (i === 0 || i === sizeN - 1 || j === 0 || j === sizeN - 1 || k === 0 || k === sizeN - 1) {
                           // No, we need all cubies for the rotation logic to work with layers.
                           // The createCubie function handles coloring inner faces appropriately.
                            const cubieData = createCubie(x, y, z, sizeN);
                            cubies.push(cubieData);
                            rubiksCubeGroup.add(cubieData.mesh);
                        // }
                    }
                }
            }
            scene.add(rubiksCubeGroup);
            setupRotationButtons(sizeN);

            const cubeActualSize = sizeN * EFFECTIVE_CUBIE_SIZE;
            camera.position.set(cubeActualSize * 1.5, cubeActualSize * 1.5, cubeActualSize * 1.5);
            camera.lookAt(0,0,0);
            controls.target.set(0,0,0);
            controls.update();
        }
        
        function animate() {
            requestAnimationFrame(animate);
            if (!isAnimating && animationQueue.length > 0) {
                const currentAnimation = animationQueue.shift();
                isAnimating = true;
                performRotationAnimation(currentAnimation);
            }
            controls.update();
            renderer.render(scene, camera);
        }

        const ANIMATION_DURATION = 300; // ms

        function performRotationAnimation({ axisVec, layerIndex, angle, onComplete, isUndo = false }) {
            const pivot = new THREE.Group();
            rubiksCubeGroup.add(pivot); // Add to cube group, rotate relative to cube's center

            const cubiesToRotate = [];
            cubies.forEach(cubieData => {
                let coord;
                if (axisVec.x !== 0) coord = cubieData.logicalPos.x;
                else if (axisVec.y !== 0) coord = cubieData.logicalPos.y;
                else coord = cubieData.logicalPos.z;

                if (Math.abs(coord - layerIndex) < 0.1) {
                    cubiesToRotate.push(cubieData);
                }
            });

            cubiesToRotate.forEach(cubieData => {
                pivot.attach(cubieData.mesh); // Attach to pivot, preserving world position
            });
            
            const startTime = performance.now();
            let currentRotation = 0;

            function stepAnimation() {
                const elapsedTime = performance.now() - startTime;
                const progress = Math.min(elapsedTime / ANIMATION_DURATION, 1);
                const easeOutCubic = t => 1 - Math.pow(1 - t, 3);
                const easedProgress = easeOutCubic(progress);

                const rotationThisFrame = (angle * easedProgress) - currentRotation;
                
                if (axisVec.x !== 0) pivot.rotation.x += rotationThisFrame * Math.sign(axisVec.x);
                else if (axisVec.y !== 0) pivot.rotation.y += rotationThisFrame * Math.sign(axisVec.y);
                else pivot.rotation.z += rotationThisFrame * Math.sign(axisVec.z);
                currentRotation += rotationThisFrame;

                if (progress < 1) {
                    requestAnimationFrame(stepAnimation);
                } else {
                    // Ensure exact final rotation
                    pivot.rotation.set(0,0,0); // Reset pivot rotation
                    if (axisVec.x !== 0) pivot.rotation.x = angle * Math.sign(axisVec.x);
                    else if (axisVec.y !== 0) pivot.rotation.y = angle * Math.sign(axisVec.y);
                    else pivot.rotation.z = angle * Math.sign(axisVec.z);
                    
                    pivot.updateMatrixWorld(true); // Force update pivot's matrix

                    cubiesToRotate.forEach(cubieData => {
                        // cubieData.mesh.updateMatrixWorld(true); // Ensure cubie mesh matrix is updated
                        rubiksCubeGroup.attach(cubieData.mesh); // Re-attach to main cube group
                        updateLogicalPosition(cubieData, axisVec, angle);
                    });

                    rubiksCubeGroup.remove(pivot);
                    isAnimating = false;
                    
                    if (!isUndo) {
                        moveHistory.push({ axisVec: axisVec.clone(), layerIndex, angle });
                    }
                    if (onComplete) onComplete();
                }
            }
            stepAnimation();
        }

        function updateLogicalPosition(cubieData, axisVec, angle) {
            const rotationMatrix = new THREE.Matrix4();
            if (axisVec.x !== 0) rotationMatrix.makeRotationX(angle * Math.sign(axisVec.x));
            else if (axisVec.y !== 0) rotationMatrix.makeRotationY(angle * Math.sign(axisVec.y));
            else rotationMatrix.makeRotationZ(angle * Math.sign(axisVec.z));
            
            cubieData.logicalPos.applyMatrix4(rotationMatrix);
            cubieData.logicalPos.x = Math.round(cubieData.logicalPos.x * 100)/100; // Mitigate float errors
            cubieData.logicalPos.y = Math.round(cubieData.logicalPos.y * 100)/100;
            cubieData.logicalPos.z = Math.round(cubieData.logicalPos.z * 100)/100;
        }
        
        function queueRotation(axisVec, layerIndex, clockwise, isUndo = false) {
            if (isAnimating && !isUndo && animationQueue.filter(a => !a.isUndo).length > 0) return;

            const angle = (clockwise ? -1 : 1) * Math.PI / 2;
            
            animationQueue.push({
                axisVec: axisVec.clone(),
                layerIndex,
                angle,
                isUndo,
                onComplete: () => { /* isAnimating handled in animate loop */ }
            });
        }

        function setupRotationButtons(currentN) {
            buttonsContainer.innerHTML = '';
            const halfN = (currentN - 1) / 2;

            const mainFaces = [
                { name: 'F (Front)', axis: new THREE.Vector3(0,0,1), layer: halfN, userCwMapsToAxisCw: true },
                { name: 'B (Back)',  axis: new THREE.Vector3(0,0,1), layer: -halfN, userCwMapsToAxisCw: false },
                { name: 'U (Up)',    axis: new THREE.Vector3(0,1,0), layer: halfN, userCwMapsToAxisCw: true },
                { name: 'D (Down)',  axis: new THREE.Vector3(0,1,0), layer: -halfN, userCwMapsToAxisCw: false },
                { name: 'R (Right)', axis: new THREE.Vector3(1,0,0), layer: halfN, userCwMapsToAxisCw: true },
                { name: 'L (Left)',  axis: new THREE.Vector3(1,0,0), layer: -halfN, userCwMapsToAxisCw: false },
            ];

            mainFaces.forEach(face => addRotationButtonSet(face.name, face.axis, face.layer, face.userCwMapsToAxisCw));
            
            if (currentN > 2) {
                const axesInfo = [
                    { namePrefix: 'X-Slice', axis: new THREE.Vector3(1,0,0) },
                    { namePrefix: 'Y-Slice', axis: new THREE.Vector3(0,1,0) },
                    { namePrefix: 'Z-Slice', axis: new THREE.Vector3(0,0,1) },
                ];
                for (let slice = 1; slice < currentN - 1; slice++) { // Iterate for inner slices
                    const layerIndex = slice - halfN;
                    axesInfo.forEach(ax => {
                         // User CW on any internal slice is typically considered CW around the positive axis.
                        addRotationButtonSet(`${ax.namePrefix} ${slice+1}`, ax.axis, layerIndex, true);
                    });
                }
            }
        }

        function addRotationButtonSet(label, axis, layerIndex, userCwMapsToAxisCw) {
            const faceDiv = document.createElement('div');
            faceDiv.classList.add('face-buttons');
            
            const labelSpan = document.createElement('span');
            labelSpan.textContent = `${label}:`;
            faceDiv.appendChild(labelSpan);

            const cwButton = document.createElement('button');
            cwButton.textContent = 'CW';
            cwButton.onclick = () => {
                if (isAnimating && animationQueue.filter(a => !a.isUndo).length > 0) return;
                queueRotation(axis, layerIndex, userCwMapsToAxisCw);
            };

            const ccwButton = document.createElement('button');
            ccwButton.textContent = 'CCW';
            ccwButton.onclick = () => {
                if (isAnimating && animationQueue.filter(a => !a.isUndo).length > 0) return;
                queueRotation(axis, layerIndex, !userCwMapsToAxisCw);
            };
            
            faceDiv.appendChild(cwButton);
            faceDiv.appendChild(ccwButton);
            buttonsContainer.appendChild(faceDiv);
        }

        function scramble() {
            if (isAnimating) return;
            animationQueue = animationQueue.filter(a => a.isUndo); // Clear any pending solve steps
            
            const numScrambleMoves = N * N * 2; // More scramble moves for larger N
            const axes = [new THREE.Vector3(1,0,0), new THREE.Vector3(0,1,0), new THREE.Vector3(0,0,1)];
            const offset = (N-1)/2;

            for (let i = 0; i < numScrambleMoves; i++) {
                const randomAxisVec = axes[Math.floor(Math.random() * axes.length)];
                // Random layer index from -offset to +offset
                const randomLayer = Math.floor(Math.random() * N) - offset; 
                const randomDirectionCw = Math.random() < 0.5; // true for CW, false for CCW (relative to positive axis)
                queueRotation(randomAxisVec, randomLayer, randomDirectionCw, false);
            }
        }

        function solve() {
            if (isAnimating || moveHistory.length === 0) return;
            animationQueue = []; // Clear any existing non-solve animations

            const solveSteps = [];
            for (let i = moveHistory.length - 1; i >= 0; i--) {
                const move = moveHistory[i];
                solveSteps.push({
                    axisVec: move.axisVec.clone(),
                    layerIndex: move.layerIndex,
                    angle: -move.angle, // Inverse angle
                    isUndo: true
                });
            }
            
            moveHistory = []; 
            animationQueue = animationQueue.concat(solveSteps);
        }

    </script>
</body>
</html>