Rubik's Cube Timer & Visualizer

A keyboard-first speedcubing app in vanilla JS: a Three.js 3D cube, WCA-style scrambles for 2x2 to 7x7, an inspection timer with +2/DNF, full session stats (Ao5/Ao12/Ao100), and a C++ solver compiled to WebAssembly, all offline-capable as a PWA.

ROLE
Builder
PERIOD
2026
DOMAIN
Web / Frontend
STATUS
Published

OVERVIEW

A keyboard-first speedcubing app built in vanilla JavaScript as 25 ES modules. It renders a Rubik's Cube in 3D with Three.js, generates WCA-style scrambles for 2x2 through 7x7, times solves with inspection and +2/DNF penalties, and keeps the averages cubers care about, best, mean, and rolling Ao5/Ao12/Ao100, with a PB timeline and Chart.js charts. A C++ cube routine compiled to WebAssembly handles scramble validation and hints, and the whole thing runs offline as an installable PWA with local persistence and JSON/CSV export.

ARRIVED AS

A serious speedcubing tool has to do several things at once in the browser: render a cube in 3D, generate correct scrambles for every puzzle size, time solves with inspection and penalties, and keep the running averages cubers actually care about, all fast, keyboard-driven, and usable offline.

Speedcubers want a timer that gets out of the way: hit a key to start inspection, release to solve, and immediately see how this solve moved the averages. Most browser timers cover the timing but skip the cube, the scrambles for larger puzzles, or offline use. This project is an attempt at the whole loop in plain JavaScript, with a 3D cube, correct multi-size scrambles, the standard rolling averages, and a WebAssembly solver, all installable and usable without a network.

WHAT I BUILT

  1. 01A vanilla-JS app split into 25 ES modules (timer, 3D cube, scramble, stats, sessions, charts, storage, PWA, and more), with no framework, wired together through a small shared state and storage layer.
  2. 02A Three.js 3D cube with keyboard-driven face turns and rotations, a 2D flat view, and scramble generation for 2x2 through 7x7 in WCA-style notation, previewed on the cube before a solve.
  3. 03An inspection timer with +2 and DNF penalties and configurable precision, feeding a full stats workflow: best, worst, mean, and rolling Ao5/Ao12/Ao100 per cube and session, with a PB timeline and Chart.js trend and histogram charts.
  4. 04A C++ cube routine compiled to WebAssembly for scramble validation and hints, loaded lazily and degrading to a clear message when the WASM module is unavailable.

WHAT CHANGED

  • Runs entirely in the browser and offline: a service worker caches the app, a manifest makes it installable, and solves, sessions, and settings persist locally with JSON/CSV export and import for moving data across devices.
  • Covers the full speedcubing loop, scramble, inspect, solve, review, in one keyboard-first interface, with session management and a leaderboard view.
  • Documented with generated architecture, module, and data-flow diagrams, a CI workflow, and the usual issue/PR templates and contribution guides.

Data flow

click a stage

A move sequence is generated for the selected puzzle size (2x2 to 7x7) and applied to both the logical cube state and the Three.js cube, with a preview.

COMPONENT

scramble.js + cubes.js

Builds a WCA-style scramble sequence for the chosen puzzle size and applies it to both the logical state and the rendered cube.

Decisions, with the cost of each.

A decision without its trade-off is marketing. Each row says what was chosen, why, and what it gave up.

Vanilla JS in 25 modules, no framework

The app is mostly direct DOM and Three.js work driven by keyboard input and a small shared state. A framework would add weight and indirection without buying much; plain ES modules with one state and one storage module kept it lean and easy to reason about.

A React/Vue SPA (heavier, more build complexity for a largely imperative 3D/keyboard app).

Compile the cube solver in C++ to WebAssembly

A solver and scramble validator are compute-heavy and already natural to express in C++. Compiling to WebAssembly with Emscripten runs that logic at near-native speed in the browser, and loading it lazily keeps it off the initial path.

A pure-JS solver (slower, and a rewrite of logic that already existed in C++); a server-side solver (needs a backend and breaks offline use).

Offline-first PWA with local persistence

Cubers use a timer repeatedly, often without caring about a network. A service worker plus localStorage makes the app installable, fast, and fully usable offline, with JSON/CSV export and import to move data between devices instead of a server account.

A server-backed app with accounts (network dependency, hosting, and sign-in friction for a single-user tool).

The part that mattered.

The numbers behind the work, and the code that produced them.

vanilla-JS app
25 modules
timer · 3D · scramble · stats · PWA
puzzle sizes
2x2 to 7x7
WCA-style scramble notation
rolling averages
Ao5/12/100
best · mean · PB timeline
scramble solver
C++ to WASM
lazy-loaded, graceful fallback
Lazy-load the WASM solver, degrade if it is missingjavascript
let wasmModule = null;

export const loadSolverModule = async () => {
  if (wasmModule !== null) return wasmModule;
  try {
    const moduleFactory = (await import("./solver.js")).default;
    wasmModule = await moduleFactory();   // Emscripten module factory
    return wasmModule;
  } catch (error) {
    console.warn("WASM solver not available.", error);
    wasmModule = null;
    return null;
  }
};

The C++ solver is compiled to WebAssembly and loaded only when needed. If the module fails to load, the loader returns null and the UI falls back to a clear message instead of breaking, the same degrade-gracefully approach used for optional features.

Call into the C++ solver from JavaScriptjavascript
const solverModule = await loadSolverModule();

if (solverModule?.cwrap) {
  const validate = solverModule.cwrap("validate_scramble", "number", ["string"]);
  const ok = validate(scramble);   // runs the compiled C++ routine
  renderOutput(ok ? "Scramble is valid." : "Scramble is invalid.");
} else {
  renderOutput("Solver unavailable in this browser.");
}

Emscripten's cwrap exposes the compiled C++ function validate_scramble as a callable JavaScript function with typed arguments, so the heavy cube logic runs as WebAssembly while the UI stays in plain JS.

Generate a scramble and apply it to both statesjavascript
export const generateScramble = () => {
  const { cubeType } = getState().settings;
  const sequence = buildScramble(cubeType);   // per puzzle size

  resetCube();
  resetCubeState();
  sequence.forEach((token) => applyMoveToState(token)); // logical state
  applyScrambleToThreeCube(sequence.filter(t => !t.includes("w"))); // 3D cube
  syncPreviewFromState();
};

A scramble is generated for the selected puzzle size, then applied to two representations at once: the logical cube state used for validation and previews, and the Three.js cube the user sees. Keeping them in sync is what lets the preview and the 3D view always match the scramble.

✓ LEARNED

  1. A framework is not always the answer: a shared state module plus a storage module was enough structure for an app that is mostly imperative 3D and keyboard handling.

  2. Compiling existing C++ to WebAssembly is a clean way to reuse heavy logic in the browser, and cwrap makes the JS-to-WASM call site read like an ordinary function.

  3. Designing for offline from the start (service worker, localStorage, export/import) fits how a timer is actually used, repeatedly and often without a network.