Skip to content

sunflowerseastar/rubiks-cube

Repository files navigation

Rubik’s Cube

Written with three.js.

Shortcuts:

keynotationaction
fFrotate front clockwise
FF’rotate front counter-clockwise
lLrotate left clockwise
LL’rotate left counter-clockwise
rRrotate right clockwise
RR’rotate right counter-clockwise
bBrotate back clockwise
BB’rotate back counter-clockwise
uUrotate up (top) clockwise
UU’rotate up (top) counter-clockwise
dDrotate down (bottom) clockwise
DD’rotate down (bottom) counter-clockwise
SPCrotation random face
sscramble (25 random rotations)
qreset

Quick Start

Dev:

# install deps
yarn
# run dev.js esbuild watcher
yarn start
# visit localhost:8080 (separate terminal)
npx http-server

Build:

# minified esbuild
yarn build

Both yarn start and yarn build create a public/out.js bundle.

Structure

  • public/ - static html/css + dev & build output
    • index.html - simple markup
    • style.css - simple styles
    • out.js - bundle file from yarn start or yarn build
  • src/ - js to be bundled
    • main.ts - entry for index.html
    • Cube/
      • Cube.ts - top-level scene/cube creation; rotate & start hooks
      • Loop.ts - animation loop; user rotation queue; rotation handling
      • cubies.ts - 27 6-colored three.js cubies positioned as a cube
      • rotationPath.ts - radius- and xyz-variable circle creation class
      • threejs-helpers/ - basic three.js scaffolding
      • utilities.ts - these are, uh, utilities

Cubies

All 27 cubies are oriented & colored the same (and have the default ’up’ of 0,1,0). The centermost cubie, which is technically unnecessary, is at world position 0,0,0.

The cubies are generated via a nested for loop in cubies.ts, and their resulting indices from 0 to 26 are an arbitrary consequence of how I originally wrote the code. These initial indices are important, however, in that they represent 27 fixed locations. When the cubies are first generated, each one’s index matches its location. Every time a cubie moves, though, its location is updated to a new location. (See cubies.org for the admittedly not-immediately-intuitive location layout.) A cubie’s position is different, in that it represents the cubie’s world space xyz position at any given moment. There are only 27 locations, but there are numerous positions since these comprise every spot along every rotation path when a cubie animates from one location to another.

  • cubieIndex: unique cubie ID
  • location: the original, unmoving 27 spots that a cubie can inhabit
  • position: a cubie’s current Vector3(x,y,z) in three.js world space

Loop

The animation loop kicks off via cube.start() in main.ts. The basic loop render is down at the very bottom of this start function in Loop.ts:

this.renderer.render(this.scene, this.camera);

A userRotationQueue is a simple FIFO for user-initiated rotations (either via keypress or click/tap). Most of the start function is wrapped with a conditional that determines if there are rotations in the queue:

if (this.userRotationQueue.length > 0) {
  // BEGIN IS-ROTATING
  // ...
} // END IS-ROTATING

If there are rotations in the queue, then the animation loop is focused on processing this.userRotationQueue[0]. When that rotation is completed, the loop will dequeue it, and automatically continue along with the new this.userRotationQueue[0] (if it exists).

The first loop for each rotation undergoes an init. The face-to-rotate and clockwise/counter-clockwise (centerCubieIndex and isCounterClockwise, respectively) are determined at the time of queueing. The init phase does a lot:

  • flips the flag isReadyToInitNewUserRotation in order to only init once
  • preps t to go up from 0 to an endingT of 0.25 (for counter-clockwise rotations), or to go down from 1 to an endingT of 0.75 (for clockwise rotations)
  • sets up a “rotation path” for both the edge cubies and corner cubies
  • assigns ‘up’ per the target rotation face’s plane normal
  • assigns three.js cubies to respective variables (ex. rotCubieL for rotation cubie Left and rotCubieBR for rotation cubie Bottom Right – see cubies.org) for each edge & corner index of the target rotation face
  • calculates where the rotation for each cubie should end up (done by multiplying each cubie’s current quaternion by 90 degrees on the ‘up’ axis)
  • updates each of the involved cubies with their new location

After init, the actual loop:

  • bumps t up (for counter-clockwise) or down (for clockwise) by the rotationSpeed amount set in constants.ts
  • determines and assigns each cubie’s new position along the edge/corner rotation path, adjusted over time via t
  • rotates each cubie by slerping (spherical linear interpolation) its initial quaternion to the calculated end rotation, adjusted over time via t * 4 (since t moves in 0.25 increments to correspond to 90 degrees of a rotation path, but slerp’s t is 0 to 1 (or 1 to 0))
  • dequeues the user rotation if t has reached or exceeded endingT; viz. the animated cubies have reached their destinations
  • flips the flag isReadyToInitNewUserRotation in order for the next user rotation to init

There are cryptic variable abbreviations littered around Loop.ts, even though I know it induces wrath from the verbosity dogmatists. This little explanation here at the end of the readme is a mea culpa, I guess.

+-------------+
| TL | T | TR |
|----+---+----|
|  L | C |  R |
|----+---+----|
| BL | B | BR |
+-------------+
VariableWhat it isExamples
rotCubiethe three.js cubie to be rotatedrotCubieL, rotCubieTR
iqinitial quaternion (pre-rotation)iql, iqtr
mqmultiplied quaternion (end goal)mql, mqtr
ptxyz point on a given rotation path per tpt90

Notes

This explorable video series of visualizing quaternions by Grant Sanderson and Ben Eater is incredible.

“Queueing” has five vowels in a row. I’d never thought about that until I wrote this readme.

Up’ is three.js’s “this side up” Vector3, used by Object3D, lights, etc. It is 0,1,0 by default.

Although I’m careful with the words “location” and “position,” I’m not careful with “rotation.” Sometimes I mean “the face (or cubie of this face) that’s being twisted,” and sometimes I mean “the actual rotation quaternion of a cubie.”