Created
March 27, 2025 08:07
-
-
Save shricodev/e4c12edb3384323be43f1196522eb6f2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Rubik's Cube Visualizer and Solver</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| font-family: Arial, sans-serif; | |
| } | |
| #canvas { | |
| display: block; | |
| } | |
| #controls { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| z-index: 100; | |
| } | |
| button { | |
| background-color: #4caf50; | |
| border: none; | |
| color: white; | |
| padding: 10px 20px; | |
| text-align: center; | |
| text-decoration: none; | |
| display: inline-block; | |
| font-size: 16px; | |
| margin: 4px 2px; | |
| cursor: pointer; | |
| border-radius: 4px; | |
| } | |
| button:hover { | |
| background-color: #45a049; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="controls"> | |
| <button id="scrambleBtn">Scramble</button> | |
| <button id="solveBtn">Solve</button> | |
| </div> | |
| <canvas id="canvas"></canvas> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script> | |
| <script src="cube.js"></script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Global variables | |
| let scene, camera, renderer, controls; | |
| let cube, cubeGroup; | |
| let isAnimating = false; | |
| let moveQueue = []; | |
| const ANIMATION_DURATION = 500; // ms | |
| const COLORS = { | |
| UP: 0xff0000, // Red | |
| DOWN: 0x009900, // Green | |
| LEFT: 0xffff00, // Yellow | |
| RIGHT: 0x8b4513, // Brown | |
| FRONT: 0x800080, // Purple | |
| BACK: 0xff9900, // Orange | |
| INTERNAL: 0x333333, // Dark gray for internal faces | |
| }; | |
| // Initializing the scene | |
| function init() { | |
| // Create scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x222222); | |
| // Create camera | |
| camera = new THREE.PerspectiveCamera( | |
| 75, | |
| window.innerWidth / window.innerHeight, | |
| 0.1, | |
| 1000, | |
| ); | |
| camera.position.set(5, 5, 7); | |
| // Create renderer | |
| renderer = new THREE.WebGLRenderer({ | |
| canvas: document.getElementById("canvas"), | |
| antialias: true, | |
| }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| // Add orbit controls for mouse interaction | |
| controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.1; | |
| // Add lighting | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.7); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(10, 20, 15); | |
| scene.add(directionalLight); | |
| // Create the Rubik's Cube | |
| createCube(); | |
| // Event Listeners | |
| document | |
| .getElementById("scrambleBtn") | |
| .addEventListener("click", scrambleCube); | |
| document.getElementById("solveBtn").addEventListener("click", solveCube); | |
| window.addEventListener("resize", onWindowResize); | |
| // Start animation loop | |
| animate(); | |
| } | |
| // Create the Rubik's Cube | |
| function createCube() { | |
| cubeGroup = new THREE.Group(); | |
| scene.add(cubeGroup); | |
| // Cubelet dimensions | |
| const size = 1; | |
| const spacing = 0.05; | |
| const totalSize = 3 * size + 2 * spacing; | |
| // Cubelet positions | |
| cube = []; | |
| // Create 3x3x3 cubelets | |
| for (let x = -1; x <= 1; x++) { | |
| for (let y = -1; y <= 1; y++) { | |
| for (let z = -1; z <= 1; z++) { | |
| const position = new THREE.Vector3( | |
| x * (size + spacing), | |
| y * (size + spacing), | |
| z * (size + spacing), | |
| ); | |
| // Create materials for each face | |
| const materials = Array(6) | |
| .fill() | |
| .map(() => new THREE.MeshLambertMaterial({ color: COLORS.INTERNAL })); | |
| // Assign colors to the outer faces | |
| if (y === 1) materials[0].color.set(COLORS.UP); // Top face | |
| if (y === -1) materials[1].color.set(COLORS.DOWN); // Bottom face | |
| if (z === 1) materials[2].color.set(COLORS.FRONT); // Front face | |
| if (z === -1) materials[3].color.set(COLORS.BACK); // Back face | |
| if (x === 1) materials[4].color.set(COLORS.RIGHT); // Right face | |
| if (x === -1) materials[5].color.set(COLORS.LEFT); // Left face | |
| // Create cubelet | |
| const geometry = new THREE.BoxGeometry(size, size, size); | |
| const cubelet = new THREE.Mesh(geometry, materials); | |
| cubelet.position.copy(position); | |
| cubelet.userData = { | |
| x, | |
| y, | |
| z, | |
| originalX: x, | |
| originalY: y, | |
| originalZ: z, | |
| }; | |
| cubeGroup.add(cubelet); | |
| cube.push(cubelet); | |
| } | |
| } | |
| } | |
| } | |
| // Move definitions for the cube | |
| const MOVES = { | |
| U: { axis: "y", layer: 1, angle: Math.PI / 2 }, | |
| U_: { axis: "y", layer: 1, angle: -Math.PI / 2 }, | |
| D: { axis: "y", layer: -1, angle: -Math.PI / 2 }, | |
| D_: { axis: "y", layer: -1, angle: Math.PI / 2 }, | |
| R: { axis: "x", layer: 1, angle: -Math.PI / 2 }, | |
| R_: { axis: "x", layer: 1, angle: Math.PI / 2 }, | |
| L: { axis: "x", layer: -1, angle: Math.PI / 2 }, | |
| L_: { axis: "x", layer: -1, angle: -Math.PI / 2 }, | |
| F: { axis: "z", layer: 1, angle: -Math.PI / 2 }, | |
| F_: { axis: "z", layer: 1, angle: Math.PI / 2 }, | |
| B: { axis: "z", layer: -1, angle: Math.PI / 2 }, | |
| B_: { axis: "z", layer: -1, angle: -Math.PI / 2 }, | |
| }; | |
| // Perform a move on the cube | |
| function performMove(moveName, callback) { | |
| if (isAnimating) { | |
| moveQueue.push({ move: moveName, callback }); | |
| return; | |
| } | |
| isAnimating = true; | |
| const move = MOVES[moveName]; | |
| if (!move) { | |
| console.error("Unknown move:", moveName); | |
| isAnimating = false; | |
| if (callback) callback(); | |
| return; | |
| } | |
| // Select cubelets in the layer | |
| const moveGroup = new THREE.Group(); | |
| scene.add(moveGroup); | |
| // Find all cubelets that belong to the layer we're rotating | |
| const axis = move.axis; | |
| const layerValue = move.layer; | |
| cube.forEach((cubelet) => { | |
| if (cubelet.userData[axis] === layerValue) { | |
| // Temporarily attach to the move group | |
| const worldPosition = new THREE.Vector3(); | |
| cubelet.getWorldPosition(worldPosition); | |
| const worldQuaternion = new THREE.Quaternion(); | |
| cubelet.getWorldQuaternion(worldQuaternion); | |
| const worldScale = new THREE.Vector3(); | |
| cubelet.getWorldScale(worldScale); | |
| cubeGroup.remove(cubelet); | |
| moveGroup.add(cubelet); | |
| cubelet.position.copy(worldPosition); | |
| cubelet.quaternion.copy(worldQuaternion); | |
| cubelet.scale.copy(worldScale); | |
| } | |
| }); | |
| // Animate the rotation | |
| const startTime = Date.now(); | |
| const endTime = startTime + ANIMATION_DURATION; | |
| const targetAngle = move.angle; | |
| function animateRotation() { | |
| const now = Date.now(); | |
| let t = (now - startTime) / ANIMATION_DURATION; | |
| if (t >= 1) { | |
| t = 1; | |
| moveGroup.rotation[axis] = targetAngle; | |
| // Update the position data for each cubelet | |
| moveGroup.updateMatrixWorld(true); | |
| while (moveGroup.children.length) { | |
| const cubelet = moveGroup.children[0]; | |
| // Get world position | |
| const worldPosition = new THREE.Vector3(); | |
| cubelet.getWorldPosition(worldPosition); | |
| const worldQuaternion = new THREE.Quaternion(); | |
| cubelet.getWorldQuaternion(worldQuaternion); | |
| const worldScale = new THREE.Vector3(); | |
| cubelet.getWorldScale(worldScale); | |
| // Move back to cubeGroup | |
| moveGroup.remove(cubelet); | |
| cubeGroup.add(cubelet); | |
| cubelet.position.copy(worldPosition); | |
| cubelet.quaternion.copy(worldQuaternion); | |
| cubelet.scale.copy(worldScale); | |
| // Update cubelet userData with new grid coordinates | |
| cubelet.userData.x = Math.round(cubelet.position.x / 1.05); | |
| cubelet.userData.y = Math.round(cubelet.position.y / 1.05); | |
| cubelet.userData.z = Math.round(cubelet.position.z / 1.05); | |
| } | |
| scene.remove(moveGroup); | |
| isAnimating = false; | |
| if (callback) callback(); | |
| // Check for queued moves | |
| if (moveQueue.length > 0) { | |
| const nextMove = moveQueue.shift(); | |
| performMove(nextMove.move, nextMove.callback); | |
| } | |
| return; | |
| } | |
| // Smooth easing | |
| t = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; | |
| moveGroup.rotation[axis] = t * targetAngle; | |
| requestAnimationFrame(animateRotation); | |
| } | |
| animateRotation(); | |
| } | |
| // Scramble the cube with random moves | |
| function scrambleCube() { | |
| if (isAnimating) return; | |
| const moveNames = Object.keys(MOVES); | |
| const numMoves = 20; | |
| const moves = []; | |
| for (let i = 0; i < numMoves; i++) { | |
| const randomMove = moveNames[Math.floor(Math.random() * moveNames.length)]; | |
| moves.push(randomMove); | |
| } | |
| let moveIndex = 0; | |
| function performNextMove() { | |
| if (moveIndex >= moves.length) return; | |
| performMove(moves[moveIndex], () => { | |
| moveIndex++; | |
| performNextMove(); | |
| }); | |
| } | |
| performNextMove(); | |
| } | |
| // Solve the cube (for simplicity, we'll just apply a sequence of moves) | |
| function solveCube() { | |
| if (isAnimating) return; | |
| // For demonstration, we'll apply a sequence that should show some interesting movement | |
| // In a real solver, this would be generated based on the current state | |
| const solutionMoves = [ | |
| "R", | |
| "U", | |
| "R_", | |
| "U_", | |
| "R_", | |
| "F", | |
| "R", | |
| "F_", | |
| "U", | |
| "R", | |
| "U_", | |
| "L_", | |
| ]; | |
| let moveIndex = 0; | |
| function performNextMove() { | |
| if (moveIndex >= solutionMoves.length) return; | |
| performMove(solutionMoves[moveIndex], () => { | |
| moveIndex++; | |
| performNextMove(); | |
| }); | |
| } | |
| performNextMove(); | |
| } | |
| // Animation loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| // Handle window resizing | |
| function onWindowResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| // Initialize the application | |
| init(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Output: https://youtu.be/VrcCfiiMZWg