@houstonp/rubiks-cube 1.2.1 → 1.3.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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  This package is a rubiks cube web component built with threejs. Camera animation smoothing is done with the tweenjs package.
4
4
 
5
+ ![cube](cube.png)
6
+
5
7
  ## Adding the component
6
8
 
7
9
  You can add the component to a webpage by adding an import statement in the index.js file. And then
@@ -19,7 +21,7 @@ import '@houstonp/rubiks-cube';
19
21
  <meta charset="utf-8" />
20
22
  </head>
21
23
  <body>
22
- <rubiks-cube animation-speed="1000" animation-style="exponential" gap="1.04"> </rubiks-cube>
24
+ <rubiks-cube animation-speed="1000" animation-style="exponential" piece-gap="1.04" camera-speed="100"> </rubiks-cube>
23
25
  <script type="module" src="index.js"></script>
24
26
  </body>
25
27
  </html>
@@ -27,11 +29,12 @@ import '@houstonp/rubiks-cube';
27
29
 
28
30
  ## component attributes
29
31
 
30
- | attribute | accepted values | Description |
31
- | --------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------- |
32
- | animation-speed | integer greater than 0 | sets the speed of the animations in milliseconds |
33
- | animation-style | "exponetial", "next", "fixed" | fixed: fixed animation lengths, next: skips to next animation, exponential: speeds up successive animations |
34
- | gap | greater than 1 | sets the gap between rubiks cube pieces |
32
+ | attribute | accepted values | Description |
33
+ | --------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | animation-speed | integer greater than or equal to 0 | sets the speed of the animations in milliseconds |
35
+ | animation-style | "exponetial", "next", "fixed", "match" | fixed: fixed animation lengths, next: skips to next animation, exponential: speeds up successive animations, match: matches the speed the frequency of events |
36
+ | piece-gap | greater than 1 | sets the gap between rubiks cube pieces |
37
+ | camera-speed | greater than or equal to 0 | sets the speed of camera animations in milliseconds |
35
38
 
36
39
  ## state of the component
37
40
 
package/index.js CHANGED
@@ -6,8 +6,10 @@ import getRotationDetailsFromNotation from './src/utils/rotation';
6
6
  import { debounce } from './src/utils/debouncer';
7
7
 
8
8
  const defaultAnimationSpeed = 100;
9
- const defaultAnimationStyle = 'exponential';
9
+ const defaultCameraSpeed = 100;
10
+ const defaultAnimationStyle = 'fixed';
10
11
  const defaultGap = 1.04;
12
+ const minimumGap = 1;
11
13
 
12
14
  class RubiksCube extends HTMLElement {
13
15
  constructor() {
@@ -19,27 +21,34 @@ class RubiksCube extends HTMLElement {
19
21
  this.attachShadow({ mode: 'open' });
20
22
  this.shadowRoot.innerHTML = `<canvas id="cube-canvas" style="display:block;"></canvas>`;
21
23
  this.canvas = this.shadowRoot.getElementById('cube-canvas');
22
- /** @type {{style: "exponential" | "next" | "fixed", speed: number, gap: number}} */
24
+ /** @type {{animationStyle: "exponential" | "next" | "fixed" | "match", animationSpeed: number, gap: number, cameraSpeed: number}} */
23
25
  this.settings = {
24
- speed: this.getAttribute('animation-speed') || defaultAnimationSpeed,
25
- style: this.getAttribute('animation-style') || defaultAnimationStyle,
26
- gap: this.getAttribute('gap') || defaultGap,
26
+ animationSpeed: this.getAttribute('animation-speed') || defaultAnimationSpeed,
27
+ animationStyle: this.getAttribute('animation-style') || defaultAnimationStyle,
28
+ gap: this.getAttribute('piece-gap') || defaultGap,
29
+ cameraSpeed: this.getAttribute('camera-speed') || defaultCameraSpeed,
27
30
  };
28
31
  }
29
32
 
30
33
  static get observedAttributes() {
31
- return ['animation-style', 'animation-speed'];
34
+ return ['animation-style', 'animation-speed', 'piece-gap', 'camera-speed'];
32
35
  }
33
36
 
34
37
  attributeChangedCallback(name, oldVal, newVal) {
35
38
  if (name === 'animation-style') {
36
- this.settings.style = newVal;
39
+ this.settings.animationStyle = newVal;
37
40
  }
38
41
  if (name === 'animation-speed') {
39
- this.settings.speed = Number(newVal);
42
+ var speed = Number(newVal);
43
+ this.settings.animationSpeed = speed > 0 ? speed : 0;
40
44
  }
41
- if (name === 'gap') {
42
- this.settings.gap = Number(newVal);
45
+ if (name === 'piece-gap') {
46
+ var gap = Number(newVal);
47
+ this.settings.gap = gap < minimumGap ? minimumGap : gap;
48
+ }
49
+ if (name === 'camera-speed') {
50
+ var speed = Number(newVal);
51
+ this.settings.cameraSpeed = speed > 0 ? speed : 0;
43
52
  }
44
53
  }
45
54
  connectedCallback() {
@@ -71,6 +80,8 @@ class RubiksCube extends HTMLElement {
71
80
 
72
81
  // add camera
73
82
  const camera = new PerspectiveCamera(75, this.clientWidth / this.clientHeight, 0.1, 1000);
83
+ /** @type {{Up: boolean, Right: boolean, UpDistance: number, RightDistance: number}} */
84
+ const cameraState = { Up: true, Right: true, UpDistance: 2.5, RightDistance: 2.5 };
74
85
  camera.position.z = 4;
75
86
  camera.position.y = 3;
76
87
  camera.position.x = 0;
@@ -115,102 +126,53 @@ class RubiksCube extends HTMLElement {
115
126
  function animate() {
116
127
  cameraAnimationGroup.update();
117
128
  controls.update();
118
- cube.update();
129
+
130
+ var cubeState = cube.update();
131
+ if (cubeState) {
132
+ sendState();
133
+ }
119
134
  renderer.render(scene, camera);
120
135
  }
121
136
 
137
+ // add event listeners for rotation and camera controls
122
138
  this.addEventListener('reset', () => {
123
139
  cube.reset();
124
140
  sendState();
125
141
  });
126
142
 
127
- // add event listeners for rotation and camera controls
128
143
  this.addEventListener('rotate', (e) => {
129
144
  const action = getRotationDetailsFromNotation(e.detail.action);
130
145
  if (action !== undefined) {
131
146
  cube.rotate(action);
132
147
  }
133
148
  });
149
+
134
150
  this.addEventListener('camera', (e) => {
135
151
  if (e.detail.action === 'peek-toggle-horizontal') {
136
- cameraAnimationGroup.add(
137
- new Tween(camera.position)
138
- .to(
139
- {
140
- x: camera.position.x > 0 ? -2.5 : 2.5,
141
- y: camera.position.y > 0 ? 2.5 : -2.5,
142
- z: 4,
143
- },
144
- 200,
145
- )
146
- .start(),
147
- );
152
+ cameraState.Right = !cameraState.Right;
148
153
  } else if (e.detail.action === 'peek-toggle-vertical') {
149
- cameraAnimationGroup.add(
150
- new Tween(camera.position)
151
- .to(
152
- {
153
- x: camera.position.x > 0 ? 2.5 : -2.5,
154
- y: camera.position.y > 0 ? -2.5 : 2.5,
155
- z: 4,
156
- },
157
- 200,
158
- )
159
- .start(),
160
- );
154
+ cameraState.Up = !cameraState.Up;
161
155
  } else if (e.detail.action === 'peek-right') {
162
- cameraAnimationGroup.add(
163
- new Tween(camera.position)
164
- .to(
165
- {
166
- x: 2.5,
167
- y: camera.position.y > 0 ? 2.5 : -2.5,
168
- z: 4,
169
- },
170
- 200,
171
- )
172
- .start(),
173
- );
156
+ cameraState.Right = true;
174
157
  } else if (e.detail.action === 'peek-left') {
175
- cameraAnimationGroup.add(
176
- new Tween(camera.position)
177
- .to(
178
- {
179
- x: -2.5,
180
- y: camera.position.y > 0 ? 2.5 : -2.5,
181
- z: 4,
182
- },
183
- 200,
184
- )
185
- .start(),
186
- );
158
+ cameraState.Right = false;
187
159
  } else if (e.detail.action === 'peek-up') {
188
- cameraAnimationGroup.add(
189
- new Tween(camera.position)
190
- .to(
191
- {
192
- x: camera.position.x > 0 ? 2.5 : -2.5,
193
- y: 2.5,
194
- z: 4,
195
- },
196
- 200,
197
- )
198
- .start(),
199
- );
160
+ cameraState.Up = true;
200
161
  } else if (e.detail.action === 'peek-down') {
201
- cameraAnimationGroup.add(
202
- new Tween(camera.position)
203
- .to(
204
- {
205
- x: camera.position.x > 0 ? 2.5 : -2.5,
206
- y: -2.5,
207
- z: 4,
208
- },
209
- 200,
210
- )
211
- .start(),
212
- );
162
+ cameraState.Up = false;
213
163
  }
164
+ cameraAnimationGroup.add(
165
+ new Tween(camera.position)
166
+ .to(
167
+ {
168
+ x: cameraState.Right ? cameraState.RightDistance : -cameraState.RightDistance,
169
+ y: cameraState.Up ? cameraState.UpDistance : -cameraState.UpDistance,
170
+ z: 4,
171
+ },
172
+ this.settings.cameraSpeed,
173
+ )
174
+ .start(),
175
+ );
214
176
  });
215
177
  }
216
178
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houstonp/rubiks-cube",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Rubiks Cube Web Component built with threejs",
5
5
  "main": "index.js",
6
6
  "author": "Houston Pearse",
package/src/cube/cube.js CHANGED
@@ -3,28 +3,34 @@ import { createCoreMesh } from '../threejs/pieces';
3
3
  import { createCubeState } from './cubeState';
4
4
  import { CubeRotation } from './cubeRotation';
5
5
 
6
- const minimumGap = 1;
7
-
8
6
  export default class Cube {
9
7
  /**
10
- * @param {{style: "exponential" | "next" | "fixed", speed: number, gap: number}} params
8
+ * @param {{style: "exponential" | "next" | "fixed", speed: number, gap: number}} settings
11
9
  */
12
- constructor({ gap, speed, style }) {
13
- /** @type {number} */
14
- this.gap = gap < minimumGap ? minimumGap : gap;
10
+ constructor(settings) {
11
+ /** @type {{animationStyle: "match" | "exponential" | "next" | "fixed", animationSpeed: number, gap: number}} */
12
+ this.settings = settings;
15
13
  /** @type {Group} */
16
- this.group = new Group();
14
+ this.group = this.createCubeGroup();
17
15
  /** @type {Group} */
18
16
  this.rotationGroup = new Group();
19
17
  /** @type {CubeRotation[]} */
20
18
  this.rotationQueue = [];
21
19
  /** @type {CubeRotation | undefined} */
22
20
  this.currentRotation = undefined;
21
+ /** @type {number | undefined} */
22
+ this._matchSpeed = undefined;
23
23
  /** @type {number} */
24
- this.animationSpeed = speed;
25
- /** @type {"exponential" | "next" | "fixed"} */
26
- this.animationStyle = style;
24
+ this._lastGap = settings.gap;
25
+ }
27
26
 
27
+ /**
28
+ * creates a ThreeJS group with all the required pieces for a cube
29
+ * @param {Group} group
30
+ * @returns {Group}
31
+ */
32
+ createCubeGroup(group) {
33
+ var group = new Group();
28
34
  const core = createCoreMesh();
29
35
  core.userData = {
30
36
  position: { x: 0, y: 0, z: 0 },
@@ -33,11 +39,11 @@ export default class Cube {
33
39
  initialRotation: { x: 0, y: 0, z: 0 },
34
40
  type: 'core',
35
41
  };
36
- this.group.add(core);
42
+ group.add(core);
37
43
 
38
44
  for (const piece of createCubeState()) {
39
45
  var pieceGroup = piece.group;
40
- pieceGroup.position.set(piece.position.x * this.gap, piece.position.y * this.gap, piece.position.z * this.gap);
46
+ pieceGroup.position.set(piece.position.x * this.settings.gap, piece.position.y * this.settings.gap, piece.position.z * this.settings.gap);
41
47
  pieceGroup.rotation.set(piece.rotation.x, piece.rotation.y, piece.rotation.z);
42
48
  pieceGroup.userData = {
43
49
  position: Object.assign({}, piece.position),
@@ -46,52 +52,111 @@ export default class Cube {
46
52
  initialRotation: Object.assign({}, piece.rotation),
47
53
  type: piece.type,
48
54
  };
49
- this.group.add(pieceGroup);
55
+ group.add(pieceGroup);
50
56
  }
57
+ return group;
51
58
  }
52
59
 
60
+ /**
61
+ * update the cube and continue any rotations
62
+ * @returns {{ up: string[][], down: string[][], front: string[][], back: string[][], left: string[][], right: string[][] } | undefined }
63
+ */
53
64
  update() {
54
65
  if (this.currentRotation === undefined) {
66
+ if (this._lastGap !== this.settings.gap) {
67
+ this.updateGap();
68
+ }
55
69
  this.currentRotation = this.rotationQueue.shift();
56
- if (this.currentRotation === undefined) return;
70
+ if (this.currentRotation === undefined) {
71
+ this._matchSpeed = undefined; // reset speed for the match animation options
72
+ return undefined;
73
+ }
74
+ }
75
+ if (this.currentRotation.status === 'pending') {
57
76
  this.rotationGroup.add(...this.getRotationLayer(this.currentRotation.rotation));
58
77
  this.currentRotation.initialise();
59
78
  }
60
-
79
+ if (this.currentRotation.status === 'initialised') {
80
+ var speed = this.getRotationSpeed();
81
+ this.currentRotation.update(this.rotationGroup, speed);
82
+ }
61
83
  if (this.currentRotation.status === 'complete') {
62
84
  this.clearRotationGroup();
63
- this.currentRotation.dispose();
64
- this.currentRotation = this.rotationQueue.shift();
65
- if (this.currentRotation === undefined) return;
66
- this.rotationGroup.add(...this.getRotationLayer(this.currentRotation.rotation));
67
- this.currentRotation.initialise();
85
+ this.currentRotation = undefined;
86
+ return this.getStickerState();
68
87
  }
88
+ return undefined;
89
+ }
69
90
 
70
- this.currentRotation.update(this.rotationGroup, this.getRotationSpeed());
91
+ /**
92
+ * Updates the gap of the pieces. To be used when the cube is not rotating
93
+ * @returns {void}
94
+ */
95
+ updateGap() {
96
+ if (this.currentRotation === undefined) {
97
+ this.group.children.forEach((piece) => {
98
+ var { x, y, z } = piece.userData.position;
99
+ piece.position.set(x * this.settings.gap, y * this.settings.gap, z * this.settings.gap);
100
+ });
101
+ this._lastGap = this.settings.gap;
102
+ }
71
103
  }
72
104
 
105
+ /**
106
+ *
107
+ * calculates the current speed of the current rotation in ms.
108
+ * calculation is dependent on animation style and animation speed settings
109
+ * - exponential: speeds up rotations depending on the queue length
110
+ * - next: an animation speed of 0 when there is another animation in the queue
111
+ * - match: will match the speed of rotations to the frequency of key presses.
112
+ * - fixed: will return a constant value
113
+ * @returns {number}
114
+ */
73
115
  getRotationSpeed() {
74
- if (this.animationStyle == 'exponential') {
75
- return this.animationSpeed / 2 ** this.rotationQueue.length;
116
+ if (this.settings.animationStyle === 'exponential') {
117
+ return this.settings.animationSpeed / 2 ** this.rotationQueue.length;
118
+ }
119
+ if (this.settings.animationStyle === 'next') {
120
+ return this.rotationQueue.length > 0 ? 0 : this.settings.animationSpeed;
121
+ }
122
+ if (this.settings.animationStyle === 'match') {
123
+ if (this.rotationQueue.length > 0) {
124
+ var lastTimeStamp = this.currentRotation.timestampMs;
125
+ var minGap = this._matchSpeed ?? this.settings.animationSpeed;
126
+ for (var i = 0; i < this.rotationQueue.length; i++) {
127
+ var gap = this.rotationQueue[i].timestampMs - lastTimeStamp;
128
+ if (gap < minGap) {
129
+ minGap = gap;
130
+ }
131
+ }
132
+ this._matchSpeed = minGap;
133
+ }
134
+ if (this._matchSpeed !== undefined) {
135
+ return this._matchSpeed;
136
+ }
137
+ return this.settings.animationSpeed;
76
138
  }
77
- if (this.animationStyle == 'next') {
78
- return this.rotationQueue.length > 0 ? 0 : this.animationSpeed;
139
+ if (this.settings.animationStyle === 'fixed') {
140
+ return this.settings.animationSpeed;
79
141
  }
80
- return this.animationSpeed;
142
+ return this.settings.animationSpeed;
81
143
  }
82
144
 
145
+ /**
146
+ * Complete the current rotation and reset the cube
147
+ * @returns {void}
148
+ */
83
149
  reset() {
84
150
  this.rotationQueue = [];
85
151
  if (this.currentRotation) {
86
152
  this.currentRotation.update(this.rotationGroup, 0);
87
153
  this.clearRotationGroup();
88
- this.currentRotation.dispose();
89
154
  this.currentRotation = undefined;
90
155
  }
91
156
  this.group.children.forEach((piece) => {
92
157
  const { x, y, z } = piece.userData.initialPosition;
93
158
  const { x: u, y: v, z: w } = piece.userData.initialRotation;
94
- piece.position.set(x * this.gap, y * this.gap, z * this.gap);
159
+ piece.position.set(x * this.settings.gap, y * this.settings.gap, z * this.settings.gap);
95
160
  piece.rotation.set(u, v, w);
96
161
  piece.userData.position.x = x;
97
162
  piece.userData.position.y = y;
@@ -102,6 +167,10 @@ export default class Cube {
102
167
  });
103
168
  }
104
169
 
170
+ /**
171
+ * Adds pieces in the rotationGroup back into the main group.
172
+ * @returns {void}
173
+ */
105
174
  clearRotationGroup() {
106
175
  if (this.currentRotation.status != 'complete') {
107
176
  throw Error('cannot clear rotation group while rotating');
@@ -121,12 +190,16 @@ export default class Cube {
121
190
  });
122
191
  this.group.add(...this.rotationGroup.children);
123
192
  this.rotationGroup.rotation.set(0, 0, 0);
193
+ this.currentRotation.status = 'disposed';
124
194
  }
125
195
 
126
196
  /**
127
197
  * @param {{axis: "x"|"y"|"z", layers: (-1|0|1)[], direction: 1|-1|2|-2}} input
128
198
  */
129
199
  rotate(input) {
200
+ var queueLength = this.rotationQueue.length;
201
+ if (queueLength > 0 && this.rotationQueue[queueLength - 1].rotation.axis === input.axis) {
202
+ }
130
203
  this.rotationQueue.push(new CubeRotation(input));
131
204
  }
132
205
 
@@ -150,6 +223,9 @@ export default class Cube {
150
223
  });
151
224
  }
152
225
 
226
+ /**
227
+ * @returns {{ up: string[][], down: string[][], front: string[][], back: string[][], left: string[][], right: string[][] }}
228
+ */
153
229
  getStickerState() {
154
230
  const state = {
155
231
  up: [[], [], []],
@@ -10,22 +10,18 @@ export class CubeRotation {
10
10
  /** @type {"pending" | "initialised" | "complete" | "disposed"} */
11
11
  this.status = 'pending';
12
12
  /** @type {number} */
13
+ this.timestampMs = performance.now();
14
+ /** @type {number} */
13
15
  this._lastUpdatedTimeMs = undefined;
14
- this._startTimeMs = undefined;
15
16
  /** @type {number} */
16
17
  this._rotationPercentage = 0;
17
18
  }
18
19
 
19
20
  initialise() {
20
21
  this._lastUpdatedTimeMs = performance.now();
21
- this._startTimeMs = this._lastUpdatedTimeMs;
22
22
  this.status = 'initialised';
23
23
  }
24
24
 
25
- dispose() {
26
- this.status = 'disposed';
27
- }
28
-
29
25
  /**
30
26
  *
31
27
  * @param {Group} rotationGroup
@@ -53,8 +49,12 @@ export class CubeRotation {
53
49
  rotationIncrement,
54
50
  );
55
51
 
56
- if (this._rotationPercentage >= 100) {
52
+ if (this._rotationPercentage === 100) {
57
53
  this.status = 'complete';
58
54
  }
55
+
56
+ if (this._rotationPercentage > 100) {
57
+ throw new Error('rotation percentage > 100');
58
+ }
59
59
  }
60
60
  }