Show description
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)
<!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>