Refactor: Implement SmartCube renderer, improve UI styling, and fix gaps

This commit is contained in:
2026-02-22 04:35:59 +00:00
parent 57abfd6b80
commit b5ddc21662
4168 changed files with 763782 additions and 1008 deletions

129
node_modules/rubiks-js/README.md generated vendored Normal file
View File

@@ -0,0 +1,129 @@
# Rubiks Cube for the web
This library can be used to embed a rubiks cube into any website. Supports mouse and touch, is pure js typed with jsdoc and does not use any libraries.
## Usage
First install
```
npm install rubiks
```
then create a rubiks cube and start it
```javascript
import {RubiksCube, defaultTexture, defaultUVs, defaultHoveringColors} from 'rubiks-js'
const rubiksCube = new RubiksCube(
attributeName,
defaultTexture,
defaultUVs,
defaultHoveringColors,
trackCenters
)
rubiksCube.start()
```
webgl is automaticly instanciated when first calling `start()`.
### `attributeName`
Specifies the data attribute on the canvas
```html
<canvas data-rubiks-cube></canvas>
<!-- attributeName = 'data-rubiks-cube' -->
```
### `texture`
Specifies how the sides of the cube look. Can be used to have pictures instead of plane colors.
### `uvs`
Specifies which sticker (facelet) uses which part of the [texture](#texture) and is a 3 dimensional number array. The first dimension corresponds to a side. This means the indices should range from 0 to 5. Detailed description for side indices can be found [here](#side). The second dimension corresponds to one of the nine facelets on a side and should be between 0 and 8. A detailed description for facelet indices can be found [here](#facelet). The last dimenstion is the actual coordinate values and should consist of 4 consecutive x and y pairs. The order of these pairs is important and should either be clockwise or anticlockwise depending on the texture. You may have to experiment.
### `hoveringColors`
Specifies a red, green and blue channel by which the colors of a side is multiplied when hovering over it.
### `trackCenters`
This is only usefull when using images. It is a boolean and if set to `true` the rotation
of all 6 centers is included in the [state](#state).
## State
This library has a simple way of tracking the current state of the cube.
Below is an example of how to store the state inside the url.
### `rubiks.on()`
```javascript
const url = new URL(location.toString())
const state = url.searchParams.get('state')
if (state != null) {
rubiksCube.setState(state)
}
rubiksCube.on('change', event => {
const url = new URL(location.toString())
url.searchParams.set('state', event.state.toString())
history.replaceState(null, '', url)
})
rubiksCube.start()
```
`rubiksCube.setState` returns `true` if the state was parsed correctly and `false`
if there was an error.
### `rubiks.reset()`
Resets the state of the cube.
## Indices
Indices have the following order: (brackets use default values)
- First from right (blue) to left (green)
- Then from bottom (yellow) to top (white)
- Then from front (red) to back (orange)
### Side
| Side | Index |
| --- | --- |
| right | 0 |
| left | 1 |
| bottom | 2 |
| top | 3 |
| front | 4 |
| back | 5 |
### Facelet
This is a bit more complicated to explain. The indices follow the same [rule](#indices) as the [sides](#side). For example if you look at the front side without moving the cube index 0 would be the bottom right facelet and index 1 would be 1 to the left. Thats because the indices go first from left to right and the from bottom to top. This means index 0, 1 and 2 are the bottom row, 3, 4 and 5 the row above and so on. And remember always from left to right. In this example we were able to ignore the third [index rule](#indices) because these facelets where pointed at the front so it doesn't make sense to use this rule. Following the same logic we can always ignore on rule when figuring the facelet indices out.
If you want a visual way to see all indices use the following code and copy the [numbers.png](https://github.com/pedeEli/rubiks-cube-v2/blob/main/number.png) image to your source files
```javascript
import {RubiksCube, defaultHovorvingColors} from 'rubiks-js'
const image = new Image()
image.src = 'number.png' // or 'https://raw.githubusercontent.com/pedeEli/rubiks-cube-v2/main/number.png'
const uvs = Array(6).fill(null).map((_, side) => {
const bottom = 0.5 + Math.floor(side / 3) * 0.5
const left = (side % 3) / 3
return Array(9).fill(null).map((_, sideIndex) => {
const b = bottom - (sideIndex % 3) / 6
const t = b - 1 / 6
const l = left + Math.floor(sideIndex / 3) / 9
const r = l + 1 / 9
return [
l, b,
r, b,
r, t,
l, t
]
})
})
image.addEventListener('load', () => {
const rubiksCube = new RubiksCube(
'data-rubiks-cube',
image,
uvs,
defaultHovorvingColors,
true
)
rubiksCube.start()
})
```

40
node_modules/rubiks-js/package.json generated vendored Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "rubiks-js",
"version": "1.0.0",
"type": "module",
"description": "A rubiks cube with no dependencies. Can use custom images for faces. The state of the cube can be safed.",
"keywords": [
"rubiks",
"cube",
"rubiks cube",
"3x3x3",
"3x3",
"state",
"jsdoc",
"image",
"picture",
"touch",
"mobile",
"phone"
],
"homepage": "https://github.com/pedeEli/rubiks/blob/main/packages/rubiks/README.md",
"bugs": {
"url": "https://github.com/pedeEli/rubiks/issues"
},
"license": "MIT",
"author": {
"name": "Elias Gerster"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pedeEli/rubiks.git"
},
"main": "./src/index.js",
"scripts": {
"check": "tsc",
"publish": "npm publish"
},
"devDependencies": {
"typescript": "^5.4.3"
}
}

91
node_modules/rubiks-js/src/converter.js generated vendored Normal file
View File

@@ -0,0 +1,91 @@
/**
* @typedef {import('./types').AIA} AIA
* @typedef {import('./state/types').TurnBase} TurnBase
* @typedef {import('./state/types').Turn} Turn
*/
/** @type {[a: TurnBase, b: TurnBase, invert: boolean][]} */
const axisToTurn = [
['R', 'L', false],
['D', 'U', true],
['F', 'B', false]
]
/**
* @param {AIA} aia
* @returns {Turn}
*/
export const convertAiaToTurn = (aia) => {
if (aia.index === 1) {
throw new Error('Cannot convert middle turns')
}
let [a, b, invert] = axisToTurn[aia.axis]
const turn = aia.index === 0 ? a : b
if (aia.angle === 2) {
return `${turn}2`
}
if (aia.index === 2) {
invert = !invert
}
if (aia.angle === 3 && !invert || aia.angle === 1 && invert) {
return `${turn}'`
}
return turn
}
/** @type {Record<TurnBase, [axis: number, index: number, invert: boolean]>} */
const turnToAia = {
R: [0, 0, false],
L: [0, 2, false],
D: [1, 0, true],
U: [1, 2, true],
F: [2, 0, false],
B: [2, 2, false]
}
/**
* @param {Turn} turn
* @returns {AIA}
*/
export const convertTurnToAia = (turn) => {
const base = /** @type {TurnBase} */ (turn[0])
const [axis, index, invert] = turnToAia[base]
if (turn.endsWith('2')) {
return {axis, index, angle: 2}
}
let prime = turn.endsWith('\'')
if (invert) {
prime = !prime
}
let angle = 3
if (index === 0 && !prime || index === 2 && prime) {
angle = 1
}
return {axis, index, angle}
}
/**
* @param {Turn} turn
* @returns {TurnBase[]}
*/
export const convertTurnToTurnBase = turn => {
const base = /** @type {TurnBase} */ (turn[0])
if (turn.endsWith('2')) {
return [base, base]
}
if (turn.endsWith('\'')) {
return [base, base, base]
}
return [base]
}

56
node_modules/rubiks-js/src/events.js generated vendored Normal file
View File

@@ -0,0 +1,56 @@
export class ChangeEvent {
/**
* Axis that was turned around
*
* 0 => x-axis (from right to left)
*
* 1 => y-axis (from bottom to top)
*
* 2 => z-axis (from front to back)
* @type {0 | 1 | 2}
*/
axis
/**
* The slice on the axis. Example using axis = 0:
*
* 0 => Left slice
*
* 2 => Right slice
*
* The middle layer is never turned. Instead both outer layers are turn
* in the opposite direction. This has the same effect.
* That way the centers always stay in the same position.
* @type {0 | 2}
*/
index
/**
* Important: The angle is always clockwise around the axis and not
* clockwise around the turning side. This means that R and L' both
* have an angle of 1
* @type {1 | 2 | 3}
*/
angle
/**
* @type {import('./state/types').Turn}
*/
turn
/**
* @type {import('./state').StateInfo}
*/
state
/**
* @param {number} axis
* @param {number} index
* @param {number} angle
* @param {import('./state/types').Turn} turn
* @param {import('./state').StateInfo} state
*/
constructor(axis, index, angle, turn, state) {
this.axis = /** @type {0 | 1 | 2} */ (axis)
this.index = /** @type {0 | 2} */ (index)
this.angle = /** @type {1 | 2 | 3} */ (angle)
this.turn = turn
this.state = state
}
}

23
node_modules/rubiks-js/src/index.js generated vendored Normal file
View File

@@ -0,0 +1,23 @@
export {RubiksCube} from './rubiksCube'
export const defaultTexture = new ImageData(new Uint8ClampedArray([
0, 0, 255, 255,
0, 255, 0, 255,
255, 255, 0, 255,
255, 255, 255, 255,
255, 0, 0, 255,
255, 127, 0, 255
]), 1, 6)
/** @type {number[][][]} */
export const defaultUVs = Array(6).fill(null).map((_, index) => {
return Array(9).fill([
0, (index + 0) / 6,
1, (index + 0) / 6,
1, (index + 1) / 6,
0, (index + 1) / 6
])
})
/** @type {number[][]} */
export const defaultHovorvingColors = Array(6).fill(Array(3).fill(.7))

13
node_modules/rubiks-js/src/math/abstractVector.d.ts generated vendored Normal file
View File

@@ -0,0 +1,13 @@
export declare abstract class Vector<V extends Vector<any>> {
abstract scale(a: number): V
abstract add(v: V): V
abstract sub(v: V): V
abstract mult(v: V): V
abstract dot(v: V): number
abstract toArray(): number[]
get squareMag(): number
get mag(): number
get normalized(): V
get negate(): V
}

67
node_modules/rubiks-js/src/math/abstractVector.js generated vendored Normal file
View File

@@ -0,0 +1,67 @@
/**
* @template {Vector<any>} V
* @abstract
*/
export class Vector {
/**
* @abstract
* @param {number} _a
*/
scale(_a) {
throw new Error('must be implemented in subclass')
}
/**
* @abstract
* @param {V} _v
* @returns {V}
*/
add(_v) {
throw new Error('must be implemented in subclass')
}
/**
* @abstract
* @param {V} _v
* @return {V}
*/
sub(_v) {
throw new Error('must be implemented in subclass')
}
/**
* @abstract
* @param {V} _v
* @return {V}
*/
mult(_v) {
throw new Error('must be implemented in subclass')
}
/**
* @abstract
* @param {V} _v
* @return {number}
*/
dot(_v) {
throw new Error('must be implemented in subclass')
}
/**
* @abstract
* @return {number[]}
*/
toArray() {
throw new Error('must be implemented in subclass')
}
get squareMag() {
/** @type {any} */
const t = this
return this.dot(t)
}
get mag() {
return Math.sqrt(this.squareMag)
}
get normalized() {
return this.scale(1 / this.mag)
}
get negate() {
return this.scale(-1)
}
}

194
node_modules/rubiks-js/src/math/matrix.js generated vendored Normal file
View File

@@ -0,0 +1,194 @@
import {V4, V3} from './vector'
/**
* @typedef {import('../types').Uniform} Uniform
* @implements {Uniform}
*/
export class M44 {
/**
* @param {V4} r1
* @param {V4} r2
* @param {V4} r3
* @param {V4} r4
*/
constructor(r1, r2, r3, r4) {
this.r1 = r1
this.r2 = r2
this.r3 = r3
this.r4 = r4
}
/** @param {number} a */
scale(a) {
return new M44(this.r1.scale(a), this.r2.scale(a), this.r3.scale(a), this.r4.scale(a))
}
/** @param {M44} m */
add({r1, r2, r3, r4}) {
return new M44(this.r1.add(r1), this.r2.add(r2), this.r3.add(r3), this.r4.add(r4))
}
/** @param {M44} m */
sub({r1, r2, r3, r4}) {
return new M44(this.r1.sub(r1), this.r2.sub(r2), this.r3.sub(r3), this.r4.sub(r4))
}
/**
* @overload
* @param {M44} m
* @return {M44}
*
* @overload
* @param {V4} m
* @return {V4}
*
* @param {M44 | V4} m
*/
mult(m) {
if ('x' in m) {
return new V4(
this.r1.dot(m),
this.r2.dot(m),
this.r3.dot(m),
this.r4.dot(m)
)
}
return new M44(
new V4(this.r1.dot(m.c1), this.r1.dot(m.c2), this.r1.dot(m.c3), this.r1.dot(m.c4)),
new V4(this.r2.dot(m.c1), this.r2.dot(m.c2), this.r2.dot(m.c3), this.r2.dot(m.c4)),
new V4(this.r3.dot(m.c1), this.r3.dot(m.c2), this.r3.dot(m.c3), this.r3.dot(m.c4)),
new V4(this.r4.dot(m.c1), this.r4.dot(m.c2), this.r4.dot(m.c3), this.r4.dot(m.c4))
)
}
toArray() {
return [...this.r1.toArray(), ...this.r2.toArray(), ...this.r3.toArray(), ...this.r4.toArray()]
}
static get identity() {
return new M44(
new V4(1, 0, 0, 0),
new V4(0, 1, 0, 0),
new V4(0, 0, 1, 0),
new V4(0, 0, 0, 1)
)
}
get c1() {
return new V4(this.r1.x, this.r2.x, this.r3.x, this.r4.x)
}
get c2() {
return new V4(this.r1.y, this.r2.y, this.r3.y, this.r4.y)
}
get c3() {
return new V4(this.r1.z, this.r2.z, this.r3.z, this.r4.z)
}
get c4() {
return new V4(this.r1.w, this.r2.w, this.r3.w, this.r4.w)
}
/**
* @param {WebGL2RenderingContext} gl
* @param {WebGLUniformLocation} location
*/
setUniform(gl, location) {
const data = new Float32Array(this.toArray())
gl.uniformMatrix4fv(location, true, data)
}
get transpose() {
return new M44(this.c1, this.c2, this.c3, this.c4)
}
get inverse() {
const [i00, i01, i02, i03] = this.r1.toArray()
const [i10, i11, i12, i13] = this.r2.toArray()
const [i20, i21, i22, i23] = this.r3.toArray()
const [i30, i31, i32, i33] = this.r4.toArray()
const s0 = i00 * i11 - i10 * i01
const s1 = i00 * i12 - i10 * i02
const s2 = i00 * i13 - i10 * i03
const s3 = i01 * i12 - i11 * i02
const s4 = i01 * i13 - i11 * i03
const s5 = i02 * i13 - i12 * i03
const c5 = i22 * i33 - i32 * i23
const c4 = i21 * i33 - i31 * i23
const c3 = i21 * i32 - i31 * i22
const c2 = i20 * i33 - i30 * i23
const c1 = i20 * i32 - i30 * i22
const c0 = i20 * i31 - i30 * i21
const det = s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0
const invDet = 1 / det
return new M44(
new V4(
(i11 * c5 - i12 * c4 + i13 * c3),
(-i01 * c5 + i02 * c4 - i03 * c3),
(i31 * s5 - i32 * s4 + i33 * s3),
(-i21 * s5 + i22 * s4 - i23 * s3)
),
new V4(
(-i10 * c5 + i12 * c2 - i13 * c1),
(i00 * c5 - i02 * c2 + i03 * c1),
(-i30 * s5 + i32 * s2 - i33 * s1),
(i20 * s5 - i22 * s2 + i23 * s1)
),
new V4(
(i10 * c4 - i11 * c2 + i13 * c0),
(-i00 * c4 + i01 * c2 - i03 * c0),
(i30 * s4 - i31 * s2 + i33 * s0),
(-i20 * s4 + i21 * s2 - i23 * s0)
),
new V4(
(-i10 * c3 + i11 * c1 - i12 * c0),
(i00 * c3 - i01 * c1 + i02 * c0),
(-i30 * s3 + i31 * s1 - i32 * s0),
(i20 * s3 - i21 * s1 + i22 * s0)
)
).scale(invDet)
}
/**
* @param {number} fovy
* @param {number} aspect
* @param {number} near
* @param {number} far
*/
static perspective(fovy, aspect, near, far) {
const tanHalfFovy = Math.tan(fovy / 2)
const x = 1 / (aspect * tanHalfFovy)
const y = 1 / tanHalfFovy
const fpn = far + near
const fmn = far - near
const oon = .5 / near
const oof = .5 / far
const z = -fpn / fmn
const w = 1 / (oof - oon)
return new M44(
new V4(x, 0, 0, 0),
new V4(0, y, 0, 0),
new V4(0, 0, z, w),
new V4(0, 0, -1, 0)
)
}
/**
* @param {V3} eye
* @param {V3} center
* @param {V3} up
*/
static lookAt(eye, center, up) {
const za = center.sub(eye).normalized
const xa = za.cross(up).normalized
const ya = xa.cross(za)
const xd = -xa.dot(eye)
const yd = -ya.dot(eye)
const zd = za.dot(eye)
return new M44(
new V4( xa.x, xa.y, xa.z, xd),
new V4( ya.x, ya.y, ya.z, yd),
new V4(-za.x, -za.y, -za.z, zd),
new V4( 0, 0, 0, 1)
)
}
}

84
node_modules/rubiks-js/src/math/quarternion.js generated vendored Normal file
View File

@@ -0,0 +1,84 @@
import {V3, V4} from './vector'
import {M44} from './matrix'
export class Quaternion {
/**
* @param {number} real
* @param {V3} im
*/
constructor(real, im) {
this.real = real
this.im = im
}
/**
* @param {V3} axis
* @param {number} angle
* @param {boolean} [degree=true]
*/
static fromAngle(axis, angle, degree = true) {
if (degree) {
angle *= Math.PI / 180
}
const half = angle / 2;
const real = Math.cos(half)
const im = axis.normalized.scale(Math.sin(half))
return new Quaternion(real, im)
}
get matrix() {
const {x, y, z} = this.im
const w = this.real
const xx = x * x
const yy = y * y
const zz = z * z
const xy = x * y
const xz = x * z
const xw = x * w
const yz = y * z
const yw = y * w
const zw = z * w
return new M44(
new V4(1 - 2 * (yy + zz), 2 * (xy - zw), 2 * (xz + yw), 0),
new V4(2 * (xy + zw), 1 - 2 * (xx + zz), 2 * (yz - xw), 0),
new V4(2 * (xz - yw), 2 * (yz + xw), 1 - 2 * (xx + yy), 0),
new V4( 0, 0, 0, 1)
)
}
/** @param {Quaternion} q */
mult({real, im}) {
return new Quaternion(this.real * real - this.im.dot(im), this.im.cross(im).add(im.scale(this.real)).add(this.im.scale(real)))
}
/** @param {V3} v */
rotate(v) {
return new Quaternion(this.real, this.im.negate).mult(new Quaternion(0, v)).mult(this).im
}
get conjugate() {
return new Quaternion(this.real, this.im.negate)
}
get mag() {
return Math.sqrt(this.real * this.real + this.im.squareMag)
}
/** @param {number} n */
power(n) {
const {mag} = this
const phi = Math.acos(this.real / mag)
const unit = this.im.normalized
const scalar = Math.pow(mag, n)
return new Quaternion(scalar * Math.cos(phi * n), unit.scale(scalar * Math.sin(phi * n)))
}
static get identity() {
return new Quaternion(1, V3.zero)
}
/**
* @param {Quaternion} q1
* @param {Quaternion} q2
* @param {number} t
*/
static slerp(q1, q2, t) {
return q1.mult(q1.conjugate.mult(q2).power(t))
}
}

28
node_modules/rubiks-js/src/math/utils.js generated vendored Normal file
View File

@@ -0,0 +1,28 @@
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
export const mod = (a, b) => {
return ((a % b) + b) % b
}
/**
* @param {number} a
* @param {number} b
* @param {number} t
* @returns {number}
*/
export const lerp = (a, b, t) => {
return a + (b - a) * t
}
/**
* @param {number} x
* @param {number} min
* @param {number} max
* @returns {number}
*/
export const clamp = (x, min, max) => {
return Math.min(Math.max(x, min), max)
}

217
node_modules/rubiks-js/src/math/vector.js generated vendored Normal file
View File

@@ -0,0 +1,217 @@
import {Vector} from './abstractVector'
/**
* @typedef {import('../types').Uniform} Uniform
*/
/**
* @extends {Vector<V2>}
* @implements {Uniform}
*/
export class V2 extends Vector {
/**
* @param {number} x
* @param {number} y
*/
constructor(x, y) {
super()
this.x = x
this.y = y
}
/** @param {number} a */
scale(a) {
return new V2(a * this.x, a * this.y)
}
/** @param {V2} v */
add({x, y}) {
return new V2(this.x + x, this.y + y)
}
/** @param {V2} v */
sub({x, y}) {
return new V2(this.x - x, this.y - y)
}
/** @param {V2} v */
mult({x, y}) {
return new V2(this.x * x, this.y * y)
}
/** @param {V2} v */
dot({x, y}) {
return this.x * x + this.y * y
}
toArray() {
return [this.x, this.y]
}
/**
* @param {WebGL2RenderingContext} gl
* @param {WebGLUniformLocation} location
*/
setUniform(gl, location) {
gl.uniform2f(location, this.x, this.y)
}
static get zero() {
return new V2(0, 0)
}
}
/**
* @extends {Vector<V3>}
* @implements {Uniform}
*/
export class V3 extends Vector {
/**
* @param {number} x
* @param {number} y
* @param {number} z
*/
constructor(x, y, z) {
super()
this.x = x
this.y = y
this.z = z
}
/** @param {number} a */
scale(a) {
return new V3(a * this.x, a * this.y, a * this.z)
}
/** @param {V3} v */
add({x, y, z}) {
return new V3(this.x + x, this.y + y, this.z + z)
}
/** @param {V3} v */
sub({x, y, z}) {
return new V3(this.x - x, this.y - y, this.z - z)
}
/** @param {V3} v */
mult({x, y, z}) {
return new V3(this.x * x, this.y * y, this.z * z)
}
/** @param {V3} v */
cross({x, y, z}) {
return new V3(this.y * z - this.z * y, this.z * x - this.x * z, this.x * y - this.y * x)
}
/** @param {V3} v */
dot({x, y, z}) {
return this.x * x + this.y * y + this.z * z
}
toArray() {
return [this.x, this.y, this.z]
}
/**
* @param {WebGL2RenderingContext} gl
* @param {WebGLUniformLocation} location
*/
setUniform(gl, location) {
gl.uniform3f(location, this.x, this.y, this.z)
}
toV2() {
return new V2(this.x, this.y)
}
static get zero() {
return new V3(0, 0, 0)
}
static get one() {
return new V3(1, 1, 1)
}
static get up() {
return new V3(0, 1, 0)
}
static get down() {
return new V3(0, -1, 0)
}
static get left() {
return new V3(1, 0, 0)
}
static get right() {
return new V3(-1, 0, 0)
}
static get forward() {
return new V3(0, 0, 1)
}
static get back() {
return new V3(0, 0, -1)
}
/**
* @param {V3} v1
* @param {V3} v2
* @param {number} t
*/
static lerp(v1, v2, t) {
return v1.add(v2.sub(v1).scale(t))
}
/**
* @param {V3} v1
* @param {V3} v2
*/
static angle(v1, v2) {
return Math.acos(v1.dot(v2) / Math.sqrt(v1.squareMag * v2.squareMag))
}
/** @param {number} axis */
static getRotationAxis(axis) {
if (axis === 0)
return V3.right
if (axis === 1)
return V3.down
return V3.back
}
}
/**
* @extends {Vector<V4>}
* @implements {Uniform}
*/
export class V4 extends Vector {
/**
* @param {number} x
* @param {number} y
* @param {number} z
* @param {number} w
*/
constructor(x, y, z, w) {
super()
this.x = x
this.y = y
this.z = z
this.w = w
}
/** @param {number} a */
scale(a) {
return new V4(a * this.x, a * this.y, a * this.z, a * this.w)
}
/** @param {V4} v */
add({x, y, z, w}) {
return new V4(this.x + x, this.y + y, this.z + z, this.w + w)
}
/** @param {V4} v */
sub({x, y, z, w}) {
return new V4(this.x - x, this.y - y, this.z - z, this.w - w)
}
/** @param {V4} v */
mult({x, y, z, w}) {
return new V4(this.x * x, this.y * y, this.z * z, this.w * w)
}
/** @param {V4} v */
dot({x, y, z, w}) {
return this.x * x + this.y * y + this.z * z + this.w * w
}
toV3() {
return new V3(this.x, this.y, this.z)
}
toArray() {
return [this.x, this.y, this.z, this.w]
}
/**
* @param {WebGL2RenderingContext} gl
* @param {WebGLUniformLocation} location
*/
setUniform(gl, location) {
gl.uniform4f(location, this.x, this.y, this.z, this.w)
}
}

300
node_modules/rubiks-js/src/rubiksCube.js generated vendored Normal file
View File

@@ -0,0 +1,300 @@
import {V3} from './math/vector'
import {Quaternion} from './math/quarternion'
import {Program} from './ui/program'
import {Camera} from './ui/camera'
import {Rubiks} from './ui/rubiks'
import {InputHandler} from './ui/inputHandler'
import debug from './ui/debugger'
import {vertex, fragment} from './shaders/facelet.glsl'
import {State} from './state'
import {convertAiaToTurn} from './converter'
import {ChangeEvent} from './events'
/** @typedef {import('./types').Events} Events */
export class RubiksCube {
#initialized = false
/** @type {WebGL2RenderingContext} */
#gl
/** @type {HTMLCanvasElement} */
#canvas
/** @type {Program} */
#program
/** @type {WebGLVertexArrayObject} */
#vao
/** @type {WebGLBuffer} */
#uvsVbo
/** @type {WebGLTexture} */
#texture
/** @type {Camera} */
#camera
/** @type {Rubiks} */
#rubiks
/** @type {InputHandler} */
#inputHandler
/** @type {State} */
#state
/** @type {boolean} */
#trackCenters
#frame = 0
#resizeHandler = RubiksCube.#getResizeHandler(this)
/** @type {string} */
#canvasData
/** @type {ImageData | HTMLImageElement} */
#image
/** @type {number[][][]} */
#uvs
/** @type {number[][]} */
#hoveringColors
/**
* @param {string} canvasData
* @param {ImageData | HTMLImageElement} image
* @param {number[][][]} uvs
* @param {number[][]} hoveringColors
* @param {boolean} trackCenters
*/
constructor(canvasData, image, uvs, hoveringColors, trackCenters) {
this.#canvasData = canvasData
this.#image = image
this.#uvs = uvs
this.#hoveringColors = hoveringColors
this.#trackCenters = trackCenters
}
/**
* Starts rendering the cube and listening for user inputs.
* Automaticly initializes webgl and pointer event listeners.
*/
start() {
if (!this.#initialized) {
this.#initialize()
this.#initialized = true
}
this.#inputHandler.addEventListeners()
window.addEventListener('resize', this.#resizeHandler)
this.#resizeHandler()
this.#program.use()
this.#gl.bindVertexArray(this.#vao)
this.#gl.activeTexture(this.#gl.TEXTURE0)
this.#gl.bindTexture(this.#gl.TEXTURE_2D, this.#texture)
this.#program.uniform('tex', {
setUniform: (gl, location) => gl.uniform1i(location, 0)
})
let lastTime = Date.now()
const loop = () => {
const currentTime = Date.now()
const deltaTime = (currentTime - lastTime) / 1000
lastTime = currentTime
this.#gl.clear(this.#gl.COLOR_BUFFER_BIT | this.#gl.DEPTH_BUFFER_BIT)
this.#program.uniform('view', this.#camera.worldToCameraMatrix)
this.#program.uniform('projection', this.#camera.projectionMatrix)
this.#rubiks.render(this.#program, this.#gl, this.#uvsVbo)
this.#rubiks.update(deltaTime)
this.#frame = requestAnimationFrame(loop)
}
this.#frame = requestAnimationFrame(loop)
}
/**
* Stops rendering the cube and removes all listeners.
*/
stop() {
window.removeEventListener('resize', this.#resizeHandler)
cancelAnimationFrame(this.#frame)
this.#inputHandler.removeEventListeners()
}
/**
* Resets the internal state and applys it to the cube.
*/
reset() {
this.#state.reset()
this.#state.applyState(this.#uvs, this.#rubiks)
}
/**
* @param {string} stateStr a base46 representation
* @returns {boolean} false if `stateStr` was invalid
*/
setState(stateStr) {
if (!this.#initialized) {
this.#initialize()
this.#initialized = true
}
if (!this.#state.decode(stateStr)) {
return false
}
this.#state.applyState(this.#uvs, this.#rubiks)
return true
}
/**
* @param {RubiksCube} rubiksCube
* @returns {() => void}
*/
static #getResizeHandler(rubiksCube) {
return () => {
const width = window.innerWidth
const height = window.innerHeight
debug.setSize(width, height)
rubiksCube.#canvas.width = width
rubiksCube.#canvas.height = height
rubiksCube.#gl.viewport(0, 0, width, height)
rubiksCube.#camera.screenSize(width, height)
}
}
#initialize() {
const canvas = document.querySelector(`[${this.#canvasData}]`)
if (!canvas) {
throw new Error(`<canvas ${this.#canvasData}> does not exist`)
}
if (!(canvas instanceof HTMLCanvasElement)) {
throw new Error(`<canvas ${this.#canvasData}> is not a canvas, it is a <${canvas.tagName}>`)
}
this.#canvas = canvas
const gl = canvas.getContext('webgl2')
if (!gl) {
throw new Error(`cannot create webgl2 context`)
}
this.#gl = gl
gl.enable(gl.DEPTH_TEST)
gl.depthFunc(gl.LESS)
this.#program = new Program('rubiksCube/shaders/facelet.glsl', vertex, fragment, gl)
const vao = gl.createVertexArray()
if (!vao) {
throw new Error('could not create a webgl vertex array object')
}
this.#vao = vao
gl.bindVertexArray(vao)
const vertices = [
0, -.5, -.5,
0, -.5, .5,
0, .5, .5,
0, .5, -.5
]
const verticesBuffer = new Float32Array(vertices)
const verticesVbo = gl.createBuffer()
const indices = [
0, 1, 3,
1, 3, 2
]
const indicesBuffer = new Int8Array(indices)
const ebo = gl.createBuffer()
const uvsVbo = gl.createBuffer()
if (!verticesVbo || !ebo || !uvsVbo) {
throw new Error('could not create a vertex buffer objects')
}
this.#uvsVbo = uvsVbo
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer, gl.STATIC_DRAW)
gl.bindBuffer(gl.ARRAY_BUFFER, verticesVbo)
gl.bufferData(gl.ARRAY_BUFFER, verticesBuffer, gl.STATIC_DRAW)
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 12, 0)
gl.enableVertexAttribArray(0)
gl.enableVertexAttribArray(1)
gl.bindVertexArray(null)
const texture = gl.createTexture()
if (!texture) {
throw new Error('could not create a texture')
}
this.#texture = texture
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
this.#image
)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.bindTexture(gl.TEXTURE_2D, null)
const hoveringColors = this.#hoveringColors.map(([r, g, b]) => new V3(r, g, b))
this.#camera = new Camera(new V3(0, 0, -10), V3.zero, V3.up, 45, window.innerWidth, window.innerHeight, .1, 100)
this.#rubiks = new Rubiks(Quaternion.identity, this.#uvs, hoveringColors, this.#turnHandler.bind(this))
this.#inputHandler = new InputHandler(canvas, this.#rubiks, this.#camera)
this.#state = new State(this.#trackCenters)
}
get transform() {
return this.#rubiks.transform
}
/** @type {Map<keyof Events, Set<(event: any) => void>>} */
#listeners = new Map()
/**
* @template {keyof Events} Name
* @param {Name} name
* @param {(event: Events[Name]) => void} callback
*/
on(name, callback) {
const set = this.#listeners.get(name) ?? new Set()
set.add(callback)
this.#listeners.set(name, set)
}
/**
* @param {import('./types').AIA} aia
*/
#turnHandler(aia) {
const turn = convertAiaToTurn(aia)
this.#state.applyTurn(turn)
/** @type {Set<(event: Events['change']) => void> | undefined} */
const changeHandlers = this.#listeners.get('change')
if (changeHandlers) {
const event = new ChangeEvent(
aia.axis,
aia.index,
aia.angle,
turn,
this.#state.stateInfo
)
for (const callback of changeHandlers) {
callback(event)
}
}
}
}

57
node_modules/rubiks-js/src/shaders/facelet.glsl.js generated vendored Normal file
View File

@@ -0,0 +1,57 @@
export const vertex = `#version 300 es
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aUV;
out vec2 uv;
out vec2 pos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
uv = aUV;
pos = aPos.yz;
}`
export const fragment = `#version 300 es
precision mediump float;
in vec2 uv;
in vec2 pos;
out vec4 FragColor;
uniform sampler2D tex;
uniform vec3 colorMult;
vec3 rime = vec3(0.07);
float outer = 0.45;
float inner = 0.44;
float map(float value, float min1, float max1, float min2, float max2)
{
return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
}
void main()
{
float x = abs(pos.x);
float y = abs(pos.y);
if (x > outer || y > outer) {
FragColor = vec4(rime, 1.0);
return;
}
vec3 color = texture(tex, uv).rgb * colorMult;
if (x > inner || y > inner) {
float t = smoothstep(0.0, 1.0, map(max(x, y), outer, inner, 0.0, 1.0));
vec3 c = color * t + rime * (1.0 - t);
FragColor = vec4(c, 1.0);
return;
}
FragColor = vec4(color, 1.0);
}`

72
node_modules/rubiks-js/src/state/centers.js generated vendored Normal file
View File

@@ -0,0 +1,72 @@
import {mod} from '../math/utils'
/**
* @typedef {import('./types').TurnBase} TurnBase
* @typedef {import('./types').Turn} Turn
* @typedef {import('./types').Center} C
* @typedef {import('./types').CenterOrientation} CO
* @typedef {[C, C, C, C, C, C]} Permutation
* @typedef {[CO, CO, CO, CO, CO, CO]} Orientation
*/
/** @type {number[]} */
const orderIndexToCubieIndex = [
12, 14, 10, 16, 4, 22
]
export class Centers {
/** @type {Permutation} */
static order = ['R', 'L', 'D', 'U', 'F', 'B']
/** @type {Orientation} */
orientation = [0, 0, 0, 0, 0, 0]
/** @param {TurnBase} turn */
applyTurn(turn) {
const index = Centers.order.indexOf(turn)
this.orientation[index] = /** @type {CO} */ (mod(this.orientation[index] + 1, 4))
}
/**
* @param {number[][][]} uvs
* @param {import('../ui/rubiks').Rubiks} rubiks
*/
applyState(uvs, rubiks) {
for (let side = 0; side < 6; side++) {
const uv = uvs[side][4]
const offset = this.orientation[side] * 2
const cubieIndex = orderIndexToCubieIndex[side]
const facelet = rubiks.cubies[cubieIndex].getFaceletOfSide(side)
facelet.uvs = []
for (let i = 0; i < 8; i++) {
facelet.uvs[i] = uv[mod(offset + i, 8)]
}
}
}
reset() {
this.orientation = [0, 0, 0, 0, 0, 0]
}
/** @returns {number[]} */
encode() {
let o = 0
for (const orientation of this.orientation) {
o = o << 2
o += orientation
}
return [
(o >> 0) & 0b11111111,
(o >> 8) & 0b11111111
]
}
/** @param {number[]} code */
decode(code) {
let o = code[0] + (code[1] << 8)
for (let i = 5; i >= 0; i--) {
this.orientation[i] = /** @type {CO} */ (o & 0b11)
o = o >> 2
}
}
}

481
node_modules/rubiks-js/src/state/corners.js generated vendored Normal file
View File

@@ -0,0 +1,481 @@
import {createSideToUvs, transformSidetoUvs, setUvs} from '.'
/**
* @typedef {import('./types').TurnBase} TurnBase
* @typedef {import('./types').Turn} Turn
* @typedef {import('./types').Corner} C
* @typedef {import('./types').CornerOrientation} CO
* @typedef {[C, C, C, C, C, C, C, C]} Permutation
* @typedef {[CO, CO, CO, CO, CO, CO, CO, CO]} Orientation
*/
/** @satisfies {Record<TurnBase, Permutation>} */
const permutations = {
R: ['DRF', 'ULF', 'ULB', 'URF', 'DRB', 'DLF', 'DLB', 'URB'],
L: ['URF', 'ULB', 'DLB', 'URB', 'DRF', 'ULF', 'DLF', 'DRB'],
U: ['URB', 'URF', 'ULF', 'ULB', 'DRF', 'DLF', 'DLB', 'DRB'],
D: ['URF', 'ULF', 'ULB', 'URB', 'DLF', 'DLB', 'DRB', 'DRF'],
F: ['ULF', 'DLF', 'ULB', 'URB', 'URF', 'DRF', 'DLB', 'DRB'],
B: ['URF', 'ULF', 'URB', 'DRB', 'DRF', 'DLF', 'ULB', 'DLB']
}
/** @satisfies {Record<TurnBase, Orientation>} */
const orientations = {
R: [2, 0, 0, 1, 1, 0, 0, 2],
L: [0, 1, 2, 0, 0, 2, 1, 0],
U: [0, 0, 0, 0, 0, 0, 0, 0],
D: [0, 0, 0, 0, 0, 0, 0, 0],
F: [1, 2, 0, 0, 2, 1, 0, 0],
B: [0, 0, 1, 2, 0, 0, 2, 1]
}
/** @satisfies {Record<C, Record<C, Record<CO, Turn[]>>>} */
const map = {
URF: {
URF: {
0: [],
1: ['R', 'U'],
2: ['F\'', 'U\'']
},
ULF: {
0: ['U'],
1: ['R', 'U2'],
2: ['F\'']
},
ULB: {
0: ['U2'],
1: ['R', 'U\''],
2: ['F\'', 'U']
},
URB: {
0: ['U\''],
1: ['R'],
2: ['F\'', 'U2']
},
DRF: {
0: ['R2', 'D\''],
1: ['R\''],
2: ['F']
},
DLF: {
0: ['F2'],
1: ['R\'', 'D\''],
2: ['F', 'D\'']
},
DLB: {
0: ['R2', 'D'],
1: ['R\'', 'D2'],
2: ['F', 'D2']
},
DRB: {
0: ['R2'],
1: ['R\'', 'D'],
2: ['F', 'D']
}
},
ULF: {
URF: {
0: ['U\''],
1: ['F'],
2: ['L\'', 'U2']
},
ULF: {
0: [],
1: ['F', 'U'],
2: ['L\'', 'U\'']
},
ULB: {
0: ['U'],
1: ['F', 'U2'],
2: ['L\'']
},
URB: {
0: ['U2'],
1: ['F', 'U\''],
2: ['L\'', 'U']
},
DRF: {
0: ['F2'],
1: ['F\'', 'D'],
2: ['L', 'D']
},
DLF: {
0: ['L2', 'D'],
1: ['F\''],
2: ['L']
},
DLB: {
0: ['L2'],
1: ['F\'', 'D\''],
2: ['L', 'D\'']
},
DRB: {
0: ['L2', 'D\''],
1: ['F\'', 'D2'],
2: ['L', 'D2']
}
},
ULB: {
URF: {
0: ['U2'],
1: ['L', 'U\''],
2: ['B\'', 'U']
},
ULF: {
0: ['U\''],
1: ['L'],
2: ['B\'', 'U2']
},
ULB: {
0: [],
1: ['L', 'U'],
2: ['B\'', 'U\'']
},
URB: {
0: ['U'],
1: ['L', 'U2'],
2: ['B\'']
},
DRF: {
0: ['L2', 'D'],
1: ['L\'', 'D2'],
2: ['B', 'D2']
},
DLF: {
0: ['L2'],
1: ['L\'', 'D'],
2: ['B', 'D']
},
DLB: {
0: ['L2', 'D'],
1: ['L\''],
2: ['B']
},
DRB: {
0: ['B2'],
1: ['L\'', 'D\''],
2: ['B', 'D\'']
}
},
URB: {
URF: {
0: ['U'],
1: ['B', 'U2'],
2: ['R\'']
},
ULF: {
0: ['U2'],
1: ['B', 'U\''],
2: ['R\'', 'U']
},
ULB: {
0: ['U\''],
1: ['B'],
2: ['R\'', 'U2']
},
URB: {
0: [],
1: ['B', 'U'],
2: ['R\'', 'U\'']
},
DRF: {
0: ['R2'],
1: ['B\'', 'D\''],
2: ['R', 'D\'']
},
DLF: {
0: ['R2', 'D\''],
1: ['B\'', 'D2'],
2: ['R', 'D2']
},
DLB: {
0: ['B2'],
1: ['B\'', 'D'],
2: ['R', 'D']
},
DRB: {
0: ['R2', 'D'],
1: ['B\''],
2: ['R']
}
},
DRF: {
URF: {
0: ['R2', 'U'],
1: ['F\''],
2: ['R']
},
ULF: {
0: ['F2'],
1: ['F\'', 'U'],
2: ['R', 'U']
},
ULB: {
0: ['R2', 'U\''],
1: ['F\'', 'U2'],
2: ['R', 'U2']
},
URB: {
0: ['R2'],
1: ['F\'', 'U\''],
2: ['R', 'U\'']
},
DRF: {
0: [],
1: ['F', 'D'],
2: ['R\'', 'D\'']
},
DLF: {
0: ['D\''],
1: ['F'],
2: ['R\'', 'D2']
},
DLB: {
0: ['D2'],
1: ['F', 'D\''],
2: ['R\'', 'D']
},
DRB: {
0: ['D'],
1: ['F', 'D2'],
2: ['R\'']
}
},
DLF: {
URF: {
0: ['F2'],
1: ['L\'', 'U\''],
2: ['F', 'U\'']
},
ULF: {
0: ['L2', 'U\''],
1: ['L\''],
2: ['F']
},
ULB: {
0: ['L2'],
1: ['L\'', 'U'],
2: ['F', 'U']
},
URB: {
0: ['L2', 'U'],
1: ['L\'', 'U2'],
2: ['F', 'U2']
},
DRF: {
0: ['D'],
1: ['L', 'D2'],
2: ['F\'']
},
DLF: {
0: [],
1: ['L', 'D'],
2: ['F\'', 'D\'']
},
DLB: {
0: ['D\''],
1: ['L'],
2: ['F\'', 'D2']
},
DRB: {
0: ['D2'],
1: ['L', 'D\''],
2: ['F\'', 'D']
}
},
DLB: {
URF: {
0: ['L2', 'U\''],
1: ['B\'', 'U2'],
2: ['L', 'U2']
},
ULF: {
0: ['L2'],
1: ['B\'', 'U\''],
2: ['L', 'U\'']
},
ULB: {
0: ['L2', 'U'],
1: ['B\''],
2: ['L']
},
URB: {
0: ['B2'],
1: ['B\'', 'U'],
2: ['L', 'U']
},
DRF: {
0: ['D2'],
1: ['B', 'D\''],
2: ['L\'', 'D']
},
DLF: {
0: ['D'],
1: ['B', 'D2'],
2: ['L\'']
},
DLB: {
0: [],
1: ['B', 'D'],
2: ['L\'', 'D\'']
},
DRB: {
0: ['D\''],
1: ['B'],
2: ['L\'', 'D2']
}
},
DRB: {
URF: {
0: ['R2'],
1: ['R\'', 'U'],
2: ['B', 'U']
},
ULF: {
0: ['R2', 'U'],
1: ['R\'', 'U2'],
2: ['B', 'U2']
},
ULB: {
0: ['B2'],
1: ['R\'', 'U\''],
2: ['B', 'U\'']
},
URB: {
0: ['R2', 'U\''],
1: ['R\''],
2: ['B']
},
DRF: {
0: ['D\''],
1: ['R'],
2: ['B\'', 'D2']
},
DLF: {
0: ['D2'],
1: ['R', 'D\''],
2: ['B\'', 'D']
},
DLB: {
0: ['D'],
1: ['R', 'D2'],
2: ['B\'']
},
DRB: {
0: [],
1: ['R', 'D'],
2: ['B\'', 'D\'']
}
}
}
/** @type {number[]} */
const orderIndexToCubieIndex = [
6, 8, 26, 24, 0, 2, 20, 18
]
export class Corners {
/** @type {Permutation} */
static order = ['URF', 'ULF', 'ULB', 'URB', 'DRF', 'DLF', 'DLB', 'DRB']
/** @type {Permutation} */
permutation = [...Corners.order]
/** @type {Orientation} */
orientation = [0, 0, 0, 0, 0, 0, 0, 0]
/** @param {TurnBase} turn */
applyTurn(turn) {
const appliedPermutation = permutations[turn]
const appliedOrientation = orientations[turn]
const permutation = [...this.permutation]
const orientation = [...this.orientation]
for (let i = 0; i < 8; i++) {
const newPermutation = appliedPermutation[i]
const orderIndex = Corners.order.indexOf(newPermutation)
this.permutation[i] = permutation[orderIndex]
const newOrientation = (appliedOrientation[i] + orientation[orderIndex]) % 3
this.orientation[i] = /** @type {CO} */ (newOrientation)
}
}
/**
* @param {number[][][]} uvs
* @param {import('../ui/rubiks').Rubiks} rubiks
*/
applyState(uvs, rubiks) {
for (let i = 0; i < 8; i++) {
const turns = this.#getTurns(i)
const originIndex = orderIndexToCubieIndex[Corners.order.indexOf(this.permutation[i])]
let sideToUvs = createSideToUvs(originIndex, uvs)
sideToUvs = transformSidetoUvs(originIndex, sideToUvs, turns)
const targetIndex = orderIndexToCubieIndex[i]
setUvs(targetIndex, rubiks, sideToUvs)
}
}
reset() {
this.permutation = [...Corners.order]
this.orientation = [0, 0, 0, 0, 0, 0, 0, 0]
}
/** @returns {number[]} */
encode() {
let p = 0
for (const permutation of this.permutation) {
p = p << 3
p += Corners.order.indexOf(permutation)
}
let o = 0
for (const orientation of this.orientation) {
o = o << 2
o += orientation
}
return [
(p >> 0) & 0b11111111,
(p >> 8) & 0b11111111,
(p >> 16) & 0b11111111,
(o >> 0) & 0b11111111,
(o >> 8) & 0b11111111
]
}
/**
* @param {number[]} code
* @returns {boolean}
*/
decode(code) {
let p = code[0] + (code[1] << 8) + (code[2] << 16)
let o = code[3] + (code[4] << 8)
for (let i = 7; i >= 0; i--) {
const p1 = p & 0b111
if (p1 >= 8) {
return false
}
this.permutation[i] = Corners.order[p1]
p = p >> 3
const o1 = o & 0b11
if (o1 > 2) {
return false
}
this.orientation[i] = /** @type {CO} */ (o1)
o = o >> 2
}
return true
}
/**
* @param {number} index
* @returns {Turn[]}
*/
#getTurns(index) {
const start = Corners.order[index]
const permutation = this.permutation[index]
const orientation = this.orientation[index]
return map[permutation][start][orientation]
}
}

745
node_modules/rubiks-js/src/state/edges.js generated vendored Normal file
View File

@@ -0,0 +1,745 @@
import {createSideToUvs, transformSidetoUvs, setUvs} from '.'
/**
* @typedef {import('./types').TurnBase} TurnBase
* @typedef {import('./types').Turn} Turn
* @typedef {import('./types').Edge} E
* @typedef {import('./types').EdgeOrientation} EO
* @typedef {[E, E, E, E, E, E, E, E, E, E, E, E]} Permutation
* @typedef {[EO, EO, EO, EO, EO, EO, EO, EO, EO, EO, EO, EO]} Orientation
*/
/** @satisfies {Record<TurnBase, Permutation>} */
const permutations = {
R: ['UF', 'UL', 'UB', 'FR', 'DR', 'FL', 'BL', 'UR', 'DF', 'DL', 'DB', 'BR'],
L: ['UF', 'BL', 'UB', 'UR', 'FR', 'UL', 'DL', 'BR', 'DF', 'FL', 'DB', 'DR'],
U: ['UR', 'UF', 'UL', 'UB', 'FR', 'FL', 'BL', 'BR', 'DF', 'DL', 'DB', 'DR'],
D: ['UF', 'UL', 'UB', 'UR', 'FR', 'FL', 'BL', 'BR', 'DL', 'DB', 'DR', 'DF'],
F: ['FL', 'UL', 'UB', 'UR', 'UF', 'DF', 'BL', 'BR', 'FR', 'DL', 'DB', 'DR'],
B: ['UF', 'UL', 'BR', 'UR', 'FR', 'FL', 'UB', 'DB', 'DF', 'DL', 'BL', 'DR']
}
/** @satisfies {Record<TurnBase, Orientation>} */
const orientations = {
R: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
L: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
U: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
D: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
F: [1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0],
B: [0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0]
}
/** @satisfies {Record<E, Record<E, Record<EO, Turn[]>>>} */
const map = {
UF: {
UF: {
0: [],
1: ['F', 'R', 'U']
},
UL: {
0: ['U'],
1: ['F\'', 'L\'']
},
UB: {
0: ['U2'],
1: ['F', 'R', 'U\'']
},
UR: {
0: ['U\''],
1: ['F', 'R']
},
FR: {
0: ['U\'', 'R\''],
1: ['F']
},
FL: {
0: ['U', 'L'],
1: ['F\'']
},
BL: {
0: ['U', 'L\''],
1: ['U2', 'B']
},
BR: {
0: ['U\'', 'R'],
1: ['U2', 'B\'']
},
DF: {
0: ['F2'],
1: ['F', 'R\'', 'D\'']
},
DL: {
0: ['F2', 'D\''],
1: ['F\'', 'L']
},
DB: {
0: ['F2', 'D2'],
1: ['F', 'R\'', 'D']
},
DR: {
0: ['F2', 'D'],
1: ['F', 'R\'']
}
},
UL: {
UF: {
0: ['U\''],
1: ['L', 'F']
},
UL: {
0: [],
1: ['L', 'F', 'U']
},
UB: {
0: ['U'],
1: ['L\'', 'B\'']
},
UR: {
0: ['U2'],
1: ['L', 'F', 'U\'']
},
FR: {
0: ['U2', 'R\''],
1: ['U\'', 'F']
},
FL: {
0: ['L'],
1: ['U\'', 'F\'']
},
BL: {
0: ['L\''],
1: ['U', 'B']
},
BR: {
0: ['U2', 'R'],
1: ['U', 'B\'']
},
DF: {
0: ['L2', 'D'],
1: ['L', 'F\'']
},
DL: {
0: ['L2'],
1: ['L', 'F\'', 'D\'']
},
DB: {
0: ['L2', 'D\''],
1: ['L\'', 'B']
},
DR: {
0: ['L2', 'D2'],
1: ['L', 'F\'', 'D']
}
},
UB: {
UF: {
0: ['U2'],
1: ['B\'', 'R\'', 'U']
},
UL: {
0: ['U\''],
1: ['B', 'L']
},
UB: {
0: [],
1: ['B\'', 'R\'', 'U\'']
},
UR: {
0: ['U'],
1: ['B\'', 'R\'']
},
FR: {
0: ['U', 'R\''],
1: ['B\'', 'R2']
},
FL: {
0: ['U\'', 'L'],
1: ['B', 'L2']
},
BL: {
0: ['U\'', 'L\''],
1: ['B']
},
BR: {
0: ['U', 'R'],
1: ['B\'']
},
DF: {
0: ['B2', 'D2'],
1: ['B\'', 'R', 'D\'']
},
DL: {
0: ['B2', 'D'],
1: ['B', 'L\'']
},
DB: {
0: ['B2'],
1: ['B\'', 'R', 'D']
},
DR: {
0: ['B2', 'D\''],
1: ['B\'', 'R']
}
},
UR: {
UF: {
0: ['U'],
1: ['R\'', 'F\'']
},
UL: {
0: ['U2'],
1: ['R\'', 'F\'', 'U']
},
UB: {
0: ['U\''],
1: ['R', 'B']
},
UR: {
0: [],
1: ['R\'', 'F\'', 'U\'']
},
FR: {
0: ['R\''],
1: ['U', 'F']
},
FL: {
0: ['U2', 'L'],
1: ['U', 'F\'']
},
BL: {
0: ['U2', 'L\''],
1: ['U\'', 'B']
},
BR: {
0: ['R'],
1: ['U\'', 'B\'']
},
DF: {
0: ['R2', 'D\''],
1: ['R\'', 'F']
},
DL: {
0: ['R2', 'D2'],
1: ['R\'', 'F', 'D\'']
},
DB: {
0: ['R2', 'D'],
1: ['R', 'B\'']
},
DR: {
0: ['R2'],
1: ['R\'', 'F', 'D']
}
},
FR: {
UF: {
0: ['R', 'U'],
1: ['F\'']
},
UL: {
0: ['R', 'U2'],
1: ['F\'', 'U']
},
UB: {
0: ['R', 'U\''],
1: ['F\'', 'U2']
},
UR: {
0: ['R'],
1: ['F\'', 'U\'']
},
FR: {
0: [],
1: ['F\'', 'U\'', 'R\'']
},
FL: {
0: ['F2'],
1: ['F\'', 'U', 'L']
},
BL: {
0: ['F2', 'L2'],
1: ['F\'', 'U', 'L\'']
},
BR: {
0: ['R2'],
1: ['F\'', 'U\'', 'R']
},
DF: {
0: ['R\'', 'D\''],
1: ['F']
},
DL: {
0: ['R\'', 'D2'],
1: ['F', 'D\'']
},
DB: {
0: ['R\'', 'D'],
1: ['F', 'D2']
},
DR: {
0: ['R\''],
1: ['F', 'D']
}
},
FL: {
UF: {
0: ['L\'', 'U\''],
1: ['F']
},
UL: {
0: ['L\''],
1: ['F', 'U']
},
UB: {
0: ['L\'', 'U'],
1: ['F', 'U2']
},
UR: {
0: ['L\'', 'U2'],
1: ['F', 'U\'']
},
FR: {
0: ['F2'],
1: ['F', 'U\'', 'R\'']
},
FL: {
0: [],
1: ['F', 'U', 'L']
},
BL: {
0: ['L2'],
1: ['F', 'U', 'L\'']
},
BR: {
0: ['F2', 'R2'],
1: ['F', 'U\'', 'R']
},
DF: {
0: ['L', 'D'],
1: ['F\'']
},
DL: {
0: ['L'],
1: ['F\'', 'D\'']
},
DB: {
0: ['L', 'D\''],
1: ['F\'', 'D2']
},
DR: {
0: ['L', 'D2'],
1: ['F\'', 'D']
}
},
BL: {
UF: {
0: ['L', 'U\''],
1: ['B\'', 'U2']
},
UL: {
0: ['L'],
1: ['B\'', 'U\'']
},
UB: {
0: ['L', 'U'],
1: ['B\'']
},
UR: {
0: ['L', 'U2'],
1: ['B\'', 'U']
},
FR: {
0: ['B2', 'R2'],
1: ['B\'', 'U', 'R\'']
},
FL: {
0: ['L2'],
1: ['B\'', 'U\'', 'L']
},
BL: {
0: [],
1: ['B\'', 'U\'', 'L\'']
},
BR: {
0: ['B2'],
1: ['B\'', 'U', 'R']
},
DF: {
0: ['L\'', 'D'],
1: ['B', 'D2']
},
DL: {
0: ['L\''],
1: ['B', 'D']
},
DB: {
0: ['L\'', 'D\''],
1: ['B']
},
DR: {
0: ['L\'', 'D2'],
1: ['B', 'D\'']
}
},
BR: {
UF: {
0: ['R\'', 'U'],
1: ['B', 'U2']
},
UL: {
0: ['R\'', 'U2'],
1: ['B', 'U\'']
},
UB: {
0: ['R\'', 'U\''],
1: ['B']
},
UR: {
0: ['R\''],
1: ['B', 'U']
},
FR: {
0: ['R2'],
1: ['B', 'U', 'R\'']
},
FL: {
0: ['B2', 'L2'],
1: ['B', 'U\'', 'L']
},
BL: {
0: ['B2'],
1: ['B', 'U\'', 'L\'']
},
BR: {
0: [],
1: ['B', 'U', 'R']
},
DF: {
0: ['R', 'D\''],
1: ['B\'', 'D2']
},
DL: {
0: ['R', 'D2'],
1: ['B\'', 'D']
},
DB: {
0: ['R', 'D'],
1: ['B\'']
},
DR: {
0: ['R'],
1: ['B\'', 'D\'']
}
},
DF: {
UF: {
0: ['F2'],
1: ['F\'', 'R', 'U']
},
UL: {
0: ['F2', 'U'],
1: ['F', 'L\'']
},
UB: {
0: ['F2', 'U2'],
1: ['F\'', 'R', 'U\'']
},
UR: {
0: ['F2', 'U\''],
1: ['F\'', 'R']
},
FR: {
0: ['D', 'R'],
1: ['F\'']
},
FL: {
0: ['D\'', 'L\''],
1: ['F']
},
BL: {
0: ['D\'', 'L'],
1: ['D2', 'B\'']
},
BR: {
0: ['D', 'R\''],
1: ['D2', 'B']
},
DF: {
0: [],
1: ['F\'', 'R\'', 'D\'']
},
DL: {
0: ['D\''],
1: ['F', 'L']
},
DB: {
0: ['D2'],
1: ['F\'', 'R\'', 'D']
},
DR: {
0: ['D'],
1: ['F\'', 'R\'']
}
},
DL: {
UF: {
0: ['L2', 'U\''],
1: ['L\'', 'F']
},
UL: {
0: ['L2'],
1: ['L\'', 'F', 'U']
},
UB: {
0: ['L2', 'U'],
1: ['L', 'B\'']
},
UR: {
0: ['L2', 'U2'],
1: ['L\'', 'F', 'U\'']
},
FR: {
0: ['D2', 'R'],
1: ['D', 'F\'']
},
FL: {
0: ['L\''],
1: ['D', 'F']
},
BL: {
0: ['L'],
1: ['D\'', 'B\'']
},
BR: {
0: ['D2', 'R\''],
1: ['D\'', 'B']
},
DF: {
0: ['D'],
1: ['L\'', 'F\'']
},
DL: {
0: [],
1: ['L\'', 'F\'', 'D\'']
},
DB: {
0: ['D\''],
1: ['L', 'B']
},
DR: {
0: ['D2'],
1: ['L\'', 'F\'', 'D']
}
},
DB: {
UF: {
0: ['B2', 'U2'],
1: ['B', 'R\'', 'U']
},
UL: {
0: ['B2', 'U\''],
1: ['B\'', 'L']
},
UB: {
0: ['B2'],
1: ['B', 'R\'', 'U\'']
},
UR: {
0: ['B2', 'U'],
1: ['B', 'R\'']
},
FR: {
0: ['D\'', 'R'],
1: ['D2', 'F\'']
},
FL: {
0: ['D', 'L\''],
1: ['D2', 'F']
},
BL: {
0: ['D', 'L'],
1: ['B\'']
},
BR: {
0: ['D\'', 'R\''],
1: ['B']
},
DF: {
0: ['D2'],
1: ['B', 'R', 'D\'']
},
DL: {
0: ['D'],
1: ['B\'', 'L\'']
},
DB: {
0: [],
1: ['B', 'R', 'D']
},
DR: {
0: ['D\''],
1: ['B', 'R']
}
},
DR: {
UF: {
0: ['R2', 'U'],
1: ['R', 'F\'']
},
UL: {
0: ['R2', 'U2'],
1: ['R', 'F\'', 'U']
},
UB: {
0: ['R2', 'U\''],
1: ['R\'', 'B']
},
UR: {
0: ['R2'],
1: ['R', 'F\'', 'U\'']
},
FR: {
0: ['R'],
1: ['D\'', 'F\'']
},
FL: {
0: ['D2', 'L\''],
1: ['D\'', 'F']
},
BL: {
0: ['D2', 'L'],
1: ['D', 'B\'']
},
BR: {
0: ['R\''],
1: ['D', 'B']
},
DF: {
0: ['D\''],
1: ['R', 'F']
},
DL: {
0: ['D2'],
1: ['R', 'F', 'D\'']
},
DB: {
0: ['D'],
1: ['R\'', 'B\'']
},
DR: {
0: [],
1: ['R', 'F', 'D']
}
}
}
/** @type {number[]} */
const orderIndexToCubieIndex = [
7, 17, 25, 15, 3, 5, 23, 21, 1, 11, 19, 9
]
export class Edges {
/** @type {Permutation} */
static order = ['UF', 'UL', 'UB', 'UR', 'FR', 'FL', 'BL', 'BR', 'DF', 'DL', 'DB', 'DR']
/** @type {Permutation} */
permutation = [...Edges.order]
/** @type {Orientation} */
orientation = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
/** @param {TurnBase} turn */
applyTurn(turn) {
const appliedPermutation = permutations[turn]
const appliedOrientation = orientations[turn]
const permutation = [...this.permutation]
const orientation = [...this.orientation]
for (let i = 0; i < 12; i++) {
const newPermutation = appliedPermutation[i]
const orderIndex = Edges.order.indexOf(newPermutation)
this.permutation[i] = permutation[orderIndex]
const newOrientation = (appliedOrientation[i] + orientation[orderIndex]) % 2
this.orientation[i] = /** @type {EO} */ (newOrientation)
}
}
/**
* @param {number[][][]} uvs
* @param {import('../ui/rubiks').Rubiks} rubiks
*/
applyState(uvs, rubiks) {
for (let i = 0; i < 12; i++) {
const turns = this.#getTurns(i)
const originIndex = orderIndexToCubieIndex[Edges.order.indexOf(this.permutation[i])]
let sideToUvs = createSideToUvs(originIndex, uvs)
sideToUvs = transformSidetoUvs(originIndex, sideToUvs, turns)
const targetIndex = orderIndexToCubieIndex[i]
setUvs(targetIndex, rubiks, sideToUvs)
}
}
reset() {
this.permutation = [...Edges.order]
this.orientation = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}
/** @returns {number[]} */
encode() {
/** @type {number[]} */
let p = []
for (let i = 0; i < 6; i++) {
const p1 = Edges.order.indexOf(this.permutation[i * 2])
const p2 = Edges.order.indexOf(this.permutation[i * 2 + 1])
p.push(
(p2 << 4) + p1
)
}
let o = 0
for (const orientation of this.orientation) {
o = o << 1
o += orientation
}
return [
...p,
(o >> 0) & 0b11111111,
(o >> 8) & 0b11111111
]
}
/**
* @param {number[]} code
* @returns {boolean}
*/
decode(code) {
for (let i = 0; i < 6; i++) {
const p1 = code[i] & 0b1111
const p2 = (code[i] >> 4) & 0b1111
if (p1 >= 12 || p2 >= 12) {
return false
}
this.permutation[i * 2] = Edges.order[p1]
this.permutation[i * 2 + 1] = Edges.order[p2]
}
let o = code[6] + (code[7] << 8)
for (let i = 11; i >= 0; i--) {
this.orientation[i] = /** @type {EO} */ (o & 0b1)
o = o >> 1
}
return true
}
/**
* @param {number} index
* @returns {Turn[]}
*/
#getTurns(index) {
const start = Edges.order[index]
const permutation = this.permutation[index]
const orientation = this.orientation[index]
return map[permutation][start][orientation]
}
}

238
node_modules/rubiks-js/src/state/index.js generated vendored Normal file
View File

@@ -0,0 +1,238 @@
import {Corners} from './corners'
import {Edges} from './edges'
import {Centers} from './centers'
import {convertTurnToTurnBase, convertTurnToAia} from '../converter'
import {uvsTransformerPresets, cubiesShiftMapper, sidesShiftMapper} from '../ui/uvs'
import {isInside, indexToPosition, positionToUvs} from '../ui/cubie'
import {mod} from '../math/utils'
/**
* @typedef {import('./types').Turn} Turn
* @typedef {import('./types').TurnBase} TurnBase
* @typedef {import('./types').Corner} Corner
*/
export class State {
#corners = new Corners()
#edges = new Edges()
#centers = new Centers()
/** @type {boolean} */
#trackCenters
stateInfo = new StateInfo(this)
/** @param {boolean} trackCenters */
constructor(trackCenters) {
this.#trackCenters = trackCenters
}
/** @param {Turn} turn */
applyTurn(turn) {
const baseTurns = convertTurnToTurnBase(turn)
for (const base of baseTurns) {
this.#corners.applyTurn(base)
this.#edges.applyTurn(base)
if (this.#trackCenters) {
this.#centers.applyTurn(base)
}
}
}
/**
* @param {number[][][]} uvs
* @param {import('../ui/rubiks').Rubiks} rubiks
*/
applyState(uvs, rubiks) {
this.#corners.applyState(uvs, rubiks)
this.#edges.applyState(uvs, rubiks)
if (this.#trackCenters) {
this.#centers.applyState(uvs, rubiks)
}
}
/** @returns {string} */
encode() {
/** @type {number[]} */
const code = []
code.push(...this.#corners.encode())
code.push(...this.#edges.encode())
if (this.#trackCenters) {
code.push(...this.#centers.encode())
}
const array = new Uint8Array(code)
return btoa(String.fromCharCode(...array))
}
/**
* @param {string} str
* @returns {boolean}
*/
decode(str) {
const data = atob(str)
if (data.length !== (this.#trackCenters ? 15 : 13)) {
return false
}
/** @type {number[]} */
const code = []
for (let i = 0; i < data.length; i++) {
code.push(data.charCodeAt(i))
}
const corners = this.#corners
/** @type {typeof corners['permutation']} */
const cp = [...corners.permutation]
/** @type {typeof corners['orientation']} */
const co = [...corners.orientation]
if (!corners.decode(code.slice(0, 5))) {
corners.permutation = cp
corners.orientation = co
return false
}
const edges = this.#edges
/** @type {typeof edges['permutation']} */
const ep = [...edges.permutation]
/** @type {typeof edges['orientation']} */
const eo = [...edges.orientation]
if (!edges.decode(code.slice(5, 13))) {
corners.permutation = cp
corners.orientation = co
edges.permutation = ep
edges.orientation = eo
return false
}
if (this.#trackCenters) {
this.#centers.decode(code.slice(13))
}
return true
}
reset() {
this.#corners.reset()
this.#edges.reset()
this.#centers.reset()
}
}
export class StateInfo {
/** @type {State} */
#state
/** @param {State} state */
constructor(state) {
this.#state = state
}
/**
* @returns {string} base64 encoded, can be used in the setState method
*/
toString() {
return this.#state.encode()
}
}
/**
* `uvsTransformers[axis][side][angle]`
* @type {Record<number, Record<number, Record<number, (uvs: number[]) => number[]>>>}
*/
const uvsTransformers = {
0: {
0: uvsTransformerPresets.rcR2Rcc,
1: uvsTransformerPresets.rcR2Rcc,
2: uvsTransformerPresets.flipV12,
3: uvsTransformerPresets.flipV12,
4: uvsTransformerPresets.flipV23,
5: uvsTransformerPresets.flipV23
},
1: {
0: uvsTransformerPresets.rcFvRcfh,
1: uvsTransformerPresets.rcFvRcfh,
2: uvsTransformerPresets.rcR2Rcc,
3: uvsTransformerPresets.rcR2Rcc,
4: uvsTransformerPresets.rcfhFhRcc,
5: uvsTransformerPresets.rcfhFhRcc
},
2: {
0: uvsTransformerPresets.flipH12,
1: uvsTransformerPresets.flipH12,
2: uvsTransformerPresets.flipH23,
3: uvsTransformerPresets.flipH23,
4: uvsTransformerPresets.rcR2Rcc,
5: uvsTransformerPresets.rcR2Rcc
}
}
/**
* @param {number} originIndex
* @param {number[][][]} uvs
* @returns {Record<number, number[]>}
*/
export const createSideToUvs = (originIndex, uvs) => {
const pos = indexToPosition(originIndex)
/** @type {Record<number, number[]>} */
const sideToUvs = {}
// populate sideToUvs
for (let side = 0; side < 6; side++) {
const inside = isInside(side, originIndex)
if (inside) {
continue
}
sideToUvs[side] = positionToUvs(pos, side, uvs)
}
return sideToUvs
}
/**
* @param {Turn[]} turns
* @param {number} originIndex
* @param {Record<number, number[]>} sideToUvs
* @returns {Record<number, number[]>}
*/
export const transformSidetoUvs = (originIndex, sideToUvs, turns) => {
for (const turn of turns) {
let {axis, index, angle} = convertTurnToAia(turn)
const innerSide = axis * 2 + Math.sign(index)
/** @type {Record<number, number[]>} */
const newSideToUvs = {}
for (const [sideStr, uvs] of Object.entries(sideToUvs)) {
const side = parseInt(sideStr)
const transformer = uvsTransformers[axis][side][angle]
if (side === innerSide) {
newSideToUvs[innerSide] = transformer(uvs)
continue
}
const sideIndex = sidesShiftMapper[axis].indexOf(side)
const newSide = sidesShiftMapper[axis][mod(sideIndex - angle, 4)]
newSideToUvs[newSide] = transformer(uvs)
}
const mapperIndex = cubiesShiftMapper[axis][index].indexOf(originIndex)
originIndex = cubiesShiftMapper[axis][index][mod(mapperIndex + angle * 2, 8)]
sideToUvs = newSideToUvs
}
return sideToUvs
}
/**
* @param {number} targetIndex
* @param {import('../ui/rubiks').Rubiks} rubiks
* @param {Record<number, number[]>} sideToUvs
*/
export const setUvs = (targetIndex, rubiks, sideToUvs) => {
const cubie = rubiks.cubies[targetIndex]
for (const facelet of cubie.facelets) {
facelet.uvs = sideToUvs[facelet.side]
}
}

19
node_modules/rubiks-js/src/state/types.d.ts generated vendored Normal file
View File

@@ -0,0 +1,19 @@
export type TurnBase = UD | RL | FB
export type Turn = TurnBase | `${TurnBase}2` | `${TurnBase}'`
export type UD = 'U' | 'D'
export type RL = 'R' | 'L'
export type FB = 'F' | 'B'
export type Corner = `${UD}${RL}${FB}`
export type CornerOrientation = 0 | 1 | 2
export type Edge = `${UD}${FB | RL}` | `${FB}${RL}`
export type EdgeOrientation = 0 | 1
export type Center = UD | RL | FB
export type CenterOrientation = 0 | 1 | 2 | 3
export type Events = {
change: import('.').State
}

59
node_modules/rubiks-js/src/types.d.ts generated vendored Normal file
View File

@@ -0,0 +1,59 @@
import type {Facelet} from './ui/facelet'
import type {Cubie} from './ui/cubie'
import type {V2, V3} from './math/vector'
import type {StateInfo} from './state'
import type {Turn} from './state/types'
export type AxisInfo = {
default: V3
inverted: V3
axis: number
index: number
}
export type SideInfo = {
dir: V2
axis: number
rotationAxis: V3
index: number
angle: number
cubies: Cubie[]
}
export type Action = {
type: 'none'
} | {
type: 'hovering',
facelet: Facelet
} | {
type: 'rotatingCube',
mouse: V2
} | {
type: 'rotatingSide',
mouse: V2,
right: SideInfo,
down: SideInfo,
side: 'right' | 'down' | null,
facelet: Facelet
} | {
type: 'gesture',
center: V2,
distance: number
}
export interface Uniform {
setUniform(gl: WebGL2RenderingContext, location: WebGLUniformLocation): void
}
export type AIA = {
axis: number,
index: number,
angle: number
}
export type Events = {
change: import('./events').ChangeEvent
}

194
node_modules/rubiks-js/src/ui/camera.js generated vendored Normal file
View File

@@ -0,0 +1,194 @@
import {V2, V3, V4} from '../math/vector'
import {M44} from '../math/matrix'
export class Camera {
/** @type {V3} */
#position
/** @type {V3} */
#lookAt
/** @type {V3} */
#forward
/** @type {V3} */
#right
/** @type {V3} */
#up
/** @type {number} */
#fov
/** @type {number} */
#aspect
/** @type {number} */
#width
/** @type {number} */
#height
/** @type {number} */
#near
/** @type {number} */
#far
/** @type {M44} */
#projectionMatrix
/** @type {M44} */
#worldToCameraMatrix
/** @type {M44} */
#projectionMatrixInverse
/** @type {M44} */
#cameraToWorldMatrix
/** @type {M44} */
#worldProjectionMatrix
/**
* @param {V3} position
* @param {V3} lookAt
* @param {V3} up
* @param {number} fov
* @param {number} width
* @param {number} height
* @param {number} near
* @param {number} far
*/
constructor(position, lookAt, up, fov, width, height, near, far) {
this.#position = position
this.#lookAt = lookAt
this.#up = up
this.#fov = fov
this.#width = width
this.#height = height
this.#aspect = width / height
this.#near = near
this.#far = far
this.calcCameraDirections()
this.calcWorldToCameraMatrix()
this.calcProjectionMatrix()
}
/** @param {V3} value */
set position(value) {
this.#position = value
this.calcCameraDirections()
this.calcWorldToCameraMatrix()
}
/** @param {V3} value */
set lookAt(value) {
this.#lookAt = value
this.calcCameraDirections()
this.calcWorldToCameraMatrix()
}
/** @param {V3} value */
set up(value) {
this.#up = value
this.calcCameraDirections()
this.calcWorldToCameraMatrix()
}
get position() {
return this.#position
}
get lookAt() {
return this.#lookAt
}
get up() {
return this.#up
}
get forward() {
return this.#forward
}
get right() {
return this.#right
}
/** @param {number} value */
set fov(value) {
this.#fov = value
this.calcProjectionMatrix()
}
/** @param {number} value */
set near(value) {
this.#near = value
this.calcProjectionMatrix()
}
/** @param {number} value */
set far(value) {
this.#far = value
this.calcProjectionMatrix()
}
get fov() {
return this.#fov
}
get aspect() {
return this.#aspect
}
get near() {
return this.#near
}
get far() {
return this.#far
}
get width() {
return this.#width
}
get height() {
return this.#height
}
/**
* @param {number} width
* @param {number} height
*/
screenSize(width, height) {
this.#width = width
this.#height = height
this.#aspect = width / height
this.calcProjectionMatrix()
}
get projectionMatrix() {
return this.#projectionMatrix
}
get projectionMatrixInverse() {
return this.#projectionMatrixInverse
}
get worldToCameraMatrix() {
return this.#worldToCameraMatrix
}
get cameraToWorldMatrix() {
return this.#cameraToWorldMatrix
}
get worldProjectionMatrix() {
return this.#worldProjectionMatrix
}
/** @param {V3} v */
worldToScreen({x, y, z}) {
const point4d = this.worldProjectionMatrix.mult(new V4(x, y, z, 1))
const screenPoint = point4d.toV3().scale(1 / point4d.w).toV2().add(new V2(1, 1)).scale(.5).mult(new V2(this.width, this.height))
return screenPoint
}
// worldDirectionToScreen({x, y, z}: V3) {
// // const rotationTransform = this.cameraToWorldMatrix.transpose
// const cameraPoint = this.worldToCameraMatrix.mult(new V4(x, y, z, 1))
// const projectedPoint = this.projectionMatrix.mult(cameraPoint)
// const viewportPoint = projectedPoint.toV3().scale(1 / projectedPoint.w).toV2()
// const screenPoint = viewportPoint.add(new V2(1, 1)).scale(.5).mult(new V2(this.width, this.height))
// // console.log(this.projectionMatrix)
// console.table({cameraPoint, projectedPoint, viewportPoint, screenPoint})
// return screenPoint
// }
calcProjectionMatrix() {
this.#projectionMatrix = M44.perspective(this.#fov * Math.PI / 180, this.#aspect, this.#near, this.#far)
this.#projectionMatrixInverse = this.#projectionMatrix.inverse
this.#worldProjectionMatrix = this.#projectionMatrix?.mult(this.#worldToCameraMatrix)
}
calcWorldToCameraMatrix() {
this.#worldToCameraMatrix = M44.lookAt(this.#position, this.#lookAt, this.#up)
this.#cameraToWorldMatrix = this.#worldToCameraMatrix.inverse
this.#worldProjectionMatrix = this.#projectionMatrix?.mult(this.#worldToCameraMatrix)
}
calcCameraDirections() {
this.#forward = this.#lookAt.sub(this.#position).normalized
this.#right = this.#up.cross(this.#forward).normalized
}
}

125
node_modules/rubiks-js/src/ui/cubie.js generated vendored Normal file
View File

@@ -0,0 +1,125 @@
import {V3} from '../math/vector'
import {Quaternion} from '../math/quarternion'
import {Facelet, FaceletTransform, InsideFacelet} from './facelet'
import {Transform} from './transform'
const positionForSide = [
V3.right,
V3.left,
V3.down,
V3.up,
V3.back,
V3.forward
].map(v => v.scale(0.5))
const rotationForSide = [
Quaternion.identity,
Quaternion.identity,
Quaternion.fromAngle(V3.back, 90),
Quaternion.fromAngle(V3.back, 90),
Quaternion.fromAngle(V3.back, 90).mult(Quaternion.fromAngle(V3.down, 90)),
Quaternion.fromAngle(V3.back, 90).mult(Quaternion.fromAngle(V3.down, 90))
]
/**
* @param {number} side
* @param {number} index
* @returns {boolean}
*/
export const isInside = (side, index) => {
const axis = Math.floor(side / 2)
const invert = side % 2
const coordinate = Math.floor(index / Math.pow(3, axis)) % 3
return coordinate === 1
|| coordinate === 0 && invert === 1
|| coordinate === 2 && invert === 0
}
/**
* @param {number} index
* @returns {[x: number, y: number, z: number]}
*/
export const indexToPosition = index => {
const x = Math.floor(index / 1) % 3
const y = Math.floor(index / 3) % 3
const z = Math.floor(index / 9) % 3
return [x, y, z]
}
/**
* @param {[x: number, y: number, z: number]} pos
* @param {number} side
* @param {number[][][]} uvs
* @returns {number[]}
*/
export const positionToUvs = (pos, side, uvs) => {
const axis = Math.floor(side / 2)
/** @type {number[]} */
const uvCoords = []
for (let i = 0; i < 3; i++) {
if (i !== axis) {
uvCoords.push(pos[i])
}
}
const sideIndex = uvCoords[0] + uvCoords[1] * 3
return uvs[side][sideIndex]
}
export class Cubie {
/** @type {Facelet[]} */
facelets = []
/** @type {Transform<Facelet | InsideFacelet, import('./rubiks').Rubiks>} */
transform
/**
* @param {number} index
* @param {number[][][]} uvs
* @param {V3[]} hoveringColors
* @param {import('./rubiks').Rubiks} parent
*/
constructor(index, uvs, hoveringColors, parent) {
this.index = index
const pos = indexToPosition(index)
const position = new V3(pos[0], pos[1], pos[2]).sub(V3.one)
this.transform = new Transform(position, Quaternion.identity, parent)
for (let side = 0; side < 6; side++) {
const inside = isInside(side, this.index)
const position = positionForSide[side]
const rotation = rotationForSide[side]
if (inside) {
const transform = new Transform(position, rotation, this)
const facelet = new InsideFacelet(transform)
this.transform.children.push(facelet)
continue
}
const uv = positionToUvs(pos, side, uvs)
const transform = new FaceletTransform(position, rotation, this)
const facelet = new Facelet(transform, side, uv, hoveringColors[side])
this.transform.children.push(facelet)
this.facelets.push(facelet)
}
}
/**
* @param {number} side
* @returns {Facelet}
*/
getFaceletOfSide(side) {
return /** @type {Facelet} */ (this.facelets.find(facelet => facelet.side === side))
}
/**
* @param {import('./program').Program} program
* @param {WebGL2RenderingContext} gl
* @param {WebGLBuffer} uvsVbo
*/
render(program, gl, uvsVbo) {
this.transform.children.forEach(child => child.render.call(child, program, gl, uvsVbo))
}
}

105
node_modules/rubiks-js/src/ui/debugger.js generated vendored Normal file
View File

@@ -0,0 +1,105 @@
import {V2} from '../math/vector'
class Debugger {
/** @type {CanvasRenderingContext2D?} */
#ctx
/** @type {HTMLCanvasElement?} */
#canvas
#strokeColor = 'white'
#fillColor = 'white'
/**
* @param {Element?} canvas
*/
constructor(canvas) {
if (!canvas) {
return
}
if (!(canvas instanceof HTMLCanvasElement)) {
throw new Error(`rubiks cube debugger is not a canvas, it is a <${canvas.tagName}>`)
}
this.#canvas = canvas
const ctx = canvas.getContext('2d')
if (!ctx)
throw new Error('cannot create 2d context for rubiks cube debugger')
this.#ctx = ctx
}
clear() {
if (this.#ctx && this.#canvas) {
this.#ctx.clearRect(0, 0, this.#canvas.width, this.#canvas.height)
}
}
/** @param {string} color */
stroke(color) {
this.#strokeColor = color
}
/** @param {string} color */
fill(color) {
this.#fillColor = color
}
/**
* @param {V2} from
* @param {V2} to
*/
line(from, to) {
if (this.#ctx && this.#canvas) {
this.#ctx.strokeStyle = this.#strokeColor
this.#ctx.lineWidth = 3
const fromF = Debugger.#flipVector(from, this.#canvas)
const toF = Debugger.#flipVector(to, this.#canvas)
this.#ctx.beginPath()
this.#ctx.moveTo(fromF.x, fromF.y)
this.#ctx.lineTo(toF.x, toF.y)
this.#ctx.stroke()
}
}
/**
* @param {V2} origin
* @param {V2} direction
* @param {number} [length=1]
*/
vector(origin, direction, length = 1) {
this.line(origin, origin.add(direction.scale(length)))
}
/**
* @param {number} width
* @param {number} height
*/
setSize(width, height) {
if (this.#canvas) {
this.#canvas.width = width
this.#canvas.height = height
}
}
/**
* @param {V2} v
* @param {HTMLCanvasElement} canvas
*/
static #flipVector({x, y}, canvas) {
return new V2(x, canvas.height - y)
}
/**
* @param {V2} pos
* @param {string} text
*/
text(pos, text) {
if (this.#ctx) {
this.#ctx.fillStyle = this.#fillColor
this.#ctx.fillText(text, pos.x, pos.y)
}
}
}
export default new Debugger(document.querySelector('[data-rubiks-cube-debug]'))

98
node_modules/rubiks-js/src/ui/facelet.js generated vendored Normal file
View File

@@ -0,0 +1,98 @@
import {V3, V4} from '../math/vector'
import {Quaternion} from '../math/quarternion'
import {Program} from './program'
import {Transform} from './transform'
import {Cubie} from './cubie'
/**
* @extends {Transform<any, Cubie>}
*/
export class FaceletTransform extends Transform {
/** @type {V3} */
left
/** @type {V3} */
top
/** @type {V3} */
normal
/** @type {V3} */
topLeft
/** @type {V3} */
bottomRight
/**
* @param {V3} position
* @param {Quaternion} rotation
* @param {Cubie} parent
*/
constructor(position, rotation, parent) {
super(position, rotation, parent)
this.setTransforms()
}
/** @protected */
setTransforms() {
super.setTransforms()
const rotationTransform = this.globalTransform.inverse.transpose
this.left = rotationTransform.mult(new V4(0, 0, -1, 1)).toV3().normalized
this.top = rotationTransform.mult(new V4(0, -1, 0, 1)).toV3().normalized
this.normal = rotationTransform.mult(new V4(-1, 0, 0, 1)).toV3().normalized
this.topLeft = this.globalTransform.mult(new V4(0, .5, .5, 1)).toV3()
this.bottomRight = this.topLeft.add(this.left).add(this.top)
}
}
export class Facelet {
hovering = false
/** @type {V3} */
#hoveringMult
/**
* @param {FaceletTransform} transform
* @param {number} side
* @param {number[]} uvs
* @param {V3} hoveringMult
*/
constructor(transform, side, uvs, hoveringMult) {
this.transform = transform
this.side = side
this.uvs = uvs
this.#hoveringMult = hoveringMult
}
/**
* @param {Program} program
* @param {WebGL2RenderingContext} gl
* @param {WebGLBuffer} uvsVbo
*/
render(program, gl, uvsVbo) {
gl.bindBuffer(gl.ARRAY_BUFFER, uvsVbo)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.uvs), gl.STATIC_DRAW)
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 8, 0)
gl.bindBuffer(gl.ARRAY_BUFFER, null)
program.uniform('model', this.transform.globalTransform)
program.uniform('colorMult', this.hovering ? this.#hoveringMult : V3.one)
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0)
}
}
export class InsideFacelet {
/** @param {Transform<any, Cubie>} transform */
constructor(transform) {
this.transform = transform
}
/**
* @param {Program} program
* @param {WebGL2RenderingContext} gl
*/
render(program, gl) {
program.uniform('model', this.transform.globalTransform)
program.uniform('colorMult', V3.zero)
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0)
}
}

393
node_modules/rubiks-js/src/ui/inputHandler.js generated vendored Normal file
View File

@@ -0,0 +1,393 @@
import {Ray} from './ray'
// import debug from './debugger'
import {V2, V3} from '../math/vector'
import {clamp} from '../math/utils'
export class InputHandler {
/** @type {HTMLCanvasElement} */
#canvas
/** @type {import('./rubiks').Rubiks} */
#rubiks
/** @type {import('./camera').Camera} */
#camera
/** @readonly */
#maxZoom = 40
/** @readonly */
#minZoom = 10
/**
* @param {HTMLCanvasElement} canvas
* @param {import('./rubiks').Rubiks} rubiks
* @param {import('./camera').Camera} camera
*/
constructor(canvas, rubiks, camera) {
this.#canvas = canvas
this.#rubiks = rubiks
this.#camera = camera
}
addEventListeners() {
this.#canvas.addEventListener('pointermove', this.#pointerMove)
this.#canvas.addEventListener('pointerdown', this.#pointerDown)
this.#canvas.addEventListener('pointerup', this.#pointerUp)
this.#canvas.addEventListener('pointerleave', this.#pointerLeave)
this.#canvas.addEventListener('wheel', this.#wheel)
}
removeEventListeners() {
this.#canvas.removeEventListener('pointermove', this.#pointerMove)
this.#canvas.removeEventListener('pointerdown', this.#pointerDown)
this.#canvas.removeEventListener('pointerup', this.#pointerUp)
this.#canvas.removeEventListener('pointerleave', this.#pointerLeave)
this.#canvas.removeEventListener('wheel', this.#wheel)
}
/** @type {Map<number, PointerEvent>} */
#pointers = new Map()
/** @type {import('../types').Action} */
#action = {type: 'none'}
/** @param {import('../types').Action} action */
#setAction(action) {
if (this.#action.type === 'hovering') {
this.#action.facelet.hovering = false
}
if (action.type === 'hovering') {
action.facelet.hovering = true
}
if (this.#action.type === 'rotatingSide' && this.#action.side) {
const info = this.#action[this.#action.side]
const angle = Math.round(info.angle / 90)
this.#rubiks.finishRotation(info.axis, info.index, angle, this.#action.facelet.side)
}
this.#action = action
}
// event handlers
#pointerDown = InputHandler.#getPointerDown(this)
/** @param {InputHandler} inputHandler */
static #getPointerDown(inputHandler) {
return (/** @type {PointerEvent}*/ event) => {
const {offsetX, offsetY, pointerId} = event
if (event.button === 0) {
inputHandler.#pointers.set(pointerId, event)
} else if (event.button === 1) {
inputHandler.#pointers.set(pointerId, event)
inputHandler.#setAction({
type: 'rotatingCube',
mouse: new V2(offsetX, offsetY)
})
return
}
if (inputHandler.#pointers.size === 1) {
if (inputHandler.#rubiks.isTurning) {
return
}
if (inputHandler.#action.type === 'none') {
inputHandler.#actionHovering(offsetX, offsetY)
}
if (inputHandler.#action.type === 'hovering') {
inputHandler.#startActionRotatingSide(offsetX, offsetY, inputHandler.#action.facelet)
return
}
inputHandler.#startActionRotatingCube(offsetX, offsetY)
} else if (inputHandler.#pointers.size === 2) {
inputHandler.#startActionGesture()
}
}
}
#pointerMove = InputHandler.#getPointerMove(this)
/** @param {InputHandler} inputHandler */
static #getPointerMove(inputHandler) {
return (/** @type {PointerEvent} */ event) => {
const {offsetX, offsetY} = event
if (inputHandler.#pointers.size === 0) {
inputHandler.#actionHovering(offsetX, offsetY)
return
}
inputHandler.#pointers.set(event.pointerId, event)
inputHandler.#actionRotatingSide(offsetX, offsetY)
inputHandler.#actionRotatingCube(offsetX, offsetY)
inputHandler.#actionGesture()
}
}
#pointerUp = InputHandler.#getPointerUp(this)
/** @param {InputHandler} inputHandler */
static #getPointerUp(inputHandler) {
return () => {
inputHandler.#pointers.clear()
inputHandler.#setAction({
type: 'none'
})
}
}
#pointerLeave = InputHandler.#getPointerLeave(this)
/** @param {InputHandler} inputHandler */
static #getPointerLeave(inputHandler) {
return () => {
inputHandler.#pointers.clear()
inputHandler.#setAction({
type: 'none'
})
}
}
#wheel = InputHandler.#getWheel(this)
/** @param {InputHandler} inputHandler */
static #getWheel(inputHandler) {
return (/** @type {WheelEvent} */ {deltaY, deltaMode}) => {
if (deltaMode !== WheelEvent.DOM_DELTA_PIXEL) {
return
}
const d = clamp(deltaY / 102, -1, 1) * .1
inputHandler.#zoomCube(d)
}
}
// actions
// rotating side
/**
* @param {number} offsetX
* @param {number} offsetY
* @param {import('./facelet').Facelet} hovering
*/
#startActionRotatingSide(offsetX, offsetY, hovering) {
const {left, top, topLeft} = hovering.transform
const screenTopLeft = this.#camera.worldToScreen(topLeft)
const screenBottomLeft = this.#camera.worldToScreen(topLeft.add(left))
const screenTopRight = this.#camera.worldToScreen(topLeft.add(top))
const rightDir = screenTopRight.sub(screenTopLeft).normalized
const downDir = screenBottomLeft.sub(screenTopLeft).normalized
const rubiksRotation = this.#rubiks.transform
const side = hovering.side
const currentAxis = Math.floor(side / 2)
const cubie = hovering.transform.parent
const [axis1, axis2] = [0, 1, 2]
.filter(axis => axis !== currentAxis)
.map(axis => {
const rotationAxis = V3.getRotationAxis(axis)
const rubiksRotationAxis = rubiksRotation.apply(rotationAxis)
const index = Math.floor(cubie.index / Math.pow(3, axis)) % 3
/** @type {import('../types').AxisInfo} */
const info = {
default: rubiksRotationAxis,
inverted: rubiksRotationAxis.negate,
axis,
index
}
return info
})
const sideInvertMap = [
false, true,
true, false,
false, true
]
const invert = sideInvertMap[hovering.side]
const mouse = new V2(offsetX, this.#canvas.height - offsetY)
if (Math.abs(axis1.default.dot(top)) > .99) {
this.#setAction({
type: 'rotatingSide',
mouse,
down: this.#getTurnDirection(axis1, top, downDir, true !== invert),
right: this.#getTurnDirection(axis2, left, rightDir, false !== invert),
side: null,
facelet: hovering
})
} else {
this.#setAction({
type: 'rotatingSide',
mouse,
down: this.#getTurnDirection(axis2, top, downDir, true !== invert),
right: this.#getTurnDirection(axis1, left, rightDir, false !== invert),
side: null,
facelet: hovering
})
}
}
/**
* @param {number} offsetX
* @param {number} offsetY
*/
#actionRotatingSide(offsetX, offsetY) {
if (this.#action.type !== 'rotatingSide') {
return
}
const mouse = new V2(offsetX, this.#canvas.height - offsetY)
if (this.#action.mouse.sub(mouse).mag < 10) {
return
}
const initialMouse = this.#action.mouse
if (this.#action.side) {
this.#rotateSide(mouse, this.#action[this.#action.side], initialMouse)
return
}
const {right, down} = this.#action
const mouseDir = mouse.sub(initialMouse).normalized
const rightDot = right.dir.dot(mouseDir)
const downDot = down.dir.dot(mouseDir)
if (Math.abs(rightDot) > Math.abs(downDot)) {
this.#action.side = 'right'
this.#rubiks.startRotation(right.cubies, right.rotationAxis)
this.#rotateSide(mouse, right, initialMouse)
} else {
this.#action.side = 'down'
this.#rubiks.startRotation(down.cubies, down.rotationAxis)
this.#rotateSide(mouse, down, initialMouse)
}
}
// rotating cube
/**
* @param {number} offsetX
* @param {number} offsetY
*/
#startActionRotatingCube(offsetX, offsetY) {
this.#setAction({
type: 'rotatingCube',
mouse: new V2(offsetX, offsetY)
})
}
/**
* @param {number} offsetX
* @param {number} offsetY
*/
#actionRotatingCube(offsetX, offsetY) {
if (this.#action.type !== 'rotatingCube') {
return
}
const mouse = new V2(offsetX, offsetY)
const delta = mouse.sub(this.#action.mouse)
this.#action.mouse = mouse
this.#rotateCube(delta)
}
// two pointer gesture
#startActionGesture() {
const [e1, e2] = this.#pointers.values()
const dx = e1.screenX - e2.screenX
const dy = e1.screenY - e2.screenY
this.#setAction({
type: 'gesture',
distance: dx * dx + dy * dy,
center: new V2(e1.screenX + e2.screenX, e1.screenY + e2.screenY).scale(.5)
})
}
#actionGesture() {
if (this.#action.type !== 'gesture') {
return
}
const [e1, e2] = this.#pointers.values()
const dx = e1.screenX - e2.screenX
const dy = e1.screenY - e2.screenY
const distance = dx * dx + dy * dy
const deltaDistance = distance - this.#action.distance
this.#action.distance = distance
const d = clamp(deltaDistance / 1000, -1, 1) * -.02
this.#zoomCube(d)
const center = new V2(e1.screenX + e2.screenX, e1.screenY + e2.screenY).scale(.5)
const deltaCenter = center.sub(this.#action.center)
this.#action.center = center
this.#rotateCube(deltaCenter)
}
// hovering
/**
* @param {number} offsetX
* @param {number} offsetY
*/
#actionHovering(offsetX, offsetY) {
const ray = new Ray(this.#camera, offsetX, offsetY, window.innerWidth, window.innerHeight)
const facelets = ray.intersectRubiks(this.#rubiks)
if (facelets.length) {
facelets.sort((a, b) => a.d - b.d)
this.#setAction({
type: 'hovering',
facelet: facelets[0].facelet
})
} else {
this.#setAction({
type: 'none'
})
}
}
// action helpers
// rotating side
/**
* @param {import('../types').AxisInfo} axisInfo
* @param {V3} vector
* @param {V2} dir
* @param {boolean} invert
* @returns {import('../types').SideInfo}
*/
#getTurnDirection(axisInfo, vector, dir, invert) {
if (axisInfo.default.dot(vector) > .99)
return {
dir,
angle: 0,
axis: axisInfo.axis,
rotationAxis: V3.getRotationAxis(axisInfo.axis).scale(invert ? -1 : 1),
index: axisInfo.index,
cubies: this.#rubiks.getPlane(axisInfo.axis, axisInfo.index)
}
return {
dir,
angle: 0,
axis: axisInfo.axis,
rotationAxis: V3.getRotationAxis(axisInfo.axis).scale(invert ? 1 : -1),
index: axisInfo.index,
cubies: this.#rubiks.getPlane(axisInfo.axis, axisInfo.index)
}
}
/**
* @param {V2} mouse
* @param {import('../types').SideInfo} info
* @param {V2} initialMouse
*/
#rotateSide(mouse, info, initialMouse) {
const length = info.dir.dot(initialMouse.sub(mouse))
const zoom = this.#getZoom()
info.angle = length / (3 - zoom * 2)
this.#rubiks.rotateManual(info.angle)
}
// rotating cube
/** @param {V2} delta */
#rotateCube(delta) {
if (delta.x === 0 && delta.y === 0) {
return
}
const n = this.#camera.up.scale(delta.y).add(this.#camera.right.scale(delta.x))
const axis = this.#camera.forward.cross(n)
const zoom = this.#getZoom()
const angle = Math.sqrt(delta.x * delta.x + delta.y * delta.y) * .3 + 2 * zoom
this.#rubiks.transform.rotate(axis, angle)
}
// zooming camera
/** @param {number} d */
#zoomCube(d) {
const {position} = this.#camera
this.#camera.position = new V3(0, 0, clamp(position.z * (1 + d), -this.#maxZoom, -this.#minZoom))
}
#getZoom() {
const zoom = -this.#camera.position.z
const minZoom = this.#minZoom
const maxZoom = this.#maxZoom
return (zoom - minZoom) / (maxZoom - minZoom)
}
}

96
node_modules/rubiks-js/src/ui/program.js generated vendored Normal file
View File

@@ -0,0 +1,96 @@
export class Program {
/** @type {WebGLProgram} */
#program
/** @type {Map<string, WebGLUniformLocation>} */
#uniformMap
/** @type {string} */
#path
/** @type {WebGL2RenderingContext} */
#gl
/**
* @param {string} path
* @param {string} vertexShaderSource
* @param {string} fragmentShaderSource
* @param {WebGL2RenderingContext} gl
*/
constructor(path, vertexShaderSource, fragmentShaderSource, gl) {
this.#path = path
this.#gl = gl
const vertexShader = this.#createShader(vertexShaderSource, this.#gl.VERTEX_SHADER, 'vertex')
const fragmentShader = this.#createShader(fragmentShaderSource, this.#gl.FRAGMENT_SHADER, 'fragment')
const p = this.#gl.createProgram()
if (!p)
throw new Error('Fatal: webgl could not create program object!')
this.#program = p
this.#gl.attachShader(this.#program, vertexShader)
this.#gl.attachShader(this.#program, fragmentShader)
this.#gl.linkProgram(this.#program)
const success = this.#gl.getProgramParameter(this.#program, this.#gl.LINK_STATUS)
if (!success) {
const info = this.#gl.getProgramInfoLog(this.#program)
this.#gl.deleteProgram(this.#program)
throw new Error(`Link Program: ${info}`)
}
const numUniforms = /** @type {number} */ (this.#gl.getProgramParameter(this.#program, this.#gl.ACTIVE_UNIFORMS))
const uniformIndices = [...Array(numUniforms).keys()]
const uniformNames = uniformIndices.map(index => {
const info = this.#gl.getActiveUniform(this.#program, index)
if (info == null) {
throw new Error('failed to get active uniform')
}
const location = this.#gl.getUniformLocation(this.#program, info.name)
if (location == null) {
throw new Error('failed to get uniform location')
}
return /** @type {[string, WebGLUniformLocation]} */ ([info.name, location])
})
this.#uniformMap = new Map(uniformNames)
}
/**
* @param {string} source
* @param {number} type
* @param {string} typeStr
* @returns {WebGLShader}
*/
#createShader(source, type, typeStr) {
const shader = this.#gl.createShader(type)
if (!shader)
throw new Error('Fatal: webgl could not create shader object!')
this.#gl.shaderSource(shader, source)
this.#gl.compileShader(shader)
const success = /** @type {boolean} */ (this.#gl.getShaderParameter(shader, this.#gl.COMPILE_STATUS))
if (success) {
return shader
}
const info = this.#gl.getShaderInfoLog(shader)
this.#gl.deleteShader(shader)
throw new Error(`Compile '${this.#path}': ${typeStr}: ${info}`)
}
use() {
if (!this.#program) {
throw new Error('Fatal: program does not exists!')
}
this.#gl.useProgram(this.#program)
}
/**
* @param {string} name
* @param {import('../types').Uniform} u
*/
uniform(name, u) {
const location = this.#uniformMap.get(name)
if (location == undefined) {
throw new Error(`Fatal: unkown name: ${name}`)
}
u.setUniform(this.#gl, location)
}
}

87
node_modules/rubiks-js/src/ui/ray.js generated vendored Normal file
View File

@@ -0,0 +1,87 @@
import {V3, V4} from '../math/vector'
export class Ray {
/** @type {V3} */
#origin
/** @type {V3} */
#direction
/**
* @param {import('./camera').Camera} camera
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
*/
constructor(camera, x, y, width, height) {
const u = (x + .5) / width * 2 - 1
const v = (height - y + .5) / height * 2 - 1
this.#origin = camera.cameraToWorldMatrix.mult(new V4(0, 0, 0, 1)).toV3()
const d1 = camera.projectionMatrixInverse.mult(new V4(u, v, 0, 1))
const d2 = camera.cameraToWorldMatrix.mult(new V4(d1.x, d1.y, d1.z, 0))
this.#direction = d2.toV3().normalized
}
/** @param {import('./rubiks').Rubiks} rubiks */
intersectRubiks(rubiks) {
return rubiks.cubies.map(cubie => this.#intersectCube(cubie)).flat(1)
}
/** @param {import('./cubie').Cubie} cubie */
#intersectCube(cubie) {
return cubie.facelets.reduce((
/** @type {{facelet: import('./facelet').Facelet, d: number}[]} */ acc,
/** @type {import('./facelet').Facelet} */ facelet
) => {
const hit = this.intersectFacelet(facelet)
if (hit.inside) {
acc.push({facelet: hit.facelet, d: hit.d})
}
return acc
}, [])
}
/**
* @param {import('./facelet').Facelet} facelet
* @returns {{
* inside: false
* } | {
* inside: true,
* facelet: import('./facelet').Facelet,
* d: number
* }}
*/
intersectFacelet(facelet) {
const {normal, top, left, topLeft, bottomRight} = facelet.transform
const denom = this.#direction.dot(normal)
if (denom === 0) {
return {inside: false}
}
const d = topLeft.sub(this.#origin).dot(normal) / denom
const intersection = this.#origin.add(this.#direction.scale(d))
const fromTopLeft = intersection.sub(topLeft).normalized
const fromBottomRight = intersection.sub(bottomRight).normalized
const dot1 = fromTopLeft.dot(left)
const dot2 = fromTopLeft.dot(top)
const dot3 = fromBottomRight.dot(left.negate)
const dot4 = fromBottomRight.dot(top.negate)
const inside = dot1 <= 1 && dot1 >= 0
&& dot2 <= 1 && dot2 >= 0
&& dot3 <= 1 && dot3 >= 0
&& dot4 <= 1 && dot4 >= 0
if (!inside) {
return {inside: false}
}
return {
inside,
facelet,
d
}
}
}

386
node_modules/rubiks-js/src/ui/rubiks.js generated vendored Normal file
View File

@@ -0,0 +1,386 @@
import {V3} from '../math/vector'
import {Quaternion} from '../math/quarternion'
import {lerp, mod} from '../math/utils'
import {Cubie} from './cubie'
import {Transform} from './transform'
import {uvsTransformerPresets, sidesShiftMapper} from './uvs'
// magic values
const rotationAxis = [
V3.getRotationAxis(0),
V3.getRotationAxis(1),
V3.getRotationAxis(2)
]
/**
* `cubeRotationOnMiddle[axis][side]`
* @type {Record<number, Record<number, number>>}
*/
const cubeRotationOnMiddle = {
0: {
2: 90, 3: -90,
4: -90, 5: 90
},
1: {
0: -90, 1: 90,
4: 90, 5: -90
},
2: {
0: 90, 1: -90,
2: -90, 3: 90
}
}
/**
* `uvsTransformers[axis][side][angle]`
* @type {Record<number, Record<number, Record<number, (uvs: number[]) => number[]>>>}
*/
const uvsTransformers = {
0: {
0: uvsTransformerPresets.rcR2Rcc,
1: uvsTransformerPresets.rcR2Rcc,
2: uvsTransformerPresets.flipV23,
3: uvsTransformerPresets.flipV23,
4: uvsTransformerPresets.flipV12,
5: uvsTransformerPresets.flipV12
},
1: {
0: uvsTransformerPresets.rcfhFvRcc,
1: uvsTransformerPresets.rcfhFvRcc,
2: uvsTransformerPresets.rcR2Rcc,
3: uvsTransformerPresets.rcR2Rcc,
4: uvsTransformerPresets.rcFhRcfh,
5: uvsTransformerPresets.rcFhRcfh
},
2: {
0: uvsTransformerPresets.flipH23,
1: uvsTransformerPresets.flipH23,
2: uvsTransformerPresets.flipH12,
3: uvsTransformerPresets.flipH12,
4: uvsTransformerPresets.rcR2Rcc,
5: uvsTransformerPresets.rcR2Rcc
}
}
/**
* @param {number} axis
* @param {number} side
* @returns {boolean}
*/
const shouldInvertAngle = (axis, side) => {
return (
side === 0 ||
side === 2 && axis === 0 ||
side === 3 && axis === 2 ||
side === 5
)
}
/**
* @extends {Transform<Cubie, null>}
*/
export class RubiksTransform extends Transform {
/** @type {[V3, V3, V3]} */
rotationAxis
/**
* @param {V3} position
* @param {Quaternion} rotation
*/
constructor(position, rotation) {
super(position, rotation, null)
this.setTransforms()
}
/** @property */
setTransforms() {
super.setTransforms()
this.rotationAxis = /** @type {[V3, V3, V3]} */ (rotationAxis
.map(axis => {
return this.apply(axis)
}))
}
}
/** @typedef {(event: import('../types').AIA) => void} TurnCallback */
export class Rubiks {
/** @type {RubiksTransform} */
transform
/** @type {Cubie[]} */
cubies
/** @type {TurnCallback} */
#turnCallback
/**
* @param {Quaternion} rotation
* @param {number[][][]} uvs
* @param {V3[]} hoveringColors
* @param {TurnCallback} turnCallback
*/
constructor(rotation, uvs, hoveringColors, turnCallback) {
this.#turnCallback = turnCallback
this.transform = new RubiksTransform(V3.zero, rotation)
this.cubies = []
for (let i = 0; i < 27; i++) {
const cubie = new Cubie(
i,
uvs,
hoveringColors,
this
)
this.cubies.push(cubie)
this.transform.children.push(cubie)
}
}
/**
* @param {import('./program').Program} program
* @param {WebGL2RenderingContext} gl
* @param {WebGLBuffer} uvsVbo
*/
render(program, gl, uvsVbo) {
this.cubies.forEach(cubie => cubie.render(program, gl, uvsVbo))
}
/** @type {[number, number][]} */
static #axisDeltasMap = [
[3, 9],
[1, 9],
[1, 3]
]
/**
* @param {number} axis
* @param {number} index
* @returns {Cubie[]}
*/
getPlane(axis, index) {
const [d1, d2] = Rubiks.#axisDeltasMap[axis]
const initial = Math.pow(3, axis) * index
/** @type {Cubie[]} */
const cubies = []
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
const index = initial + i * d1 + j * d2
const cubie = this.cubies[index]
cubies.push(cubie)
}
}
return cubies
}
/**
* @param {number} axis
* @param {number} index
* @param {number} angle
* @param {number} side
* @param {Cubie[]} plane
*/
#turn(axis, index, angle, side, plane) {
angle = mod(angle, 4)
if (angle === 0) {
return
}
if (index === 1) {
this.#turn(axis, 0, -angle, side, this.getPlane(axis, 0))
this.#turn(axis, 2, -angle, side, this.getPlane(axis, 2))
this.transform.rotate(this.transform.rotationAxis[axis], angle * cubeRotationOnMiddle[axis][side])
return
}
if (shouldInvertAngle(axis, side)) {
angle = mod(-angle, 4)
}
this.#swapOuterFacelets(plane, axis, angle)
this.#swapInnerFacelets(plane, axis, index, angle)
this.#turnCallback({axis, index, angle})
}
/**
* @param {Cubie[]} plane
* @param {number} axis
* @param {number} angle
*/
#swapOuterFacelets(plane, axis, angle) {
const [s0, s1, s2, s3] = sidesShiftMapper[axis]
const facelets = [
plane[0].getFaceletOfSide(s0),
plane[1].getFaceletOfSide(s0),
plane[2].getFaceletOfSide(s0),
plane[2].getFaceletOfSide(s1),
plane[5].getFaceletOfSide(s1),
plane[8].getFaceletOfSide(s1),
plane[8].getFaceletOfSide(s2),
plane[7].getFaceletOfSide(s2),
plane[6].getFaceletOfSide(s2),
plane[6].getFaceletOfSide(s3),
plane[3].getFaceletOfSide(s3),
plane[0].getFaceletOfSide(s3)
]
/** @type {number[][]} */
const shiftedUvs = []
for (let index = 0; index < 12; index++) {
const shiftedIndex = mod(index + 3 * angle, 12)
const {uvs} = facelets[shiftedIndex]
const facelet = facelets[index]
const transformer = uvsTransformers[axis][facelet.side][angle]
shiftedUvs[index] = transformer(uvs)
}
for (let index = 0; index < 12; index++) {
facelets[index].uvs = shiftedUvs[index]
}
}
/**
* @param {Cubie[]} plane
* @param {number} axis
* @param {number} index
* @param {number} angle
*/
#swapInnerFacelets(plane, axis, index, angle) {
const side = axis * 2 + Math.sign(index)
const facelets = [
plane[0].getFaceletOfSide(side),
plane[1].getFaceletOfSide(side),
plane[2].getFaceletOfSide(side),
plane[5].getFaceletOfSide(side),
plane[8].getFaceletOfSide(side),
plane[7].getFaceletOfSide(side),
plane[6].getFaceletOfSide(side),
plane[3].getFaceletOfSide(side)
]
/** @type {number[][]} */
const shiftedUvs = []
const transformer = uvsTransformers[axis][side][angle]
for (let index = 0; index < 8; index++) {
const shiftedIndex = mod(index + 2 * angle, 8)
const {uvs} = facelets[shiftedIndex]
shiftedUvs[index] = transformer(uvs)
}
for (let index = 0; index < 8; index++) {
facelets[index].uvs = shiftedUvs[index]
}
const center = plane[4].getFaceletOfSide(side)
center.uvs = transformer(center.uvs)
}
// manual rotation
/** @type {Array<{cubie: Cubie, backupPosition: V3, backupRotation: Quaternion, directionFromCenter: V3}>} */
#rotatingCubies = []
#rotationAxis = V3.zero
#rotationCenter = V3.zero
#currentAngle = 0
/**
* @param {Cubie[]} cubies
* @param {V3} axis
*/
startRotation(cubies, axis) {
this.#rotationAxis = axis
this.#rotationCenter = cubies[4].transform.position
this.#rotatingCubies = cubies.map(cubie => {
return {
cubie,
backupPosition: cubie.transform.position,
backupRotation: cubie.transform.rotation,
directionFromCenter: cubie.transform.position.sub(this.#rotationCenter)
}
})
}
/** @param {number} angle */
rotateManual(angle) {
const rotation = Quaternion.fromAngle(this.#rotationAxis, -angle)
this.#currentAngle = angle
this.#rotatingCubies.forEach(({cubie, backupRotation, directionFromCenter}) => {
const rotatedDirectionFromCenter = rotation.rotate(directionFromCenter)
const newPosition = this.#rotationCenter.add(rotatedDirectionFromCenter)
cubie.transform.position = newPosition
cubie.transform.rotation = backupRotation
cubie.transform.rotate(this.#rotationAxis, angle)
})
}
// automatic rotation
#initialAngle = 0
#rotatingAutomatic = false
#turnProgress = 0
#turnSpeed = 4
#targetAngle = 0
#rotationIndex = 0
#rotationAxisIndex = 0
#side = 0
/**
* @param {number} axis
* @param {number} index
* @param {number} angle
* @param {number} side
*/
finishRotation(axis, index, angle, side) {
this.#rotatingAutomatic = true
this.#turnProgress = 0
this.#targetAngle = angle
this.#initialAngle = this.#currentAngle
this.#rotationIndex = index
this.#rotationAxisIndex = axis
this.#side = side
}
/** @param {number} delta */
update(delta) {
if (!this.#rotatingAutomatic) {
return
}
this.#turnProgress += delta * this.#turnSpeed
if (this.#turnProgress >= 1) {
this.#rotatingAutomatic = false
this.#rotatingCubies.forEach(({cubie, backupPosition, backupRotation}) => {
cubie.transform.position = backupPosition
cubie.transform.rotation = backupRotation
})
this.#turn(
this.#rotationAxisIndex,
this.#rotationIndex,
this.#targetAngle,
this.#side,
this.#rotatingCubies.map(({cubie}) => cubie)
)
return
}
const currentAngle = lerp(this.#initialAngle, this.#targetAngle * 90, this.#turnProgress)
const rotation = Quaternion.fromAngle(this.#rotationAxis, -currentAngle)
this.#rotatingCubies.forEach(({cubie, backupRotation, directionFromCenter}) => {
cubie.transform.rotation = backupRotation.mult(Quaternion.fromAngle(this.#rotationAxis, currentAngle))
const rotatedDirectionFromCenter = rotation.rotate(directionFromCenter)
const newPosition = this.#rotationCenter.add(rotatedDirectionFromCenter)
cubie.transform.position = newPosition
})
}
get isTurning() {
return this.#rotatingAutomatic
}
}

95
node_modules/rubiks-js/src/ui/transform.js generated vendored Normal file
View File

@@ -0,0 +1,95 @@
import {Quaternion} from '../math/quarternion'
import {V4} from '../math/vector'
/**
* @typedef {{transform: Transform<any, any>}} WithTransform
* @typedef {import('../math/matrix').M44} M44
* @typedef {import('../math/vector').V3} V3
*/
/**
* @template {WithTransform} TChild
* @template {WithTransform | null} TParent
*/
export class Transform {
/** @type {M44} */
localTransform
/** @type {M44} */
globalTransform
/** @type {TChild[]} */
children = []
/**
* @param {V3} position
* @param {Quaternion} rotation
* @param {TParent} parent
*/
constructor(position, rotation, parent) {
/** @protected */
this._position = position
/** @protected */
this._rotation = rotation
this.parent = parent
this.setTransforms()
}
/** @protected */
setTransforms() {
const localTransform = Transform.getLocalTransform(this._position, this._rotation)
this.localTransform = localTransform
if (!this.parent) {
this.globalTransform = localTransform
} else {
const parentTransform = this.parent.transform.globalTransform
this.globalTransform = parentTransform.mult(localTransform)
}
this.children.forEach(child => child.transform.setTransforms())
}
/** @param {V3} v */
apply({x, y, z}) {
return this.globalTransform.mult(new V4(x, y, z, 1)).toV3()
}
/**
* @param {V3} axis
* @param {number} angle
*/
rotate(axis, angle) {
this.rotation = this._rotation.mult(Quaternion.fromAngle(this._rotation.rotate(axis), angle))
}
/** @param {V3} value */
set position(value) {
this._position = value
this.setTransforms()
}
/** @param {Quaternion} value */
set rotation(value) {
this._rotation = value
this.setTransforms()
}
get position() {
return this._position
}
get rotation() {
return this._rotation
}
/**
* @param {V3} position
* @param {Quaternion} rotation
* @returns {M44}
*/
static getLocalTransform({x, y, z}, rotation) {
const transform = rotation.matrix
transform.r1.w = x
transform.r2.w = y
transform.r3.w = z
return transform
}
}

94
node_modules/rubiks-js/src/ui/uvs.js generated vendored Normal file
View File

@@ -0,0 +1,94 @@
/** @satisfies {Record<string, (uvs: number[]) => number[]>} */
const uvsTransformers = {
identity: uvs => [...uvs],
flipH: uvs => [uvs[6], uvs[7], uvs[4], uvs[5], uvs[2], uvs[3], uvs[0], uvs[1]],
flipV: uvs => [uvs[2], uvs[3], uvs[0], uvs[1], uvs[6], uvs[7], uvs[4], uvs[5]],
rotateCC: uvs => [uvs[6], uvs[7], uvs[0], uvs[1], uvs[2], uvs[3], uvs[4], uvs[5]],
rotateC: uvs => [uvs[2], uvs[3], uvs[4], uvs[5], uvs[6], uvs[7], uvs[0], uvs[1]],
rotate2: uvs => [uvs[4], uvs[5], uvs[6], uvs[7], uvs[0], uvs[1], uvs[2], uvs[3]],
rotateCFlipH: uvs => [uvs[0], uvs[1], uvs[6], uvs[7], uvs[4], uvs[5], uvs[2], uvs[3]]
}
/** @satisfies {Record<string, Record<number, (uvs: number[]) => number[]>>} */
export const uvsTransformerPresets = {
flipV12: {
1: uvsTransformers.flipV,
2: uvsTransformers.flipV,
3: uvsTransformers.identity
},
flipV23: {
1: uvsTransformers.identity,
2: uvsTransformers.flipV,
3: uvsTransformers.flipV
},
flipH12: {
1: uvsTransformers.flipH,
2: uvsTransformers.flipH,
3: uvsTransformers.identity
},
flipH23: {
1: uvsTransformers.identity,
2: uvsTransformers.flipH,
3: uvsTransformers.flipH
},
rcfhFvRcc: {
1: uvsTransformers.rotateCFlipH,
2: uvsTransformers.flipV,
3: uvsTransformers.rotateCC
},
rcFvRcfh: {
1: uvsTransformers.rotateC,
2: uvsTransformers.flipV,
3: uvsTransformers.rotateCFlipH
},
rcFhRcfh: {
1: uvsTransformers.rotateC,
2: uvsTransformers.flipH,
3: uvsTransformers.rotateCFlipH
},
rcfhFhRcc: {
1: uvsTransformers.rotateCFlipH,
2: uvsTransformers.flipH,
3: uvsTransformers.rotateCC
},
rcR2Rcc: {
1: uvsTransformers.rotateC,
2: uvsTransformers.rotate2,
3: uvsTransformers.rotateCC,
},
rccR2Rc: {
1: uvsTransformers.rotateCC,
2: uvsTransformers.rotate2,
3: uvsTransformers.rotateC,
}
}
/**
* `cubiesShiftMapper[axis][index]`
* @type {Record<number, Record<number, number[]>>}
*/
export const cubiesShiftMapper = {
0: {
0: [0, 3, 6, 15, 24, 21, 18, 9],
2: [2, 5, 8, 17, 26, 23, 20, 11]
},
1: {
0: [0, 1, 2, 11, 20, 19, 18, 9],
2: [6, 7, 8, 17, 26, 25, 24, 15]
},
2: {
0: [0, 1, 2, 5, 8, 7, 6, 3],
2: [18, 19, 20, 23, 26, 25, 24, 21]
}
}
/**
* `sidesShiftMapper[axis]`
* @type {[number, number, number, number][]}
*/
export const sidesShiftMapper = [
[2, 5, 3, 4],
[0, 5, 1, 4],
[0, 3, 1, 2]
]

16
node_modules/rubiks-js/tsconfig.json generated vendored Normal file
View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"resolveJsonModule": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"checkJs": true,
"strictPropertyInitialization": false,
"noEmit": true
},
"include": ["src"]
}