@houstonp/rubiks-cube 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # Rubiks Cube Web Component
2
+
3
+ This package is a rubiks cube web component built with threejs. Camera animation smoothing is done with the tweenjs package.
4
+
5
+ ## adding the component
6
+
7
+ You can dd the component to a webpage by adding a module script tag with the index.js file. And then
8
+ by adding the webcomponent tag.
9
+
10
+ ```html
11
+ <!DOCTYPE html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="utf-8" />
15
+ </head>
16
+ <body>
17
+ <rubiks-cube></rubiks-cube>
18
+ <script type="module" src="index.js"></script>
19
+ </body>
20
+ </html>
21
+ ```
22
+
23
+ ## updating the component
24
+
25
+ The Rubiks cube web component listens for custom events to perform twists, rotations and camera changes. As per convention, the starting rotation has green facing forward, white facing up and red facing to the right.
26
+
27
+ ### Camera events
28
+
29
+ The rubiks-cube element listens for the `camera` custom event and moves the camera to the specified position.
30
+
31
+ The camera position specified in the event details must be one of the following:
32
+
33
+ - `peek-right` - Camera is moved to the right of the cube so that the right face is visible
34
+ - `peek-left` - Camera is moved to the left of the cube so that the left face is visible
35
+ - `peek-top` - Camera is moved above the cube so that the top face is visible
36
+ - `peek-bottom` - Camera is moved below the cube so that the bottom face is visible
37
+ - `peek-toggle-horizontal` - Camera is moved to the opposite side of the cube in the horizontal plane
38
+ - `peek-toggle-vertical` - Camera is moved to the opposite side of the cube in the vertical plane
39
+
40
+ #### Example
41
+
42
+ ```js
43
+ const cube = document.querySelector("rubiks-cube");
44
+ cube.dispatchEvent(
45
+ new CustomEvent("camera", {
46
+ detail: { action: "peek-right" },
47
+ })
48
+ );
49
+ ```
50
+
51
+ ### Rotation event
52
+
53
+ The rubiks-cube element listens for the `rotate` custom event and rotates a face or entire cube in the direction specified by the event details.
54
+
55
+ The rotation type specified in the event details must follow standard rubiks cube notation.
56
+
57
+ #### Rubiks Cube Notation
58
+
59
+ Notations can include the number of roations of a face. For example, `U2` means rotate the upper face 180 degrees.
60
+
61
+ Noations can also include a prime symbol `'` to indicate a counter clockwise rotation. For example, `U'` means rotate the upper face counter clockwise. The direction is always determined relative to the face being moved.
62
+
63
+ When both a number and a prime symbol are included the number is stated before the prime symbol. For example, `U2'` means rotate the upper face 180 degrees counter clockwise. and `U'2` is invalid.
64
+
65
+ | Notation | Movement |
66
+ | -------- | ------------------------------------------------ |
67
+ | U | Top face clockwise |
68
+ | u | Top two layers clockwise |
69
+ | D | Bottom face clockwise |
70
+ | d | Bottom two layers clockwise |
71
+ | L | Left face clockwise |
72
+ | l | Left two layers clockwise |
73
+ | R | Right face clockwise |
74
+ | r | Right two layers clockwise |
75
+ | F | Front face clockwise |
76
+ | f | Front two layers clockwise |
77
+ | B | Back face clockwise |
78
+ | b | Back two layers clockwise |
79
+ | M | Middle layer clockwise (relative to L) |
80
+ | E | Equatorial layer clockwise (relative to D) |
81
+ | S | Standing layer clockwise (relative to F) |
82
+ | x | Rotate cube on x axis clockwise (direction of R) |
83
+ | y | Rotate cube on y axis clockwise (direction of U) |
84
+ | z | Rotate cube on z axis clockwise (direction of F) |
85
+
86
+ #### Example
87
+
88
+ ```js
89
+ const cube = document.querySelector("rubiks-cube");
90
+ cube.dispatchEvent(
91
+ new CustomEvent("rotate", {
92
+ detail: { action: "u2'" },
93
+ })
94
+ );
95
+ ```
package/index.js ADDED
@@ -0,0 +1,194 @@
1
+ import * as THREE from "three";
2
+ import * as TWEEN from "@tweenjs/tween.js";
3
+ import { OrbitControls } from "three/examples/jsm/Addons.js";
4
+ import Cube from "./src/cube";
5
+ import getRotationDetails from "./src/rotation";
6
+ import { AnimationQueue, Animation } from "./src/animation";
7
+
8
+ class RubiksCube extends HTMLElement {
9
+ constructor() {
10
+ super();
11
+ this.attachShadow({ mode: "open" });
12
+ this.shadowRoot.innerHTML = `<canvas id="cube-canvas" style="display:block;"></canvas>`;
13
+ this.canvas = this.shadowRoot.getElementById("cube-canvas");
14
+ }
15
+
16
+ connectedCallback() {
17
+ this.init();
18
+ }
19
+
20
+ init() {
21
+ // defined core threejs objects
22
+ const canvas = this.canvas;
23
+ const scene = new THREE.Scene();
24
+ const renderer = new THREE.WebGLRenderer({
25
+ alpha: true,
26
+ canvas,
27
+ antialias: true,
28
+ });
29
+ renderer.setSize(this.clientWidth, this.clientHeight);
30
+ renderer.setAnimationLoop(animate);
31
+ renderer.setPixelRatio(2);
32
+
33
+ //update renderer and camera when container resizes. debouncing events to reduce frequency
34
+ function debounce(f, delay) {
35
+ let timer = 0;
36
+ return function (...args) {
37
+ clearTimeout(timer);
38
+ timer = setTimeout(() => f.apply(this, args), delay);
39
+ };
40
+ }
41
+ const resizeObserver = new ResizeObserver(
42
+ debounce((entries) => {
43
+ const { width, height } = entries[0].contentRect;
44
+ camera.aspect = width / height;
45
+ camera.updateProjectionMatrix();
46
+ renderer.setSize(width, height);
47
+ }, 30)
48
+ );
49
+ resizeObserver.observe(this);
50
+
51
+ // add camera
52
+ const camera = new THREE.PerspectiveCamera(
53
+ 75,
54
+ this.clientWidth / this.clientHeight,
55
+ 0.1,
56
+ 1000
57
+ );
58
+ camera.position.z = 4;
59
+ camera.position.y = 3;
60
+ camera.position.x = 0;
61
+
62
+ // add orbit controls for camera
63
+ const controls = new OrbitControls(camera, renderer.domElement);
64
+ controls.enableZoom = false;
65
+ controls.enablePan = false;
66
+ controls.enableDamping = true;
67
+ controls.maxAzimuthAngle = Math.PI / 4;
68
+ controls.minAzimuthAngle = -Math.PI / 4;
69
+ controls.maxPolarAngle = (3 * Math.PI) / 4;
70
+ controls.minPolarAngle = Math.PI / 4;
71
+
72
+ // add lighting to scene
73
+ const ambientLight = new THREE.AmbientLight("white", 0.5);
74
+ const spotLight1 = new THREE.DirectionalLight("white", 2);
75
+ const spotLight2 = new THREE.DirectionalLight("white", 2);
76
+ const spotLight3 = new THREE.DirectionalLight("white", 2);
77
+ const spotLight4 = new THREE.DirectionalLight("white", 2);
78
+ spotLight1.position.set(5, 5, 5);
79
+ spotLight2.position.set(-5, 5, 5);
80
+ spotLight3.position.set(5, -5, 0);
81
+ spotLight4.position.set(-10, -5, -5);
82
+ scene.add(ambientLight, spotLight1, spotLight2, spotLight3, spotLight4);
83
+
84
+ // create cube and add to scene
85
+ const cube = new Cube();
86
+ scene.add(cube.group);
87
+
88
+ // animation queue
89
+ const animationQueue = new AnimationQueue();
90
+
91
+ // initial camera animation
92
+ new TWEEN.Tween(camera.position)
93
+ .to({ x: 3, y: 3, z: 4 }, 1000)
94
+ .easing(TWEEN.Easing.Cubic.InOut)
95
+ .start();
96
+
97
+ // animation loop
98
+ function animate() {
99
+ TWEEN.update();
100
+ controls.update();
101
+ animationQueue.update();
102
+ const animationGroup = animationQueue.getAnimationGroup();
103
+ if (animationGroup !== undefined) scene.add(animationGroup);
104
+ renderer.render(scene, camera);
105
+ }
106
+
107
+ // add event listeners for rotation and camera controls
108
+ this.addEventListener("rotate", (e) => {
109
+ const action = getRotationDetails(e.detail.action);
110
+ if (action !== undefined) {
111
+ const animation = new Animation(
112
+ cube,
113
+ action.axis,
114
+ action.layers,
115
+ action.direction,
116
+ 200
117
+ );
118
+ animationQueue.add(animation);
119
+ }
120
+ });
121
+ this.addEventListener("camera", (e) => {
122
+ console.log(cube.getStickerState());
123
+ new TWEEN.Tween(camera.position);
124
+ if (e.detail.action === "peek-toggle-horizontal") {
125
+ new TWEEN.Tween(camera.position)
126
+ .to(
127
+ {
128
+ x: camera.position.x > 0 ? -2.5 : 2.5,
129
+ y: camera.position.y > 0 ? 2.5 : -2.5,
130
+ z: 4,
131
+ },
132
+ 200
133
+ )
134
+ .start();
135
+ } else if (e.detail.action === "peek-toggle-vertical") {
136
+ new TWEEN.Tween(camera.position)
137
+ .to(
138
+ {
139
+ x: camera.position.x > 0 ? 2.5 : -2.5,
140
+ y: camera.position.y > 0 ? -2.5 : 2.5,
141
+ z: 4,
142
+ },
143
+ 200
144
+ )
145
+ .start();
146
+ } else if (e.detail.action === "peek-right") {
147
+ new TWEEN.Tween(camera.position)
148
+ .to(
149
+ {
150
+ x: 2.5,
151
+ y: camera.position.y > 0 ? 2.5 : -2.5,
152
+ z: 4,
153
+ },
154
+ 200
155
+ )
156
+ .start();
157
+ } else if (e.detail.action === "peek-left") {
158
+ new TWEEN.Tween(camera.position)
159
+ .to(
160
+ {
161
+ x: -2.5,
162
+ y: camera.position.y > 0 ? 2.5 : -2.5,
163
+ z: 4,
164
+ },
165
+ 200
166
+ )
167
+ .start();
168
+ } else if (e.detail.action === "peek-up") {
169
+ new TWEEN.Tween(camera.position)
170
+ .to(
171
+ {
172
+ x: camera.position.x > 0 ? 2.5 : -2.5,
173
+ y: 2.5,
174
+ z: 4,
175
+ },
176
+ 200
177
+ )
178
+ .start();
179
+ } else if (e.detail.action === "peek-down") {
180
+ new TWEEN.Tween(camera.position)
181
+ .to(
182
+ {
183
+ x: camera.position.x > 0 ? 2.5 : -2.5,
184
+ y: -2.5,
185
+ z: 4,
186
+ },
187
+ 200
188
+ )
189
+ .start();
190
+ }
191
+ });
192
+ }
193
+ }
194
+ customElements.define("rubiks-cube", RubiksCube);
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@houstonp/rubiks-cube",
3
+ "version": "1.0.0",
4
+ "description": "Rubiks Cube Web Component built with threejs",
5
+ "main": "index.js",
6
+ "author": "Houston Pearse",
7
+ "license": "MIT",
8
+ "dependencies": {
9
+ "@tweenjs/tween.js": "^25.0.0",
10
+ "three": "^0.167.1"
11
+ },
12
+ "devDependencies": {},
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/houstonpearse/rubiks-cube.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/houstonpearse/rubiks-cube/issues"
19
+ },
20
+ "homepage": "https://github.com/houstonpearse/rubiks-cube#readme"
21
+ }
@@ -0,0 +1,158 @@
1
+ import * as THREE from "three";
2
+ import Cube from "./cube";
3
+
4
+ export class AnimationQueue {
5
+ constructor(type = "exponential", factor = 1.3) {
6
+ /** @type {Animation[]} */
7
+ this.queue = [];
8
+ /** @type {Animation | undefined} */
9
+ this.currentAnimation = undefined;
10
+ /** @type {{type: "fast-forward" | "exponential", factor: number}} */
11
+ this.type = type;
12
+ this.factor = factor;
13
+ }
14
+
15
+ /**
16
+ * @param {Animation} animation
17
+ */
18
+ add(animation) {
19
+ if (this.type === "fast-forward") {
20
+ this.fastForward();
21
+ } else if (this.type === "exponential") {
22
+ this.exponential();
23
+ }
24
+ this.queue.push(animation);
25
+ }
26
+
27
+ /* exponentially increases the animation speed with the depth of the queue */
28
+ exponential() {
29
+ let animations = [];
30
+ if (this.currentAnimation) animations.push(this.currentAnimation);
31
+ animations = animations.concat(this.queue);
32
+ for (let i = 0; i < animations.length; i++) {
33
+ animations[i].setSpeed(this.factor ** (animations.length - i - 1));
34
+ }
35
+ }
36
+
37
+ /* instantly completes any queued animations */
38
+ fastForward() {
39
+ if (this.currentAnimation) {
40
+ this.currentAnimation.setFastForward();
41
+ }
42
+ if (this.queue.length) {
43
+ for (const a of this.queue) {
44
+ a.setFastForward();
45
+ }
46
+ }
47
+ }
48
+
49
+ update() {
50
+ if (
51
+ this.currentAnimation === undefined ||
52
+ this.currentAnimation.finished()
53
+ ) {
54
+ this.currentAnimation = this.queue.shift();
55
+ }
56
+ if (this.currentAnimation === undefined) return;
57
+ this.currentAnimation.update();
58
+ }
59
+
60
+ /**
61
+ *
62
+ * @returns {THREE.Group | undefined}
63
+ */
64
+ getAnimationGroup() {
65
+ if (this.currentAnimation === undefined) return undefined;
66
+ return this.currentAnimation.getGroup();
67
+ }
68
+ }
69
+
70
+ export class Animation {
71
+ /**
72
+ *
73
+ * @param {Cube} cube
74
+ * @param {"x"|"y"|"z"} axis
75
+ * @param {(-1|0|1)[]} layers
76
+ * @param {1|-1|2|-2} direction
77
+ * @param {number} duration milliseconds
78
+ */
79
+ constructor(cube, axis, layers, direction, duration) {
80
+ this._cube = cube;
81
+ this._axis = axis;
82
+ this._layers = layers;
83
+ this._direction = direction;
84
+ this._duration = duration;
85
+ this._layerGroup = new THREE.Group();
86
+ this._finished = false;
87
+ this._lastUpdate = undefined;
88
+ this._totalRotation = 0;
89
+ this.fastforward = false;
90
+ this.speed = 1;
91
+ }
92
+
93
+ setFastForward(value = true) {
94
+ this.fastforward = value;
95
+ }
96
+
97
+ setSpeed(value = 1) {
98
+ this.speed = value;
99
+ }
100
+
101
+ init() {
102
+ this._lastUpdate = Date.now();
103
+ const layerObjects = this._cube.getRotationLayer(this._axis, this._layers);
104
+ this._layerGroup.add(...layerObjects);
105
+ this._cube.group.remove(...layerObjects);
106
+ }
107
+
108
+ teardown() {
109
+ this._finished = true;
110
+ this._layerGroup.children.forEach((piece) => {
111
+ piece.getWorldPosition(piece.position);
112
+ piece.getWorldQuaternion(piece.quaternion);
113
+ piece.userData.position.x = Math.round(piece.position.x);
114
+ piece.userData.position.y = Math.round(piece.position.y);
115
+ piece.userData.position.z = Math.round(piece.position.z);
116
+ piece.userData.rotation.x = piece.rotation.x;
117
+ piece.userData.rotation.y = piece.rotation.y;
118
+ piece.userData.rotation.z = piece.rotation.z;
119
+ });
120
+ this._cube.group.add(...this._layerGroup.children);
121
+ this._layerGroup.clear();
122
+ }
123
+
124
+ update() {
125
+ if (this._lastUpdate === undefined) {
126
+ this.init();
127
+ }
128
+
129
+ var interval = (Date.now() - this._lastUpdate) * this.speed;
130
+ this._lastUpdate = Date.now();
131
+ if (this.fastforward || interval + this._totalRotation > this._duration) {
132
+ interval = this._duration - this._totalRotation;
133
+ }
134
+ const rotationIncrement =
135
+ (Math.abs(this._direction) * ((interval / this._duration) * Math.PI)) / 2;
136
+ this._totalRotation += interval;
137
+ this._layerGroup.rotateOnWorldAxis(
138
+ new THREE.Vector3(
139
+ this._axis === "x" ? this._direction : 0,
140
+ this._axis === "y" ? this._direction : 0,
141
+ this._axis === "z" ? this._direction : 0
142
+ ).normalize(),
143
+ rotationIncrement
144
+ );
145
+
146
+ if (this._totalRotation >= this._duration) {
147
+ this.teardown();
148
+ }
149
+ }
150
+
151
+ getGroup() {
152
+ return this._layerGroup;
153
+ }
154
+
155
+ finished() {
156
+ return this._finished;
157
+ }
158
+ }
package/src/center.js ADDED
@@ -0,0 +1,23 @@
1
+ import * as THREE from "three";
2
+ /**
3
+ * @param {THREE.Geometry} sticker
4
+ * @param {THREE.Material} frontMaterial
5
+ * @param {THREE.Material} topMaterial
6
+ * @param {THREE.Material} coreMaterial
7
+ * @returns {{group: THREE.Group, frontSticker: THREE.Mesh, topSticker:THREE.Mesh}}
8
+ */
9
+ export default function newCenter(sticker, frontMaterial, coreMaterial) {
10
+ const group = new THREE.Group();
11
+ const boxGeom = new THREE.BoxGeometry(1, 1, 1);
12
+ const boxMesh = new THREE.Mesh(boxGeom, coreMaterial);
13
+ boxMesh.userData = { type: "piece" };
14
+ group.add(boxMesh);
15
+
16
+ const frontSticker = new THREE.Mesh(sticker, frontMaterial);
17
+ frontSticker.userData = { type: "sticker" };
18
+ frontSticker.position.set(0, 0, 0.5);
19
+ frontSticker.rotation.set(0, 0, 0);
20
+ group.add(frontSticker);
21
+
22
+ return { group, frontSticker };
23
+ }
package/src/corner.js ADDED
@@ -0,0 +1,45 @@
1
+ import * as THREE from "three";
2
+ /**
3
+ * @param {THREE.Geometry} sticker
4
+ * @param {THREE.Material} frontMaterial
5
+ * @param {THREE.Material} rightMaterial
6
+ * @param {THREE.Material} topMaterial
7
+ * @param {THREE.Material} coreMaterial
8
+ * @returns {{group: THREE.Group, frontSticker: THREE.Mesh, rightSticker:THREE.Mesh, topSticker:THREE.Mesh}}
9
+ */
10
+ export default function newCorner(
11
+ sticker,
12
+ frontMaterial,
13
+ rightMaterial,
14
+ topMaterial,
15
+ coreMaterial
16
+ ) {
17
+ const group = new THREE.Group();
18
+ const boxGeom = new THREE.BoxGeometry(1, 1, 1);
19
+ const boxMesh = new THREE.Mesh(boxGeom, coreMaterial);
20
+ boxMesh.userData = { type: "piece" };
21
+ group.add(boxMesh);
22
+
23
+ // front
24
+ const frontSticker = new THREE.Mesh(sticker, frontMaterial);
25
+ frontSticker.userData = { type: "sticker" };
26
+ frontSticker.position.set(0, 0, 0.5);
27
+ frontSticker.rotation.set(0, 0, 0);
28
+ group.add(frontSticker);
29
+
30
+ //right
31
+ const rightSticker = new THREE.Mesh(sticker, rightMaterial);
32
+ rightSticker.userData = { type: "sticker" };
33
+ rightSticker.position.set(0.5, 0, 0);
34
+ rightSticker.rotation.set(Math.PI / 2, Math.PI / 2, 0);
35
+ group.add(rightSticker);
36
+
37
+ //white/yellow
38
+ const topSticker = new THREE.Mesh(sticker, topMaterial);
39
+ topSticker.userData = { type: "sticker" };
40
+ topSticker.position.set(0, 0.5, 0);
41
+ topSticker.rotation.set(-Math.PI / 2, 0, -Math.PI / 2);
42
+ group.add(topSticker);
43
+
44
+ return { group, frontSticker, rightSticker, topSticker };
45
+ }
package/src/cube.js ADDED
@@ -0,0 +1,318 @@
1
+ import * as THREE from "three";
2
+ import Materials from "./materials";
3
+ import Stickers from "./stickers";
4
+ import newCorner from "./corner";
5
+ import newEdge from "./edge";
6
+ import newCenter from "./center";
7
+ import { newCubeState } from "./cubeState";
8
+
9
+ export default class Cube {
10
+ constructor() {
11
+ this.group = new THREE.Group();
12
+ const core = this.createCore();
13
+ core.userData = {
14
+ position: { x: 0, y: 0, z: 0 },
15
+ rotation: { x: 0, y: 0, z: 0 },
16
+ initialPosition: { x: 0, y: 0, z: 0 },
17
+ initialRotation: { x: 0, y: 0, z: 0 },
18
+ type: "core",
19
+ };
20
+ this.group.add(core);
21
+
22
+ for (const state of newCubeState()) {
23
+ const piece = this.createPiece(state.position, state.type);
24
+ piece.position.set(
25
+ state.position.x * 1.04,
26
+ state.position.y * 1.04,
27
+ state.position.z * 1.04
28
+ );
29
+ piece.rotation.set(state.rotation.x, state.rotation.y, state.rotation.z);
30
+ piece.userData = {
31
+ position: state.position,
32
+ rotation: state.rotation,
33
+ initialPosition: state.position,
34
+ initialRotation: state.rotation,
35
+ type: state.type,
36
+ };
37
+ this.group.add(piece);
38
+ }
39
+ }
40
+ /**
41
+ * @param {"x"|"y"|"z"} axis
42
+ * @param {{-1|0|1}[]} layers
43
+ * @returns {THREE.Object3D[]}
44
+ */
45
+ getRotationLayer(axis, layers) {
46
+ if (layers.length === 0) {
47
+ return [...this.group.children];
48
+ }
49
+ return this.group.children.filter((piece) => {
50
+ if (axis === "x") {
51
+ return layers.includes(Math.round(piece.userData.position.x));
52
+ } else if (axis === "y") {
53
+ return layers.includes(Math.round(piece.userData.position.y));
54
+ } else if (axis === "z") {
55
+ return layers.includes(Math.round(piece.userData.position.z));
56
+ }
57
+ return false;
58
+ });
59
+ }
60
+
61
+ getStickerState() {
62
+ const state = {
63
+ up: [[], [], []],
64
+ down: [[], [], []],
65
+ front: [[], [], []],
66
+ back: [[], [], []],
67
+ left: [[], [], []],
68
+ right: [[], [], []],
69
+ };
70
+ this.group.children.forEach((piece) => {
71
+ if (piece.userData.type === "core") {
72
+ return;
73
+ }
74
+ piece.children.forEach((mesh) => {
75
+ if (mesh.userData.type === "sticker") {
76
+ const piecepos = new THREE.Vector3();
77
+ piecepos.copy(piece.position);
78
+ piecepos.round();
79
+ const stickerpos = new THREE.Vector3();
80
+ mesh.getWorldPosition(stickerpos);
81
+ stickerpos.sub(piecepos);
82
+ stickerpos.multiplyScalar(2);
83
+ stickerpos.round();
84
+ if (stickerpos.x === 1) {
85
+ state.right[1 - Math.round(piece.position.y)][
86
+ 1 - Math.round(piece.position.z)
87
+ ] = mesh.material.userData.face;
88
+ } else if (stickerpos.x === -1) {
89
+ state.left[1 - Math.round(piece.position.y)][
90
+ 1 + Math.round(piece.position.z)
91
+ ] = mesh.material.userData.face;
92
+ } else if (stickerpos.y === 1) {
93
+ state.up[1 + Math.round(piece.position.z)][
94
+ 1 + Math.round(piece.position.x)
95
+ ] = mesh.material.userData.face;
96
+ } else if (stickerpos.y === -1) {
97
+ state.down[1 - Math.round(piece.position.z)][
98
+ 1 + Math.round(piece.position.x)
99
+ ] = mesh.material.userData.face;
100
+ } else if (stickerpos.z === 1) {
101
+ state.front[1 - Math.round(piece.position.y)][
102
+ 1 + Math.round(piece.position.x)
103
+ ] = mesh.material.userData.face;
104
+ } else if (stickerpos.z === -1) {
105
+ state.back[1 - Math.round(piece.position.y)][
106
+ 1 - Math.round(piece.position.x)
107
+ ] = mesh.material.userData.face;
108
+ }
109
+ }
110
+ });
111
+ });
112
+ return state;
113
+ }
114
+
115
+ /**
116
+ * @param {{x:number,y:number,z:number}} position
117
+ * @param {"corner | edge | center"} type
118
+ * @returns {THREE.Group}
119
+ */
120
+ createPiece(position, type) {
121
+ if (type === "corner") {
122
+ return this.createCorner(position).group;
123
+ } else if (type === "edge") {
124
+ return this.createEdge(position).group;
125
+ } else if (type === "center") {
126
+ return this.createCenter(position).group;
127
+ } else {
128
+ throw new Error("Invalid type: " + type);
129
+ }
130
+ }
131
+ /**
132
+ * @param {{x:number,y:number,z:number}} position
133
+ * @returns {THREE.Group}
134
+ */
135
+ createCorner(position) {
136
+ if (position.x == 1 && position.y == 1 && position.z == 1) {
137
+ return newCorner(
138
+ Stickers.corner,
139
+ Materials.front,
140
+ Materials.right,
141
+ Materials.up,
142
+ Materials.core
143
+ );
144
+ } else if (position.x == 1 && position.y == 1 && position.z == -1) {
145
+ return newCorner(
146
+ Stickers.corner,
147
+ Materials.right,
148
+ Materials.back,
149
+ Materials.up,
150
+ Materials.core
151
+ );
152
+ } else if (position.x == 1 && position.y == -1 && position.z == 1) {
153
+ return newCorner(
154
+ Stickers.corner,
155
+ Materials.right,
156
+ Materials.front,
157
+ Materials.down,
158
+ Materials.core
159
+ );
160
+ } else if (position.x == 1 && position.y == -1 && position.z == -1) {
161
+ return newCorner(
162
+ Stickers.corner,
163
+ Materials.back,
164
+ Materials.right,
165
+ Materials.down,
166
+ Materials.core
167
+ );
168
+ } else if (position.x == -1 && position.y == 1 && position.z == 1) {
169
+ return newCorner(
170
+ Stickers.corner,
171
+ Materials.left,
172
+ Materials.front,
173
+ Materials.up,
174
+ Materials.core
175
+ );
176
+ } else if (position.x == -1 && position.y == 1 && position.z == -1) {
177
+ return newCorner(
178
+ Stickers.corner,
179
+ Materials.back,
180
+ Materials.left,
181
+ Materials.up,
182
+ Materials.core
183
+ );
184
+ } else if (position.x == -1 && position.y == -1 && position.z == 1) {
185
+ return newCorner(
186
+ Stickers.corner,
187
+ Materials.front,
188
+ Materials.left,
189
+ Materials.down,
190
+ Materials.core
191
+ );
192
+ } else if (position.x == -1 && position.y == -1 && position.z == -1) {
193
+ return newCorner(
194
+ Stickers.corner,
195
+ Materials.left,
196
+ Materials.back,
197
+ Materials.down,
198
+ Materials.core
199
+ );
200
+ } else {
201
+ throw new Error("Invalid corner position: " + position);
202
+ }
203
+ }
204
+ /**
205
+ * @param {{x:number,y:number,z:number}} position
206
+ * @returns {THREE.Group}
207
+ */
208
+ createEdge(position) {
209
+ if (position.x == 1 && position.y == 1 && position.z == 0) {
210
+ return newEdge(
211
+ Stickers.edge,
212
+ Materials.right,
213
+ Materials.up,
214
+ Materials.core
215
+ );
216
+ } else if (position.x == 1 && position.y == -1 && position.z == 0) {
217
+ return newEdge(
218
+ Stickers.edge,
219
+ Materials.right,
220
+ Materials.down,
221
+ Materials.core
222
+ );
223
+ } else if (position.x == 1 && position.y == 0 && position.z == 1) {
224
+ return newEdge(
225
+ Stickers.edge,
226
+ Materials.front,
227
+ Materials.right,
228
+ Materials.core
229
+ );
230
+ } else if (position.x == 1 && position.y == 0 && position.z == -1) {
231
+ return newEdge(
232
+ Stickers.edge,
233
+ Materials.right,
234
+ Materials.back,
235
+ Materials.core
236
+ );
237
+ } else if (position.x == -1 && position.y == 1 && position.z == 0) {
238
+ return newEdge(
239
+ Stickers.edge,
240
+ Materials.left,
241
+ Materials.up,
242
+ Materials.core
243
+ );
244
+ } else if (position.x == -1 && position.y == -1 && position.z == 0) {
245
+ return newEdge(
246
+ Stickers.edge,
247
+ Materials.left,
248
+ Materials.down,
249
+ Materials.core
250
+ );
251
+ } else if (position.x == -1 && position.y == 0 && position.z == 1) {
252
+ return newEdge(
253
+ Stickers.edge,
254
+ Materials.front,
255
+ Materials.left,
256
+ Materials.core
257
+ );
258
+ } else if (position.x == -1 && position.y == 0 && position.z == -1) {
259
+ return newEdge(
260
+ Stickers.edge,
261
+ Materials.left,
262
+ Materials.back,
263
+ Materials.core
264
+ );
265
+ } else if (position.x == 0 && position.y == 1 && position.z == 1) {
266
+ return newEdge(
267
+ Stickers.edge,
268
+ Materials.front,
269
+ Materials.up,
270
+ Materials.core
271
+ );
272
+ } else if (position.x == 0 && position.y == 1 && position.z == -1) {
273
+ return newEdge(
274
+ Stickers.edge,
275
+ Materials.up,
276
+ Materials.back,
277
+ Materials.core
278
+ );
279
+ } else if (position.x == 0 && position.y == -1 && position.z == 1) {
280
+ return newEdge(
281
+ Stickers.edge,
282
+ Materials.down,
283
+ Materials.front,
284
+ Materials.core
285
+ );
286
+ } else if (position.x == 0 && position.y == -1 && position.z == -1) {
287
+ return newEdge(
288
+ Stickers.edge,
289
+ Materials.back,
290
+ Materials.down,
291
+ Materials.core
292
+ );
293
+ } else {
294
+ throw new Error("Invalid edge position: " + position);
295
+ }
296
+ }
297
+ /**
298
+ * @param {{x:number,y:number,z:number}} position
299
+ * @returns {THREE.Group}
300
+ */
301
+ createCenter(position) {
302
+ var centerColor = Materials.up;
303
+ if (position.x !== 0) {
304
+ centerColor = position.x > 0 ? Materials.right : Materials.left;
305
+ } else if (position.y !== 0) {
306
+ centerColor = position.y > 0 ? Materials.up : Materials.down;
307
+ } else if (position.z !== 0) {
308
+ centerColor = position.z > 0 ? Materials.front : Materials.back;
309
+ }
310
+ return newCenter(Stickers.center, centerColor, Materials.core);
311
+ }
312
+ /**
313
+ * @returns {THREE.Group}
314
+ */
315
+ createCore() {
316
+ return new THREE.Mesh(new THREE.SphereGeometry(1.55), Materials.core);
317
+ }
318
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @typedef {{x: number,y: number,z: number}} vector
3
+ */
4
+
5
+ /**
6
+ * @type {vector[]}
7
+ */
8
+ const corners = [
9
+ { position: { x: 1, y: 1, z: 1 }, rotation: { x: 0, y: 0, z: 0 } },
10
+ { position: { x: 1, y: 1, z: -1 }, rotation: { x: 0, y: Math.PI / 2, z: 0 } },
11
+ {
12
+ position: { x: 1, y: -1, z: 1 },
13
+ rotation: { x: 0, y: Math.PI / 2, z: Math.PI },
14
+ },
15
+ {
16
+ position: { x: 1, y: -1, z: -1 },
17
+ rotation: { x: 0, y: Math.PI, z: Math.PI },
18
+ },
19
+
20
+ {
21
+ position: { x: -1, y: 1, z: 1 },
22
+ rotation: { x: 0, y: -Math.PI / 2, z: 0 },
23
+ },
24
+ { position: { x: -1, y: 1, z: -1 }, rotation: { x: 0, y: Math.PI, z: 0 } },
25
+ { position: { x: -1, y: -1, z: 1 }, rotation: { x: 0, y: 0, z: Math.PI } },
26
+ {
27
+ position: { x: -1, y: -1, z: -1 },
28
+ rotation: { x: 0, y: -Math.PI / 2, z: Math.PI },
29
+ },
30
+ ];
31
+
32
+ /**
33
+ * @type {vector[]}
34
+ */
35
+ const edges = [
36
+ { position: { x: 1, y: 1, z: 0 }, rotation: { x: 0, y: Math.PI / 2, z: 0 } },
37
+ {
38
+ position: { x: 1, y: 0, z: 1 },
39
+ rotation: { x: 0, y: 0, z: -Math.PI / 2 },
40
+ },
41
+ {
42
+ position: { x: 1, y: 0, z: -1 },
43
+ rotation: { x: 0, y: Math.PI / 2, z: -Math.PI / 2 },
44
+ },
45
+ {
46
+ position: { x: 1, y: -1, z: 0 },
47
+ rotation: { x: Math.PI, y: Math.PI / 2, z: 0 },
48
+ },
49
+ { position: { x: 0, y: 1, z: 1 }, rotation: { x: 0, y: 0, z: 0 } },
50
+ {
51
+ position: { x: 0, y: 1, z: -1 },
52
+ rotation: { x: -Math.PI / 2, y: 0, z: 0 },
53
+ },
54
+ { position: { x: 0, y: -1, z: 1 }, rotation: { x: Math.PI / 2, y: 0, z: 0 } },
55
+ { position: { x: 0, y: -1, z: -1 }, rotation: { x: Math.PI, y: 0, z: 0 } },
56
+ {
57
+ position: { x: -1, y: 1, z: 0 },
58
+ rotation: { x: 0, y: -Math.PI / 2, z: 0 },
59
+ },
60
+ { position: { x: -1, y: 0, z: 1 }, rotation: { x: 0, y: 0, z: Math.PI / 2 } },
61
+ {
62
+ position: { x: -1, y: 0, z: -1 },
63
+ rotation: { x: 0, y: -Math.PI / 2, z: Math.PI / 2 },
64
+ },
65
+ {
66
+ position: { x: -1, y: -1, z: 0 },
67
+ rotation: { x: 0, y: -Math.PI / 2, z: Math.PI },
68
+ },
69
+ ];
70
+
71
+ /**
72
+ * @type {vector[]}
73
+ */
74
+ const centers = [
75
+ { position: { x: 1, y: 0, z: 0 }, rotation: { x: 0, y: Math.PI / 2, z: 0 } },
76
+ { position: { x: 0, y: 1, z: 0 }, rotation: { x: -Math.PI / 2, y: 0, z: 0 } },
77
+ { position: { x: 0, y: 0, z: 1 }, rotation: { x: 0, y: 0, z: 0 } },
78
+ { position: { x: 0, y: 0, z: -1 }, rotation: { x: 0, y: Math.PI, z: 0 } },
79
+ { position: { x: 0, y: -1, z: 0 }, rotation: { x: Math.PI / 2, y: 0, z: 0 } },
80
+ {
81
+ position: { x: -1, y: 0, z: 0 },
82
+ rotation: { x: 0, y: -Math.PI / 2, z: 0 },
83
+ },
84
+ ];
85
+
86
+ /**
87
+ * @typedef {Object} state
88
+ * @property {string} type
89
+ * @property {vector} initialPosition
90
+ * @property {vector} initialRotation
91
+ * @property {vector} position
92
+ * @property {vector} rotation
93
+ */
94
+
95
+ /**
96
+ * @returns {state[]}
97
+ */
98
+ export function newCubeState() {
99
+ let cubeState = [];
100
+ corners.forEach(({ position, rotation }) => {
101
+ cubeState.push({
102
+ type: "corner",
103
+ initialPosition: position,
104
+ initialRotation: rotation,
105
+ position: position,
106
+ rotation: rotation,
107
+ });
108
+ });
109
+ edges.forEach(({ position, rotation }) => {
110
+ cubeState.push({
111
+ type: "edge",
112
+ initialPosition: position,
113
+ initialRotation: rotation,
114
+ position: position,
115
+ rotation: rotation,
116
+ });
117
+ });
118
+ centers.forEach(({ position, rotation }) => {
119
+ cubeState.push({
120
+ type: "center",
121
+ initialPosition: position,
122
+ initialRotation: rotation,
123
+ position: position,
124
+ rotation: rotation,
125
+ });
126
+ });
127
+ return cubeState;
128
+ }
package/src/edge.js ADDED
@@ -0,0 +1,36 @@
1
+ import * as THREE from "three";
2
+ /**
3
+ * @param {THREE.Geometry} sticker
4
+ * @param {THREE.Material} frontMaterial
5
+ * @param {THREE.Material} topMaterial
6
+ * @param {THREE.Material} coreMaterial
7
+ * @returns {{group: THREE.Group, frontSticker: THREE.Mesh, topSticker:THREE.Mesh}}
8
+ */
9
+ export default function newEdge(
10
+ sticker,
11
+ frontMaterial,
12
+ topMaterial,
13
+ coreMaterial
14
+ ) {
15
+ const group = new THREE.Group();
16
+ const boxGeom = new THREE.BoxGeometry(1, 1, 1);
17
+ const boxMesh = new THREE.Mesh(boxGeom, coreMaterial);
18
+ boxMesh.userData = { type: "piece" };
19
+ group.add(boxMesh);
20
+
21
+ // front
22
+ const frontSticker = new THREE.Mesh(sticker, frontMaterial);
23
+ frontSticker.userData = { type: "sticker" };
24
+ frontSticker.position.set(0, 0, 0.5);
25
+ frontSticker.rotation.set(0, 0, 0);
26
+ group.add(frontSticker);
27
+
28
+ // top
29
+ const topSticker = new THREE.Mesh(sticker, topMaterial);
30
+ topSticker.userData = { type: "sticker" };
31
+ topSticker.position.set(0, 0.5, 0);
32
+ topSticker.rotation.set(-Math.PI / 2, 0, Math.PI);
33
+ group.add(topSticker);
34
+
35
+ return { group, frontSticker, topSticker };
36
+ }
@@ -0,0 +1,42 @@
1
+ import * as THREE from "three";
2
+ export default class Materials {
3
+ static front = new THREE.MeshStandardMaterial({
4
+ color: "#2cbf13",
5
+ metalness: 0,
6
+ roughness: 0.4,
7
+ userData: { face: "front", color: "green" },
8
+ });
9
+ static back = new THREE.MeshStandardMaterial({
10
+ color: "blue",
11
+ metalness: 0,
12
+ roughness: 0.4,
13
+ userData: { face: "back", color: "blue" },
14
+ });
15
+ static up = new THREE.MeshStandardMaterial({
16
+ color: "white",
17
+ metalness: 0,
18
+ roughness: 0.4,
19
+ userData: { face: "up", color: "white" },
20
+ });
21
+ static down = new THREE.MeshStandardMaterial({
22
+ color: "yellow",
23
+ metalness: 0,
24
+ roughness: 0.4,
25
+ userData: { face: "down", color: "yellow" },
26
+ });
27
+ static left = new THREE.MeshStandardMaterial({
28
+ color: "#fc9a05",
29
+ metalness: 0,
30
+ roughness: 0.4,
31
+ userData: { face: "left", color: "orange" },
32
+ });
33
+ static right = new THREE.MeshStandardMaterial({
34
+ color: "red",
35
+ metalness: 0,
36
+ roughness: 0.4,
37
+ userData: { face: "right", color: "red" },
38
+ });
39
+ static core = new THREE.MeshBasicMaterial({
40
+ color: "black",
41
+ });
42
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @param {string} action
3
+ * @returns {{axis: "x"|"y"|"z", layers: (0|1|-1)[], direction: (1|-1|2|-2)}}
4
+ */
5
+ export default function getRotationDetails(action) {
6
+ if (!action) return;
7
+ const reverse = action.includes("'") ? -1 : 1;
8
+ action = action.replace("'", "");
9
+ const multiplier = action.includes("2") ? 2 : 1;
10
+ action = action.replace("2", "");
11
+ if (!action) return;
12
+ const move = action[0];
13
+
14
+ if (move === "x") {
15
+ return { axis: "x", layers: [], direction: -reverse * multiplier };
16
+ } else if (move === "y") {
17
+ return { axis: "y", layers: [], direction: -reverse * multiplier };
18
+ } else if (move === "z") {
19
+ return { axis: "z", layers: [], direction: -reverse * multiplier };
20
+ } else if (move === "U") {
21
+ return { axis: "y", layers: [1], direction: -reverse * multiplier };
22
+ } else if (move === "u") {
23
+ return { axis: "y", layers: [1, 0], direction: -reverse * multiplier };
24
+ } else if (move === "R") {
25
+ return { axis: "x", layers: [1], direction: -reverse * multiplier };
26
+ } else if (move === "r") {
27
+ return { axis: "x", layers: [1, 0], direction: -reverse * multiplier };
28
+ } else if (move === "L") {
29
+ return { axis: "x", layers: [-1], direction: -reverse * multiplier };
30
+ } else if (move == "l") {
31
+ return { axis: "x", layers: [-1, 0], direction: -reverse * multiplier };
32
+ } else if (move === "D") {
33
+ return { axis: "y", layers: [-1], direction: -reverse * multiplier };
34
+ } else if (move === "d") {
35
+ return { axis: "y", layers: [-1, 0], direction: -reverse * multiplier };
36
+ } else if (move === "F") {
37
+ return { axis: "z", layers: [1], direction: -reverse * multiplier };
38
+ } else if (move === "f") {
39
+ return { axis: "z", layers: [1, 0], direction: -reverse * multiplier };
40
+ } else if (move === "B") {
41
+ return { axis: "z", layers: [-1], direction: -reverse * multiplier };
42
+ } else if (move === "b") {
43
+ return { axis: "z", layers: [-1, 0], direction: -reverse * multiplier };
44
+ } else if (move === "M") {
45
+ return { axis: "x", layers: [0], direction: -reverse * multiplier };
46
+ } else if (move === "E") {
47
+ return { axis: "y", layers: [0], direction: -reverse * multiplier };
48
+ } else if (move === "S") {
49
+ return { axis: "z", layers: [0], direction: -reverse * multiplier };
50
+ }
51
+ return undefined;
52
+ }
@@ -0,0 +1,48 @@
1
+ import * as THREE from "three";
2
+ import { SVGLoader } from "three/examples/jsm/Addons.js";
3
+
4
+ const loader = new SVGLoader();
5
+ const cornerSVG = loader.parse(`
6
+ <svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com">
7
+ <path d="M 25 0 H 500 V 500 H 0 V 25 A 25 25 0 0 1 25 0 Z" bx:shape="rect 0 0 500 500 25 0 0 0 1@a864c1ee"/>
8
+ </svg>
9
+ `);
10
+ const edgeSVG = loader.parse(`
11
+ <svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
12
+ <path d="M 150 0 L 350 0 C 450 0 500 50 500 120 L 500 500 L 0 500 L 0 120 C 0 50 50 0 150 0 Z"></path>
13
+ </svg>
14
+ `);
15
+ const centerSVG = loader.parse(`
16
+ <svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
17
+ <path d="M 120 0 L 380 0 C 450 0 500 50 500 120 L 500 380 C 500 450 450 500 380 500 L 120 500 C 50 500 0 450 0 380 L 0 120 C 0 50 50 0 120 0 Z"></path>
18
+ </svg>
19
+ `);
20
+
21
+ export default class Stickers {
22
+ static center = new THREE.ExtrudeGeometry(
23
+ SVGLoader.createShapes(centerSVG.paths[0])[0],
24
+ {
25
+ depth: 15,
26
+ }
27
+ )
28
+ .scale(0.002, 0.002, 0.002)
29
+ .translate(-0.5, -0.5, 0);
30
+
31
+ static edge = new THREE.ExtrudeGeometry(
32
+ SVGLoader.createShapes(edgeSVG.paths[0])[0],
33
+ {
34
+ depth: 15,
35
+ }
36
+ )
37
+ .scale(0.002, 0.002, 0.002)
38
+ .translate(-0.5, -0.5, 0);
39
+
40
+ static corner = new THREE.ExtrudeGeometry(
41
+ SVGLoader.createShapes(cornerSVG.paths[0])[0],
42
+ {
43
+ depth: 15,
44
+ }
45
+ )
46
+ .scale(0.002, 0.002, 0.002)
47
+ .translate(-0.5, -0.5, 0);
48
+ }