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-server
Build:
# minified esbuild
yarn build
Both 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 start
oryarn 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-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 from0
to anendingT
of0.25
(for counter-clockwise rotations), or to go down from1
to anendingT
of0.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 androtCubieBR
for 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
t
up (for counter-clockwise) or down (for clockwise) by therotationSpeed
amount 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
(sincet
moves in 0.25 increments to correspond to 90 degrees of a rotation path, but slerp’st
is 0 to 1 (or 1 to 0)) - dequeues the user rotation if
t
has reached or exceededendingT
; 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 | +-------------+
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.”