@houstonp/rubiks-cube 1.5.2 → 2.1.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 +280 -170
- package/package.json +33 -3
- package/src/camera/cameraState.js +81 -0
- package/src/core.js +447 -0
- package/src/cube/animationSlice.js +205 -0
- package/src/cube/animationState.js +96 -0
- package/src/cube/cubeSettings.js +19 -0
- package/src/cube/cubeState.js +285 -139
- package/src/cube/stickerState.js +188 -0
- package/src/debouncer.js +16 -0
- package/src/globals.ts +9 -0
- package/src/index.js +621 -0
- package/src/settings.js +138 -0
- package/src/three/centerPiece.js +44 -0
- package/src/three/cornerPiece.js +60 -0
- package/src/three/cube.js +492 -0
- package/src/three/edgePiece.js +50 -0
- package/src/three/sticker.js +37 -0
- package/tests/common.js +27 -0
- package/tests/cube.five.test.js +126 -0
- package/tests/cube.four.test.js +126 -0
- package/tests/cube.seven.test.js +126 -0
- package/tests/cube.six.test.js +126 -0
- package/tests/cube.three.test.js +151 -0
- package/tests/cube.two.test.js +125 -0
- package/tests/setup.js +36 -0
- package/types/camera/cameraState.d.ts +19 -0
- package/types/core.d.ts +454 -0
- package/types/cube/animationSlice.d.ts +26 -0
- package/types/cube/animationState.d.ts +41 -0
- package/types/cube/cubeSettings.d.ts +17 -0
- package/types/cube/cubeState.d.ts +47 -0
- package/types/cube/stickerState.d.ts +21 -0
- package/types/debouncer.d.ts +13 -0
- package/types/globals.d.ts +7 -0
- package/types/index.d.ts +87 -0
- package/types/settings.d.ts +38 -0
- package/types/three/centerPiece.d.ts +15 -0
- package/types/three/cornerPiece.d.ts +24 -0
- package/types/three/cube.d.ts +130 -0
- package/types/three/edgePiece.d.ts +16 -0
- package/types/three/sticker.d.ts +15 -0
- package/.prettierrc +0 -7
- package/index.js +0 -274
- package/src/cube/cube.js +0 -276
- package/src/cube/cubeRotation.js +0 -63
- package/src/threejs/materials.js +0 -42
- package/src/threejs/pieces.js +0 -103
- package/src/threejs/stickers.js +0 -48
- package/src/utils/debouncer.js +0 -7
- package/src/utils/rotation.js +0 -53
package/index.js
DELETED
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import { Scene, WebGLRenderer, PerspectiveCamera, AmbientLight, DirectionalLight, Spherical } from 'three';
|
|
2
|
-
import { OrbitControls } from 'three/examples/jsm/Addons.js';
|
|
3
|
-
import Cube from './src/cube/cube';
|
|
4
|
-
import getRotationDetailsFromNotation from './src/utils/rotation';
|
|
5
|
-
import { debounce } from './src/utils/debouncer';
|
|
6
|
-
import gsap from 'gsap';
|
|
7
|
-
|
|
8
|
-
/** @typedef {{ animationStyle: "exponential" | "next" | "fixed" | "match", animationSpeedMs: number, pieceGap: number, cameraSpeedMs: number, cameraRadius: number, cameraPeekAngleHorizontal: number, cameraPeekAngleVertical: number, cameraFieldOfView: number }} Settings */
|
|
9
|
-
/** @type {Settings} */
|
|
10
|
-
const defaultSettings = {
|
|
11
|
-
animationSpeedMs: 100,
|
|
12
|
-
animationStyle: 'fixed',
|
|
13
|
-
pieceGap: 1.04,
|
|
14
|
-
cameraSpeedMs: 100,
|
|
15
|
-
cameraRadius: 5,
|
|
16
|
-
cameraPeekAngleHorizontal: 0.6,
|
|
17
|
-
cameraPeekAngleVertical: 0.6,
|
|
18
|
-
cameraFieldOfView: 75,
|
|
19
|
-
};
|
|
20
|
-
const minGap = 1;
|
|
21
|
-
const minRadius = 4;
|
|
22
|
-
const minFieldOfView = 30;
|
|
23
|
-
const maxFieldOfView = 100;
|
|
24
|
-
const maxAzimuthAngle = (5 * Math.PI) / 16;
|
|
25
|
-
const polarAngleOffset = Math.PI / 2;
|
|
26
|
-
const maxPolarAngle = (5 * Math.PI) / 16;
|
|
27
|
-
|
|
28
|
-
class RubiksCube extends HTMLElement {
|
|
29
|
-
constructor() {
|
|
30
|
-
super();
|
|
31
|
-
this.attachShadow({ mode: 'open' });
|
|
32
|
-
this.shadowRoot.innerHTML = `<canvas id="cube-canvas" style="display:block;"></canvas>`;
|
|
33
|
-
this.canvas = this.shadowRoot.getElementById('cube-canvas');
|
|
34
|
-
/** @type {Settings} */
|
|
35
|
-
this.settings = {
|
|
36
|
-
animationSpeedMs: this.getAttribute('animation-speed-ms') || defaultSettings.animationSpeedMs,
|
|
37
|
-
animationStyle: this.getAttribute('animation-style') || defaultSettings.animationStyle,
|
|
38
|
-
pieceGap: this.getAttribute('piece-gap') || defaultSettings.pieceGap,
|
|
39
|
-
cameraSpeedMs: this.getAttribute('camera-speed-ms') || defaultSettings.cameraSpeedMs,
|
|
40
|
-
cameraRadius: this.getAttribute('camera-radius') || defaultSettings.cameraRadius,
|
|
41
|
-
cameraPeekAngleHorizontal: this.getAttribute('camera-peek-angle-horizontal') || defaultSettings.cameraPeekAngleHorizontal,
|
|
42
|
-
cameraPeekAngleVertical: this.getAttribute('camera-peek-angle-vertical') || defaultSettings.cameraPeekAngleVertical,
|
|
43
|
-
cameraFieldOfView: this.getAttribute('camera-field-of-view') || defaultSettings.cameraFieldOfView,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
static get observedAttributes() {
|
|
48
|
-
return [
|
|
49
|
-
'animation-style',
|
|
50
|
-
'animation-speed-ms',
|
|
51
|
-
'piece-gap',
|
|
52
|
-
'camera-speed-ms',
|
|
53
|
-
'camera-radius',
|
|
54
|
-
'camera-peek-angle-horizontal',
|
|
55
|
-
'camera-peek-angle-vertical',
|
|
56
|
-
'camera-field-of-view',
|
|
57
|
-
];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
attributeChangedCallback(name, oldVal, newVal) {
|
|
61
|
-
if (name === 'animation-style') {
|
|
62
|
-
this.settings.animationStyle = newVal;
|
|
63
|
-
}
|
|
64
|
-
if (name === 'animation-speed-ms') {
|
|
65
|
-
var speed = Number(newVal);
|
|
66
|
-
this.settings.animationSpeedMs = speed > 0 ? speed : 0;
|
|
67
|
-
}
|
|
68
|
-
if (name === 'piece-gap') {
|
|
69
|
-
var gap = Number(newVal);
|
|
70
|
-
this.settings.pieceGap = gap < minGap ? minGap : gap;
|
|
71
|
-
}
|
|
72
|
-
if (name === 'camera-speed-ms') {
|
|
73
|
-
var speed = Number(newVal);
|
|
74
|
-
this.settings.cameraSpeedMs = speed > 0 ? speed : 0;
|
|
75
|
-
}
|
|
76
|
-
if (name === 'camera-radius') {
|
|
77
|
-
var radius = Number(newVal);
|
|
78
|
-
this.settings.cameraRadius = radius < minRadius ? minRadius : radius;
|
|
79
|
-
if (oldVal !== newVal && oldVal !== null) {
|
|
80
|
-
this.dispatchEvent(new CustomEvent('cameraSettingsChanged'));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (name === 'camera-peek-angle-horizontal') {
|
|
84
|
-
var angle = Number(newVal);
|
|
85
|
-
angle = angle > 0 ? angle : 0;
|
|
86
|
-
angle = angle < 1 ? angle : 1;
|
|
87
|
-
this.settings.cameraPeekAngleHorizontal = angle > 0 ? angle : 0;
|
|
88
|
-
if (oldVal !== newVal && oldVal !== null) {
|
|
89
|
-
this.dispatchEvent(new CustomEvent('cameraSettingsChanged'));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (name === 'camera-peek-angle-vertical') {
|
|
93
|
-
var angle = Number(newVal);
|
|
94
|
-
angle = angle > 0 ? angle : 0;
|
|
95
|
-
angle = angle < 1 ? angle : 1;
|
|
96
|
-
this.settings.cameraPeekAngleVertical = angle;
|
|
97
|
-
if (oldVal !== newVal && oldVal !== null) {
|
|
98
|
-
this.dispatchEvent(new CustomEvent('cameraSettingsChanged'));
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (name == 'camera-field-of-view') {
|
|
102
|
-
var fov = Number(newVal);
|
|
103
|
-
fov = fov > minFieldOfView ? fov : minFieldOfView;
|
|
104
|
-
fov = fov < maxFieldOfView ? fov : maxFieldOfView;
|
|
105
|
-
this.settings.cameraFieldOfView = fov;
|
|
106
|
-
if (oldVal !== newVal && oldVal !== null) {
|
|
107
|
-
this.dispatchEvent(new CustomEvent('cameraFieldOfViewChanged'));
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
connectedCallback() {
|
|
113
|
-
this.init();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
init() {
|
|
117
|
-
// defined core threejs objects
|
|
118
|
-
const canvas = this.canvas;
|
|
119
|
-
const scene = new Scene();
|
|
120
|
-
const renderer = new WebGLRenderer({
|
|
121
|
-
alpha: true,
|
|
122
|
-
canvas,
|
|
123
|
-
antialias: true,
|
|
124
|
-
});
|
|
125
|
-
renderer.setSize(this.clientWidth, this.clientHeight);
|
|
126
|
-
renderer.setAnimationLoop(animate);
|
|
127
|
-
renderer.setPixelRatio(2);
|
|
128
|
-
|
|
129
|
-
//update renderer and camera when container resizes. debouncing events to reduce frequency
|
|
130
|
-
new ResizeObserver(
|
|
131
|
-
debounce((entries) => {
|
|
132
|
-
const { width, height } = entries[0].contentRect;
|
|
133
|
-
camera.aspect = width / height;
|
|
134
|
-
camera.updateProjectionMatrix();
|
|
135
|
-
renderer.setSize(width, height);
|
|
136
|
-
}, 30),
|
|
137
|
-
).observe(this);
|
|
138
|
-
|
|
139
|
-
// add camera
|
|
140
|
-
const camera = new PerspectiveCamera(this.settings.cameraFieldOfView, this.clientWidth / this.clientHeight, 1, 2000);
|
|
141
|
-
const cameraSpherical = new Spherical(50, (3 * Math.PI) / 8, -Math.PI / 4);
|
|
142
|
-
camera.position.setFromSpherical(cameraSpherical);
|
|
143
|
-
/** @type {{ Up: boolean, Right: boolean }} */
|
|
144
|
-
const cameraState = { Up: true, Right: true };
|
|
145
|
-
|
|
146
|
-
// add orbit controls for camera
|
|
147
|
-
const controls = new OrbitControls(camera, renderer.domElement);
|
|
148
|
-
controls.enableZoom = false;
|
|
149
|
-
controls.enablePan = false;
|
|
150
|
-
controls.enableDamping = true;
|
|
151
|
-
controls.maxAzimuthAngle = maxAzimuthAngle;
|
|
152
|
-
controls.minAzimuthAngle = -maxAzimuthAngle;
|
|
153
|
-
controls.maxPolarAngle = polarAngleOffset + maxPolarAngle;
|
|
154
|
-
controls.minPolarAngle = polarAngleOffset - maxPolarAngle;
|
|
155
|
-
|
|
156
|
-
// add lighting to scene
|
|
157
|
-
const ambientLight = new AmbientLight('white', 0.5);
|
|
158
|
-
const spotLight1 = new DirectionalLight('white', 2);
|
|
159
|
-
const spotLight2 = new DirectionalLight('white', 2);
|
|
160
|
-
const spotLight3 = new DirectionalLight('white', 2);
|
|
161
|
-
const spotLight4 = new DirectionalLight('white', 2);
|
|
162
|
-
spotLight1.position.set(5, 5, 5);
|
|
163
|
-
spotLight2.position.set(-5, 5, 5);
|
|
164
|
-
spotLight3.position.set(5, -5, 0);
|
|
165
|
-
spotLight4.position.set(-10, -5, -5);
|
|
166
|
-
scene.add(ambientLight, spotLight1, spotLight2, spotLight3, spotLight4);
|
|
167
|
-
|
|
168
|
-
// create cube and add to scene
|
|
169
|
-
const cube = new Cube(this.settings);
|
|
170
|
-
scene.add(cube.group, cube.rotationGroup);
|
|
171
|
-
|
|
172
|
-
const sendState = (eventId) => {
|
|
173
|
-
const event = new CustomEvent('state', { detail: { eventId, state: cube.currentState } });
|
|
174
|
-
this.dispatchEvent(event);
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
// animation loop
|
|
178
|
-
function animate() {
|
|
179
|
-
controls.update();
|
|
180
|
-
|
|
181
|
-
var eventId = cube.update();
|
|
182
|
-
if (eventId) {
|
|
183
|
-
sendState(eventId);
|
|
184
|
-
}
|
|
185
|
-
renderer.render(scene, camera);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// add event listeners for rotation and camera controls
|
|
189
|
-
this.addEventListener('reset', () => {
|
|
190
|
-
cube.reset();
|
|
191
|
-
sendState('reset');
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
this.addEventListener('action', (e) => {
|
|
195
|
-
/** @type {{eventId: string, action: {type: "movement" | "camera" | "rotation", actionId: string }}} move */
|
|
196
|
-
var move = e.detail.move;
|
|
197
|
-
if (move.action.type === 'camera') {
|
|
198
|
-
updateCameraState(move.action.actionId);
|
|
199
|
-
updateCameraPosition();
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
if (move.action.type === 'movement' || move.action.type === 'rotation') {
|
|
203
|
-
handleRotationAction(move.eventId, move.action.actionId);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* @param {string} eventId
|
|
210
|
-
* @param {string} actionId
|
|
211
|
-
*/
|
|
212
|
-
const handleRotationAction = (eventId, actionId) => {
|
|
213
|
-
const rotationDetails = getRotationDetailsFromNotation(actionId);
|
|
214
|
-
if (rotationDetails !== undefined) {
|
|
215
|
-
cube.rotate(eventId, rotationDetails);
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* @param {'peek-toggle-horizontal' | 'peek-toggle-vertical' | 'peek-right' | 'peek-left' | 'peek-up' | 'peek-down' } actionId
|
|
221
|
-
*/
|
|
222
|
-
const updateCameraState = (actionId) => {
|
|
223
|
-
if (actionId === 'peek-toggle-horizontal') {
|
|
224
|
-
cameraState.Right = !cameraState.Right;
|
|
225
|
-
} else if (actionId === 'peek-toggle-vertical') {
|
|
226
|
-
cameraState.Up = !cameraState.Up;
|
|
227
|
-
} else if (actionId === 'peek-right') {
|
|
228
|
-
cameraState.Right = true;
|
|
229
|
-
} else if (actionId === 'peek-left') {
|
|
230
|
-
cameraState.Right = false;
|
|
231
|
-
} else if (actionId === 'peek-up') {
|
|
232
|
-
cameraState.Up = true;
|
|
233
|
-
} else if (actionId === 'peek-down') {
|
|
234
|
-
cameraState.Up = false;
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* @param {number | null} cameraSpeedMs
|
|
240
|
-
* @param {gsap.EaseString | gsap.EaseFunction | undefined} ease
|
|
241
|
-
*/
|
|
242
|
-
const updateCameraPosition = (cameraSpeedMs = this.settings.cameraSpeedMs, ease = 'none') => {
|
|
243
|
-
cameraSpeedMs = cameraSpeedMs ? cameraSpeedMs : this.settings.cameraSpeedMs;
|
|
244
|
-
var phi = polarAngleOffset + (cameraState.Up ? -this.settings.cameraPeekAngleVertical : this.settings.cameraPeekAngleVertical) * maxPolarAngle;
|
|
245
|
-
var theta = (cameraState.Right ? this.settings.cameraPeekAngleHorizontal : -this.settings.cameraPeekAngleHorizontal) * maxAzimuthAngle;
|
|
246
|
-
const startSpherical = new Spherical().setFromVector3(camera.position);
|
|
247
|
-
const targetSpherical = new Spherical(this.settings.cameraRadius, phi, theta);
|
|
248
|
-
gsap.to(startSpherical, {
|
|
249
|
-
radius: targetSpherical.radius,
|
|
250
|
-
theta: targetSpherical.theta,
|
|
251
|
-
phi: targetSpherical.phi,
|
|
252
|
-
duration: cameraSpeedMs / 1000,
|
|
253
|
-
ease: ease,
|
|
254
|
-
onUpdate: function () {
|
|
255
|
-
camera.position.setFromSpherical(startSpherical);
|
|
256
|
-
camera.lookAt(cube.group.position);
|
|
257
|
-
controls.update();
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
this.addEventListener('cameraSettingsChanged', () => {
|
|
263
|
-
updateCameraPosition(); // animate settings changes
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
this.addEventListener('cameraFieldOfViewChanged', () => {
|
|
267
|
-
camera.fov = this.settings.cameraFieldOfView;
|
|
268
|
-
camera.updateProjectionMatrix();
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
updateCameraPosition(1000, 'power4.inOut'); // initial animation
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
customElements.define('rubiks-cube', RubiksCube);
|
package/src/cube/cube.js
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import { Group, Vector3 } from 'three';
|
|
2
|
-
import { createCoreMesh } from '../threejs/pieces';
|
|
3
|
-
import { createCubeState } from './cubeState';
|
|
4
|
-
import { CubeRotation } from './cubeRotation';
|
|
5
|
-
|
|
6
|
-
export default class Cube {
|
|
7
|
-
/**
|
|
8
|
-
* @param {import('../..').Settings} settings
|
|
9
|
-
*/
|
|
10
|
-
constructor(settings) {
|
|
11
|
-
/** @type {import('../..').Settings} */
|
|
12
|
-
this.settings = settings;
|
|
13
|
-
/** @type {Group} */
|
|
14
|
-
this.group = this.createCubeGroup();
|
|
15
|
-
/** @type {Group} */
|
|
16
|
-
this.rotationGroup = new Group();
|
|
17
|
-
/** @type {CubeRotation[]} */
|
|
18
|
-
this.rotationQueue = [];
|
|
19
|
-
/** @type {CubeRotation | undefined} */
|
|
20
|
-
this.currentRotation = undefined;
|
|
21
|
-
/** @type {{ up: string[][], down: string[][], front: string[][], back: string[][], left: string[][], right: string[][] }} */
|
|
22
|
-
this.currentState = this.getStickerState();
|
|
23
|
-
/** @type {number | undefined} */
|
|
24
|
-
this._matchSpeed = undefined;
|
|
25
|
-
/** @type {number} */
|
|
26
|
-
this._lastGap = settings.gap;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* creates a ThreeJS group with all the required pieces for a cube
|
|
31
|
-
* @param {Group} group
|
|
32
|
-
* @returns {Group}
|
|
33
|
-
*/
|
|
34
|
-
createCubeGroup(group) {
|
|
35
|
-
var group = new Group();
|
|
36
|
-
const core = createCoreMesh();
|
|
37
|
-
core.userData = {
|
|
38
|
-
position: { x: 0, y: 0, z: 0 },
|
|
39
|
-
rotation: { x: 0, y: 0, z: 0 },
|
|
40
|
-
initialPosition: { x: 0, y: 0, z: 0 },
|
|
41
|
-
initialRotation: { x: 0, y: 0, z: 0 },
|
|
42
|
-
type: 'core',
|
|
43
|
-
};
|
|
44
|
-
group.add(core);
|
|
45
|
-
|
|
46
|
-
for (const piece of createCubeState()) {
|
|
47
|
-
var pieceGroup = piece.group;
|
|
48
|
-
pieceGroup.position.set(
|
|
49
|
-
piece.position.x * this.settings.pieceGap,
|
|
50
|
-
piece.position.y * this.settings.pieceGap,
|
|
51
|
-
piece.position.z * this.settings.pieceGap,
|
|
52
|
-
);
|
|
53
|
-
pieceGroup.rotation.set(piece.rotation.x, piece.rotation.y, piece.rotation.z);
|
|
54
|
-
pieceGroup.userData = {
|
|
55
|
-
position: Object.assign({}, piece.position),
|
|
56
|
-
rotation: Object.assign({}, piece.rotation),
|
|
57
|
-
initialPosition: Object.assign({}, piece.position),
|
|
58
|
-
initialRotation: Object.assign({}, piece.rotation),
|
|
59
|
-
type: piece.type,
|
|
60
|
-
};
|
|
61
|
-
group.add(pieceGroup);
|
|
62
|
-
}
|
|
63
|
-
return group;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* update the cube and continue any rotations
|
|
68
|
-
* @returns {string}
|
|
69
|
-
*/
|
|
70
|
-
update() {
|
|
71
|
-
if (this.currentRotation === undefined) {
|
|
72
|
-
if (this._lastGap !== this.settings.pieceGap) {
|
|
73
|
-
this.updateGap();
|
|
74
|
-
}
|
|
75
|
-
this.currentRotation = this.rotationQueue.shift();
|
|
76
|
-
if (this.currentRotation === undefined) {
|
|
77
|
-
this._matchSpeed = undefined; // reset speed for the match animation options
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if (this.currentRotation.status === 'pending') {
|
|
82
|
-
this.rotationGroup.add(...this.getRotationLayer(this.currentRotation.rotation));
|
|
83
|
-
this.currentRotation.initialise();
|
|
84
|
-
}
|
|
85
|
-
if (this.currentRotation.status === 'initialised') {
|
|
86
|
-
var speed = this.getRotationSpeed();
|
|
87
|
-
this.currentRotation.update(this.rotationGroup, speed);
|
|
88
|
-
}
|
|
89
|
-
if (this.currentRotation.status === 'complete') {
|
|
90
|
-
this.clearRotationGroup();
|
|
91
|
-
var eventId = this.currentRotation.eventId;
|
|
92
|
-
this.currentRotation = undefined;
|
|
93
|
-
this.currentState = this.getStickerState();
|
|
94
|
-
return eventId;
|
|
95
|
-
}
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Updates the gap of the pieces. To be used when the cube is not rotating
|
|
101
|
-
* @returns {void}
|
|
102
|
-
*/
|
|
103
|
-
updateGap() {
|
|
104
|
-
if (this.currentRotation === undefined) {
|
|
105
|
-
this.group.children.forEach((piece) => {
|
|
106
|
-
var { x, y, z } = piece.userData.position;
|
|
107
|
-
piece.position.set(x * this.settings.pieceGap, y * this.settings.pieceGap, z * this.settings.pieceGap);
|
|
108
|
-
});
|
|
109
|
-
this._lastGap = this.settings.pieceGap;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
*
|
|
115
|
-
* calculates the current speed of the current rotation in ms.
|
|
116
|
-
* calculation is dependent on animation style and animation speed settings
|
|
117
|
-
* - exponential: speeds up rotations depending on the queue length
|
|
118
|
-
* - next: an animation speed of 0 when there is another animation in the queue
|
|
119
|
-
* - match: will match the speed of rotations to the frequency of key presses.
|
|
120
|
-
* - fixed: will return a constant value
|
|
121
|
-
* @returns {number}
|
|
122
|
-
*/
|
|
123
|
-
getRotationSpeed() {
|
|
124
|
-
if (this.settings.animationStyle === 'exponential') {
|
|
125
|
-
return this.settings.animationSpeedMs / 2 ** this.rotationQueue.length;
|
|
126
|
-
}
|
|
127
|
-
if (this.settings.animationStyle === 'next') {
|
|
128
|
-
return this.rotationQueue.length > 0 ? 0 : this.settings.animationSpeedMs;
|
|
129
|
-
}
|
|
130
|
-
if (this.settings.animationStyle === 'match') {
|
|
131
|
-
if (this.rotationQueue.length > 0) {
|
|
132
|
-
var lastTimeStamp = this.currentRotation.timestampMs;
|
|
133
|
-
var minGap = this._matchSpeed ?? this.settings.animationSpeedMs;
|
|
134
|
-
for (var i = 0; i < this.rotationQueue.length; i++) {
|
|
135
|
-
var gap = this.rotationQueue[i].timestampMs - lastTimeStamp;
|
|
136
|
-
if (gap < minGap) {
|
|
137
|
-
minGap = gap;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
this._matchSpeed = minGap;
|
|
141
|
-
}
|
|
142
|
-
if (this._matchSpeed !== undefined) {
|
|
143
|
-
return this._matchSpeed;
|
|
144
|
-
}
|
|
145
|
-
return this.settings.animationSpeedMs;
|
|
146
|
-
}
|
|
147
|
-
if (this.settings.animationStyle === 'fixed') {
|
|
148
|
-
return this.settings.animationSpeedMs;
|
|
149
|
-
}
|
|
150
|
-
return this.settings.animationSpeedMs;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Complete the current rotation and reset the cube
|
|
155
|
-
* @returns {void}
|
|
156
|
-
*/
|
|
157
|
-
reset() {
|
|
158
|
-
this.rotationQueue = [];
|
|
159
|
-
if (this.currentRotation) {
|
|
160
|
-
this.currentRotation.update(this.rotationGroup, 0);
|
|
161
|
-
this.clearRotationGroup();
|
|
162
|
-
this.currentRotation = undefined;
|
|
163
|
-
}
|
|
164
|
-
this.group.children.forEach((piece) => {
|
|
165
|
-
const { x, y, z } = piece.userData.initialPosition;
|
|
166
|
-
const { x: u, y: v, z: w } = piece.userData.initialRotation;
|
|
167
|
-
piece.position.set(x * this.settings.pieceGap, y * this.settings.pieceGap, z * this.settings.pieceGap);
|
|
168
|
-
piece.rotation.set(u, v, w);
|
|
169
|
-
piece.userData.position.x = x;
|
|
170
|
-
piece.userData.position.y = y;
|
|
171
|
-
piece.userData.position.z = z;
|
|
172
|
-
piece.userData.rotation.x = u;
|
|
173
|
-
piece.userData.rotation.y = v;
|
|
174
|
-
piece.userData.rotation.z = w;
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Adds pieces in the rotationGroup back into the main group.
|
|
180
|
-
* @returns {void}
|
|
181
|
-
*/
|
|
182
|
-
clearRotationGroup() {
|
|
183
|
-
if (this.currentRotation.status != 'complete') {
|
|
184
|
-
throw Error('cannot clear rotation group while rotating');
|
|
185
|
-
}
|
|
186
|
-
this.rotationGroup.children.forEach((piece) => {
|
|
187
|
-
piece.getWorldPosition(piece.position);
|
|
188
|
-
piece.getWorldQuaternion(piece.quaternion);
|
|
189
|
-
var x = Math.round(piece.position.x);
|
|
190
|
-
var y = Math.round(piece.position.y);
|
|
191
|
-
var z = Math.round(piece.position.z);
|
|
192
|
-
piece.userData.position.x = Math.abs(x) > 1 ? Math.sign(x) : x;
|
|
193
|
-
piece.userData.position.y = Math.abs(y) > 1 ? Math.sign(y) : y;
|
|
194
|
-
piece.userData.position.z = Math.abs(z) > 1 ? Math.sign(z) : z;
|
|
195
|
-
piece.userData.rotation.x = piece.rotation.x;
|
|
196
|
-
piece.userData.rotation.y = piece.rotation.y;
|
|
197
|
-
piece.userData.rotation.z = piece.rotation.z;
|
|
198
|
-
});
|
|
199
|
-
this.group.add(...this.rotationGroup.children);
|
|
200
|
-
this.rotationGroup.rotation.set(0, 0, 0);
|
|
201
|
-
this.currentRotation.status = 'disposed';
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* @param {string} eventId
|
|
206
|
-
* @param {{axis: "x"|"y"|"z", layers: (-1|0|1)[], direction: 1|-1|2|-2}} input
|
|
207
|
-
*/
|
|
208
|
-
rotate(eventId, input) {
|
|
209
|
-
this.rotationQueue.push(new CubeRotation(eventId, input));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* @param {{axis: "x"|"y"|"z", layers: (-1|0|1)[], direction: 1|-1|2|-2}}
|
|
214
|
-
* @returns {Object3D[]}
|
|
215
|
-
*/
|
|
216
|
-
getRotationLayer({ axis, layers, direction }) {
|
|
217
|
-
if (layers.length === 0) {
|
|
218
|
-
return [...this.group.children];
|
|
219
|
-
}
|
|
220
|
-
return this.group.children.filter((piece) => {
|
|
221
|
-
if (axis === 'x') {
|
|
222
|
-
return layers.includes(Math.round(piece.userData.position.x));
|
|
223
|
-
} else if (axis === 'y') {
|
|
224
|
-
return layers.includes(Math.round(piece.userData.position.y));
|
|
225
|
-
} else if (axis === 'z') {
|
|
226
|
-
return layers.includes(Math.round(piece.userData.position.z));
|
|
227
|
-
}
|
|
228
|
-
return false;
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* @returns {{ up: string[][], down: string[][], front: string[][], back: string[][], left: string[][], right: string[][] }}
|
|
234
|
-
*/
|
|
235
|
-
getStickerState() {
|
|
236
|
-
const state = {
|
|
237
|
-
up: [[], [], []],
|
|
238
|
-
down: [[], [], []],
|
|
239
|
-
front: [[], [], []],
|
|
240
|
-
back: [[], [], []],
|
|
241
|
-
left: [[], [], []],
|
|
242
|
-
right: [[], [], []],
|
|
243
|
-
};
|
|
244
|
-
this.group.children.forEach((piece) => {
|
|
245
|
-
if (piece.userData.type === 'core') {
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
piece.children.forEach((mesh) => {
|
|
249
|
-
if (mesh.userData.type === 'sticker') {
|
|
250
|
-
const piecepos = new Vector3();
|
|
251
|
-
piecepos.copy(piece.userData.position);
|
|
252
|
-
piecepos.round();
|
|
253
|
-
const stickerpos = new Vector3();
|
|
254
|
-
mesh.getWorldPosition(stickerpos);
|
|
255
|
-
stickerpos.sub(piecepos);
|
|
256
|
-
stickerpos.multiplyScalar(2);
|
|
257
|
-
stickerpos.round();
|
|
258
|
-
if (stickerpos.x === 1) {
|
|
259
|
-
state.right[1 - Math.round(piecepos.y)][1 - Math.round(piecepos.z)] = mesh.material.userData.face;
|
|
260
|
-
} else if (stickerpos.x === -1) {
|
|
261
|
-
state.left[1 - Math.round(piecepos.y)][1 + Math.round(piecepos.z)] = mesh.material.userData.face;
|
|
262
|
-
} else if (stickerpos.y === 1) {
|
|
263
|
-
state.up[1 + Math.round(piecepos.z)][1 + Math.round(piecepos.x)] = mesh.material.userData.face;
|
|
264
|
-
} else if (stickerpos.y === -1) {
|
|
265
|
-
state.down[1 - Math.round(piecepos.z)][1 + Math.round(piecepos.x)] = mesh.material.userData.face;
|
|
266
|
-
} else if (stickerpos.z === 1) {
|
|
267
|
-
state.front[1 - Math.round(piecepos.y)][1 + Math.round(piecepos.x)] = mesh.material.userData.face;
|
|
268
|
-
} else if (stickerpos.z === -1) {
|
|
269
|
-
state.back[1 - Math.round(piecepos.y)][1 - Math.round(piecepos.x)] = mesh.material.userData.face;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
return state;
|
|
275
|
-
}
|
|
276
|
-
}
|
package/src/cube/cubeRotation.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { Vector3, Group } from 'three';
|
|
2
|
-
|
|
3
|
-
export class CubeRotation {
|
|
4
|
-
/**
|
|
5
|
-
* @param {string} eventId
|
|
6
|
-
* @param {{axis: "x"|"y"|"z", layers: (-1|0|1)[], direction: 1|-1|2|-2}} rotationDetails
|
|
7
|
-
*/
|
|
8
|
-
constructor(eventId, rotationDetails) {
|
|
9
|
-
/** @type {string} */
|
|
10
|
-
this.eventId = eventId;
|
|
11
|
-
/** @type {{axis: "x"|"y"|"z", layers: (-1|0|1)[], direction: 1|-1|2|-2}} */
|
|
12
|
-
this.rotation = rotationDetails;
|
|
13
|
-
/** @type {"pending" | "initialised" | "complete" | "disposed"} */
|
|
14
|
-
this.status = 'pending';
|
|
15
|
-
/** @type {number} */
|
|
16
|
-
this.timestampMs = performance.now();
|
|
17
|
-
/** @type {number} */
|
|
18
|
-
this._lastUpdatedTimeMs = undefined;
|
|
19
|
-
/** @type {number} */
|
|
20
|
-
this._rotationPercentage = 0;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
initialise() {
|
|
24
|
-
this._lastUpdatedTimeMs = performance.now();
|
|
25
|
-
this.status = 'initialised';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
*
|
|
30
|
-
* @param {Group} rotationGroup
|
|
31
|
-
* @param {number} speedMs
|
|
32
|
-
*/
|
|
33
|
-
update(rotationGroup, speedMs) {
|
|
34
|
-
var intervalMs = performance.now() - this._lastUpdatedTimeMs;
|
|
35
|
-
this._lastUpdatedTimeMs = performance.now();
|
|
36
|
-
|
|
37
|
-
var increment = 100 - this._rotationPercentage;
|
|
38
|
-
if (speedMs != 0) {
|
|
39
|
-
var potentialIncrement = (intervalMs / speedMs) * 100;
|
|
40
|
-
if (potentialIncrement + this._rotationPercentage < 100) {
|
|
41
|
-
increment = potentialIncrement;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
const rotationIncrement = (Math.abs(this.rotation.direction) * ((increment / 100) * Math.PI)) / 2;
|
|
45
|
-
this._rotationPercentage += increment;
|
|
46
|
-
rotationGroup.rotateOnWorldAxis(
|
|
47
|
-
new Vector3(
|
|
48
|
-
this.rotation.axis === 'x' ? this.rotation.direction : 0,
|
|
49
|
-
this.rotation.axis === 'y' ? this.rotation.direction : 0,
|
|
50
|
-
this.rotation.axis === 'z' ? this.rotation.direction : 0,
|
|
51
|
-
).normalize(),
|
|
52
|
-
rotationIncrement,
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
if (this._rotationPercentage === 100) {
|
|
56
|
-
this.status = 'complete';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (this._rotationPercentage > 100) {
|
|
60
|
-
throw new Error('rotation percentage > 100');
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
package/src/threejs/materials.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { MeshStandardMaterial, MeshBasicMaterial } from "three";
|
|
2
|
-
export default class Materials {
|
|
3
|
-
static front = new MeshStandardMaterial({
|
|
4
|
-
color: "#2cbf13",
|
|
5
|
-
metalness: 0,
|
|
6
|
-
roughness: 0.4,
|
|
7
|
-
userData: { face: "front", color: "green" },
|
|
8
|
-
});
|
|
9
|
-
static back = new MeshStandardMaterial({
|
|
10
|
-
color: "blue",
|
|
11
|
-
metalness: 0,
|
|
12
|
-
roughness: 0.4,
|
|
13
|
-
userData: { face: "back", color: "blue" },
|
|
14
|
-
});
|
|
15
|
-
static up = new MeshStandardMaterial({
|
|
16
|
-
color: "white",
|
|
17
|
-
metalness: 0,
|
|
18
|
-
roughness: 0.4,
|
|
19
|
-
userData: { face: "up", color: "white" },
|
|
20
|
-
});
|
|
21
|
-
static down = new MeshStandardMaterial({
|
|
22
|
-
color: "yellow",
|
|
23
|
-
metalness: 0,
|
|
24
|
-
roughness: 0.4,
|
|
25
|
-
userData: { face: "down", color: "yellow" },
|
|
26
|
-
});
|
|
27
|
-
static left = new MeshStandardMaterial({
|
|
28
|
-
color: "#fc9a05",
|
|
29
|
-
metalness: 0,
|
|
30
|
-
roughness: 0.4,
|
|
31
|
-
userData: { face: "left", color: "orange" },
|
|
32
|
-
});
|
|
33
|
-
static right = new MeshStandardMaterial({
|
|
34
|
-
color: "red",
|
|
35
|
-
metalness: 0,
|
|
36
|
-
roughness: 0.4,
|
|
37
|
-
userData: { face: "right", color: "red" },
|
|
38
|
-
});
|
|
39
|
-
static core = new MeshBasicMaterial({
|
|
40
|
-
color: "black",
|
|
41
|
-
});
|
|
42
|
-
}
|