{"id":20662390,"url":"https://github.com/evanzhoudev/cubie","last_synced_at":"2026-04-18T22:32:51.480Z","repository":{"id":199183703,"uuid":"702288662","full_name":"EvanZhouDev/cubie","owner":"EvanZhouDev","description":null,"archived":false,"fork":false,"pushed_at":"2023-10-11T04:09:11.000Z","size":63,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-10T05:54:42.839Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/EvanZhouDev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-10-09T02:46:52.000Z","updated_at":"2023-12-11T13:56:17.000Z","dependencies_parsed_at":"2023-10-11T06:00:10.431Z","dependency_job_id":null,"html_url":"https://github.com/EvanZhouDev/cubie","commit_stats":null,"previous_names":["evanzhoudev/cubie"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/EvanZhouDev/cubie","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EvanZhouDev%2Fcubie","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EvanZhouDev%2Fcubie/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EvanZhouDev%2Fcubie/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EvanZhouDev%2Fcubie/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/EvanZhouDev","download_url":"https://codeload.github.com/EvanZhouDev/cubie/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EvanZhouDev%2Fcubie/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31987855,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T20:23:30.271Z","status":"ssl_error","status_checked_at":"2026-04-18T20:23:29.375Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-16T19:14:00.271Z","updated_at":"2026-04-18T22:32:51.458Z","avatar_url":"https://github.com/EvanZhouDev.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cpicture\u003e\n\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"./assets/banner@dark.svg\"\u003e\n  \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"./assets/banner@light.svg\"\u003e\n  \u003cimg alt=\"Cubie Banner\" src=\"./assets/banner@light.svg\"\u003e\n\u003c/picture\u003e\n\n\u003cp align=\"center\"\u003e\nA tiny Rubik's cube simulator, powered by linear algebra.\n\u003c/p\u003e\n\n\u003e Due to the heavily mathematical nature of this simulator, the notation may be slightly different than the Rubik's cube notation you may expect. Check out [cubekit](https://github.com/EvanZhouDev/cubekit) for a more user-friendly Rubik's cube library.\n\n## Naming\n\n\u003e ### **cu·bie**\n\u003e *noun*\n\u003e 1. One of the smaller [cubes](https://duckduckgo.com/?q=cubes+definition\u0026ia=definition\u0026atb=v368-1) making up a [Rubik's cube](https://duckduckgo.com/?q=Rubik%27s+cube+definition\u0026ia=definition\u0026atb=v368-1).\n\nWanting to create a tiny and clean Rubik's cube simulator, it is only fitting that I took a cubing term that meant a smaller cube. A simple name that's just as nerdy as this project is 😅\n\n## Usage\n\nAll cube logic is stored in the `Cube` object.\n\nTo get started, create a new instance:\n\n```javascript\nlet myCube = new Cube();\n```\n\n### Rotating a Layer\n\nNext, nearly all turns can be defined with the following functions: `rx()`, `ry()`, and `rz()`. You will have to pass the result of this function into `Cube.turn()`.\n\nThese functions all take 3 parameters: `rotations`, `layerStart`, and `layerEnd`. Depending on the axis you want to turn, choose one of the three functions.\n\nThen, determine how many 90° rotations you want to do, putting it into the first parameter.\nNext, select an interval of layers to turn. On each axis, the layers are -1, 0, and 1. For example, turning the `+x` layer by 90° will use a rotation function like this: `rx(1, 1)`. Notice how the `layerEnd` parameter can be skipped if the start and end are the same.\n\nNow, to actually mutate the `Cube`, do the following:\n\n```javascript\nmyCube.turn(rx(1, 1));\n```\n\n### Seeing the Result\n\nIn order to see that you have actually successfully turned a layer, you will need to use the `Cube.flatten()` method. This will return an object with keys in this format: `x,y,z`, where these values designate the side of the cube we are viewing.\n\nFor example, `\"1,0,0\"` represents the `+x` side, `\"0,-1,0\"` the `-y` side, and so on. Each of these is assigned a Color number, in which the color for `-x` is 0, `+x` is 1, `-y` is 2, `+y` is 3, `-z` is 4, and finally, `+z` is 5.\n\n## How it Works\n\nA typical way to simulate a Rubik's cube is by storing each face of the Rubik's cube, and modifying each \"sticker\" on that face individually during a turn. However, I went with an approach that better mimics how a Rubik's cube works in real life—storing each piece individually, and simulating turns with [3D rotation matricies](https://en.wikipedia.org/wiki/Rotation_matrix#In_three_dimensions).\n\nI will begin by explaining the mathematical properties of the Rubik's cube, then transitioning into how I have implemented them in code.\n\n### A Piece\n\nA piece is made up of two vectors, the Position Vector ($\\vec{p}$) and the Color Vector ($\\vec{c}$).\n\nThe position vector denotes the location of the piece on the cube. The \"core\" of the 3x3 Rubik's cube is at $(0,0,0)$, and the each piece looks something like this.\n\n```math\n\\mathbf{p} = \\begin{bmatrix}\nx\\\\\ny\\\\\nz\n\\end{bmatrix}, \\quad \\text{where } x, y, z \\in \\{-1, 0, 1\\}\n```\n\nNext, I assigned each of the 6 colors on the Rubik's cube a number from $0$ to $5$, in which the color for `-x` is 0, `+x` is 1, `-y` is 2, `+y` is 3, `-z` is 4, and finally, `+z` is 5.\n\n```math\n\\mathbf{c} = \\begin{bmatrix}\nc_x\\\\\nc_y\\\\\nc_z\n\\end{bmatrix}, \\quad \\text{where } c_x, c_y, c_z \\in \\{0, 1, 2, 3, 4, 5\\}\n```\n\nThe color associated on the $x$, $y$, and $z$ dimensions are the colors of the \"stickers\" on that side. Notice how each piece on the cube has at most 3 colors, so we do not have to specify 6 different colors for one piece.\n\nIt is important to note that the color on the $x$ face can be interpreted to be on both the $+x$ face and the $-x$ face, and similarly for $y$ and $z$.\n\n#### Impact of Rotation on a Piece\n\nRotating a piece involves two significant parts. Transforming the Position Vector, and then the Color Vector.\n\nThe use of the 3D elemental rotation matrix to move the piece may be misleading in this case—instead, see the \"rotation\" as moving the piece's Position Vector, without actually rotating it. Then, we have to actually rotate the colors on it by changing the Color Vector.\n\n##### Position:\n\nHere are the 3 elemental rotation matricies, which is what we are using to rotate a piece by a certain amount.\n\n```math\nR_x(\\theta) = \\begin{bmatrix}\n1 \u0026 0 \u0026 0\\\\\n0 \u0026 \\cos(\\theta) \u0026 -\\sin(\\theta)\\\\\n0 \u0026 \\sin(\\theta) \u0026 \\cos(\\theta)\n\\end{bmatrix}\n```\n\n```math\nR_y(\\theta) = \\begin{bmatrix}\n\\cos(\\theta) \u0026 0 \u0026 \\sin(\\theta)\\\\\n0 \u0026 1 \u0026 0\\\\\n-\\sin(\\theta) \u0026 0 \u0026 \\cos(\\theta)\n\\end{bmatrix}\n```\n\n```math\nR_z(\\theta) = \\begin{bmatrix}\n\\cos(\\theta) \u0026 -\\sin(\\theta) \u0026 0\\\\\n\\sin(\\theta) \u0026 \\cos(\\theta) \u0026 0\\\\\n0 \u0026 0 \u0026 1\n\\end{bmatrix}\n```\n\nIn order to rotate our piece, we simply multiply the rotation matrix, in this example on the x-axis, to our position vector.\n\n```math\nR_x(\\theta)\\cdot\\vec{p} = \\begin{bmatrix}\n1 \u0026 0 \u0026 0\\\\\n0 \u0026 \\cos(\\theta) \u0026 -\\sin(\\theta)\\\\\n0 \u0026 \\sin(\\theta) \u0026 \\cos(\\theta)\n\\end{bmatrix}\\cdot\\begin{bmatrix}\nx \\\\\ny \\\\\nz\n\\end{bmatrix}\n```\n\n##### Color:\n\nHandling the colors are a bit more complicated. Let's consider what happens to the colors on a corner piece when it is rotated. I will be refering to this changing arrangement of colors as Sticker Parity.\n\nIf a corner piece is rotated in the x-axis, notice how the sticker that was originally facing the x-axis is still facing the x-axis. However, the color that was originally facing the y-axis is now facing the z-axis. Similarly, the sticker originally facing the z-axis is now facing the y-axis.\n\nConsidering this for rotations on y and z axes, you will notice that it is always true that the sticker facing the axis you are rotating stays in that direction, while the stickers facing the other two axes will swap directions.\n\nNote that the stickers may move from the positive side of an axis to the negative side of another axis. This is why it is important that our Color Vector contains 3 colors for each axes that is transient over both the positive and negative sides of an axes.\n\nIn order to mathematically represent this, I have created some simple transformation matrices that simply swap 2 components of a Vector. Let's call these $T$, for a general transformation.\n\nOne further note before seeing the formulas below is that if we turn the side $90\\degree\\cdot(2n+1) \\text{ for }n\\in\\mathbb{N}$ the Sticker Parity rule applies. However, for any multiple of $180\\degree$, the same Sticker Parity before and after _will be the same_. Thus, we will need to keep track of how many $90\\degree$ rotations we are applying (Referred to hereinafter as $\\alpha$, not to be confused with $\\theta$), and use modulo to detect if there is Sticker Parity.\n\n```math\nT_x(\\alpha) = \\begin{bmatrix}\n1 \u0026 0 \u0026 0\\\\\n0 \u0026 1 - \\alpha\\bmod2 \u0026 \\alpha\\bmod2\\\\\n0 \u0026 \\alpha\\bmod2 \u0026 1 - \\alpha\\bmod2\n\\end{bmatrix}\n```\n\n```math\nT_y(\\alpha) = \\begin{bmatrix}\n1 - \\alpha\\bmod2 \u0026 0 \u0026 \\alpha\\bmod2\\\\\n0 \u0026 1 \u0026 0\\\\\n\\alpha\\bmod2 \u0026 0 \u0026 1 - \\alpha\\bmod2\n\\end{bmatrix}\n```\n\n```math\nT_z(\\alpha) = \\begin{bmatrix}\n1 - \\alpha\\bmod2 \u0026 \\alpha\\bmod2 \u0026 0 \\\\\n\\alpha\\bmod2 \u0026 1 - \\alpha\\bmod2 \u0026 0 \\\\\n0 \u0026 0 \u0026 1\n\\end{bmatrix}\n```\n\nSimilarly to the position matricies above, we simply multiply it onto our color matrix.\n\n```math\nT_x(\\alpha)\\cdot\\vec{c} = \\begin{bmatrix}\n1 \u0026 0 \u0026 0\\\\\n0 \u0026 1 - \\alpha\\bmod2 \u0026 \\alpha\\bmod2\\\\\n0 \u0026 \\alpha\\bmod2 \u0026 1 - \\alpha\\bmod2\n\\end{bmatrix}\\cdot\\begin{bmatrix}\nx \\\\\ny \\\\\nz\n\\end{bmatrix}\n```\n\nHowever, there's one more tricky thing that you may notice (I sure didn't on the first time I implemented it!). Let's first rewrite these modulos in terms of $\\sin$ and $\\cos$. I'll show the one for $T_x$ for demo:\n\n```math\nT_x(\\alpha) = \\begin{bmatrix}\n1 \u0026 0 \u0026 0\\\\\n0 \u0026 |\\cos(\\alpha)| \u0026 |\\sin(\\alpha)|\\\\\n0 \u0026 |\\sin(\\alpha)| \u0026 |\\cos(\\alpha)|\n\\end{bmatrix}\n```\n\nNotice how this is extremely similar to our rotation matricies. In fact, other than the absolute values, they are _exactly the same_!\n\nSo, instead of using two different transformation matrices, we will only be using one—our rotation matricies we defined above. Then, when presenting the values to the user, we will just need to ensure that the values are positive.\n\nNow, let's look at how I have organized this data structure in code.\n\n### The Cube\n\nLet's get the logistics out of the way first.\n\nAll \"angles\" from this point on will be referring to a multiple of $\\frac{\\pi}{2}$. I have defined `sin` and `cos`'s inputs to be multiplied by $\\frac{\\pi}{2}$, then have the output rounded to an integer (as rounding errors occur if they don't).\n\nThen, all matricies are flattened into an array to not make it too ugly. This means your rotation matrix will look like this:\n\n```javascript\n[1, 0, 0, 0, cos(T), -sin(T), 0, sin(T), cos(T)];\n```\n\nAnd your position matrix will look like this: `[1,1,1]`.\n\nA custom matrix-multiplication method is written to matrix-multiply these arrays.\n\nAll cube logic is contained in a class called `Cube`, and each Piece is an array (no point to make it an object) that looks like this:\n\n```javascript\n[\n\t[px, py, pz],\n\t[cx, cy, cz],\n];\n```\n\nAnd these Pieces are contained in a list in the `Cube` class.\n\nNow, for the fun part...\n\n#### Defining a Overarching Rotation Function\n\nTo make this clean and simple, I wanted a 3 rotation functions, one for each axis, that looked something like this:\n\n| Input A                       | Input B          | Output                 |\n| ----------------------------- | ---------------- | ---------------------- |\n| Angle to Rotate (in $\\alpha$) | Layers to Rotate | A Single Array of Data |\n\nThe reason I wanted this is so that I could rotate a layer by calling the `turn` method on `Cube` with one argument, that would dictate an entire turn.\n\nFor example, rotating the +x layer by 90° could look like this:\n\n```js\nlet myCube = new Cube();\nmyCube.turn(rx(1, 1));\n```\n\nThis is much cleaner and mathematically efficient than having a different turn function for \"R\", \"U\", \"L\", etc.\n\nIt took a lot of iteration, but I eventually ended up with the following format for a rotation function:\n\n```javascript\nlet rx = (alpha, x1 = 1, x2 = x1) =\u003e [\n\t1,\n\t0,\n\t0,\n\t0,\n\tcos(alpha),\n\t-sin(alpha),\n\t0,\n\tsin(alpha),\n\tcos(alpha),\n\t0,\n\tx1,\n\tx2,\n];\n```\n\nNote the format of this. The first 9 elements of the array is my 3x3 rotation matrix. The last 3 elements of the array are the metadata, which tells the `Cube.turn` method which axis it is turning on, and which layers to turn.\n\nRemember that \"custom matrix-multiplication method\" mentioned above? Well, it only uses the first 9 elements of the array, so the remaining elements can be read harmlessly.\n\n#### Putting it All Into a Cube\n\nNow, to make the cube class, we first initiate a total of 26 pieces ($3^3-1$) with correct positions and colors.\n\nIn order to generate the colors, we take the position of the piece, and transform each component a little bit to turn each component into a unique color. As `x`, `y`, and `z` are either -1 or 1 (or 0, but we do not need to worry about those colors), we do `(component + 1) / 2` to compress it into a 0 or a 1. Then, offset each axis by 2. The code looks something like this:\n\n```js\n[x, y, z].map((x, i) =\u003e (x + 1) / 2 + i * 2);\n```\n\nAs mentioned before, the colors are numbers, and they are completely abstract—up for the user to assign actual meaning to. However, this is the pattern: `-x` is 0, `+x` is 1, `-y` is 2, `+y` is 3, `-z` is 4, and finally, `+z` is 5.\n\n##### Doing a Turn:\n\nNow, to `turn` the cube, we have to use the `metadata` line of the rotation function we defined above:\n\n```\n[axis, start, end]\n```\n\nIterate over all the pieces, finding all pieces whose component for the given axis (a number from 0 to 2, where 0 is `x`, 1 is `y`, and 2 is `z`) is between `start` and `end`, and apply the rotation matrix to both the Color and Position Vectors (again, remember that applying the rotation matrix to the color vector will result in negatives, but that will be fixed in the flatten method, described below).\n\nThe entire code looks like this (where `R` is our rotation matrix w/ metadata):\n\n```js\nconst [axis, start, end] = R.slice(9);\n\nfor (let i in this.pieces) {\n\tconst piece = this.pieces[i],\n\t\tpos = piece[0][axis];\n\tif (pos \u003e= start \u0026\u0026 pos \u003c= end) {\n\t\tthis.pieces[i] = piece.map((x) =\u003e mult(R, x));\n\t}\n}\n```\n\n#### Flattening the Cube\n\nThe final step is to turn the collection of pieces into a (somewhat) human-readable object.\n\nThe direction of the flattening is shown in the diagram below, with the arrow pointing on the \"up\" direction (top of the 2D array) on each face.\n\n```\n    +---+\n    |^+y|\n+---+---+---+---+\n|^-x|^+z|^+x|^-z|\n+---+---+---+---+\n    |^-y|\n    +---+\n```\n\nEach face is identified with one of these strings: `[\"1,0,0\", \"-1,0,0\", \"0,1,0\", \"0,-1,0\", \"0,0,1\", \"0,0,-1\"]`\n\nThese strings are coordinate pairs identifying the face axis and whether its positive or negative.\n\nFor example, `\"1,0,0\"` represents the `+x` face, `\"0,-1,0\"` represents the `-y` face, and so on.\n\nFlattening the cube is done through some projection mathematics. Essentially, for every face, two numbers are calculated for the corresponding `i` and `j` coordinates for that face.\n\nLet's take an example of the x face. Through some simple visualization, we are able to see that in order to project a piece's x-face color onto the `-x` plane, the coordinate on the **-x plane** is $(2 - (y+1), z + 1)$. To project the piece's x-face color onto the `+x` plane, the coordinate on the **+x plane** is $(2 - (y+1), 2 - (z+1))$. Let us simplify these two with absolute values.\n\n`-x`: $(|y-1|, |z+1|)$\n\n`+x`: $(|y-1|, |z-1|)$\n\nFor sake of symmetry, I've added an absolute value to the `-x` plane calculation as well. Now, we can generalize everything in terms of x:\n\n$(|y-1|, |z-\\text{sign}(x)|)$\n\nNow, just do this for all 3 axes, and it looks something like this:\n\n| `x`                                        | `y`                                        | `z`   |\n| ---------------------------------------- | ---------------------------------------- | --- |\n| $(\\|y-1\\|, \\|z-\\text{sign}(x)\\|)$ | $(\\|z+\\text{sign}(y))\\|, \\|x+1\\|)$ |  $(\\|y-\\text{sign}(z))\\|, \\|x+\\text{sign}(z))\\|)$  |\n\nNow, just remember that we will need to assign the **absolute value** of the color vector to the plane.\n\nFinally, putting it all together, let's implement it in code.\n\nOne thing that we have to note is that some pieces are not projected onto any plane. Upon visual inspection, pieces that are not projected onto face $n$ have a 0 in component $n$. Now, the trick is to create a result object with a key `\"0,0,0\"`, that will get assigned randomly all of these non-existent (one may say \"imaginary\") stickers. Then, we delete this at the end.\n\nThe final function looks like this:\n\n```js\nflatten() {\n        const flattened = [\"1,0,0\", \"-1,0,0\", \"0,1,0\", \"0,-1,0\", \"0,0,1\", \"0,0,-1\", \"0,0,0\"]\n            .reduce((acc, piece) =\u003e ({ ...acc, [piece]: Array(3).fill().map(_ =\u003e Array(3)) }), {});\n\n        for (const [p, [cx, cy, cz]] of this.pieces) {\n            const [x, y, z] = p;\n            flattened[String([x, 0, 0])][abs(y - 1)][abs(z - sign(x))] = abs(cx);\n            flattened[String([0, y, 0])][abs(z + sign(y))][abs(x + 1)] = abs(cy);\n            flattened[String([0, 0, z])][abs(y - sign(z))][abs(x + sign(z))] = abs(cz);\n        }\n\n        delete flattened[String([0, 0, 0])]\n        return flattened;\n    }\n```\n\nAnd that's all!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fevanzhoudev%2Fcubie","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fevanzhoudev%2Fcubie","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fevanzhoudev%2Fcubie/lists"}