brilliantsole 0.0.29 → 0.0.30

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.
Files changed (105) hide show
  1. package/build/brilliantsole.cjs +5630 -494
  2. package/build/brilliantsole.cjs.map +1 -1
  3. package/build/brilliantsole.js +21293 -3088
  4. package/build/brilliantsole.js.map +1 -1
  5. package/build/brilliantsole.ls.js +23153 -6240
  6. package/build/brilliantsole.ls.js.map +1 -1
  7. package/build/brilliantsole.min.js +1 -1
  8. package/build/brilliantsole.min.js.map +1 -1
  9. package/build/brilliantsole.module.d.ts +1158 -74
  10. package/build/brilliantsole.module.js +21259 -3089
  11. package/build/brilliantsole.module.js.map +1 -1
  12. package/build/brilliantsole.module.min.d.ts +1158 -74
  13. package/build/brilliantsole.module.min.js +1 -1
  14. package/build/brilliantsole.module.min.js.map +1 -1
  15. package/build/brilliantsole.node.module.d.ts +869 -70
  16. package/build/brilliantsole.node.module.js +5608 -495
  17. package/build/brilliantsole.node.module.js.map +1 -1
  18. package/build/dts/BS.d.ts +20 -1
  19. package/build/dts/Device.d.ts +135 -13
  20. package/build/dts/DeviceManager.d.ts +3 -3
  21. package/build/dts/DisplayManager.d.ts +320 -0
  22. package/build/dts/FileTransferManager.d.ts +10 -4
  23. package/build/dts/connection/BaseConnectionManager.d.ts +2 -2
  24. package/build/dts/connection/bluetooth/BluetoothUUID.d.ts +12 -0
  25. package/build/dts/devicePair/DevicePair.d.ts +5 -5
  26. package/build/dts/sensor/SensorConfigurationManager.d.ts +2 -1
  27. package/build/dts/server/BaseClient.d.ts +4 -4
  28. package/build/dts/server/udp/UDPUtils.d.ts +1 -1
  29. package/build/dts/utils/ArrayBufferUtils.d.ts +1 -0
  30. package/build/dts/utils/BitmapUtils.d.ts +17 -0
  31. package/build/dts/utils/ColorUtils.d.ts +5 -0
  32. package/build/dts/utils/DisplayBitmapUtils.d.ts +47 -0
  33. package/build/dts/utils/DisplayCanvasHelper.d.ts +270 -0
  34. package/build/dts/utils/DisplayContextCommand.d.ts +300 -0
  35. package/build/dts/utils/DisplayContextState.d.ts +51 -0
  36. package/build/dts/utils/DisplayContextStateHelper.d.ts +9 -0
  37. package/build/dts/utils/DisplayManagerInterface.d.ts +173 -0
  38. package/build/dts/utils/DisplaySpriteSheetUtils.d.ts +72 -0
  39. package/build/dts/utils/DisplayUtils.d.ts +70 -0
  40. package/build/dts/utils/MathUtils.d.ts +16 -0
  41. package/build/dts/utils/PathUtils.d.ts +4 -0
  42. package/build/dts/utils/RangeHelper.d.ts +7 -0
  43. package/build/dts/utils/SpriteSheetUtils.d.ts +20 -0
  44. package/build/index.d.ts +1156 -72
  45. package/build/index.node.d.ts +867 -68
  46. package/examples/3d-generic/index.html +5 -0
  47. package/examples/3d-generic/script.js +1 -0
  48. package/examples/basic/index.html +335 -0
  49. package/examples/basic/script.js +1303 -3
  50. package/examples/camera/utils.js +1 -1
  51. package/examples/display-3d/index.html +195 -0
  52. package/examples/display-3d/script.js +1235 -0
  53. package/examples/display-canvas/aframe.js +42950 -0
  54. package/examples/display-canvas/index.html +245 -0
  55. package/examples/display-canvas/script.js +2312 -0
  56. package/examples/display-image/index.html +189 -0
  57. package/examples/display-image/script.js +1093 -0
  58. package/examples/display-spritesheet/index.html +960 -0
  59. package/examples/display-spritesheet/script.js +4243 -0
  60. package/examples/display-text/index.html +195 -0
  61. package/examples/display-text/script.js +1418 -0
  62. package/examples/display-wireframe/index.html +204 -0
  63. package/examples/display-wireframe/script.js +1167 -0
  64. package/examples/glasses-gestures/index.html +6 -1
  65. package/examples/glasses-gestures/script.js +10 -8
  66. package/examples/microphone/index.html +3 -1
  67. package/examples/punch/index.html +4 -1
  68. package/examples/server/script.js +0 -1
  69. package/package.json +10 -2
  70. package/src/BS.ts +92 -1
  71. package/src/CameraManager.ts +6 -2
  72. package/src/Device.ts +544 -13
  73. package/src/DisplayManager.ts +2989 -0
  74. package/src/FileTransferManager.ts +79 -26
  75. package/src/InformationManager.ts +8 -7
  76. package/src/MicrophoneManager.ts +10 -3
  77. package/src/TfliteManager.ts +4 -2
  78. package/src/WifiManager.ts +4 -1
  79. package/src/connection/BaseConnectionManager.ts +2 -0
  80. package/src/connection/bluetooth/bluetoothUUIDs.ts +36 -1
  81. package/src/devicePair/DevicePairPressureSensorDataManager.ts +1 -1
  82. package/src/scanner/NobleScanner.ts +1 -1
  83. package/src/sensor/SensorConfigurationManager.ts +16 -8
  84. package/src/server/udp/UDPServer.ts +4 -4
  85. package/src/server/udp/UDPUtils.ts +1 -1
  86. package/src/server/websocket/WebSocketClient.ts +50 -1
  87. package/src/utils/ArrayBufferUtils.ts +23 -5
  88. package/src/utils/AudioUtils.ts +1 -1
  89. package/src/utils/ColorUtils.ts +66 -0
  90. package/src/utils/DisplayBitmapUtils.ts +695 -0
  91. package/src/utils/DisplayCanvasHelper.ts +4222 -0
  92. package/src/utils/DisplayContextCommand.ts +1566 -0
  93. package/src/utils/DisplayContextState.ts +138 -0
  94. package/src/utils/DisplayContextStateHelper.ts +48 -0
  95. package/src/utils/DisplayManagerInterface.ts +1356 -0
  96. package/src/utils/DisplaySpriteSheetUtils.ts +782 -0
  97. package/src/utils/DisplayUtils.ts +529 -0
  98. package/src/utils/EventDispatcher.ts +59 -14
  99. package/src/utils/MathUtils.ts +88 -2
  100. package/src/utils/ObjectUtils.ts +6 -1
  101. package/src/utils/PathUtils.ts +192 -0
  102. package/src/utils/RangeHelper.ts +15 -3
  103. package/src/utils/Timer.ts +1 -1
  104. package/src/utils/environment.ts +15 -6
  105. package/examples/microphone/gender.js +0 -54
@@ -0,0 +1,1235 @@
1
+ import * as BS from "../../build/brilliantsole.module.js";
2
+ /** @typedef {import("../utils/three/three.module.min").Vector2} TVector2 */
3
+ /** @typedef {import("../utils/three/three.module.min").Vector3} TVector3 */
4
+ /** @typedef {import("../utils/three/three.module.min").Quaternion} TQuaternion */
5
+ /** @typedef {import("../utils/three/three.module.min").Euler} TEuler */
6
+
7
+ // DEVICE
8
+ const device = new BS.Device();
9
+ window.device = device;
10
+ window.BS = BS;
11
+
12
+ const rotationDevice = new BS.Device();
13
+ window.rotationDevice = rotationDevice;
14
+
15
+ // CONNECT
16
+
17
+ const toggleConnectionButton = document.getElementById("toggleConnection");
18
+ toggleConnectionButton.addEventListener("click", () =>
19
+ device.toggleConnection()
20
+ );
21
+ device.addEventListener("connectionStatus", () => {
22
+ let disabled = false;
23
+ let innerText = device.connectionStatus;
24
+ switch (device.connectionStatus) {
25
+ case "notConnected":
26
+ innerText = "connect";
27
+ break;
28
+ case "connected":
29
+ innerText = "disconnect";
30
+ break;
31
+ }
32
+ toggleConnectionButton.disabled = disabled;
33
+ toggleConnectionButton.innerText = innerText;
34
+ });
35
+
36
+ const toggleRotationConnectionButton = document.getElementById(
37
+ "toggleRotationConnection"
38
+ );
39
+ toggleRotationConnectionButton.addEventListener("click", () =>
40
+ rotationDevice.toggleConnection()
41
+ );
42
+ rotationDevice.addEventListener("connectionStatus", () => {
43
+ let disabled = false;
44
+ let innerText = rotationDevice.connectionStatus;
45
+ switch (rotationDevice.connectionStatus) {
46
+ case "notConnected":
47
+ innerText = "connect to rotator";
48
+ break;
49
+ case "connected":
50
+ innerText = "disconnect from rotator";
51
+ break;
52
+ }
53
+ toggleRotationConnectionButton.disabled = disabled;
54
+ toggleRotationConnectionButton.innerText = innerText;
55
+ });
56
+
57
+ // CANVAS
58
+ /** @type {HTMLCanvasElement} */
59
+ const displayCanvas = document.getElementById("display");
60
+
61
+ // DISPLAY CANVAS HELPER
62
+ const displayCanvasHelper = new BS.DisplayCanvasHelper();
63
+ // displayCanvasHelper.setBrightness("veryLow");
64
+ displayCanvasHelper.canvas = displayCanvas;
65
+ window.displayCanvasHelper = displayCanvasHelper;
66
+
67
+ device.addEventListener("connected", () => {
68
+ if (device.isDisplayAvailable) {
69
+ displayCanvasHelper.device = device;
70
+ } else {
71
+ console.error("device doesn't have a display");
72
+ device.disconnect();
73
+ }
74
+ });
75
+
76
+ // BRIGHTNESS
77
+ /** @type {HTMLSelectElement} */
78
+ const setDisplayBrightnessSelect = document.getElementById(
79
+ "setDisplayBrightnessSelect"
80
+ );
81
+ /** @type {HTMLOptGroupElement} */
82
+ const setDisplayBrightnessSelectOptgroup =
83
+ setDisplayBrightnessSelect.querySelector("optgroup");
84
+ BS.DisplayBrightnesses.forEach((displayBrightness) => {
85
+ setDisplayBrightnessSelectOptgroup.appendChild(new Option(displayBrightness));
86
+ });
87
+ setDisplayBrightnessSelect.addEventListener("input", () => {
88
+ displayCanvasHelper.setBrightness(setDisplayBrightnessSelect.value);
89
+ });
90
+
91
+ setDisplayBrightnessSelect.value = displayCanvasHelper.brightness;
92
+
93
+ // COLORS
94
+
95
+ /** @type {HTMLTemplateElement} */
96
+ const displayColorTemplate = document.getElementById("displayColorTemplate");
97
+ const displayColorsContainer = document.getElementById("displayColors");
98
+ const setDisplayColor = BS.ThrottleUtils.throttle(
99
+ (colorIndex, colorString) => {
100
+ console.log({ colorIndex, colorString });
101
+ displayCanvasHelper.setColor(colorIndex, colorString, true);
102
+ },
103
+ 100,
104
+ true
105
+ );
106
+ /** @type {HTMLInputElement[]} */
107
+ const displayColorInputs = [];
108
+ const setupColors = () => {
109
+ displayColorsContainer.innerHTML = "";
110
+ for (
111
+ let colorIndex = 0;
112
+ colorIndex < displayCanvasHelper.numberOfColors;
113
+ colorIndex++
114
+ ) {
115
+ const displayColorContainer = displayColorTemplate.content
116
+ .cloneNode(true)
117
+ .querySelector(".displayColor");
118
+
119
+ const colorIndexSpan = displayColorContainer.querySelector(".colorIndex");
120
+ colorIndexSpan.innerText = `color #${colorIndex}`;
121
+ const colorInput = displayColorContainer.querySelector("input");
122
+ displayColorInputs[colorIndex] = colorInput;
123
+ colorInput.addEventListener("input", () => {
124
+ setDisplayColor(colorIndex, colorInput.value);
125
+ });
126
+ displayColorsContainer.appendChild(displayColorContainer);
127
+ }
128
+ };
129
+ setupColors();
130
+ displayCanvasHelper.addEventListener("numberOfColors", () => setupColors());
131
+ displayCanvasHelper.addEventListener("color", (event) => {
132
+ const { colorHex, colorIndex } = event.message;
133
+ displayColorInputs[colorIndex].value = colorHex;
134
+ });
135
+
136
+ // CAMERA
137
+ const cameraEntity = document.getElementById("camera");
138
+ const cameraRigEntity = document.getElementById("cameraRig");
139
+
140
+ // MODEL
141
+ const modelEntity = document.getElementById("model");
142
+ const testEntity = document.getElementById("test");
143
+ let isTall = true;
144
+ const normalizeEntity = (entity) => {
145
+ const object3D = entity.getObject3D("mesh");
146
+
147
+ if (!object3D) return;
148
+
149
+ const box = new THREE.Box3().setFromObject(object3D);
150
+ const size = new THREE.Vector3();
151
+ box.getSize(size);
152
+ // isTall = size.y >= size.x;
153
+ console.log({ isTall });
154
+ const center = new THREE.Vector3();
155
+ box.getCenter(center);
156
+
157
+ object3D.position.sub(center);
158
+
159
+ const maxDim = Math.max(size.x, size.y, size.z);
160
+ const fitHeightDistance =
161
+ 1 / (2 * Math.tan(THREE.MathUtils.degToRad(50) / 2));
162
+ const fitScreenScale = fitHeightDistance / maxDim;
163
+
164
+ entity.object3D.scale.setScalar(fitScreenScale * 2.0);
165
+ };
166
+ let numberOfSteps = 10;
167
+ let waitTime = 5;
168
+ modelEntity.addEventListener("model-loaded", async () => {
169
+ const modelEntity = document.querySelector("#model");
170
+ modelEntity.object3D.scale.set(1, 1, 1);
171
+ modelEntity.object3D.rotation.set(0, 0, 0);
172
+ await waitForFrame();
173
+
174
+ normalizeEntity(modelEntity);
175
+
176
+ if (!jitRender) {
177
+ await generateSpriteSheet(numberOfSteps, waitTime);
178
+ }
179
+ });
180
+ testEntity.addEventListener("model-loaded", async () => {
181
+ const testEntity = document.querySelector("#model");
182
+ testEntity.object3D.scale.set(1, 1, 1);
183
+ testEntity.object3D.rotation.set(0, 0, 0);
184
+ await waitForFrame();
185
+
186
+ normalizeEntity(testEntity);
187
+ });
188
+
189
+ const sceneEl = document.querySelector("a-scene");
190
+ const croppedCanvas = document.createElement("canvas");
191
+ const croppedCtx = croppedCanvas.getContext("2d");
192
+ const tempCanvas = document.createElement("canvas");
193
+ const tempCtx = tempCanvas.getContext("2d");
194
+ async function captureModelSnapshot(alphaThreshold = 10, reposition = true) {
195
+ const position = cameraRigEntity.object3D.position.clone();
196
+ if (reposition) {
197
+ cameraRigEntity.object3D.position.set(0, 0, 3);
198
+ cameraRigEntity.setAttribute("look-controls", { enabled: false });
199
+ await waitForFrame();
200
+ cameraRigEntity.object3D.rotation.set(0, 0, 0);
201
+ await waitForFrame();
202
+ }
203
+
204
+ const renderer = sceneEl.renderer;
205
+ const camera = sceneEl.camera;
206
+ const object3D = modelEntity.getObject3D("mesh");
207
+ if (!object3D) return null;
208
+
209
+ // --- Step 1: project model’s bounding box into screen space ---
210
+ const box = new THREE.Box3().setFromObject(object3D);
211
+ const vertices = [
212
+ new THREE.Vector3(box.min.x, box.min.y, box.min.z),
213
+ new THREE.Vector3(box.min.x, box.min.y, box.max.z),
214
+ new THREE.Vector3(box.min.x, box.max.y, box.min.z),
215
+ new THREE.Vector3(box.min.x, box.max.y, box.max.z),
216
+ new THREE.Vector3(box.max.x, box.min.y, box.min.z),
217
+ new THREE.Vector3(box.max.x, box.min.y, box.max.z),
218
+ new THREE.Vector3(box.max.x, box.max.y, box.min.z),
219
+ new THREE.Vector3(box.max.x, box.max.y, box.max.z),
220
+ ];
221
+
222
+ const min = new THREE.Vector2(Infinity, Infinity);
223
+ const max = new THREE.Vector2(-Infinity, -Infinity);
224
+
225
+ vertices.forEach((v) => {
226
+ v.project(camera);
227
+ const x = (v.x * 0.5 + 0.5) * renderer.domElement.width;
228
+ const y = (1 - (v.y * 0.5 + 0.5)) * renderer.domElement.height;
229
+ min.x = Math.min(min.x, x);
230
+ min.y = Math.min(min.y, y);
231
+ max.x = Math.max(max.x, x);
232
+ max.y = Math.max(max.y, y);
233
+ });
234
+
235
+ const width = Math.max(1, Math.floor(max.x - min.x));
236
+ const height = Math.max(1, Math.floor(max.y - min.y));
237
+
238
+ // --- Step 2: force renderer transparent ---
239
+ const prevClearColor = new THREE.Color();
240
+ const prevClearAlpha = renderer.getClearAlpha();
241
+ renderer.getClearColor(prevClearColor);
242
+
243
+ renderer.setClearColor(0x000000, 0);
244
+
245
+ // Hide any <a-sky> entities
246
+ const skies = sceneEl.querySelectorAll("a-sky");
247
+ skies.forEach((sky) => (sky.object3D.visible = false));
248
+
249
+ // --- Step 3: render scene ---
250
+ renderer.render(sceneEl.object3D, camera);
251
+
252
+ // --- Step 4: copy to temporary canvas ---
253
+ tempCanvas.width = renderer.domElement.width;
254
+ tempCanvas.height = renderer.domElement.height;
255
+ tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
256
+ tempCtx.drawImage(renderer.domElement, 0, 0);
257
+
258
+ // --- Step 5: crop initial bounding box ---
259
+ croppedCanvas.width = width;
260
+ croppedCanvas.height = height;
261
+ croppedCtx.clearRect(0, 0, width, height);
262
+ croppedCtx.drawImage(
263
+ tempCanvas,
264
+ min.x,
265
+ min.y,
266
+ width,
267
+ height,
268
+ 0,
269
+ 0,
270
+ width,
271
+ height
272
+ );
273
+
274
+ // --- Step 6: pixel-level trim for zero-padding ---
275
+ const imageData = croppedCtx.getImageData(0, 0, width, height);
276
+ const data = imageData.data;
277
+
278
+ let top = height,
279
+ bottom = 0,
280
+ left = width,
281
+ right = 0;
282
+ for (let y = 0; y < height; y++) {
283
+ for (let x = 0; x < width; x++) {
284
+ const idx = (y * width + x) * 4;
285
+ const alpha = data[idx + 3];
286
+ if (alpha > alphaThreshold) {
287
+ if (x < left) left = x;
288
+ if (x > right) right = x;
289
+ if (y < top) top = y;
290
+ if (y > bottom) bottom = y;
291
+ }
292
+ }
293
+ }
294
+
295
+ // Handle case where model is invisible
296
+ if (right < left || bottom < top) {
297
+ renderer.setClearColor(prevClearColor, prevClearAlpha);
298
+ skies.forEach((sky) => (sky.object3D.visible = true));
299
+ return null;
300
+ }
301
+
302
+ const finalW = right - left + 1;
303
+ const finalH = bottom - top + 1;
304
+ const finalCanvas = document.createElement("canvas");
305
+ const finalCtx = finalCanvas.getContext("2d");
306
+ finalCanvas.width = finalW;
307
+ finalCanvas.height = finalH;
308
+ finalCtx.clearRect(0, 0, finalW, finalH);
309
+
310
+ const finalImageData = finalCtx.createImageData(finalW, finalH);
311
+ for (let y = 0; y < finalH; y++) {
312
+ const srcStart = ((y + top) * width + left) * 4;
313
+ const destStart = y * finalW * 4;
314
+ finalImageData.data.set(
315
+ data.subarray(srcStart, srcStart + finalW * 4),
316
+ destStart
317
+ );
318
+ }
319
+ finalCtx.putImageData(finalImageData, 0, 0);
320
+
321
+ // --- Step 7: restore renderer and skies ---
322
+ renderer.setClearColor(prevClearColor, prevClearAlpha);
323
+ skies.forEach((sky) => (sky.object3D.visible = true));
324
+
325
+ if (reposition) {
326
+ cameraRigEntity.setAttribute("look-controls", { enabled: true });
327
+ cameraRigEntity.object3D.position.copy(position);
328
+ }
329
+
330
+ return finalCanvas; // tight transparent PNG
331
+ }
332
+ window.captureModelSnapshot = captureModelSnapshot;
333
+
334
+ async function captureEntityRotations(
335
+ numberOfSteps = 10,
336
+ waitTime = 100,
337
+ skipPicture = false
338
+ ) {
339
+ const captures = [];
340
+ const step = 360 / numberOfSteps;
341
+ const minPitch = -80; // stop short of bottom pole
342
+ const maxPitch = 80; // stop short of top pole
343
+
344
+ //Single top pole (+90°)
345
+ if (isTall) {
346
+ modelEntity.object3D.rotation.set(THREE.MathUtils.degToRad(90), 0, 0);
347
+ } else {
348
+ modelEntity.object3D.rotation.set(Math.PI, 0, 0);
349
+ }
350
+ if (!skipPicture) {
351
+ await waitForFrame();
352
+ await BS.wait(waitTime);
353
+ const canvas = await captureModelSnapshot();
354
+ captures.push({
355
+ euler: modelEntity.object3D.rotation.clone(),
356
+ quaternion: modelEntity.object3D.quaternion.clone(),
357
+ canvas,
358
+ });
359
+ }
360
+
361
+ // Loop over pitch, skipping true poles
362
+ for (let pitch = maxPitch; pitch >= minPitch; pitch -= step) {
363
+ for (let i = 0; i < numberOfSteps; i++) {
364
+ const yaw = i * step;
365
+
366
+ // Rotate entity
367
+ if (isTall) {
368
+ modelEntity.object3D.rotation.set(
369
+ THREE.MathUtils.degToRad(pitch), // X = pitch
370
+ THREE.MathUtils.degToRad(yaw), // Y = yaw
371
+ THREE.MathUtils.degToRad(0), // Z = roll (upright),
372
+ "ZXY"
373
+ );
374
+ } else {
375
+ modelEntity.object3D.rotation.set(
376
+ THREE.MathUtils.degToRad(pitch + 90), // X = pitch
377
+ THREE.MathUtils.degToRad(0), // Y = yaw
378
+ THREE.MathUtils.degToRad(yaw), // Z = roll (upright),
379
+ "XYZ"
380
+ );
381
+ }
382
+
383
+ // Wait for next frame so rotation is applied
384
+ await waitForFrame();
385
+ await BS.wait(waitTime);
386
+
387
+ if (!skipPicture) {
388
+ // Take picture
389
+ const canvas = await captureModelSnapshot();
390
+ captures.push({
391
+ euler: modelEntity.object3D.rotation.clone(),
392
+ quaternion: modelEntity.object3D.quaternion.clone(),
393
+ canvas,
394
+ });
395
+ }
396
+ }
397
+ }
398
+
399
+ // Single bottom pole (-90°)
400
+ if (isTall) {
401
+ modelEntity.object3D.rotation.set(THREE.MathUtils.degToRad(-90), 0, 0);
402
+ } else {
403
+ modelEntity.object3D.rotation.set(0, 0, 0);
404
+ }
405
+ if (!skipPicture) {
406
+ await waitForFrame();
407
+ await BS.wait(waitTime);
408
+ const canvas = await captureModelSnapshot();
409
+ captures.push({
410
+ euler: modelEntity.object3D.rotation.clone(),
411
+ quaternion: modelEntity.object3D.quaternion.clone(),
412
+ canvas,
413
+ });
414
+ }
415
+
416
+ modelEntity.object3D.rotation.set(0, 0, 0);
417
+
418
+ return captures;
419
+ }
420
+ window.captureEntityRotations = captureEntityRotations;
421
+
422
+ let createSinglePalette = false;
423
+ let skipVoidColor = false;
424
+
425
+ /** @type {import("../utils/three/three.module.min")} */
426
+ const THREE = window.THREE;
427
+
428
+ /** @type {{sprite: BS.DisplaySprite, euler: TEuler, quaternion: TQuaternion, image: HTMLImageElement}[]} */
429
+ const spriteCaptures = [];
430
+ window.spriteCaptures = spriteCaptures;
431
+ const generateSpriteSheet = async (numberOfSteps, waitTime) => {
432
+ generateSpriteSheetButton.disabled = true;
433
+ const captures = await captureEntityRotations(numberOfSteps, waitTime);
434
+
435
+ let maxHeight = -Infinity;
436
+ let maxWidth = -Infinity;
437
+ captures.forEach(({ canvas }) => {
438
+ maxHeight = Math.max(maxHeight, canvas.height);
439
+ maxWidth = Math.max(maxWidth, canvas.width);
440
+ });
441
+ const scalar = drawInputHeight / maxHeight;
442
+ const previewScalar = maxSpritePreviewHeight / maxHeight;
443
+
444
+ spriteCaptures.forEach(({ image }) => image.remove());
445
+ spriteCaptures.length = 0;
446
+
447
+ spriteSheet = { name: "model", sprites: [] };
448
+ if (createSinglePalette) {
449
+ // FILL - create palette from default front angle
450
+ }
451
+ for (let i in captures) {
452
+ const { canvas, euler, quaternion } = captures[i];
453
+
454
+ const height = canvas.height * scalar;
455
+ const width = canvas.width * scalar;
456
+
457
+ const resizedCanvas = BS.resizeImage(canvas, width, height);
458
+
459
+ const { sprite, blob } = await BS.canvasToSprite(
460
+ resizedCanvas,
461
+ i,
462
+ skipVoidColor
463
+ ? Math.min(
464
+ BS.pixelDepthToNumberOfColors(pixelDepth),
465
+ displayCanvasHelper.numberOfColors - 1
466
+ )
467
+ : BS.pixelDepthToNumberOfColors(pixelDepth),
468
+ createSinglePalette ? "palette" : i,
469
+ !createSinglePalette,
470
+ spriteSheet,
471
+ skipVoidColor ? 1 : 0
472
+ );
473
+ //console.log(sprite, blob);
474
+
475
+ const previewHeight = canvas.height * previewScalar;
476
+ const previewWidth = canvas.width * previewScalar;
477
+
478
+ const image = new Image();
479
+ image.width = previewWidth;
480
+ image.height = previewHeight;
481
+ image.src = URL.createObjectURL(blob);
482
+ document.body.appendChild(image);
483
+ spriteCaptures.push({
484
+ sprite,
485
+ euler,
486
+ quaternion,
487
+ image,
488
+ });
489
+ }
490
+
491
+ checkSpriteSheetSize();
492
+
493
+ generateSpriteSheetButton.disabled = false;
494
+ if (uploadWholeSpriteSheet) {
495
+ await displayCanvasHelper.uploadSpriteSheet(spriteSheet);
496
+ await displayCanvasHelper.selectSpriteSheet(spriteSheet.name);
497
+ }
498
+ await draw();
499
+ };
500
+ let maxSpritePreviewHeight = 50;
501
+ window.generateSpriteSheet = generateSpriteSheet;
502
+
503
+ const generateSpriteSheetButton = document.getElementById(
504
+ "generateSpriteSheet"
505
+ );
506
+ generateSpriteSheetButton.addEventListener("click", () => {
507
+ generateSpriteSheet(numberOfSteps, waitTime);
508
+ });
509
+
510
+ /** @type {HTMLInputElement} */
511
+ const modelInput = document.getElementById("modelInput");
512
+ modelInput.addEventListener("input", () => {
513
+ const file = modelInput.files[0];
514
+ if (!file) return;
515
+ const url = URL.createObjectURL(file);
516
+ loadModel(url);
517
+ modelInput.value = "";
518
+ });
519
+ const loadModel = (url) => {
520
+ console.log("model url", url);
521
+ modelEntity.setAttribute("gltf-model", `url(${url})`);
522
+ testEntity.setAttribute("gltf-model", `url(${url})`);
523
+ };
524
+
525
+ window.addEventListener(
526
+ "keydown",
527
+ function (e) {
528
+ if (e.target.nodeName == "INPUT") {
529
+ return;
530
+ }
531
+
532
+ const keysToPrevent = [
533
+ "ArrowUp",
534
+ "ArrowDown",
535
+ "ArrowLeft",
536
+ "ArrowRight",
537
+ " ",
538
+ ];
539
+
540
+ if (keysToPrevent.includes(e.key)) {
541
+ e.preventDefault();
542
+ }
543
+ },
544
+ { passive: false }
545
+ );
546
+
547
+ const waitForFrame = async () => await new Promise(requestAnimationFrame);
548
+ const lookAtEntity = document.getElementById("lookAt");
549
+ async function getRelativeModelQuaternion() {
550
+ lookAtEntity.object3D.position.copy(modelEntity.object3D.position);
551
+ lookAtEntity.object3D.lookAt(cameraRigEntity.object3D.position);
552
+ await waitForFrame();
553
+
554
+ const lookAtEntityQuaternion = lookAtEntity.object3D.quaternion
555
+ .clone()
556
+ .invert();
557
+ const modelEntityQuaternion = new THREE.Quaternion();
558
+ modelEntity.object3D.getWorldQuaternion(modelEntityQuaternion);
559
+
560
+ lookAtEntityQuaternion.multiply(modelEntityQuaternion);
561
+ testEntity.object3D.quaternion.copy(lookAtEntityQuaternion);
562
+ return lookAtEntityQuaternion;
563
+ }
564
+ window.getRelativeModelQuaternion = getRelativeModelQuaternion;
565
+
566
+ async function getModelScreenBoundingBox(alphaThreshold = 10) {
567
+ sceneEl.object3D.updateMatrixWorld(true);
568
+
569
+ const renderer = sceneEl.renderer;
570
+ const canvas = renderer.domElement;
571
+ const camera = sceneEl.camera;
572
+
573
+ // drawing buffer (physical pixels)
574
+ const dbW = canvas.width,
575
+ dbH = canvas.height;
576
+
577
+ // CSS pixel size (use this for UI overlays)
578
+ const size = renderer.getSize(new THREE.Vector2());
579
+ const cssW = size.x,
580
+ cssH = size.y;
581
+
582
+ const object3D = modelEntity.getObject3D("mesh");
583
+ if (!object3D) return null;
584
+
585
+ // --- Project 8 corners of world-space AABB ---
586
+ const box = new THREE.Box3().setFromObject(object3D);
587
+ if (!isFinite(box.min.x)) return null;
588
+
589
+ const corners = [
590
+ new THREE.Vector3(box.min.x, box.min.y, box.min.z),
591
+ new THREE.Vector3(box.min.x, box.min.y, box.max.z),
592
+ new THREE.Vector3(box.min.x, box.max.y, box.min.z),
593
+ new THREE.Vector3(box.min.x, box.max.y, box.max.z),
594
+ new THREE.Vector3(box.max.x, box.min.y, box.min.z),
595
+ new THREE.Vector3(box.max.x, box.min.y, box.max.z),
596
+ new THREE.Vector3(box.max.x, box.max.y, box.min.z),
597
+ new THREE.Vector3(box.max.x, box.max.y, box.max.z),
598
+ ];
599
+
600
+ let minX = Infinity,
601
+ minY = Infinity;
602
+ let maxX = -Infinity,
603
+ maxY = -Infinity;
604
+
605
+ for (const v of corners) {
606
+ v.project(camera); // NDC
607
+ const x = (v.x * 0.5 + 0.5) * dbW; // -> drawing buffer pixels
608
+ const y = (1 - (v.y * 0.5 + 0.5)) * dbH;
609
+ minX = Math.min(minX, x);
610
+ minY = Math.min(minY, y);
611
+ maxX = Math.max(maxX, x);
612
+ maxY = Math.max(maxY, y);
613
+ }
614
+
615
+ // Unclamped AABB (may be <0 or >dbW/dbH)
616
+ const unclamped = { minX, minY, maxX, maxY };
617
+
618
+ // Clamp for safe readPixels (ROI)
619
+ const sx0 = Math.max(0, Math.floor(minX));
620
+ const sy0 = Math.max(0, Math.floor(minY));
621
+ const sx1 = Math.min(dbW, Math.ceil(maxX));
622
+ const sy1 = Math.min(dbH, Math.ceil(maxY));
623
+ const roiW = Math.max(0, sx1 - sx0);
624
+ const roiH = Math.max(0, sy1 - sy0);
625
+ if (roiW === 0 || roiH === 0) return null;
626
+
627
+ // Transparent render pass
628
+ const prevClearColor = new THREE.Color();
629
+ const prevClearAlpha = renderer.getClearAlpha();
630
+ renderer.getClearColor(prevClearColor);
631
+ renderer.setClearColor(0x000000, 0);
632
+
633
+ const skies = sceneEl.querySelectorAll("a-sky");
634
+ const prevSkyVis = [];
635
+ skies.forEach(
636
+ (sky, i) => (
637
+ (prevSkyVis[i] = sky.object3D.visible), (sky.object3D.visible = false)
638
+ )
639
+ );
640
+
641
+ if (typeof waitForFrame === "function") await waitForFrame();
642
+ renderer.render(sceneEl.object3D, camera);
643
+
644
+ // Read pixels in ROI (WebGL is bottom-left origin)
645
+ const gl = renderer.getContext();
646
+ const pixels = new Uint8Array(roiW * roiH * 4);
647
+ gl.readPixels(sx0, dbH - sy1, roiW, roiH, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
648
+
649
+ // Alpha trim within ROI
650
+ let left = roiW,
651
+ right = -1,
652
+ bottom = 0,
653
+ topBL = roiH;
654
+ for (let rowBL = 0; rowBL < roiH; rowBL++) {
655
+ const rowOffset = rowBL * roiW * 4;
656
+ for (let col = 0; col < roiW; col++) {
657
+ const a = pixels[rowOffset + col * 4 + 3];
658
+ if (a > alphaThreshold) {
659
+ if (col < left) left = col;
660
+ if (col > right) right = col;
661
+ if (rowBL < topBL) topBL = rowBL;
662
+ if (rowBL > bottom) bottom = rowBL;
663
+ }
664
+ }
665
+ }
666
+
667
+ // Restore renderer + skies ASAP
668
+ renderer.setClearColor(prevClearColor, prevClearAlpha);
669
+ skies.forEach((sky, i) => (sky.object3D.visible = prevSkyVis[i]));
670
+
671
+ if (right < left || bottom < topBL) return null;
672
+
673
+ // Convert ROI (bottom-left origin) to screen (top-left origin)
674
+ const topRowTopOrigin = roiH - 1 - bottom;
675
+ const bottomRowTopOrigin = roiH - 1 - topBL;
676
+
677
+ const tightMinX = sx0 + left;
678
+ const tightMaxX = sx0 + right;
679
+ const tightMinY = sy0 + topRowTopOrigin;
680
+ const tightMaxY = sy0 + bottomRowTopOrigin;
681
+
682
+ // Helper: normalize a box
683
+ const toNorm = (box, denomW, denomH) => ({
684
+ minX: box.minX / denomW,
685
+ minY: box.minY / denomH,
686
+ maxX: box.maxX / denomW,
687
+ maxY: box.maxY / denomH,
688
+ // no +1: use continuous extents for normalized sizes
689
+ width: (box.maxX - box.minX) / denomW,
690
+ height: (box.maxY - box.minY) / denomH,
691
+ });
692
+
693
+ // Build outputs in both spaces (choose 'css' for HTML overlays)
694
+ const tightBox = {
695
+ minX: tightMinX,
696
+ minY: tightMinY,
697
+ maxX: tightMaxX,
698
+ maxY: tightMaxY,
699
+ };
700
+ const aabbBox = unclamped;
701
+
702
+ return {
703
+ // Normalized to CSS size (recommended for DOM overlays)
704
+ css: {
705
+ tight: toNorm(tightBox, cssW, cssH),
706
+ aabb: toNorm(aabbBox, cssW, cssH), // can go <0 or >1 if off-screen
707
+ },
708
+ // Normalized to drawing buffer (rarely what you want for UI)
709
+ buffer: {
710
+ tight: toNorm(tightBox, dbW, dbH),
711
+ aabb: toNorm(aabbBox, dbW, dbH),
712
+ },
713
+ };
714
+ }
715
+
716
+ window.getModelScreenBoundingBox = getModelScreenBoundingBox;
717
+
718
+ async function getClosestModelSprite() {
719
+ const _quaternion = await getRelativeModelQuaternion();
720
+ const _euler = new THREE.Euler().setFromQuaternion(_quaternion);
721
+ let rotation;
722
+ if (isTall) {
723
+ _euler.reorder("ZXY");
724
+ rotation = -_euler.z;
725
+ _euler.z = 0;
726
+ } else {
727
+ _euler.reorder("XYZ");
728
+ rotation = -_euler.y;
729
+ _euler.y = 0;
730
+ }
731
+
732
+ _quaternion.setFromEuler(_euler);
733
+ // console.log("euler", _euler);
734
+
735
+ let closestIndex = -1;
736
+ let closestAngle = Infinity;
737
+ spriteCaptures.forEach(({ quaternion, euler }, index) => {
738
+ //euler.z = _euler.z;
739
+ const angle = Math.abs(quaternion.angleTo(_quaternion));
740
+ if (angle < closestAngle) {
741
+ closestAngle = angle;
742
+ closestIndex = index;
743
+ }
744
+ });
745
+ // console.log({ closestAngle, closestIndex });
746
+
747
+ const { sprite, image } = spriteCaptures[closestIndex];
748
+
749
+ if (selectedImage != image) {
750
+ if (selectedImage) {
751
+ selectedImage.classList.remove("selected");
752
+ }
753
+ selectedImage = image;
754
+ selectedImage.classList.add("selected");
755
+ }
756
+
757
+ return {
758
+ sprite,
759
+ rotation,
760
+ };
761
+ }
762
+ let selectedImage;
763
+ window.getClosestModelSprite = getClosestModelSprite;
764
+
765
+ // DRAGOVER
766
+ window.addEventListener("dragover", (e) => {
767
+ e.preventDefault();
768
+ });
769
+
770
+ window.addEventListener("drop", (e) => {
771
+ e.preventDefault();
772
+ const file = e.dataTransfer.files[0];
773
+ console.log(file);
774
+ if (isModelFilename(file?.name)) {
775
+ const url = URL.createObjectURL(file);
776
+ loadModel(url);
777
+ return;
778
+ }
779
+ });
780
+
781
+ // REDRAW ON CHANGE
782
+ const redrawOnChangeInput = document.getElementById("redrawOnChange");
783
+ redrawOnChangeInput.addEventListener("input", () => {
784
+ setRedrawOnChange(redrawOnChangeInput.checked);
785
+ });
786
+ let redrawOnChange = redrawOnChangeInput.checked;
787
+ const setRedrawOnChange = (newRedrawOnChange) => {
788
+ redrawOnChange = newRedrawOnChange;
789
+ console.log({ redrawOnChange });
790
+ redrawOnChangeInput.checked = redrawOnChange;
791
+ };
792
+
793
+ // AUTODRAW
794
+ const autoDrawInput = document.getElementById("autoDraw");
795
+ autoDrawInput.addEventListener("input", () => {
796
+ setAutoDraw(autoDrawInput.checked);
797
+ });
798
+ let autoDraw = autoDrawInput.checked;
799
+ const setAutoDraw = (newAutoDraw) => {
800
+ autoDraw = newAutoDraw;
801
+ console.log({ autoDraw });
802
+ autoDrawInput.checked = autoDraw;
803
+ if (autoDraw) {
804
+ draw();
805
+ }
806
+ };
807
+
808
+ // PASTE
809
+ function isValidUrl(string) {
810
+ try {
811
+ new URL(string);
812
+ return true;
813
+ } catch (_) {
814
+ return false;
815
+ }
816
+ }
817
+ window.addEventListener("paste", (event) => {
818
+ const string = event.clipboardData.getData("text");
819
+ if (!isValidUrl(string)) {
820
+ return;
821
+ }
822
+ // eg https://modelviewer.dev/shared-assets/models/NeilArmstrong.glb
823
+ if (isModelFilename(string)) {
824
+ console.log("pasted model url");
825
+ }
826
+ });
827
+ const modelFileExtensions = [".glb", ".gltf"];
828
+ const isModelFilename = (string) =>
829
+ modelFileExtensions.some((extension) => string.endsWith(extension));
830
+ window.addEventListener("paste", (event) => {
831
+ const items = event.clipboardData.items;
832
+ for (let i = 0; i < items.length; i++) {
833
+ const item = items[i];
834
+ if (item.kind == "file") {
835
+ const file = item.getAsFile();
836
+ if (isModelFilename(file.name)) {
837
+ const url = URL.createObjectURL(file);
838
+ loadModel(url);
839
+ return;
840
+ }
841
+ }
842
+ }
843
+ });
844
+
845
+ // DRAW PARAMS
846
+
847
+ const drawXContainer = document.getElementById("drawX");
848
+ const drawXInput = drawXContainer.querySelector("input");
849
+ const drawXSpan = drawXContainer.querySelector("span.value");
850
+ let drawX = Number(drawXInput.value);
851
+
852
+ drawXInput.addEventListener("input", () => {
853
+ setDrawX(Number(drawXInput.value));
854
+ if (redrawOnChange) {
855
+ draw();
856
+ }
857
+ });
858
+ const setDrawX = (newDrawX) => {
859
+ drawX = newDrawX;
860
+ drawXInput.value = drawX;
861
+ // console.log({ drawX });
862
+ drawXSpan.innerText = Math.round(drawX);
863
+ };
864
+
865
+ const drawYContainer = document.getElementById("drawY");
866
+ const drawYInput = drawYContainer.querySelector("input");
867
+ const drawYSpan = drawYContainer.querySelector("span.value");
868
+ let drawY = Number(drawYInput.value);
869
+
870
+ drawYInput.addEventListener("input", () => {
871
+ setDrawY(Number(drawYInput.value));
872
+ if (redrawOnChange) {
873
+ draw();
874
+ }
875
+ });
876
+ const setDrawY = (newDrawY) => {
877
+ drawY = newDrawY;
878
+ drawYInput.value = drawY;
879
+ // console.log({ drawY });
880
+ drawYSpan.innerText = Math.round(drawY);
881
+ };
882
+
883
+ const drawRotationContainer = document.getElementById("drawRotation");
884
+ const drawRotationInput = drawRotationContainer.querySelector("input");
885
+ const drawRotationSpan = drawRotationContainer.querySelector("span.value");
886
+ let drawRotation = Number(drawRotationInput.value);
887
+
888
+ drawRotationInput.addEventListener("input", () => {
889
+ // console.log({ drawRotation });
890
+ setDrawRotation(Number(drawRotationInput.value));
891
+ if (redrawOnChange) {
892
+ draw();
893
+ }
894
+ });
895
+ const setDrawRotation = (newDrawRotation) => {
896
+ drawRotation = newDrawRotation;
897
+ drawRotationSpan.innerText = Math.round(newDrawRotation);
898
+ drawRotationInput.value = drawRotation;
899
+ };
900
+
901
+ const drawInputHeightContainer = document.getElementById("drawInputHeight");
902
+ const drawInputHeightInput = drawInputHeightContainer.querySelector("input");
903
+ const drawInputHeightSpan =
904
+ drawInputHeightContainer.querySelector("span.value");
905
+ let drawInputHeight = Number(drawInputHeightInput.value);
906
+
907
+ drawInputHeightInput.addEventListener("input", () => {
908
+ drawInputHeight = Number(drawInputHeightInput.value);
909
+ //console.log({ drawInputHeight });
910
+ drawInputHeightSpan.innerText = drawInputHeight;
911
+ });
912
+
913
+ const drawOutputHeightContainer = document.getElementById("drawOutputHeight");
914
+ const drawOutputHeightInput = drawOutputHeightContainer.querySelector("input");
915
+ const drawOutputHeightSpan =
916
+ drawOutputHeightContainer.querySelector("span.value");
917
+ let drawOutputHeight = Number(drawOutputHeightInput.value);
918
+ drawOutputHeightInput.addEventListener("input", () => {
919
+ setDrawOutputHeight(Number(drawOutputHeightInput.value));
920
+ if (redrawOnChange) {
921
+ draw();
922
+ }
923
+ });
924
+ const setDrawOutputHeight = (newDrawOutputHeight) => {
925
+ drawOutputHeight = newDrawOutputHeight;
926
+ // console.log({ drawOutputHeight });
927
+
928
+ drawOutputHeightSpan.innerText = Math.round(drawOutputHeight);
929
+ drawOutputHeightInput.value = drawOutputHeight;
930
+ };
931
+
932
+ // PIXEL DEPTH
933
+
934
+ let pixelDepth = BS.DisplayPixelDepths[1];
935
+ const setPixelDepth = (newPixelDepth) => {
936
+ pixelDepth = newPixelDepth;
937
+ // console.log({ pixelDepth });
938
+ };
939
+ const pixelDepthSelect = document.getElementById("pixelDepth");
940
+ const pixelDepthOptgroup = pixelDepthSelect.querySelector("optgroup");
941
+ pixelDepthSelect.addEventListener("input", () => {
942
+ setPixelDepth(pixelDepthSelect.value);
943
+ });
944
+ BS.DisplayPixelDepths.forEach((pixelDepth) => {
945
+ pixelDepthOptgroup.appendChild(
946
+ new Option(
947
+ `${BS.pixelDepthToNumberOfColors(pixelDepth)} colors`,
948
+ pixelDepth
949
+ )
950
+ );
951
+ });
952
+ pixelDepthSelect.value = pixelDepth;
953
+
954
+ // DRAW
955
+ let defaultMaxFileLength = 10 * 1024; // 10kb
956
+ let isDrawing = false;
957
+ /** @type {BS.DisplaySpriteSheet} */
958
+ let spriteSheet;
959
+ let uploadWholeSpriteSheet = true;
960
+ let jitRender = false;
961
+ let drawWhenReady = false;
962
+
963
+ window.bbScalar = 1;
964
+ const draw = async () => {
965
+ if (isDrawing) {
966
+ console.log("busy drawing");
967
+ drawWhenReady = true;
968
+ return;
969
+ }
970
+ if (!spriteSheet && !jitRender) {
971
+ console.error("no sprite sheet");
972
+ return;
973
+ }
974
+ isDrawing = true;
975
+
976
+ const boundingBox = await getModelScreenBoundingBox();
977
+ if (boundingBox) {
978
+ let { minX, maxX, maxY, minY, width, height } = boundingBox.buffer.aabb;
979
+ const widthScalar = displayCanvasHelper.width;
980
+ const heightScalar = displayCanvasHelper.height;
981
+
982
+ height *= heightScalar * bbScalar;
983
+ width *= widthScalar * bbScalar;
984
+
985
+ setDrawX((widthScalar * (minX + maxX)) / 2);
986
+ setDrawY((heightScalar * (minY + maxY)) / 2);
987
+
988
+ // await displayCanvasHelper.setRotation(0);
989
+ // await displayCanvasHelper.drawRect(drawX, drawY, width, height);
990
+
991
+ if (jitRender) {
992
+ setDrawOutputHeight(height);
993
+ let spriteScale = drawOutputHeight / drawInputHeight;
994
+ await displayCanvasHelper.setSpriteScale(spriteScale);
995
+
996
+ const canvas = await captureModelSnapshot(10, false);
997
+ const width = canvas.width * (drawInputHeight / canvas.height);
998
+
999
+ const resizedCanvas = BS.resizeImage(canvas, width, drawInputHeight);
1000
+ const maxFileLength = displayCanvasHelper.device?.isConnected
1001
+ ? displayCanvasHelper.device.maxFileLength
1002
+ : defaultMaxFileLength;
1003
+
1004
+ spriteSheet = await BS.canvasToSpriteSheet(
1005
+ resizedCanvas,
1006
+ "model",
1007
+ skipVoidColor
1008
+ ? Math.min(
1009
+ BS.pixelDepthToNumberOfColors(pixelDepth),
1010
+ displayCanvasHelper.numberOfColors - 1
1011
+ )
1012
+ : BS.pixelDepthToNumberOfColors(pixelDepth),
1013
+ "palette",
1014
+ maxFileLength
1015
+ );
1016
+ checkSpriteSheetSize();
1017
+ await displayCanvasHelper.drawSpriteFromSpriteSheet(
1018
+ drawX,
1019
+ drawY,
1020
+ spriteSheet.sprites[0].name,
1021
+ spriteSheet,
1022
+ "palette"
1023
+ );
1024
+ } else {
1025
+ const { sprite, rotation } = await getClosestModelSprite();
1026
+
1027
+ setDrawRotation(rotation);
1028
+ await displayCanvasHelper.setRotation(drawRotation, true);
1029
+
1030
+ const innerBox = innerBoxSize(
1031
+ width,
1032
+ height,
1033
+ rotation,
1034
+ sprite.height / sprite.width
1035
+ );
1036
+
1037
+ let spriteScale = innerBox.height / sprite.height;
1038
+ await displayCanvasHelper.setSpriteScale(spriteScale);
1039
+
1040
+ if (uploadWholeSpriteSheet) {
1041
+ console.log("drawing sprite");
1042
+ await displayCanvasHelper.drawSprite(drawX, drawY, sprite.name);
1043
+ if (!createSinglePalette) {
1044
+ await displayCanvasHelper.selectSpriteSheetPalette(sprite.name);
1045
+ }
1046
+ } else {
1047
+ console.log("uploadng whole sprite");
1048
+ await displayCanvasHelper.drawSpriteFromSpriteSheet(
1049
+ drawX,
1050
+ drawY,
1051
+ sprite.name,
1052
+ spriteSheet,
1053
+ createSinglePalette ? "model" : sprite.name
1054
+ );
1055
+ }
1056
+ }
1057
+ }
1058
+ await displayCanvasHelper.show();
1059
+ };
1060
+
1061
+ function innerBoxSize(W, H, theta, aspectRatio) {
1062
+ const cos = Math.cos(theta);
1063
+ const sin = Math.sin(theta);
1064
+ const absCos = Math.abs(cos);
1065
+ const absSin = Math.abs(sin);
1066
+
1067
+ const w1 = W / (absCos + aspectRatio * absSin);
1068
+ const w2 = H / (absSin + aspectRatio * absCos);
1069
+
1070
+ const w = Math.min(w1, w2);
1071
+ const h = aspectRatio * w;
1072
+
1073
+ return { width: w, height: h };
1074
+ }
1075
+
1076
+ const saveSpriteSheet = () => {
1077
+ console.log("saveSpriteSheet");
1078
+ const spritesheetString = JSON.stringify(spriteSheet, null, 2);
1079
+ const blob = new Blob([spritesheetString], { type: "application/json" });
1080
+ const url = URL.createObjectURL(blob);
1081
+
1082
+ const a = document.createElement("a");
1083
+ a.href = url;
1084
+ a.download = `spritesheet-${spriteSheet.name}.json`;
1085
+ a.click();
1086
+
1087
+ URL.revokeObjectURL(url);
1088
+ };
1089
+ window.saveSpriteSheet = saveSpriteSheet;
1090
+
1091
+ const redrawButton = document.getElementById("draw");
1092
+ redrawButton.addEventListener("click", () => {
1093
+ draw();
1094
+ });
1095
+
1096
+ displayCanvasHelper.addEventListener("ready", () => {
1097
+ isDrawing = false;
1098
+ if (drawWhenReady || autoDraw) {
1099
+ drawWhenReady = false;
1100
+ draw();
1101
+ }
1102
+ });
1103
+
1104
+ // PROGRESS
1105
+
1106
+ /** @type {HTMLProgressElement} */
1107
+ const fileTransferProgress = document.getElementById("fileTransferProgress");
1108
+
1109
+ device.addEventListener("fileTransferProgress", (event) => {
1110
+ const progress = event.message.progress;
1111
+ //console.log({ progress });
1112
+ fileTransferProgress.value = progress == 1 ? 0 : progress;
1113
+ });
1114
+ device.addEventListener("fileTransferStatus", () => {
1115
+ if (device.fileTransferStatus == "idle") {
1116
+ fileTransferProgress.value = 0;
1117
+ }
1118
+ });
1119
+
1120
+ const checkSpriteSheetSizeButton = document.getElementById(
1121
+ "checkSpriteSheetSize"
1122
+ );
1123
+ const checkSpriteSheetSize = () => {
1124
+ const arrayBuffer = displayCanvasHelper.serializeSpriteSheet(spriteSheet);
1125
+ checkSpriteSheetSizeButton.innerText = `size: ${(
1126
+ arrayBuffer.byteLength / 1024
1127
+ ).toFixed(2)}kb`;
1128
+ if (displayCanvasHelper.device?.isConnected) {
1129
+ checkSpriteSheetSizeButton.innerText += ` (max ${(
1130
+ displayCanvasHelper.device.maxFileLength / 1024
1131
+ ).toFixed(2)}kb)`;
1132
+ }
1133
+ };
1134
+ checkSpriteSheetSizeButton.addEventListener("click", () => {
1135
+ checkSpriteSheetSize();
1136
+ });
1137
+
1138
+ // ROTATOR
1139
+ const toggleRotationInput = document.getElementById("toggleRotation");
1140
+ toggleRotationInput.addEventListener("input", () => {
1141
+ setToggleRotation(toggleRotationInput.checked);
1142
+ });
1143
+ let rotationEnabled = toggleRotationInput.checked;
1144
+ const setToggleRotation = (newRotationEnabled) => {
1145
+ rotationEnabled = newRotationEnabled;
1146
+ console.log({ rotationEnabled });
1147
+ toggleRotationInput.checked = rotationEnabled;
1148
+ if (rotationDevice.isConnected) {
1149
+ updateSensorConfig();
1150
+ }
1151
+ };
1152
+ rotationDevice.addEventListener("connected", () => {
1153
+ updateSensorConfig();
1154
+ });
1155
+ const updateSensorConfig = () => {
1156
+ if (rotationDevice.isConnected) {
1157
+ let sensorRate = rotationEnabled ? 20 : 0;
1158
+ rotationDevice.setSensorConfiguration({
1159
+ gameRotation: sensorRate,
1160
+ linearAcceleration: sensorRate,
1161
+ });
1162
+ }
1163
+ };
1164
+
1165
+ /** @type {TQuaternion} */
1166
+ const _quaternion = new THREE.Quaternion();
1167
+ /** @type {TQuaternion} */
1168
+ const targetQuaternion = new THREE.Quaternion();
1169
+ /**
1170
+ * @param {BS.Quaternion} quaternion
1171
+ * @param {boolean} applyOffset
1172
+ */
1173
+ const updateQuaternion = (quaternion, applyOffset = false) => {
1174
+ _quaternion.copy(quaternion);
1175
+ targetQuaternion.copy(_quaternion);
1176
+ if (applyOffset) {
1177
+ targetQuaternion.premultiply(offsetQuaternion);
1178
+ }
1179
+ targetRotationEntity.object3D.quaternion.slerp(
1180
+ targetQuaternion,
1181
+ window.interpolationSmoothing
1182
+ );
1183
+ };
1184
+ rotationDevice.addEventListener("gameRotation", (event) => {
1185
+ let gameRotation = event.message.gameRotation;
1186
+ updateQuaternion(gameRotation, true);
1187
+ });
1188
+ rotationDevice.addEventListener("rotation", (event) => {
1189
+ const rotation = event.message.rotation;
1190
+ updateQuaternion(rotation, true);
1191
+ });
1192
+
1193
+ window.sensorRate = 20;
1194
+ window.interpolationSmoothing = 0.4;
1195
+ window.positionScalar = 0.2;
1196
+
1197
+ /** @type {TVector3} */
1198
+ const _position = new THREE.Vector3();
1199
+
1200
+ /** @param {BS.Vector3} position */
1201
+ const updatePosition = (position) => {
1202
+ _position.copy(position).multiplyScalar(window.positionScalar);
1203
+ targetPositionEntity.object3D.position.lerp(
1204
+ _position,
1205
+ window.interpolationSmoothing
1206
+ );
1207
+ };
1208
+
1209
+ rotationDevice.addEventListener("acceleration", (event) => {
1210
+ const acceleration = event.message.acceleration;
1211
+ updatePosition(acceleration);
1212
+ });
1213
+ rotationDevice.addEventListener("gravity", () => {
1214
+ const gravity = event.message.gravity;
1215
+ updatePosition(gravity);
1216
+ });
1217
+ rotationDevice.addEventListener("linearAcceleration", (event) => {
1218
+ const linearAcceleration = event.message.linearAcceleration;
1219
+ updatePosition(linearAcceleration);
1220
+ });
1221
+
1222
+ /** @type {TQuaternion} */
1223
+ const offsetQuaternion = new THREE.Quaternion();
1224
+ const resetOrientation = () => {
1225
+ offsetQuaternion.copy(_quaternion).invert();
1226
+ };
1227
+
1228
+ const targetPositionEntity = document.getElementById("position");
1229
+ const targetRotationEntity = document.getElementById("rotation");
1230
+
1231
+ /** @type {HTMLButtonElement} */
1232
+ const resetOrientationButton = document.getElementById("resetOrientation");
1233
+ resetOrientationButton.addEventListener("click", () => {
1234
+ resetOrientation();
1235
+ });