Claude Haiku 4.5 · Claude Code Web — Simulador de cubo magico 3D em um unico HTML
src/index.html
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simulador de Cubo Mágico 3D</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #1a1a1a;
color: #fff;
}
#canvas {
width: 100%;
height: 100%;
display: block;
}
#ui {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.panel {
position: absolute;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 16px;
pointer-events: all;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.header-panel {
top: 16px;
left: 16px;
min-width: 280px;
}
.controls-panel {
top: 16px;
right: 16px;
max-width: 340px;
}
.info-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
font-size: 14px;
}
.info-label {
color: #888;
min-width: 80px;
}
.info-value {
font-weight: 600;
font-family: 'Monaco', 'Courier New', monospace;
color: #0f0;
}
.timer {
font-size: 28px;
color: #0f0;
font-family: 'Monaco', 'Courier New', monospace;
font-weight: 600;
letter-spacing: 2px;
}
h3 {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
color: #888;
margin-bottom: 12px;
margin-top: 16px;
}
h3:first-child {
margin-top: 0;
}
.button-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
flex-wrap: wrap;
}
button {
flex: 1;
min-width: 50px;
padding: 8px 12px;
background: rgba(0, 150, 255, 0.8);
border: 1px solid rgba(0, 180, 255, 0.5);
color: #fff;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: all 0.2s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}
button:hover {
background: rgba(0, 180, 255, 1);
border-color: rgba(0, 220, 255, 1);
transform: translateY(-1px);
}
button:active {
transform: translateY(0);
opacity: 0.8;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.button-row.small button {
font-size: 11px;
padding: 6px 10px;
}
.help-text {
font-size: 12px;
color: #666;
line-height: 1.4;
margin-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding-top: 12px;
}
.help-text strong {
color: #0f0;
}
.action-button {
background: rgba(255, 100, 0, 0.8);
border-color: rgba(255, 150, 0, 0.5);
}
.action-button:hover {
background: rgba(255, 120, 0, 1);
border-color: rgba(255, 180, 0, 1);
}
.reset-button {
background: rgba(200, 0, 0, 0.8);
border-color: rgba(255, 0, 0, 0.5);
}
.reset-button:hover {
background: rgba(220, 0, 0, 1);
border-color: rgba(255, 50, 0, 1);
}
@media (max-width: 1024px) {
.controls-panel {
max-width: 100%;
right: 16px;
left: 16px;
top: auto;
bottom: 16px;
}
.header-panel {
top: auto;
bottom: 280px;
}
h3 {
font-size: 11px;
}
.button-row {
gap: 6px;
}
button {
padding: 6px 10px;
font-size: 11px;
}
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="canvas"></div>
<div id="ui">
<div class="panel header-panel">
<div class="info-row">
<span class="info-label">Movimentos:</span>
<span class="info-value" id="moveCount">0</span>
</div>
<div class="info-row">
<span class="info-label">Tempo:</span>
</div>
<div class="timer" id="timer">00:00</div>
</div>
<div class="panel controls-panel">
<h3>Faces (U D L R F B)</h3>
<div class="button-row">
<button id="btn-U">U</button>
<button id="btn-D">D</button>
<button id="btn-L">L</button>
<button id="btn-R">R</button>
<button id="btn-F">F</button>
<button id="btn-B">B</button>
</div>
<div class="button-row small">
<button id="btn-U2">U'</button>
<button id="btn-D2">D'</button>
<button id="btn-L2">L'</button>
<button id="btn-R2">R'</button>
<button id="btn-F2">F'</button>
<button id="btn-B2">B'</button>
</div>
<h3>Ações</h3>
<div class="button-row">
<button id="btn-shuffle" class="action-button">Embaralhar</button>
<button id="btn-undo" class="action-button">Desfazer</button>
<button id="btn-reset" class="reset-button">Resetar</button>
</div>
<div class="help-text">
<strong>Teclado:</strong> U/D/L/R/F/B para movimentos, Shift para inverter, Espaço para embaralhar, Ctrl+Z para desfazer<br>
<strong>Mouse:</strong> Arraste para rotacionar câmera, roda para zoom
</div>
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
class RubiksCubeSimulator {
constructor() {
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x1a1a1a);
const canvasContainer = document.getElementById('canvas');
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
canvasContainer.appendChild(this.renderer.domElement);
this.camera.position.set(4, 4, 4);
this.camera.lookAt(0, 0, 0);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
this.controls.autoRotate = false;
this.controls.minDistance = 3;
this.controls.maxDistance = 10;
this.pieces = [];
this.isAnimating = false;
this.moveCount = 0;
this.startTime = Date.now();
this.moveHistory = [];
this.createCube();
this.setupLights();
this.setupEvents();
this.animate();
}
createCube() {
const colors = [0xffffff, 0xffff00, 0xff0000, 0xffa500, 0x0066ff, 0x00aa00];
this.pieces = [];
for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
for (let z = -1; z <= 1; z++) {
const geometry = new THREE.BoxGeometry(0.9, 0.9, 0.9);
const materials = [];
for (let i = 0; i < 6; i++) {
let colorIdx = 0;
if (i === 4 && y === 1) colorIdx = 0; // white on top
else if (i === 5 && y === -1) colorIdx = 1; // yellow on bottom
else if (i === 2 && x === 1) colorIdx = 2; // red on right
else if (i === 3 && x === -1) colorIdx = 3; // orange on left
else if (i === 0 && z === 1) colorIdx = 4; // blue on front
else if (i === 1 && z === -1) colorIdx = 5; // green on back
else colorIdx = 0;
materials.push(new THREE.MeshPhongMaterial({
color: colors[colorIdx],
emissive: 0x111111,
shininess: 100
}));
}
const mesh = new THREE.Mesh(geometry, materials);
mesh.position.set(x, y, z);
mesh.userData = { x, y, z };
this.scene.add(mesh);
this.pieces.push(mesh);
}
}
}
}
setupLights() {
const light1 = new THREE.DirectionalLight(0xffffff, 1);
light1.position.set(5, 5, 5);
this.scene.add(light1);
const light2 = new THREE.DirectionalLight(0xffffff, 0.5);
light2.position.set(-5, -5, -5);
this.scene.add(light2);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
this.scene.add(ambientLight);
}
setupEvents() {
const moves = ['U', 'D', 'L', 'R', 'F', 'B'];
moves.forEach(move => {
document.getElementById(`btn-${move}`).addEventListener('click', () => this.move(move, false));
document.getElementById(`btn-${move}2`).addEventListener('click', () => this.move(move, true));
});
document.getElementById('btn-shuffle').addEventListener('click', () => this.shuffle());
document.getElementById('btn-undo').addEventListener('click', () => this.undo());
document.getElementById('btn-reset').addEventListener('click', () => this.reset());
document.addEventListener('keydown', (e) => {
if (this.isAnimating) return;
const key = e.key.toUpperCase();
if (['U', 'D', 'L', 'R', 'F', 'B'].includes(key)) {
e.preventDefault();
this.move(key, e.shiftKey);
} else if (e.code === 'Space') {
e.preventDefault();
this.shuffle();
} else if (e.key === 'z' && e.ctrlKey) {
e.preventDefault();
this.undo();
}
});
window.addEventListener('resize', () => this.onWindowResize());
}
move(face, prime) {
if (this.isAnimating) return;
this.saveState();
this.isAnimating = true;
const targetPieces = this.pieces.filter(p => this.isPieceInLayer(p, face));
const duration = 300;
const startTime = Date.now();
const axis = this.getAxisForFace(face);
const angle = prime ? -Math.PI / 2 : Math.PI / 2;
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const currentAngle = angle * progress;
targetPieces.forEach(piece => {
piece.rotateOnWorldAxis(axis, (currentAngle - (piece.userData.lastAngle || 0)));
piece.userData.lastAngle = currentAngle;
});
if (progress < 1) {
requestAnimationFrame(animate);
} else {
targetPieces.forEach(piece => {
piece.userData.lastAngle = 0;
this.updatePiecePosition(piece);
});
this.moveCount++;
this.isAnimating = false;
}
};
animate();
}
isPieceInLayer(piece, face) {
const { x, y, z } = piece.userData;
switch (face) {
case 'U': return y === 1;
case 'D': return y === -1;
case 'L': return x === -1;
case 'R': return x === 1;
case 'F': return z === 1;
case 'B': return z === -1;
default: return false;
}
}
getAxisForFace(face) {
switch (face) {
case 'U':
case 'D': return new THREE.Vector3(0, 1, 0);
case 'L':
case 'R': return new THREE.Vector3(1, 0, 0);
case 'F':
case 'B': return new THREE.Vector3(0, 0, 1);
}
}
updatePiecePosition(piece) {
const pos = piece.position;
piece.userData.x = Math.round(pos.x);
piece.userData.y = Math.round(pos.y);
piece.userData.z = Math.round(pos.z);
piece.position.set(piece.userData.x, piece.userData.y, piece.userData.z);
}
saveState() {
const state = this.pieces.map(p => ({
pos: { ...p.userData },
rot: p.quaternion.clone()
}));
this.moveHistory.push(state);
}
undo() {
if (this.moveHistory.length === 0) return;
const state = this.moveHistory.pop();
this.pieces.forEach((piece, i) => {
piece.userData = { ...state[i].pos };
piece.quaternion.copy(state[i].rot);
piece.position.set(piece.userData.x, piece.userData.y, piece.userData.z);
});
if (this.moveCount > 0) this.moveCount--;
}
shuffle() {
const moves = ['U', 'D', 'L', 'R', 'F', 'B'];
let count = 0;
const doMove = () => {
if (count < 15) {
const move = moves[Math.floor(Math.random() * moves.length)];
const prime = Math.random() < 0.3;
this.move(move, prime);
count++;
setTimeout(doMove, 350);
}
};
doMove();
}
reset() {
this.scene.clear();
this.createCube();
this.moveCount = 0;
this.startTime = Date.now();
this.moveHistory = [];
this.setupLights();
}
updateUI() {
document.getElementById('moveCount').textContent = this.moveCount;
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
const min = Math.floor(elapsed / 60);
const sec = elapsed % 60;
document.getElementById('timer').textContent =
`${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
}
animate() {
requestAnimationFrame(() => this.animate());
this.controls.update();
this.updateUI();
this.renderer.render(this.scene, this.camera);
}
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
}
new RubiksCubeSimulator();
</script>
</body>
</html>nenhum arquivo aberto
Crie um simulador de cubo magico 3D em um unico arquivo HTML.
Requisitos funcionais:
- Cubo 3x3x3 renderizado em 3D com Three.js, com as seis cores nas faces
- Rotacao animada de camadas, uma camada por vez, sem quebrar o estado do cubo
- Notacao padrao (U, D, L, R, F, B) com as variantes ' (anti-horario) e 2 (meia volta)
- Botoes de embaralhar, desfazer o ultimo movimento e resetar
- Contador de movimentos e cronometro
- Controle por teclado e por botoes na tela
- Camera orbital com o mouse (arrastar para girar, scroll para zoom)
Restricoes tecnicas (obrigatorias):
- UM unico arquivo index.html. Todo o CSS e todo o JavaScript inline. Nao crie nenhum outro arquivo
- Three.js vem do CDN, declarado num <script type="importmap"> inline e consumido por um <script type="module"> tambem inline:
<script type="importmap">
{"imports":{
"three":"https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
"three/addons/":"https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/"
}}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
</script>
Atencao: importar https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/controls/OrbitControls.js pela URL direta NAO funciona — esse arquivo faz `import ... from 'three'`, e o especificador bare so resolve pelo importmap
- NAO use <script type="module" src="..."> apontando para arquivo local
- NAO use localStorage nem sessionStorage — eles lancam SecurityError no ambiente onde a pagina roda
- A pagina roda dentro de um iframe sandbox de origem opaca: sem cookies, sem acesso a janela pai
- Deve caber e ficar legivel em 1440x900- Arquivo unico de 14 KB / 529 linhas, o menor dos seis artefatos — e o unico que nao entrega o que o enunciado pediu. Dois defeitos, os dois confirmados no codigo e na tela. (1) O cubo nao mostra as seis cores: o mapa de material do BoxGeometry trata os indices 0/1 como +Z/-Z quando eles sao +X/-X, e o ramo final `else colorIdx = 0` pinta toda face sem adesivo com a cor 0, que e branca — o resultado e um cubo quase todo branco com poucas faces coloridas soltas. (2) O giro de camada nao gira camada nenhuma: `piece.rotateOnWorldAxis()` altera a orientacao do cubie mas nunca a sua `position`, e o `updatePiecePosition` seguinte so arredonda a posicao que nao mudou. Os cubies giram no proprio eixo e o estado logico do cubo nunca muda. Falta ainda a variante 2 (meia volta) da notacao, pedida no enunciado: a barra lateral so tem os giros horario e anti-horario. Carrega sem erro de console e o contador de movimentos sobe normalmente — o defeito e silencioso.