@houstonp/rubiks-cube 1.5.1 → 2.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 +100 -194
- package/package.json +34 -4
- package/src/cameraState.js +81 -0
- package/src/core.js +127 -0
- package/src/cube/cube.js +123 -75
- package/src/cube/cubeRotation.js +26 -10
- package/src/cube/cubeSettings.js +18 -0
- package/src/cube/cubeState.js +1 -0
- package/src/cube/slice.js +143 -0
- package/src/debouncer.js +16 -0
- package/src/globals.ts +9 -0
- package/src/index.js +496 -0
- package/src/schema.js +22 -0
- package/src/settings.js +126 -0
- package/src/threejs/materials.js +52 -40
- package/src/threejs/pieces.js +1 -4
- package/src/threejs/stickers.js +18 -26
- package/types/cameraState.d.ts +19 -0
- package/types/core.d.ts +125 -0
- package/types/cube/cube.d.ts +102 -0
- package/types/cube/cubeRotation.d.ts +33 -0
- package/types/cube/cubeSettings.d.ts +17 -0
- package/types/cube/cubeState.d.ts +16 -0
- package/types/cube/slice.d.ts +15 -0
- package/types/debouncer.d.ts +13 -0
- package/types/globals.d.ts +7 -0
- package/types/index.d.ts +65 -0
- package/types/schema.d.ts +11 -0
- package/types/settings.d.ts +34 -0
- package/types/threejs/materials.d.ts +21 -0
- package/types/threejs/pieces.d.ts +28 -0
- package/types/threejs/stickers.d.ts +6 -0
- package/.prettierrc +0 -7
- package/index.js +0 -274
- package/src/utils/debouncer.js +0 -7
- package/src/utils/rotation.js +0 -53
package/src/index.js
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/// <reference path="./globals.ts" preserve="true" />
|
|
3
|
+
import { Scene, PerspectiveCamera, AmbientLight, DirectionalLight, Spherical } from 'three';
|
|
4
|
+
import { WebGPURenderer } from 'three/webgpu';
|
|
5
|
+
import { OrbitControls } from 'three/examples/jsm/Addons.js';
|
|
6
|
+
import Cube from './cube/cube';
|
|
7
|
+
import { debounce } from './debouncer';
|
|
8
|
+
import { gsap } from 'gsap';
|
|
9
|
+
import Settings from './settings';
|
|
10
|
+
import CubeSettings from './cube/cubeSettings';
|
|
11
|
+
import { CameraState } from './cameraState';
|
|
12
|
+
import { AttributeNames } from './schema';
|
|
13
|
+
|
|
14
|
+
const maxAzimuthAngle = (5 * Math.PI) / 16;
|
|
15
|
+
const polarAngleOffset = Math.PI / 2;
|
|
16
|
+
const maxPolarAngle = (5 * Math.PI) / 16;
|
|
17
|
+
const InternalEvents = Object.freeze({
|
|
18
|
+
rotation: 'rotation',
|
|
19
|
+
rotationComplete: 'rotationComplete',
|
|
20
|
+
rotationFailed: 'rotationFailed',
|
|
21
|
+
movement: 'movement',
|
|
22
|
+
movementComplete: 'movementComplete',
|
|
23
|
+
movementFailed: 'movementFailed',
|
|
24
|
+
reset: 'reset',
|
|
25
|
+
resetComplete: 'resetComplete',
|
|
26
|
+
cameraSettingsChanged: 'cameraSettingsChanged',
|
|
27
|
+
cameraFieldOfViewChanged: 'cameraFieldOfViewChanged',
|
|
28
|
+
cameraPeek: 'cameraPeek',
|
|
29
|
+
cameraPeekComplete: 'cameraPeekComplete',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export class RubiksCubeElement extends HTMLElement {
|
|
33
|
+
constructor() {
|
|
34
|
+
super();
|
|
35
|
+
this.attachShadow({ mode: 'open' });
|
|
36
|
+
const root = /** @type {ShadowRoot} */ (this.shadowRoot);
|
|
37
|
+
root.innerHTML = `<canvas id="cube-canvas" style="display:block;"></canvas>`;
|
|
38
|
+
/** @private @type {HTMLCanvasElement} */
|
|
39
|
+
this.canvas = /** @type {HTMLCanvasElement} */ (root.getElementById('cube-canvas'));
|
|
40
|
+
/** @private @type {Settings} */
|
|
41
|
+
this.settings = new Settings();
|
|
42
|
+
/** @private @type {CubeSettings} */
|
|
43
|
+
this.cubeSettings = new CubeSettings(this.settings.pieceGap, this.settings.animationSpeedMs, this.settings.animationStyle);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} tagName the name of the tag to register the web component under
|
|
48
|
+
*/
|
|
49
|
+
static register(tagName = 'rubiks-cube') {
|
|
50
|
+
customElements.define(tagName, this);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static get observedAttributes() {
|
|
54
|
+
return [
|
|
55
|
+
AttributeNames.pieceGap,
|
|
56
|
+
AttributeNames.animationSpeed,
|
|
57
|
+
AttributeNames.animationStyle,
|
|
58
|
+
AttributeNames.cameraSpeed,
|
|
59
|
+
AttributeNames.cameraRadius,
|
|
60
|
+
AttributeNames.cameraFieldOfView,
|
|
61
|
+
AttributeNames.cameraPeekAngleHorizontal,
|
|
62
|
+
AttributeNames.cameraPeekAngleVertical,
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} name
|
|
68
|
+
* @param {string} oldVal
|
|
69
|
+
* @param {string} newVal
|
|
70
|
+
* */
|
|
71
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
72
|
+
switch (name) {
|
|
73
|
+
case AttributeNames.pieceGap:
|
|
74
|
+
this.settings.setPieceGap(newVal);
|
|
75
|
+
this.cubeSettings.pieceGap = this.settings.pieceGap;
|
|
76
|
+
break;
|
|
77
|
+
case AttributeNames.animationSpeed:
|
|
78
|
+
this.settings.setAnimationSpeed(newVal);
|
|
79
|
+
this.cubeSettings.animationSpeedMs = this.settings.animationSpeedMs;
|
|
80
|
+
break;
|
|
81
|
+
case AttributeNames.animationStyle:
|
|
82
|
+
this.settings.setAnimationStyle(newVal);
|
|
83
|
+
this.cubeSettings.animationStyle = this.settings.animationStyle;
|
|
84
|
+
break;
|
|
85
|
+
case AttributeNames.cameraSpeed:
|
|
86
|
+
this.settings.setCameraSpeed(newVal);
|
|
87
|
+
break;
|
|
88
|
+
case AttributeNames.cameraRadius:
|
|
89
|
+
this.settings.setCameraRadius(newVal);
|
|
90
|
+
if (oldVal !== newVal && oldVal !== null) {
|
|
91
|
+
this.animateCameraSetting();
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
case AttributeNames.cameraFieldOfView:
|
|
95
|
+
this.settings.setCameraFieldOfView(newVal);
|
|
96
|
+
if (oldVal !== newVal && oldVal !== null) {
|
|
97
|
+
this.updateCameraFOV();
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case AttributeNames.cameraPeekAngleHorizontal:
|
|
101
|
+
this.settings.setCameraPeekAngleHorizontal(newVal);
|
|
102
|
+
if (oldVal !== newVal && oldVal !== null) {
|
|
103
|
+
this.animateCameraSetting();
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
case AttributeNames.cameraPeekAngleVertical:
|
|
107
|
+
this.settings.setCameraPeekAngleVertical(newVal);
|
|
108
|
+
if (oldVal !== newVal && oldVal !== null) {
|
|
109
|
+
this.animateCameraSetting();
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
connectedCallback() {
|
|
116
|
+
if (this.hasAttribute(AttributeNames.pieceGap)) {
|
|
117
|
+
this.settings.setPieceGap(this.getAttribute(AttributeNames.pieceGap));
|
|
118
|
+
this.cubeSettings.pieceGap = this.settings.pieceGap;
|
|
119
|
+
}
|
|
120
|
+
if (this.hasAttribute(AttributeNames.animationSpeed)) {
|
|
121
|
+
this.settings.setAnimationSpeed(this.getAttribute(AttributeNames.animationSpeed));
|
|
122
|
+
this.cubeSettings.animationSpeedMs = this.settings.animationSpeedMs;
|
|
123
|
+
}
|
|
124
|
+
if (this.hasAttribute(AttributeNames.animationStyle)) {
|
|
125
|
+
this.settings.setAnimationStyle(this.getAttribute(AttributeNames.animationStyle));
|
|
126
|
+
this.cubeSettings.animationStyle = this.settings.animationStyle;
|
|
127
|
+
}
|
|
128
|
+
if (this.hasAttribute(AttributeNames.cameraSpeed)) {
|
|
129
|
+
this.settings.setCameraSpeed(this.getAttribute(AttributeNames.cameraSpeed));
|
|
130
|
+
}
|
|
131
|
+
if (this.hasAttribute(AttributeNames.cameraRadius)) {
|
|
132
|
+
this.settings.setCameraRadius(this.getAttribute(AttributeNames.cameraRadius));
|
|
133
|
+
}
|
|
134
|
+
if (this.hasAttribute(AttributeNames.cameraFieldOfView)) {
|
|
135
|
+
this.settings.setCameraFieldOfView(this.getAttribute(AttributeNames.cameraFieldOfView));
|
|
136
|
+
}
|
|
137
|
+
if (this.hasAttribute(AttributeNames.cameraPeekAngleHorizontal)) {
|
|
138
|
+
this.settings.setCameraPeekAngleHorizontal(this.getAttribute(AttributeNames.cameraPeekAngleHorizontal));
|
|
139
|
+
}
|
|
140
|
+
if (this.hasAttribute(AttributeNames.cameraPeekAngleVertical)) {
|
|
141
|
+
this.settings.setCameraPeekAngleVertical(this.getAttribute(AttributeNames.cameraPeekAngleVertical));
|
|
142
|
+
}
|
|
143
|
+
this.init();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** @private */
|
|
147
|
+
animateCameraSetting() {
|
|
148
|
+
this.dispatchEvent(new CustomEvent(InternalEvents.cameraSettingsChanged));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @private */
|
|
152
|
+
updateCameraFOV() {
|
|
153
|
+
this.dispatchEvent(new CustomEvent(InternalEvents.cameraFieldOfViewChanged));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** @import {Movement} from './core' */
|
|
157
|
+
/** @internal @typedef {{eventId: string, move: Movement}} MovementEvent */
|
|
158
|
+
/** @internal @typedef {{eventId: string, move: Movement, state: string}} MovementCompleteEventData */
|
|
159
|
+
/** @internal @typedef {{eventId: string, move: Movement, reason: string}} MovementFailedEventData */
|
|
160
|
+
/**
|
|
161
|
+
* @param {Movement} move
|
|
162
|
+
* @returns {Promise<string>}
|
|
163
|
+
*/
|
|
164
|
+
move(move) {
|
|
165
|
+
/** @type {MovementEvent} */
|
|
166
|
+
const data = { eventId: crypto.randomUUID(), move };
|
|
167
|
+
this.dispatchEvent(new CustomEvent(InternalEvents.movement, { detail: data }));
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
/** @param {CustomEvent<MovementCompleteEventData> | Event} event */
|
|
170
|
+
const completedHandler = (event) => {
|
|
171
|
+
const customEvent = /** @type {CustomEvent<MovementCompleteEventData>} */ (event);
|
|
172
|
+
if (customEvent.detail.eventId === data.eventId) {
|
|
173
|
+
cleanup();
|
|
174
|
+
resolve(customEvent.detail.state);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** @param {CustomEvent<MovementFailedEventData> | Event} event */
|
|
179
|
+
const failedHandler = (event) => {
|
|
180
|
+
const customEvent = /** @type {CustomEvent<MovementFailedEventData>} */ (event);
|
|
181
|
+
if (customEvent.detail.eventId === data.eventId) {
|
|
182
|
+
cleanup();
|
|
183
|
+
reject(customEvent.detail.reason);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const timeoutId = setTimeout(
|
|
188
|
+
() => {
|
|
189
|
+
cleanup();
|
|
190
|
+
reject('movement timed out');
|
|
191
|
+
},
|
|
192
|
+
Math.max(this.settings.animationSpeedMs * 100, 100),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const cleanup = () => {
|
|
196
|
+
this.removeEventListener(InternalEvents.movementComplete, completedHandler);
|
|
197
|
+
this.removeEventListener(InternalEvents.movementFailed, failedHandler);
|
|
198
|
+
clearTimeout(timeoutId);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
this.addEventListener(InternalEvents.movementComplete, completedHandler);
|
|
202
|
+
this.addEventListener(InternalEvents.movementFailed, failedHandler);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** @import {Rotation} from './core' */
|
|
207
|
+
/** @internal @typedef {{eventId: string, rotation: Rotation}} RotationEventData */
|
|
208
|
+
/** @internal @typedef {{eventId: string, rotation: Rotation, state: string, }} RotationCompleteEventData*/
|
|
209
|
+
/** @internal @typedef {{eventId: string, rotation: Rotation, reason: string, }} RotationFailedEventData*/
|
|
210
|
+
/**
|
|
211
|
+
* @param {Rotation} rotation
|
|
212
|
+
* @returns {Promise<string>}
|
|
213
|
+
*/
|
|
214
|
+
rotate(rotation) {
|
|
215
|
+
/** @type {RotationEventData} */
|
|
216
|
+
const data = { eventId: crypto.randomUUID(), rotation };
|
|
217
|
+
this.dispatchEvent(new CustomEvent(InternalEvents.rotation, { detail: data }));
|
|
218
|
+
return new Promise((resolve, reject) => {
|
|
219
|
+
/** @param {CustomEvent<RotationCompleteEventData> | Event} event */
|
|
220
|
+
const completeHanlder = (event) => {
|
|
221
|
+
const customEvent = /** @type {CustomEvent<RotationCompleteEventData>} */ (event);
|
|
222
|
+
if (customEvent.detail.eventId === data.eventId) {
|
|
223
|
+
cleanup();
|
|
224
|
+
resolve(customEvent.detail.state);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/** @param {CustomEvent<RotationFailedEventData> | Event} event */
|
|
229
|
+
const failedHandler = (event) => {
|
|
230
|
+
const customEvent = /** @type {CustomEvent<RotationFailedEventData>} */ (event);
|
|
231
|
+
if (customEvent.detail.eventId === data.eventId) {
|
|
232
|
+
cleanup();
|
|
233
|
+
reject(customEvent.detail.reason);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const timeoutId = setTimeout(
|
|
238
|
+
() => {
|
|
239
|
+
cleanup();
|
|
240
|
+
reject('rotation timed out');
|
|
241
|
+
},
|
|
242
|
+
Math.max(this.settings.animationSpeedMs * 100, 100),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const cleanup = () => {
|
|
246
|
+
this.removeEventListener(InternalEvents.rotationComplete, completeHanlder);
|
|
247
|
+
this.removeEventListener(InternalEvents.rotationFailed, failedHandler);
|
|
248
|
+
clearTimeout(timeoutId);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
this.addEventListener(InternalEvents.rotationComplete, completeHanlder);
|
|
252
|
+
this.addEventListener(InternalEvents.rotationFailed, failedHandler);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** @internal @typedef {{state: string }} ResetCompleteEventData */
|
|
257
|
+
/**
|
|
258
|
+
* @returns {Promise<string>}
|
|
259
|
+
*/
|
|
260
|
+
reset() {
|
|
261
|
+
this.dispatchEvent(new CustomEvent(InternalEvents.reset));
|
|
262
|
+
return new Promise((resolve, reject) => {
|
|
263
|
+
/** @param {CustomEvent<ResetCompleteEventData> | Event} event */
|
|
264
|
+
const handler = (event) => {
|
|
265
|
+
const customEvent = /** @type {CustomEvent<ResetCompleteEventData>} */ (event);
|
|
266
|
+
cleanup();
|
|
267
|
+
resolve(customEvent.detail.state);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const timeoutId = setTimeout(() => {
|
|
271
|
+
cleanup();
|
|
272
|
+
reject('reset timed out');
|
|
273
|
+
}, 1000);
|
|
274
|
+
|
|
275
|
+
const cleanup = () => {
|
|
276
|
+
this.removeEventListener(InternalEvents.resetComplete, handler);
|
|
277
|
+
clearTimeout(timeoutId);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
this.addEventListener(InternalEvents.resetComplete, handler);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** @import {PeekType} from './core' */
|
|
285
|
+
/** @internal @typedef {{eventId: string, peekType: PeekType}} CameraPeekEventData */
|
|
286
|
+
/** @import {PeekState} from './core' */
|
|
287
|
+
/** @internal @typedef {{eventId: string, peekState: PeekState }} CameraPeekCompleteEventData */
|
|
288
|
+
/**
|
|
289
|
+
* This function changes the camera position to one of four states depending on the arguments passed.
|
|
290
|
+
*
|
|
291
|
+
* @param {PeekType} peekType
|
|
292
|
+
* @returns {Promise<PeekState>}
|
|
293
|
+
*/
|
|
294
|
+
peek(peekType) {
|
|
295
|
+
/** @type {CameraPeekEventData} */
|
|
296
|
+
const data = { eventId: crypto.randomUUID(), peekType };
|
|
297
|
+
this.dispatchEvent(new CustomEvent(InternalEvents.cameraPeek, { detail: data }));
|
|
298
|
+
return new Promise((resolve, reject) => {
|
|
299
|
+
/** @param {CustomEvent<CameraPeekCompleteEventData> | Event} event */ const handler = (event) => {
|
|
300
|
+
const customEvent = /** @type {CustomEvent<CameraPeekCompleteEventData>} */ (event);
|
|
301
|
+
if (customEvent.detail.eventId === data.eventId) {
|
|
302
|
+
cleanup();
|
|
303
|
+
resolve(customEvent.detail.peekState);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const timeoutId = setTimeout(() => {
|
|
308
|
+
cleanup();
|
|
309
|
+
reject('peek timed out');
|
|
310
|
+
}, 1000);
|
|
311
|
+
|
|
312
|
+
const cleanup = () => {
|
|
313
|
+
this.removeEventListener(InternalEvents.cameraPeekComplete, handler);
|
|
314
|
+
clearTimeout(timeoutId);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
this.addEventListener(InternalEvents.cameraPeekComplete, handler);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** @private */
|
|
322
|
+
init() {
|
|
323
|
+
// defined core threejs objects
|
|
324
|
+
const canvas = this.canvas;
|
|
325
|
+
const scene = new Scene();
|
|
326
|
+
const renderer = new WebGPURenderer({
|
|
327
|
+
alpha: true,
|
|
328
|
+
canvas,
|
|
329
|
+
antialias: true,
|
|
330
|
+
});
|
|
331
|
+
renderer.setSize(this.clientWidth, this.clientHeight);
|
|
332
|
+
renderer.setAnimationLoop(animate);
|
|
333
|
+
renderer.setPixelRatio(window.devicePixelRatio);
|
|
334
|
+
|
|
335
|
+
//update renderer and camera when container resizes. debouncing events to reduce frequency
|
|
336
|
+
new ResizeObserver(
|
|
337
|
+
debounce((/** @type {{ contentRect: { width: number; height: number; }; }[]} */ entries) => {
|
|
338
|
+
const { width, height } = entries[0].contentRect;
|
|
339
|
+
camera.aspect = width / height;
|
|
340
|
+
camera.updateProjectionMatrix();
|
|
341
|
+
renderer.setSize(width, height);
|
|
342
|
+
}, 30),
|
|
343
|
+
).observe(this);
|
|
344
|
+
|
|
345
|
+
// add camera
|
|
346
|
+
const camera = new PerspectiveCamera(this.settings.cameraFieldOfView, this.clientWidth / this.clientHeight, 1, 2000);
|
|
347
|
+
const cameraSpherical = new Spherical(50, (3 * Math.PI) / 8, -Math.PI / 4);
|
|
348
|
+
camera.position.setFromSpherical(cameraSpherical);
|
|
349
|
+
const cameraState = new CameraState();
|
|
350
|
+
|
|
351
|
+
// add orbit controls for camera
|
|
352
|
+
const controls = new OrbitControls(camera, renderer.domElement);
|
|
353
|
+
controls.enableZoom = false;
|
|
354
|
+
controls.enablePan = false;
|
|
355
|
+
controls.enableDamping = true;
|
|
356
|
+
// controls.maxAzimuthAngle = maxAzimuthAngle;
|
|
357
|
+
// controls.minAzimuthAngle = -maxAzimuthAngle;
|
|
358
|
+
// controls.maxPolarAngle = polarAngleOffset + maxPolarAngle;
|
|
359
|
+
// controls.minPolarAngle = polarAngleOffset - maxPolarAngle;
|
|
360
|
+
|
|
361
|
+
// add lighting to scene
|
|
362
|
+
const ambientLight = new AmbientLight('white', 0.4);
|
|
363
|
+
const spotLight1 = new DirectionalLight('white', 2);
|
|
364
|
+
const spotLight2 = new DirectionalLight('white', 2);
|
|
365
|
+
const spotLight3 = new DirectionalLight('white', 2);
|
|
366
|
+
const spotLight4 = new DirectionalLight('white', 2);
|
|
367
|
+
spotLight1.position.set(5, 5, 5);
|
|
368
|
+
spotLight2.position.set(-5, 5, 5);
|
|
369
|
+
spotLight3.position.set(5, -5, 0);
|
|
370
|
+
spotLight4.position.set(-10, -5, -5);
|
|
371
|
+
scene.add(ambientLight, spotLight1, spotLight2, spotLight3, spotLight4);
|
|
372
|
+
|
|
373
|
+
// create cube and add to scene
|
|
374
|
+
const cube = new Cube(this.cubeSettings);
|
|
375
|
+
scene.add(cube.group, cube.rotationGroup);
|
|
376
|
+
|
|
377
|
+
// animation loop
|
|
378
|
+
function animate() {
|
|
379
|
+
controls.update();
|
|
380
|
+
cube.update();
|
|
381
|
+
renderer.render(scene, camera);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Cube Events
|
|
385
|
+
this.addEventListener(InternalEvents.rotation, (event) => {
|
|
386
|
+
const customEvent = /** @type {CustomEvent<RotationEventData>} */ (event);
|
|
387
|
+
const completedCallback = (/** @type {string} */ state) =>
|
|
388
|
+
this.dispatchEvent(
|
|
389
|
+
new CustomEvent(InternalEvents.rotationComplete, {
|
|
390
|
+
detail: /** @type {RotationCompleteEventData} */ ({
|
|
391
|
+
eventId: customEvent.detail.eventId,
|
|
392
|
+
state: state,
|
|
393
|
+
rotation: customEvent.detail.rotation,
|
|
394
|
+
}),
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
const failedCallback = (/** @type {string} */ reason) =>
|
|
398
|
+
this.dispatchEvent(
|
|
399
|
+
new CustomEvent(InternalEvents.rotationFailed, {
|
|
400
|
+
detail: /** @type {RotationFailedEventData} */ ({
|
|
401
|
+
eventId: customEvent.detail.eventId,
|
|
402
|
+
reason: reason,
|
|
403
|
+
rotation: customEvent.detail.rotation,
|
|
404
|
+
}),
|
|
405
|
+
}),
|
|
406
|
+
);
|
|
407
|
+
cube.rotation(customEvent.detail.eventId, customEvent.detail.rotation, completedCallback, failedCallback);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
this.addEventListener(InternalEvents.movement, (event) => {
|
|
411
|
+
const customEvent = /** @type {CustomEvent<MovementEvent>} */ (event);
|
|
412
|
+
const completedCallback = (/** @type {string} */ state) =>
|
|
413
|
+
this.dispatchEvent(
|
|
414
|
+
new CustomEvent(InternalEvents.movementComplete, {
|
|
415
|
+
detail: /** @type {MovementCompleteEventData} */ ({
|
|
416
|
+
eventId: customEvent.detail.eventId,
|
|
417
|
+
state: state,
|
|
418
|
+
move: customEvent.detail.move,
|
|
419
|
+
}),
|
|
420
|
+
}),
|
|
421
|
+
);
|
|
422
|
+
const failedCallback = (/** @type {string} */ reason) =>
|
|
423
|
+
this.dispatchEvent(
|
|
424
|
+
new CustomEvent(InternalEvents.movementFailed, {
|
|
425
|
+
detail: /** @type {MovementFailedEventData} */ ({
|
|
426
|
+
eventId: customEvent.detail.eventId,
|
|
427
|
+
reason: reason,
|
|
428
|
+
move: customEvent.detail.move,
|
|
429
|
+
}),
|
|
430
|
+
}),
|
|
431
|
+
);
|
|
432
|
+
cube.movement(customEvent.detail.eventId, customEvent.detail.move, completedCallback, failedCallback);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
this.addEventListener(InternalEvents.reset, () => {
|
|
436
|
+
const completedCallback = (/** @type {string} */ state) =>
|
|
437
|
+
this.dispatchEvent(
|
|
438
|
+
new CustomEvent(InternalEvents.resetComplete, {
|
|
439
|
+
detail: /** @type {ResetCompleteEventData} */ ({
|
|
440
|
+
state: state,
|
|
441
|
+
}),
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
444
|
+
cube.reset(completedCallback);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Camera Events
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* @param {number} cameraSpeedMs
|
|
451
|
+
* @param {gsap.EaseString | gsap.EaseFunction | undefined} ease
|
|
452
|
+
* @param { undefined | (() => void) } completedCallback
|
|
453
|
+
*/
|
|
454
|
+
const updateCameraPosition = (cameraSpeedMs, ease, completedCallback = undefined) => {
|
|
455
|
+
cameraSpeedMs = cameraSpeedMs ? cameraSpeedMs : this.settings.cameraSpeedMs;
|
|
456
|
+
var phi = polarAngleOffset + (cameraState.Up ? -this.settings.cameraPeekAngleVertical : this.settings.cameraPeekAngleVertical) * maxPolarAngle;
|
|
457
|
+
var theta = (cameraState.Right ? this.settings.cameraPeekAngleHorizontal : -this.settings.cameraPeekAngleHorizontal) * maxAzimuthAngle;
|
|
458
|
+
const startSpherical = new Spherical().setFromVector3(camera.position);
|
|
459
|
+
const targetSpherical = new Spherical(this.settings.cameraRadius, phi, theta);
|
|
460
|
+
gsap.to(startSpherical, {
|
|
461
|
+
radius: targetSpherical.radius,
|
|
462
|
+
theta: targetSpherical.theta,
|
|
463
|
+
phi: targetSpherical.phi,
|
|
464
|
+
duration: cameraSpeedMs / 1000,
|
|
465
|
+
ease: ease,
|
|
466
|
+
overwrite: false,
|
|
467
|
+
onUpdate: () => {
|
|
468
|
+
camera.position.setFromSpherical(startSpherical);
|
|
469
|
+
camera.lookAt(cube.group.position);
|
|
470
|
+
controls.update();
|
|
471
|
+
},
|
|
472
|
+
onComplete: completedCallback,
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
this.addEventListener(InternalEvents.cameraPeek, (event) => {
|
|
477
|
+
const customEvent = /** @type {CustomEvent<CameraPeekEventData>} */ (event);
|
|
478
|
+
cameraState.peekCamera(customEvent.detail.peekType);
|
|
479
|
+
/** @type {CameraPeekCompleteEventData} */
|
|
480
|
+
const data = { eventId: customEvent.detail.eventId, peekState: cameraState.toPeekState() };
|
|
481
|
+
const completedCallback = () => this.dispatchEvent(new CustomEvent(InternalEvents.cameraPeekComplete, { detail: data }));
|
|
482
|
+
updateCameraPosition(this.settings.cameraSpeedMs, 'none', completedCallback);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
this.addEventListener(InternalEvents.cameraSettingsChanged, () => {
|
|
486
|
+
updateCameraPosition(this.settings.cameraSpeedMs, 'none');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
this.addEventListener(InternalEvents.cameraFieldOfViewChanged, () => {
|
|
490
|
+
camera.fov = this.settings.cameraFieldOfView;
|
|
491
|
+
camera.updateProjectionMatrix();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
updateCameraPosition(1000, 'power4.inOut'); // initial animation
|
|
495
|
+
}
|
|
496
|
+
}
|
package/src/schema.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {typeof AttributeNames[keyof typeof AttributeNames]} AttributeName
|
|
4
|
+
*/
|
|
5
|
+
export const AttributeNames = {
|
|
6
|
+
/** @type {"piece-gap"} */
|
|
7
|
+
pieceGap: 'piece-gap',
|
|
8
|
+
/** @type {"animation-speed-ms"} */
|
|
9
|
+
animationSpeed: 'animation-speed-ms',
|
|
10
|
+
/** @type {"animation-style"} */
|
|
11
|
+
animationStyle: 'animation-style',
|
|
12
|
+
/** @type {"camera-speed-ms"} */
|
|
13
|
+
cameraSpeed: 'camera-speed-ms',
|
|
14
|
+
/** @type {"camera-radius"} */
|
|
15
|
+
cameraRadius: 'camera-radius',
|
|
16
|
+
/** @type {"camera-field-of-view"} */
|
|
17
|
+
cameraFieldOfView: 'camera-field-of-view',
|
|
18
|
+
/** @type {"camera-peek-angle-horizontal"} */
|
|
19
|
+
cameraPeekAngleHorizontal: 'camera-peek-angle-horizontal',
|
|
20
|
+
/** @type {"camera-peek-angle-vertical"} */
|
|
21
|
+
cameraPeekAngleVertical: 'camera-peek-angle-vertical',
|
|
22
|
+
};
|
package/src/settings.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
const defaultSettings = {
|
|
3
|
+
animationSpeedMs: 100,
|
|
4
|
+
/** @type {import("./cube/cubeSettings").AnimationStyle} */
|
|
5
|
+
animationStyle: 'fixed',
|
|
6
|
+
pieceGap: 1.04,
|
|
7
|
+
cameraSpeedMs: 100,
|
|
8
|
+
cameraRadius: 5,
|
|
9
|
+
cameraPeekAngleHorizontal: 0.6,
|
|
10
|
+
cameraPeekAngleVertical: 0.6,
|
|
11
|
+
cameraFieldOfView: 75,
|
|
12
|
+
};
|
|
13
|
+
/** @type {import("./cube/cubeSettings").AnimationStyle[]} */
|
|
14
|
+
const validAnimationStyles = ['exponential', 'next', 'fixed', 'match'];
|
|
15
|
+
const minGap = 1;
|
|
16
|
+
const minRadius = 4;
|
|
17
|
+
const minFieldOfView = 30;
|
|
18
|
+
const maxFieldOfView = 100;
|
|
19
|
+
|
|
20
|
+
export default class Settings {
|
|
21
|
+
constructor() {
|
|
22
|
+
/** @type {number} */
|
|
23
|
+
this.pieceGap = defaultSettings.pieceGap;
|
|
24
|
+
/** @type {number} */
|
|
25
|
+
this.animationSpeedMs = defaultSettings.animationSpeedMs;
|
|
26
|
+
/** @type {import("./cube/cubeSettings").AnimationStyle} */
|
|
27
|
+
this.animationStyle = defaultSettings.animationStyle;
|
|
28
|
+
/** @type {number} */
|
|
29
|
+
this.cameraSpeedMs = defaultSettings.cameraSpeedMs;
|
|
30
|
+
/** @type {number} */
|
|
31
|
+
this.cameraRadius = defaultSettings.cameraRadius;
|
|
32
|
+
/** @type {number} */
|
|
33
|
+
this.cameraFieldOfView = defaultSettings.cameraFieldOfView;
|
|
34
|
+
/** @type {number} */
|
|
35
|
+
this.cameraPeekAngleHorizontal = defaultSettings.cameraPeekAngleHorizontal;
|
|
36
|
+
/** @type {number} */
|
|
37
|
+
this.cameraPeekAngleVertical = defaultSettings.cameraPeekAngleVertical;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {string | null} value*/
|
|
41
|
+
setPieceGap(value) {
|
|
42
|
+
const gap = Number(value);
|
|
43
|
+
if (gap >= minGap && value != null) {
|
|
44
|
+
this.pieceGap = gap;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
console.warn(`Invalid pieceGap value. Min is ${minGap}. Value is ${value}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @param {string | null} value in ms */
|
|
51
|
+
setAnimationSpeed(value) {
|
|
52
|
+
var speed = Number(value);
|
|
53
|
+
if (speed >= 0 && value != null) {
|
|
54
|
+
this.animationSpeedMs = speed;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
console.warn(`Invalid animation speed value. Min is 0. Value is ${value}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @param {string | null} value */
|
|
61
|
+
setAnimationStyle(value) {
|
|
62
|
+
if (validAnimationStyles.some((style) => style === value)) {
|
|
63
|
+
const validStyle = /** @type {import("./cube/cubeSettings").AnimationStyle} */ (value);
|
|
64
|
+
this.animationStyle = validStyle;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
console.warn(`Invalid animation style value. Accepted Values are [${validAnimationStyles.join(', ')}] Value is ${value}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {string | null} value in ms */
|
|
71
|
+
setCameraSpeed(value) {
|
|
72
|
+
var speed = Number(value);
|
|
73
|
+
if (speed >= 0 && value != null) {
|
|
74
|
+
this.cameraSpeedMs = speed;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.warn(`Invalid camera speed value. Min is 0. Value is ${value}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @param {string | null} value */
|
|
81
|
+
setCameraRadius(value) {
|
|
82
|
+
var radius = Number(value);
|
|
83
|
+
if (radius >= minRadius && value != null) {
|
|
84
|
+
this.cameraRadius = radius;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
console.warn(`Invalid camera radius value. Min is ${radius}. Value is ${value}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @param {string | null} value in ms */
|
|
91
|
+
setCameraPeekAngleHorizontal(value) {
|
|
92
|
+
var angle = Number(value);
|
|
93
|
+
if (angle >= 0 && angle <= 1 && value != null) {
|
|
94
|
+
this.cameraPeekAngleHorizontal = angle;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
console.warn(`Invalid camera peek angle horizontal value. Min is 0, Max is 1. Value is ${value}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @param {string | null} value in ms */
|
|
101
|
+
setCameraPeekAngleVertical(value) {
|
|
102
|
+
var angle = Number(value);
|
|
103
|
+
if (angle >= 0 && angle <= 1 && value != null) {
|
|
104
|
+
this.cameraPeekAngleVertical = angle;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
console.warn(`Invalid camera peek angle vertical value. Min is 0, Max is 1. Value is ${value}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @param {string | null} value in ms */
|
|
111
|
+
setCameraFieldOfView(value) {
|
|
112
|
+
var fov = Number(value);
|
|
113
|
+
if (fov < minFieldOfView && value != null) {
|
|
114
|
+
console.warn(`Invalid camera FOV value. Min is ${minFieldOfView} Max is ${maxFieldOfView}. Value is ${value} which is below the minimum.`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (fov > maxFieldOfView && value != null) {
|
|
118
|
+
console.warn(`Invalid camera FOV value. Min is ${minFieldOfView} Max is ${maxFieldOfView}. Value is ${value} which is aboe the maximum.`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (value == null) {
|
|
122
|
+
console.warn(`Invalid camera FOV value. Min is ${minFieldOfView} Max is ${maxFieldOfView}. Value is ${value}.`);
|
|
123
|
+
}
|
|
124
|
+
this.cameraFieldOfView = fov;
|
|
125
|
+
}
|
|
126
|
+
}
|