Written with three.js.
Shortcuts:
| key | notation | action |
|---|---|---|
| f | F | rotate front clockwise |
| F | F’ | rotate front counter-clockwise |
| l | L | rotate left clockwise |
| L | L’ | rotate left counter-clockwise |
| r | R | rotate right clockwise |
| R | R’ | rotate right counter-clockwise |
| b | B | rotate back clockwise |
| B | B’ | rotate back counter-clockwise |
| u | U | rotate up (top) clockwise |
| U | U’ | rotate up (top) counter-clockwise |
| d | D | rotate down (bottom) clockwise |
| D | D’ | rotate down (bottom) counter-clockwise |
| SPC | rotation random face | |
| s | scramble (25 random rotations) | |
| q | reset |
Dev:
# install deps
yarn
# run dev.js esbuild watcher
yarn start
# visit localhost:8080 (separate terminal)
npx http-serverBuild:
# minified esbuild
yarn buildBoth yarn start and yarn build create a public/out.js bundle.
public/- static html/css + dev & build outputindex.html- simple markupstyle.css- simple stylesout.js- bundle file fromyarn startoryarn build
src/- js to be bundledmain.ts- entry for index.htmlCube/Cube.ts- top-level scene/cube creation; rotate & start hooksLoop.ts- animation loop; user rotation queue; rotation handlingcubies.ts- 27 6-colored three.js cubies positioned as a cuberotationPath.ts- radius- and xyz-variable circle creation classthreejs-helpers/- basic three.js scaffoldingutilities.ts- these are, uh, utilities
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
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-ROTATINGIf 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
isReadyToInitNewUserRotationin order to only init once - preps
tto go up from0to anendingTof0.25(for counter-clockwise rotations), or to go down from1to anendingTof0.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.
rotCubieLfor rotation cubie Left androtCubieBRfor rotation cubie Bottom Right – seecubies.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
tup (for counter-clockwise) or down (for clockwise) by therotationSpeedamount set inconstants.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(sincetmoves in 0.25 increments to correspond to 90 degrees of a rotation path, but slerp’stis 0 to 1 (or 1 to 0)) - dequeues the user rotation if
thas reached or exceededendingT; viz. the animated cubies have reached their destinations - flips the flag
isReadyToInitNewUserRotationin 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 | +-------------+
| Variable | What it is | Examples |
|---|---|---|
| rotCubie | the three.js cubie to be rotated | rotCubieL, rotCubieTR |
| iq | initial quaternion (pre-rotation) | iql, iqtr |
| mq | multiplied quaternion (end goal) | mql, mqtr |
| pt | xyz point on a given rotation path per t | pt90 |
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.”