Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Rubik's 3D - Ultimate Edition</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| body { margin: 0; overflow: hidden; background: #050505; touch-action: none; font-family: 'Segoe UI', Roboto, sans-serif; color: white; } | |
| canvas { display: block; } | |
| .glass { background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); } | |
| .btn-interact { transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); cursor: pointer; } | |
| .btn-interact:active { transform: scale(0.92); } | |
| .ui-fade { transition: opacity 0.4s ease, visibility 0.4s; } | |
| .hidden-ui { opacity: 0; visibility: hidden; pointer-events: none; } | |
| .timer-race { color: #f87171; text-shadow: 0 0 10px rgba(248, 113, 113, 0.5); } | |
| .timer-zen { color: #60a5fa; } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- HUD --> | |
| <div id="hud" class="absolute inset-0 pointer-events-none p-6 flex flex-col justify-between hidden-ui z-10"> | |
| <div class="flex justify-between items-start"> | |
| <div class="glass p-4 rounded-2xl flex flex-col items-center min-w-[100px] border-blue-500/30"> | |
| <span id="mode-label" class="text-[10px] uppercase tracking-widest font-bold">Zen</span> | |
| <span id="timer-val" class="text-2xl font-mono font-black">00:00</span> | |
| </div> | |
| <div class="flex flex-col gap-3 pointer-events-auto"> | |
| <button onclick="togglePause()" class="glass w-12 h-12 rounded-full flex items-center justify-center btn-interact hover:bg-white/10"> | |
| <svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- MAIN HOME MENU --> | |
| <div id="home-menu" class="absolute inset-0 z-50 flex items-center justify-center p-6 bg-black"> | |
| <div class="w-full max-w-sm text-center"> | |
| <h1 class="text-6xl font-black italic tracking-tighter mb-10 text-transparent bg-clip-text bg-gradient-to-br from-white to-gray-600">CUBE.X</h1> | |
| <div class="flex flex-col gap-4"> | |
| <button id="resume-btn" onclick="resumeGameAction()" class="glass p-5 rounded-2xl hidden flex items-center justify-between group btn-interact border-emerald-500/50 bg-emerald-500/5"> | |
| <div class="text-left"> | |
| <div class="text-lg font-bold text-emerald-400">Continue</div> | |
| <div id="resume-info" class="text-xs text-gray-400">Resume saved state</div> | |
| </div> | |
| <div class="text-emerald-400"><svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"></path></svg></div> | |
| </button> | |
| <button onclick="setupGame('zen')" class="glass p-5 rounded-2xl flex items-center justify-between group btn-interact hover:bg-white/10 border-blue-500/20"> | |
| <div class="text-left"> | |
| <div class="text-lg font-bold">Zen Mode</div> | |
| <div class="text-xs text-gray-400">Relax, learn, and practice</div> | |
| </div> | |
| <div class="text-blue-400 font-bold text-xs uppercase tracking-tighter">Unlimited</div> | |
| </button> | |
| <button onclick="setupGame('timed')" class="glass p-5 rounded-2xl flex items-center justify-between group btn-interact hover:bg-white/10 border-red-500/20"> | |
| <div class="text-left"> | |
| <div class="text-lg font-bold">Timed Race</div> | |
| <div class="text-xs text-gray-400">Solve as fast as you can</div> | |
| </div> | |
| <div class="text-red-400 font-bold text-xs uppercase tracking-tighter">Speedrun</div> | |
| </button> | |
| <button onclick="resetGlobal()" class="mt-6 text-[10px] text-gray-600 hover:text-red-400 uppercase tracking-widest font-bold transition-colors">Reset Progress</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- PAUSE OVERLAY --> | |
| <div id="pause-overlay" class="absolute inset-0 z-40 flex items-center justify-center bg-black/80 backdrop-blur-md hidden-ui ui-fade"> | |
| <div class="text-center glass p-10 rounded-3xl max-w-xs w-full mx-6"> | |
| <h2 class="text-3xl font-black mb-6">PAUSED</h2> | |
| <div class="flex flex-col gap-4"> | |
| <button onclick="togglePause()" class="bg-white text-black font-bold py-4 rounded-xl btn-interact">Resume</button> | |
| <button onclick="saveAndGoHome()" class="glass font-bold py-4 rounded-xl btn-interact hover:bg-white/10">Save & Quit</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // --- IMPROVED AUDIO ENGINE --- | |
| const AudioEngine = { | |
| ctx: null, | |
| init() { | |
| if (!this.ctx) this.ctx = new (window.AudioContext || window.webkitAudioContext)(); | |
| if (this.ctx.state === 'suspended') this.ctx.resume(); | |
| }, | |
| playTurn() { | |
| this.init(); | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.type = 'triangle'; | |
| osc.frequency.setValueAtTime(180, this.ctx.currentTime); | |
| osc.frequency.exponentialRampToValueAtTime(60, this.ctx.currentTime + 0.15); | |
| gain.gain.setValueAtTime(0.1, this.ctx.currentTime); | |
| gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.15); | |
| osc.connect(gain); gain.connect(this.ctx.destination); | |
| osc.start(); osc.stop(this.ctx.currentTime + 0.15); | |
| }, | |
| playClick() { | |
| this.init(); | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.type = 'sine'; | |
| osc.frequency.setValueAtTime(400, this.ctx.currentTime); | |
| osc.frequency.exponentialRampToValueAtTime(10, this.ctx.currentTime + 0.05); | |
| gain.gain.setValueAtTime(0.05, this.ctx.currentTime); | |
| gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.05); | |
| osc.connect(gain); gain.connect(this.ctx.destination); | |
| osc.start(); osc.stop(this.ctx.currentTime + 0.05); | |
| } | |
| }; | |
| // --- GAME ENGINE --- | |
| let scene, camera, renderer, cubeParent; | |
| const pieces = []; | |
| let isRotating = false, isPaused = false, gameActive = false; | |
| let lon = -45, lat = 30, startLon = 0, startLat = 0; | |
| const raycaster = new THREE.Raycaster(), mouse = new THREE.Vector2(), touchStart = new THREE.Vector2(); | |
| let intersected = null; | |
| let gameTime = 0, timerInterval, currentMode = 'zen'; | |
| const step = 1.03; | |
| const COLORS = { front:0xef4444, back:0xf97316, up:0xffffff, down:0xffd700, left:0x22c55e, right:0x2563eb, internal:0x111111 }; | |
| init(); | |
| animate(); | |
| function init() { | |
| scene = new THREE.Scene(); | |
| camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| document.body.appendChild(renderer.domElement); | |
| scene.add(new THREE.AmbientLight(0xffffff, 0.8)); | |
| const light = new THREE.DirectionalLight(0xffffff, 0.4); | |
| light.position.set(10, 20, 10); | |
| scene.add(light); | |
| createCube(); | |
| updateCamera(); | |
| checkSaves(); | |
| window.addEventListener('touchstart', onTouchStart, { passive: false }); | |
| window.addEventListener('touchmove', onTouchMove, { passive: false }); | |
| window.addEventListener('touchend', () => intersected = null); | |
| window.addEventListener('resize', onResize); | |
| } | |
| function createCube() { | |
| if (cubeParent) scene.remove(cubeParent); | |
| cubeParent = new THREE.Group(); scene.add(cubeParent); | |
| pieces.length = 0; | |
| const geo = new THREE.BoxGeometry(1, 1, 1); | |
| for (let x = -1; x <= 1; x++) { | |
| for (let y = -1; y <= 1; y++) { | |
| for (let z = -1; z <= 1; z++) { | |
| const mats = [ | |
| new THREE.MeshLambertMaterial({ color: x === 1 ? COLORS.right : COLORS.internal }), | |
| new THREE.MeshLambertMaterial({ color: x === -1 ? COLORS.left : COLORS.internal }), | |
| new THREE.MeshLambertMaterial({ color: y === 1 ? COLORS.up : COLORS.internal }), | |
| new THREE.MeshLambertMaterial({ color: y === -1 ? COLORS.down : COLORS.internal }), | |
| new THREE.MeshLambertMaterial({ color: z === 1 ? COLORS.front : COLORS.internal }), | |
| new THREE.MeshLambertMaterial({ color: z === -1 ? COLORS.back : COLORS.internal }) | |
| ]; | |
| const cubie = new THREE.Mesh(geo, mats); | |
| cubie.position.set(x * step, y * step, z * step); | |
| const edges = new THREE.EdgesGeometry(geo); | |
| cubie.add(new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x000000 }))); | |
| cubeParent.add(cubie); pieces.push(cubie); | |
| } | |
| } | |
| } | |
| } | |
| function setupGame(mode) { | |
| AudioEngine.playClick(); | |
| currentMode = mode; | |
| gameTime = 0; | |
| const timerEl = document.getElementById('timer-val'); | |
| const labelEl = document.getElementById('mode-label'); | |
| labelEl.textContent = mode; | |
| timerEl.className = 'text-2xl font-mono font-black ' + (mode === 'timed' ? 'timer-race' : 'timer-zen'); | |
| createCube(); | |
| scramble(15); | |
| document.getElementById('home-menu').classList.add('hidden-ui'); | |
| document.getElementById('hud').classList.remove('hidden-ui'); | |
| gameActive = true; isPaused = false; | |
| startTimer(); | |
| } | |
| function togglePause() { | |
| AudioEngine.playClick(); | |
| isPaused = !isPaused; | |
| const overlay = document.getElementById('pause-overlay'); | |
| if (isPaused) { | |
| overlay.classList.remove('hidden-ui'); | |
| clearInterval(timerInterval); | |
| } else { | |
| overlay.classList.add('hidden-ui'); | |
| startTimer(); | |
| } | |
| } | |
| function saveAndGoHome() { | |
| AudioEngine.playClick(); | |
| saveState(); | |
| location.reload(); | |
| } | |
| function saveState() { | |
| const data = { | |
| mode: currentMode, | |
| time: gameTime, | |
| pieces: pieces.map(p => ({ | |
| pos: p.position.toArray(), | |
| rot: [p.rotation.x, p.rotation.y, p.rotation.z] | |
| })) | |
| }; | |
| localStorage.setItem('cube_ultimate_save', JSON.stringify(data)); | |
| } | |
| function checkSaves() { | |
| const save = localStorage.getItem('cube_ultimate_save'); | |
| if (save) { | |
| document.getElementById('resume-btn').classList.remove('hidden'); | |
| } | |
| } | |
| function resumeGameAction() { | |
| AudioEngine.playClick(); | |
| const save = JSON.parse(localStorage.getItem('cube_ultimate_save')); | |
| if (!save) return; | |
| currentMode = save.mode; | |
| gameTime = save.time; | |
| document.getElementById('mode-label').textContent = currentMode; | |
| save.pieces.forEach((pData, i) => { | |
| pieces[i].position.fromArray(pData.pos); | |
| pieces[i].rotation.set(pData.rot[0], pData.rot[1], pData.rot[2]); | |
| }); | |
| document.getElementById('home-menu').classList.add('hidden-ui'); | |
| document.getElementById('hud').classList.remove('hidden-ui'); | |
| gameActive = true; isPaused = false; | |
| startTimer(); | |
| } | |
| function startTimer() { | |
| if (timerInterval) clearInterval(timerInterval); | |
| timerInterval = setInterval(() => { | |
| gameTime++; | |
| const mins = Math.floor(gameTime / 60).toString().padStart(2, '0'); | |
| const secs = (gameTime % 60).toString().padStart(2, '0'); | |
| document.getElementById('timer-val').textContent = `${mins}:${secs}`; | |
| }, 1000); | |
| } | |
| function onTouchStart(e) { | |
| if (isRotating || isPaused || !gameActive) return; | |
| const t = e.touches[0]; | |
| touchStart.set(t.clientX, t.clientY); | |
| startLon = lon; startLat = lat; | |
| mouse.x = (t.clientX / window.innerWidth) * 2 - 1; | |
| mouse.y = -(t.clientY / window.innerHeight) * 2 + 1; | |
| raycaster.setFromCamera(mouse, camera); | |
| const hits = raycaster.intersectObjects(pieces); | |
| intersected = hits.length > 0 ? hits[0] : null; | |
| } | |
| function onTouchMove(e) { | |
| if (isRotating || isPaused || !gameActive) return; | |
| const t = e.touches[0]; | |
| const dx = t.clientX - touchStart.x; | |
| const dy = t.clientY - touchStart.y; | |
| const dist = Math.hypot(dx, dy); | |
| if (!intersected || dist < 15) { | |
| lon = startLon + dx * 0.3; lat = startLat + dy * 0.3; | |
| updateCamera(); | |
| } else if (dist > 30) { | |
| handleSwipe(dx, dy); | |
| intersected = null; | |
| } | |
| } | |
| function handleSwipe(dx, dy) { | |
| const hit = intersected; | |
| const normal = hit.face.normal.clone().applyQuaternion(hit.object.getWorldQuaternion(new THREE.Quaternion())).normalize(); | |
| const camForward = new THREE.Vector3(); camera.getWorldDirection(camForward); | |
| const camRight = new THREE.Vector3().crossVectors(camera.up, camForward).normalize(); | |
| const camUp = camera.up.clone().normalize(); | |
| const swipeWorld = camRight.clone().multiplyScalar(dx).add(camUp.clone().multiplyScalar(-dy)).normalize(); | |
| const axisVec = new THREE.Vector3().crossVectors(normal, swipeWorld).normalize(); | |
| const axes = ["x", "y", "z"]; | |
| let axis = "x", maxVal = 0; | |
| axes.forEach(a => { if (Math.abs(axisVec[a]) > maxVal) { maxVal = Math.abs(axisVec[a]); axis = a; } }); | |
| rotateLayer(axis, hit.object.position[axis], (Math.PI / 2) * (axisVec[axis] > 0 ? 1 : -1)); | |
| } | |
| function rotateLayer(axis, layer, angle) { | |
| if (isRotating) return; | |
| isRotating = true; | |
| AudioEngine.playTurn(); | |
| const group = new THREE.Group(); scene.add(group); | |
| const activePieces = pieces.filter(p => Math.abs(p.position[axis] - layer) < 0.1); | |
| activePieces.forEach(p => group.add(p)); | |
| const start = performance.now(); | |
| function anim(now) { | |
| const t = Math.min((now - start) / 240, 1); | |
| group.rotation[axis] = angle * (t * (2 - t)); | |
| if (t < 1) requestAnimationFrame(anim); | |
| else { | |
| group.rotation[axis] = angle; group.updateMatrixWorld(); | |
| activePieces.forEach(p => { | |
| p.applyMatrix4(group.matrixWorld); | |
| p.position.x = Math.round(p.position.x / step) * step; | |
| p.position.y = Math.round(p.position.y / step) * step; | |
| p.position.z = Math.round(p.position.z / step) * step; | |
| p.rotation.set(Math.round(p.rotation.x/(Math.PI/2))*(Math.PI/2), Math.round(p.rotation.y/(Math.PI/2))*(Math.PI/2), Math.round(p.rotation.z/(Math.PI/2))*(Math.PI/2)); | |
| cubeParent.add(p); | |
| }); | |
| scene.remove(group); isRotating = false; | |
| } | |
| } requestAnimationFrame(anim); | |
| } | |
| function scramble(moves = 10) { | |
| let count = 0; | |
| const interval = setInterval(() => { | |
| const axis = ['x', 'y', 'z'][Math.floor(Math.random() * 3)]; | |
| const layers = [-step, 0, step]; | |
| rotateLayer(axis, layers[Math.floor(Math.random() * 3)], Math.random() > 0.5 ? Math.PI/2 : -Math.PI/2); | |
| if (++count >= moves) clearInterval(interval); | |
| }, 260); | |
| } | |
| function updateCamera() { | |
| lat = Math.max(-85, Math.min(85, lat)); | |
| const phi = THREE.MathUtils.degToRad(90 - lat); | |
| const theta = THREE.MathUtils.degToRad(lon); | |
| camera.position.set(12 * Math.sin(phi) * Math.cos(theta), 12 * Math.cos(phi), 12 * Math.sin(phi) * Math.sin(theta)); | |
| camera.lookAt(0, 0, 0); | |
| } | |
| function resetGlobal() { localStorage.removeItem('cube_ultimate_save'); location.reload(); } | |
| function onResize() { camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } | |
| function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } | |
| </script> | |
| </body> | |
| </html> |