comparisons

Claude Haiku 4.5 · Claude Code Web — 3D Rubik's cube simulator in a single HTML file

~/tests/cubo-magico/claude-haiku-4-5-claude-code-web  — 3D Rubik's cube simulator in a single HTML file

open in new tab
Claude Haiku 4.5 Anthropic Claude Code Web
  • Leonardo
  • ~2min
loading artifact

src/index.html

html529 lines14 KB
<!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>
claude-haiku-4-5-claude-code-web 1 file 530 lines 14 KB shortcuts p preview tab c code tab [ previous file ] next file esc leave the artifact

prompt used in this run

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
author's notes
A single 14 KB / 529-line file, the smallest of the six artifacts — and the only one that does not deliver what the brief asked for. Two defects, both confirmed in the code and on screen. (1) The cube never shows six colors: the BoxGeometry material map treats indices 0/1 as +Z/-Z when they are +X/-X, and the final `else colorIdx = 0` branch paints every non-sticker face with color 0, which is white — the result is a nearly all-white cube with a few stray colored faces. (2) Layer rotation rotates no layer at all: `piece.rotateOnWorldAxis()` changes a cubie's orientation but never its `position`, and the `updatePiecePosition` call that follows merely rounds a position that never moved. The cubies spin on their own axes and the cube's logical state never changes. The half-turn (2) variant required by the brief is also missing: the sidebar only offers clockwise and counter-clockwise turns. It loads with no console errors and the move counter increments normally — the defect is silent.