Create a 3D Rubik's Cube in the browser using Three.js or p5.js. Allow the user to rotate different layers of the cube with the arrow keys: Up/Down/Left/Right. Include scramble and reset functionality. Add realistic 3D animations and make the controls responsive to keyboard input. Write the entire code in one HTML file.
HTML
预览
复制
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Rubik's Cube</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
#info {
position: absolute;
top: 20px;
width: 100%;
text-align: center;
color: white;
font-size: 18px;
font-weight: 300;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
pointer-events: none;
}
#controls {
position: absolute;
bottom: 30px;
width: 100%;
text-align: center;
color: white;
font-size: 16px;
font-weight: 300;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
button {
margin: 0 10px;
padding: 12px 24px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
color: white;
border: none;
border-radius: 25px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
#scrambleBtn {
background: linear-gradient(135deg, #ff6b6b, #ff8787);
}
#resetBtn {
background: linear-gradient(135deg, #4ecdc4, #44a08d);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}
button:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
#layerIndicator {
position: absolute;
top: 60px;
left: 20px;
color: white;
font-size: 16px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
background: rgba(0,0,0,0.2);
padding: 10px 15px;
border-radius: 10px;
backdrop-filter: blur(10px);
}
.key-hint {
display: inline-block;
background: rgba(255,255,255,0.2);
padding: 4px 8px;
border-radius: 4px;
margin: 0 2px;
font-family: monospace;
}
</style>
</head>
<body>
<div id="info">
<div>Rubik's Cube 3D</div>
<small>Use mouse to rotate view • Click arrows to rotate layers</small>
</div>
<div id="layerIndicator">Current Layer: <span id="currentLayer">Middle</span></div>
<div id="controls">
<button id="scrambleBtn">🔀 Scramble</button>
<button id="resetBtn">🔄 Reset</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.min.js"></script>
<script>
// Initialize Three.js
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222244);
scene.fog = new THREE.Fog(0x222244, 10, 50);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(5, 5, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// Add orbit controls
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// Add enhanced lighting
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
directionalLight.shadow.camera.near = 0.1;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
scene.add(directionalLight);
const pointLight = new THREE.PointLight(0xffffff, 0.5);
pointLight.position.set(-5, 5, -5);
scene.add(pointLight);
// Cube colors
const colors = {
front: 0xff0000, // Red
back: 0xffa500, // Orange
up: 0xffffff, // White
down: 0xffff00, // Yellow
right: 0x00ff00, // Green
left: 0x0000ff, // Blue
inner: 0x333333 // Dark gray for inner parts
};
// Create Rubik's Cube
const cubeSize = 3;
const cubeSpacing = 1.05;
const cubes = [];
const cubeGroup = new THREE.Group();
scene.add(cubeGroup);
// Create small cubes
function createCube(x, y, z) {
const geometry = new THREE.BoxGeometry(1, 1, 1);
const materials = [];
// Create materials for each face
for (let i = 0; i < 6; i++) {
let color = colors.inner;
// Determine which face this is and set appropriate color
if (i === 0 && z === 1) color = colors.front; // Front
if (i === 1 && z === -1) color = colors.back; // Back
if (i === 2 && y === 1) color = colors.up; // Top
if (i === 3 && y === -1) color = colors.down; // Bottom
if (i === 4 && x === 1) color = colors.right; // Right
if (i === 5 && x === -1) color = colors.left; // Left
materials.push(new THREE.MeshPhongMaterial({
color: color,
shininess: 100,
specular: 0x111111
}));
}
const cube = new THREE.Mesh(geometry, materials);
cube.position.set(x * cubeSpacing, y * cubeSpacing, z * cubeSpacing);
cube.castShadow = true;
cube.receiveShadow = true;
cube.originalPosition = { x, y, z };
cube.userData = { x, y, z };
return cube;
}
// Create all 27 small cubes
for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
for (let z = -1; z <= 1; z++) {
if (x !== 0 || y !== 0 || z !== 0) { // Skip the center cube
const cube = createCube(x, y, z);
cubes.push(cube);
cubeGroup.add(cube);
}
}
}
}
// Animation variables
let isAnimating = false;
let currentLayer = 'middle'; // 'top', 'middle', 'bottom', 'left', 'center', 'right'
let rotationAxis = 'y'; // 'x', 'y', 'z'
let rotationGroup = new THREE.Group();
scene.add(rotationGroup);
// Functions to get cubes in specific layers
function getLayerCubes(layer, axis) {
return cubes.filter(cube => {
const pos = cube.position[axis] / cubeSpacing;
switch (layer) {
case 'top': return pos > 0.5;
case 'middle': return Math.abs(pos) < 0.5;
case 'bottom': return pos < -0.5;
case 'left': return pos < -0.5;
case 'center': return Math.abs(pos) < 0.5;
case 'right': return pos > 0.5;
default: return false;
}
});
}
// Rotate a layer
function rotateLayer(layer, axis, direction) {
if (isAnimating) return;
isAnimating = true;
// Get cubes in the layer
let layerCubes;
switch (axis) {
case 'x':
layerCubes = getLayerCubes(layer, 'x');
break;
case 'y':
layerCubes = getLayerCubes(layer, 'y');
break;
case 'z':
layerCubes = getLayerCubes(layer, 'z');
break;
}
// Move cubes to rotation group
layerCubes.forEach(cube => {
cubeGroup.remove(cube);
rotationGroup.add(cube);
});
// Animate rotation
const rotationAngle = Math.PI / 2 * direction;
const startTime = Date.now();
const duration = 300; // milliseconds
function animateRotation() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
// Smooth easing
const easeProgress = 1 - Math.pow(1 - progress, 3);
if (axis === 'x') {
rotationGroup.rotation.x = rotationAngle * easeProgress;
} else if (axis === 'y') {
rotationGroup.rotation.y = rotationAngle * easeProgress;
} else if (axis === 'z') {
rotationGroup.rotation.z = rotationAngle * easeProgress;
}
if (progress < 1) {
requestAnimationFrame(animateRotation);
} else {
// Rotation complete, update cube positions and colors
layerCubes.forEach(cube => {
// Remove from rotation group and add back to cube group
rotationGroup.remove(cube);
cubeGroup.add(cube);
// Snap to final rotation
if (axis === 'x') {
rotationGroup.rotation.x = rotationAngle;
} else if (axis === 'y') {
rotationGroup.rotation.y = rotationAngle;
} else if (axis === 'z') {
rotationGroup.rotation.z = rotationAngle;
}
// Update the cube's world matrix
cube.updateMatrixWorld();
// Get the cube's world position
const worldPos = new THREE.Vector3();
cube.getWorldPosition(worldPos);
// Round the position to snap to grid
worldPos.x = Math.round(worldPos.x / cubeSpacing) * cubeSpacing;
worldPos.y = Math.round(worldPos.y / cubeSpacing) * cubeSpacing;
worldPos.z = Math.round(worldPos.z / cubeSpacing) * cubeSpacing;
// Set the new position
cube.position.copy(worldPos);
// Rotate the cube's user data to reflect the new orientation
const oldData = { ...cube.userData };
if (axis === 'x') {
cube.userData.y = direction > 0 ? oldData.z : -oldData.z;
cube.userData.z = direction > 0 ? -oldData.y : oldData.y;
} else if (axis === 'y') {
cube.userData.x = direction > 0 ? -oldData.z : oldData.z;
cube.userData.z = direction > 0 ? oldData.x : -oldData.x;
} else if (axis === 'z') {
cube.userData.x = direction > 0 ? oldData.y : -oldData.y;
cube.userData.y = direction > 0 ? -oldData.x : oldData.x;
}
});
// Reset rotation group
rotationGroup.rotation.set(0, 0, 0);
isAnimating = false;
}
}
animateRotation();
}
// Reset the cube to solved state
function resetCube() {
if (isAnimating) return;
// Remove all cubes
while (cubes.length > 0) {
const cube = cubes.pop();
cubeGroup.remove(cube);
}
// Create new cubes
for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
for (let z = -1; z <= 1; z++) {
if (x !== 0 || y !== 0 || z !== 0) {
const cube = createCube(x, y, z);
cubes.push(cube);
cubeGroup.add(cube);
}
}
}
}
}
// Scramble the cube
function scrambleCube() {
if (isAnimating) return;
const layers = ['top', 'middle', 'bottom'];
const axes = ['x', 'y', 'z'];
const directions = [1, -1];
let moveCount = 0;
const totalMoves = 20;
function makeRandomMove() {
if (moveCount >= totalMoves) return;
const randomLayer = layers[Math.floor(Math.random() * layers.length)];
const randomAxis = axes[Math.floor(Math.random() * axes.length)];
const randomDirection = directions[Math.floor(Math.random() * directions.length)];
rotateLayer(randomLayer, randomAxis, randomDirection);
moveCount++;
setTimeout(makeRandomMove, 350);
}
makeRandomMove();
}
// Handle keyboard input
document.addEventListener('keydown', (event) => {
if (isAnimating) return;
switch (event.key) {
case 'ArrowUp':
if (rotationAxis === 'y') rotateLayer(currentLayer, 'x', -1);
else if (rotationAxis === 'x') rotateLayer(currentLayer, 'y', 1);
else rotateLayer(currentLayer, 'y', 1);
break;
case 'ArrowDown':
if (rotationAxis === 'y') rotateLayer(currentLayer, 'x', 1);
else if (rotationAxis === 'x') rotateLayer(currentLayer, 'y', -1);
else rotateLayer(currentLayer, 'y', -1);
break;
case 'ArrowLeft':
if (rotationAxis === 'y') rotateLayer(currentLayer, 'y', -1);
else if (rotationAxis === 'x') rotateLayer(currentLayer, 'x', -1);
else rotateLayer(currentLayer, 'x', -1);
break;
case 'ArrowRight':
if (rotationAxis === 'y') rotateLayer(currentLayer, 'y', 1);
else if (rotationAxis === 'x') rotateLayer(currentLayer, 'x', 1);
else rotateLayer(currentLayer, 'x', 1);
break;
case '1':
currentLayer = 'top';
rotationAxis = 'y';
document.getElementById('currentLayer').textContent = 'Top';
break;
case '2':
currentLayer = 'middle';
rotationAxis = 'y';
document.getElementById('currentLayer').textContent = 'Middle';
break;
case '3':
currentLayer = 'bottom';
rotationAxis = 'y';
document.getElementById('currentLayer').textContent = 'Bottom';
break;
case '4':
currentLayer = 'left';
rotationAxis = 'x';
document.getElementById('currentLayer').textContent = 'Left';
break;
case '5':
currentLayer = 'center';
rotationAxis = 'x';
document.getElementById('currentLayer').textContent = 'Center';
break;
case '6':
currentLayer = 'right';
rotationAxis = 'x';
document.getElementById('currentLayer').textContent = 'Right';
break;
case '7':
currentLayer = 'left';
rotationAxis = 'z';
document.getElementById('currentLayer').textContent = 'Front';
break;
case '8':
currentLayer = 'center';
rotationAxis = 'z';
document.getElementById('currentLayer').textContent = 'Middle';
break;
case '9':
currentLayer = 'right';
rotationAxis = 'z';
document.getElementById('currentLayer').textContent = 'Back';
break;
}
});
// Button event listeners
document.getElementById('scrambleBtn').addEventListener('click', scrambleCube);
document.getElementById('resetBtn').addEventListener('click', resetCube);
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Animation loop
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>