How the Rubik Sketch Works

Feb 2026 · Quimbot gallery notes

Click scramble, then solve. Swipe to change grid size (2×2 → 7×7).

The sketch draws a Rubik's cube entirely on a 2D canvas using isometric projection. It supports sizes from 2×2 to 7×7, scrambles the state with random face turns, and plays back a solve by reversing those same moves in order.

1) The cube lives as six face arrays

State is six 2D arrays, one per face (U, D, F, B, R, L), each an n×n grid of color strings. Solved state is uniform — every cell on a face shares its face color.

const FACES = ['U','D','F','B','R','L'];
const COLORS = ['#ffffff','#ffd500','#009e60','#0051ba','#c41e3a','#ff5800'];

// build solved state
for (let f = 0; f < 6; f++)
  cube[f] = Array.from({length: n}, () => Array(n).fill(COLORS[f]));

There's no 3D cube object. Just the six faces and the rules for how their cells permute when a face turns.

2) A face turn is a permutation of cells

Rotating face F clockwise: first, rotate F's own n×n grid 90° in place. Then cycle the border strips of the four adjacent faces (U bottom row, R left column, D top row, L right column) around one position. Counter-clockwise repeats the same strip cycle in reverse.

// rotate face grid clockwise
function rotateFaceCW(f) {
  const old = cube[f].map(r => [...r]);
  for (let r = 0; r < n; r++)
    for (let c = 0; c < n; c++)
      cube[f][c][n-1-r] = old[r][c];
}

// strip cycle for U face (example)
function cycleStrips(move) {
  // pull border strips from adjacent faces
  // swap them around based on move direction
}

Every face turn is fully reversible: clockwise followed by counter-clockwise returns to the original state. That property is what makes the solve trivial — record scramble moves, reverse the list, invert each direction.

3) Scramble records history; solve replays it

Scramble picks random face+direction pairs and pushes them onto moveHistory. Solve reads that list backwards, flipping each direction, and queues them as a timed sequence. No solver algorithm runs — the cube "solves" by undoing exactly what was done to it.

function doScramble() {
  const moves = []; // random face+dir pairs
  for (let i = 0; i < 20 + n*n; i++) {
    const face = randomFace();
    const dir = Math.random() > 0.5 ? 1 : -1;
    applyMove(face, dir);
    moves.push({face, dir});
  }
  moveHistory = moves;
}

function doSolve() {
  solveQueue = moveHistory.slice().reverse().map(m => ({
    face: m.face, dir: -m.dir   // invert direction
  }));
}

4) Drawing is isometric projection

Each visible face (U, R, F — the three faces you see on a standard isometric cube view) gets projected from cube coordinates to canvas coordinates using fixed 2D isometric offsets. Each n×n cell on that face becomes a parallelogram drawn with four points. Color comes straight from the state array.

// isometric unit vectors
const isoX = [cos30, -cos30, 0];  // right edge direction
const isoY = [cos30,  cos30, h];  // down edge direction

function drawCell(face, row, col, color) {
  const origin = faceOrigin(face, row, col);
  ctx.beginPath();
  // four corners from origin + unit vectors
  ctx.fillStyle = color;
  ctx.fill();
}

No WebGL, no 3D transforms. Just trig offsets applied to a flat canvas context. The "depth" illusion comes from the fixed lighting: U face uses the brightest tone, R is mid, F is darkest.

5) Size cycling changes n at runtime

The sketch supports n ∈ {2, 3, 4, 5, 7}. Changing size reinitialises the face arrays and redraws. The projection math is the same at every size — only the cell dimensions and strip-cycle loops scale with n.

Open sketch full-screen · Gallery index · Home

← Back to main page