mudra-skills 3.0.2 → 3.0.4

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 (146) hide show
  1. package/package.json +1 -1
  2. package/skills/mudra-master/SKILL.md +0 -196
  3. package/skills/mudra-master/mudra-preview/SKILL.md +0 -150
  4. package/skills/mudra-master/mudra-preview/assets/ar-menu.html +0 -382
  5. package/skills/mudra-master/mudra-preview/assets/document-scroller.html +0 -468
  6. package/skills/mudra-master/mudra-preview/assets/drum-machine.html +0 -788
  7. package/skills/mudra-master/mudra-preview/assets/emg-visualizer.html +0 -390
  8. package/skills/mudra-master/mudra-preview/assets/generative-art.html +0 -408
  9. package/skills/mudra-master/mudra-preview/assets/gesture-assistant.html +0 -342
  10. package/skills/mudra-master/mudra-preview/assets/gesture-speech.html +0 -444
  11. package/skills/mudra-master/mudra-preview/assets/hands-free-desktop.html +0 -376
  12. package/skills/mudra-master/mudra-preview/assets/model-rotator.html +0 -365
  13. package/skills/mudra-master/mudra-preview/assets/mudra-duel.html +0 -3200
  14. package/skills/mudra-master/mudra-preview/assets/mudra-monitor.html +0 -1451
  15. package/skills/mudra-master/mudra-preview/assets/mudra-ultimate-template.html +0 -891
  16. package/skills/mudra-master/mudra-preview/assets/music-sequencer.html +0 -455
  17. package/skills/mudra-master/mudra-preview/assets/neural-pong.html +0 -380
  18. package/skills/mudra-master/mudra-preview/assets/neural-snake.html +0 -364
  19. package/skills/mudra-master/mudra-preview/assets/presentation-controller.html +0 -345
  20. package/skills/mudra-master/mudra-preview/assets/pressure-painter.html +0 -285
  21. package/skills/mudra-master/mudra-preview/assets/runner.html +0 -1812
  22. package/skills/mudra-master/mudra-preview/assets/smart-home.html +0 -461
  23. package/skills/mudra-master/mudra-preview/assets/space-invaders.html +0 -1154
  24. package/skills/mudra-master/mudra-preview/assets/waterful-ring-toss.html +0 -1624
  25. package/skills/mudra-master/mudra-preview/references/agent_protocol.json +0 -634
  26. package/skills/mudra-master/mudra-preview/references/prompt.md +0 -16746
  27. package/skills/mudra-master/mudra-xr/SKILL.md +0 -245
  28. package/skills/mudra-master/mudra-xr/assets/demos/3dgs-walkthrough.html +0 -242
  29. package/skills/mudra-master/mudra-xr/assets/demos/aisimulator.html +0 -792
  30. package/skills/mudra-master/mudra-xr/assets/demos/balloonpop.html +0 -422
  31. package/skills/mudra-master/mudra-xr/assets/demos/ballpit.html +0 -284
  32. package/skills/mudra-master/mudra-xr/assets/demos/drone.html +0 -41
  33. package/skills/mudra-master/mudra-xr/assets/demos/gemini-icebreakers.html +0 -338
  34. package/skills/mudra-master/mudra-xr/assets/demos/gemini-xrobject.html +0 -302
  35. package/skills/mudra-master/mudra-xr/assets/demos/math3d.html +0 -188
  36. package/skills/mudra-master/mudra-xr/assets/demos/measure.html +0 -247
  37. package/skills/mudra-master/mudra-xr/assets/demos/occlusion.html +0 -251
  38. package/skills/mudra-master/mudra-xr/assets/demos/rain.html +0 -498
  39. package/skills/mudra-master/mudra-xr/assets/demos/screenwiper.html +0 -432
  40. package/skills/mudra-master/mudra-xr/assets/demos/splash.html +0 -519
  41. package/skills/mudra-master/mudra-xr/assets/demos/webcam_gestures.html +0 -253
  42. package/skills/mudra-master/mudra-xr/assets/demos/xremoji.html +0 -833
  43. package/skills/mudra-master/mudra-xr/assets/demos/xrpoet.html +0 -151
  44. package/skills/mudra-master/mudra-xr/assets/samples/depthmap.html +0 -266
  45. package/skills/mudra-master/mudra-xr/assets/samples/depthmesh.html +0 -100
  46. package/skills/mudra-master/mudra-xr/assets/samples/game_rps.html +0 -1099
  47. package/skills/mudra-master/mudra-xr/assets/samples/gestures_custom.html +0 -467
  48. package/skills/mudra-master/mudra-xr/assets/samples/gestures_heuristic.html +0 -256
  49. package/skills/mudra-master/mudra-xr/assets/samples/lighting.html +0 -163
  50. package/skills/mudra-master/mudra-xr/assets/samples/mesh_detection.html +0 -46
  51. package/skills/mudra-master/mudra-xr/assets/samples/modelviewer.html +0 -183
  52. package/skills/mudra-master/mudra-xr/assets/samples/paint.html +0 -133
  53. package/skills/mudra-master/mudra-xr/assets/samples/planar-vst.html +0 -45
  54. package/skills/mudra-master/mudra-xr/assets/samples/reticle.html +0 -95
  55. package/skills/mudra-master/mudra-xr/assets/samples/skybox_agent.html +0 -384
  56. package/skills/mudra-master/mudra-xr/assets/samples/sound.html +0 -390
  57. package/skills/mudra-master/mudra-xr/assets/samples/ui.html +0 -496
  58. package/skills/mudra-master/mudra-xr/assets/samples/virtual-screens.html +0 -808
  59. package/skills/mudra-master/mudra-xr/assets/templates/0_basic.html +0 -64
  60. package/skills/mudra-master/mudra-xr/assets/templates/1_ui.html +0 -107
  61. package/skills/mudra-master/mudra-xr/assets/templates/2_hands.html +0 -183
  62. package/skills/mudra-master/mudra-xr/assets/templates/3_depth.html +0 -89
  63. package/skills/mudra-master/mudra-xr/assets/templates/4_stereo.html +0 -84
  64. package/skills/mudra-master/mudra-xr/assets/templates/5_camera.html +0 -117
  65. package/skills/mudra-master/mudra-xr/assets/templates/6_ai.html +0 -154
  66. package/skills/mudra-master/mudra-xr/assets/templates/7_ai_live.html +0 -253
  67. package/skills/mudra-master/mudra-xr/assets/templates/8_objects.html +0 -95
  68. package/skills/mudra-master/mudra-xr/assets/templates/9_xr-toggle.html +0 -90
  69. package/skills/mudra-master/mudra-xr/assets/templates/heuristic_hand_gestures.html +0 -102
  70. package/skills/mudra-master/mudra-xr/assets/templates/meshes.html +0 -47
  71. package/skills/mudra-master/mudra-xr/assets/templates/planes.html +0 -47
  72. package/skills/mudra-master/mudra-xr/assets/templates/uikit.html +0 -187
  73. package/skills/mudra-master/mudra-xr/references/agent_protocol.json +0 -865
  74. package/skills/mudra-master/mudra-xr/references/prompt.md +0 -1605
  75. package/skills/mudra-preview/SKILL.md +0 -159
  76. package/skills/mudra-preview/assets/ar-menu.html +0 -382
  77. package/skills/mudra-preview/assets/document-scroller.html +0 -468
  78. package/skills/mudra-preview/assets/drum-machine.html +0 -788
  79. package/skills/mudra-preview/assets/emg-visualizer.html +0 -390
  80. package/skills/mudra-preview/assets/generative-art.html +0 -408
  81. package/skills/mudra-preview/assets/gesture-assistant.html +0 -342
  82. package/skills/mudra-preview/assets/gesture-speech.html +0 -444
  83. package/skills/mudra-preview/assets/hands-free-desktop.html +0 -376
  84. package/skills/mudra-preview/assets/model-rotator.html +0 -365
  85. package/skills/mudra-preview/assets/mudra-duel.html +0 -3200
  86. package/skills/mudra-preview/assets/mudra-monitor.html +0 -1451
  87. package/skills/mudra-preview/assets/mudra-ultimate-template.html +0 -997
  88. package/skills/mudra-preview/assets/music-sequencer.html +0 -455
  89. package/skills/mudra-preview/assets/neural-pong.html +0 -380
  90. package/skills/mudra-preview/assets/neural-snake.html +0 -364
  91. package/skills/mudra-preview/assets/presentation-controller.html +0 -345
  92. package/skills/mudra-preview/assets/pressure-painter.html +0 -285
  93. package/skills/mudra-preview/assets/runner.html +0 -1812
  94. package/skills/mudra-preview/assets/smart-home.html +0 -461
  95. package/skills/mudra-preview/assets/space-invaders.html +0 -1154
  96. package/skills/mudra-preview/assets/waterful-ring-toss.html +0 -1624
  97. package/skills/mudra-preview/references/agent_protocol.json +0 -701
  98. package/skills/mudra-preview/references/prompt.md +0 -16731
  99. package/skills/mudra-xr/SKILL.md +0 -245
  100. package/skills/mudra-xr/assets/demos/3dgs-walkthrough.html +0 -242
  101. package/skills/mudra-xr/assets/demos/aisimulator.html +0 -792
  102. package/skills/mudra-xr/assets/demos/balloonpop.html +0 -422
  103. package/skills/mudra-xr/assets/demos/ballpit.html +0 -284
  104. package/skills/mudra-xr/assets/demos/drone.html +0 -41
  105. package/skills/mudra-xr/assets/demos/gemini-icebreakers.html +0 -338
  106. package/skills/mudra-xr/assets/demos/gemini-xrobject.html +0 -302
  107. package/skills/mudra-xr/assets/demos/math3d.html +0 -188
  108. package/skills/mudra-xr/assets/demos/measure.html +0 -247
  109. package/skills/mudra-xr/assets/demos/occlusion.html +0 -251
  110. package/skills/mudra-xr/assets/demos/rain.html +0 -498
  111. package/skills/mudra-xr/assets/demos/screenwiper.html +0 -432
  112. package/skills/mudra-xr/assets/demos/splash.html +0 -519
  113. package/skills/mudra-xr/assets/demos/webcam_gestures.html +0 -253
  114. package/skills/mudra-xr/assets/demos/xremoji.html +0 -833
  115. package/skills/mudra-xr/assets/demos/xrpoet.html +0 -151
  116. package/skills/mudra-xr/assets/samples/depthmap.html +0 -266
  117. package/skills/mudra-xr/assets/samples/depthmesh.html +0 -100
  118. package/skills/mudra-xr/assets/samples/game_rps.html +0 -1099
  119. package/skills/mudra-xr/assets/samples/gestures_custom.html +0 -467
  120. package/skills/mudra-xr/assets/samples/gestures_heuristic.html +0 -256
  121. package/skills/mudra-xr/assets/samples/lighting.html +0 -163
  122. package/skills/mudra-xr/assets/samples/mesh_detection.html +0 -46
  123. package/skills/mudra-xr/assets/samples/modelviewer.html +0 -183
  124. package/skills/mudra-xr/assets/samples/paint.html +0 -133
  125. package/skills/mudra-xr/assets/samples/planar-vst.html +0 -45
  126. package/skills/mudra-xr/assets/samples/reticle.html +0 -95
  127. package/skills/mudra-xr/assets/samples/skybox_agent.html +0 -384
  128. package/skills/mudra-xr/assets/samples/sound.html +0 -390
  129. package/skills/mudra-xr/assets/samples/ui.html +0 -496
  130. package/skills/mudra-xr/assets/samples/virtual-screens.html +0 -808
  131. package/skills/mudra-xr/assets/templates/0_basic.html +0 -64
  132. package/skills/mudra-xr/assets/templates/1_ui.html +0 -107
  133. package/skills/mudra-xr/assets/templates/2_hands.html +0 -183
  134. package/skills/mudra-xr/assets/templates/3_depth.html +0 -89
  135. package/skills/mudra-xr/assets/templates/4_stereo.html +0 -84
  136. package/skills/mudra-xr/assets/templates/5_camera.html +0 -117
  137. package/skills/mudra-xr/assets/templates/6_ai.html +0 -154
  138. package/skills/mudra-xr/assets/templates/7_ai_live.html +0 -253
  139. package/skills/mudra-xr/assets/templates/8_objects.html +0 -95
  140. package/skills/mudra-xr/assets/templates/9_xr-toggle.html +0 -90
  141. package/skills/mudra-xr/assets/templates/heuristic_hand_gestures.html +0 -102
  142. package/skills/mudra-xr/assets/templates/meshes.html +0 -47
  143. package/skills/mudra-xr/assets/templates/planes.html +0 -47
  144. package/skills/mudra-xr/assets/templates/uikit.html +0 -187
  145. package/skills/mudra-xr/references/agent_protocol.json +0 -865
  146. package/skills/mudra-xr/references/prompt.md +0 -1605
@@ -1,1099 +0,0 @@
1
- <!doctype html>
2
- <!-- reference: samples/game_rps -->
3
- <html lang="en">
4
- <head>
5
- <title>Rock Paper Scissors | XR Blocks</title>
6
-
7
- <meta
8
- name="viewport"
9
- content="width=device-width, initial-scale=1.0, user-scalable=no"
10
- />
11
-
12
- <link type="text/css" rel="stylesheet" href="https://xrblocks.github.io/css/xr.css" />
13
- <script>
14
- window.litDisableBundleWarning = true;
15
- </script>
16
- <script type="importmap">
17
- {
18
- "imports": {
19
- "lit": "https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js",
20
- "lit/": "https://esm.run/lit@3/",
21
- "three": "https://cdn.jsdelivr.net/npm/three@0.182.0/build/three.module.js",
22
- "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.182.0/examples/jsm/",
23
- "troika-three-text": "https://cdn.jsdelivr.net/gh/protectwise/troika@028b81cf308f0f22e5aa8e78196be56ec1997af5/packages/troika-three-text/src/index.js",
24
- "troika-three-utils": "https://cdn.jsdelivr.net/gh/protectwise/troika@v0.52.4/packages/troika-three-utils/src/index.js",
25
- "troika-worker-utils": "https://cdn.jsdelivr.net/gh/protectwise/troika@v0.52.4/packages/troika-worker-utils/src/index.js",
26
- "bidi-js": "https://esm.sh/bidi-js@%5E1.0.2?target=es2022",
27
- "webgl-sdf-generator": "https://esm.sh/webgl-sdf-generator@1.1.1/es2022/webgl-sdf-generator.mjs",
28
- "@tensorflow/tfjs-core": "https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-core/+esm",
29
- "@tensorflow/tfjs": "https://cdn.jsdelivr.net/npm/@tensorflow/tfjs/+esm",
30
- "@tensorflow/tfjs-backend-webgpu": "https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-webgpu/+esm",
31
- "@litertjs/core": "https://unpkg.com/@litertjs/core@0.2.1",
32
- "@litertjs/tfjs-interop": "https://unpkg.com/@litertjs/tfjs-interop@1.0.1",
33
- "@litertjs/wasm-utils": "https://unpkg.com/@litertjs/wasm-utils@0.2.1",
34
- "xrblocks": "https://cdn.jsdelivr.net/npm/xrblocks@0.11.0/build/xrblocks.js",
35
- "xrblocks/addons/": "https://cdn.jsdelivr.net/npm/xrblocks@0.11.0/build/addons/"
36
- }
37
- }
38
- </script>
39
- </head>
40
-
41
- <body>
42
- <script type="module">
43
- import 'xrblocks/addons/simulator/SimulatorAddons.js';
44
-
45
- // LiteRt
46
- import {loadLiteRt, setWebGpuDevice} from '@litertjs/core';
47
- import {runWithTfjsTensors} from '@litertjs/tfjs-interop';
48
- // TensorflowJS + WebGPU backend
49
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
50
- import {WebGPUBackend} from '@tensorflow/tfjs-backend-webgpu';
51
- import * as tf from '@tensorflow/tfjs';
52
- import * as THREE from 'three';
53
- import * as xb from 'xrblocks';
54
-
55
- // --- Inlined: GestureDetectionHandler.js ---
56
- const GESTURE_RETURN_LABELS_MAX_ENUM = 5;
57
-
58
- const UNKNOWN_GESTURE = 0;
59
-
60
- /**
61
- * Threshold for a dot product to indicate a straight or extended finger segment.
62
- * 0.9 implies ~25.8 degrees tolerance.
63
- * @const {number}
64
- */
65
- const STRAIGHT_FINGER_THRESHOLD = 0.9;
66
-
67
- /**
68
- * Indices in the `relativeHandBoneAngles` tensor, derived directly from the
69
- * order in the HAND_BONE_IDX_CONNECTION_MAP provided.
70
- * @enum {number}
71
- */
72
- const ANGLE_INDICES = {
73
- // Thumb angles (2 angles for straightness)
74
- THUMB_SEGMENT_ANGLE_1: 0,
75
- THUMB_SEGMENT_ANGLE_2: 1,
76
-
77
- // Index finger angles (3 angles for straightness)
78
- INDEX_SEGMENT_ANGLE_1: 2,
79
- INDEX_SEGMENT_ANGLE_2: 3,
80
- INDEX_SEGMENT_ANGLE_3: 4,
81
-
82
- // Middle finger angles (3 angles for straightness)
83
- MIDDLE_SEGMENT_ANGLE_1: 5,
84
- MIDDLE_SEGMENT_ANGLE_2: 6,
85
- MIDDLE_SEGMENT_ANGLE_3: 7,
86
-
87
- // Ring finger angles (3 angles for straightness)
88
- RING_SEGMENT_ANGLE_1: 8,
89
- RING_SEGMENT_ANGLE_2: 9,
90
- RING_SEGMENT_ANGLE_3: 10,
91
-
92
- // Pinky finger angles (3 angles for straightness)
93
- PINKY_SEGMENT_ANGLE_1: 11,
94
- PINKY_SEGMENT_ANGLE_2: 12,
95
- PINKY_SEGMENT_ANGLE_3: 13,
96
- };
97
-
98
- /**
99
- * A queue that only processes the most recent task, discarding outdated ones.
100
- */
101
- class LatestTaskQueue {
102
- constructor() {
103
- this.latestTask = null;
104
- this.isProcessing = false;
105
- }
106
-
107
- enqueue(task) {
108
- if (typeof task !== 'function') {
109
- console.error('Task must be a function.');
110
- return;
111
- }
112
- this.latestTask = task;
113
- if (!this.isProcessing) {
114
- this.processLatestTask();
115
- }
116
- }
117
-
118
- processLatestTask() {
119
- if (this.latestTask) {
120
- this.isProcessing = true;
121
- const taskToProcess = this.latestTask;
122
- this.latestTask = null; // Clear the reference immediately
123
-
124
- // Execute the task asynchronously using setTimeout (or queueMicrotask)
125
- setTimeout(async () => {
126
- try {
127
- await taskToProcess(); // If the task is async
128
- } catch (error) {
129
- console.error('Error processing latest task:', error);
130
- } finally {
131
- this.isProcessing = false;
132
- if (this.latestTask) {
133
- this.processLatestTask();
134
- }
135
- }
136
- }, 0); // Delay of 0ms puts it in the event queue
137
- }
138
- }
139
-
140
- getSize() {
141
- return this.latestTask === null ? 0 : 1;
142
- }
143
-
144
- isEmpty() {
145
- return this.latestTask === null;
146
- }
147
- }
148
-
149
- /**
150
- * Handles gesture detection using TFJS and LiteRT.
151
- */
152
- class GestureDetectionHandler {
153
- constructor() {
154
- // model
155
- this.modelPath = './custom_gestures_model.tflite';
156
- this.modelState = 'None';
157
-
158
- // Enable/Disable paper gesture detection
159
- this.enablePaperDetection = false;
160
-
161
- //
162
- // left and right hand queues
163
- this.queue = [];
164
- this.queue.push(new LatestTaskQueue());
165
- this.queue.push(new LatestTaskQueue());
166
-
167
- setTimeout(() => {
168
- this.setBackendAndLoadModel();
169
- }, 1);
170
- }
171
-
172
- /**
173
- * Enables or disables specific paper gesture detection logic.
174
- * @param {boolean} value
175
- */
176
- enablePaperGesture(value) {
177
- this.enablePaperDetection = value;
178
- }
179
-
180
- /**
181
- * Sets up the WebGPU backend and loads the TFLite model.
182
- */
183
- async setBackendAndLoadModel() {
184
- this.modelState = 'Loading';
185
- try {
186
- await tf.setBackend('webgpu');
187
- await tf.ready();
188
-
189
- const wasmPath = 'https://unpkg.com/@litertjs/core@0.2.1/wasm/';
190
- const liteRt = await loadLiteRt(wasmPath);
191
-
192
- // Makes LiteRT use the same GPU device as TFJS (for tensor conversion).
193
- const backend = tf.backend(); // as WebGPUBackend;
194
- setWebGpuDevice(backend.device);
195
-
196
- // Loads model via LiteRT.
197
- await this.loadModel(liteRt);
198
-
199
- if (this.model) {
200
- // Prints model details to the log.
201
- console.log('MODEL DETAILS: ', this.model.getInputDetails());
202
- }
203
- this.modelState = 'Ready';
204
- } catch (error) {
205
- console.error('Failed to load model or backend:', error);
206
- }
207
- }
208
-
209
- /**
210
- * Loads the TFLite model.
211
- * @param {Object} liteRt The LiteRT instance.
212
- */
213
- async loadModel(liteRt) {
214
- try {
215
- this.model = await liteRt.loadAndCompile(this.modelPath, {
216
- // Currently, only 'webgpu' is supported.
217
- accelerator: 'webgpu',
218
- });
219
- } catch (error) {
220
- this.model = null;
221
- console.error('Error loading model:', error);
222
- }
223
- }
224
-
225
- calculateRelativeHandBoneAngles(jointPositions) {
226
- // Reshape jointPositions.
227
- let jointPositionsReshaped = [];
228
-
229
- jointPositionsReshaped = jointPositions.reshape([xb.HAND_JOINT_COUNT, 3]);
230
-
231
- // Calculates bone vectors.
232
- const boneVectors = [];
233
- xb.HAND_JOINT_IDX_CONNECTION_MAP.forEach(([joint1, joint2]) => {
234
- const boneVector = jointPositionsReshaped
235
- .slice([joint2, 0], [1, 3])
236
- .sub(jointPositionsReshaped.slice([joint1, 0], [1, 3]))
237
- .squeeze();
238
- const norm = boneVector.norm();
239
- const normalizedBoneVector = boneVector.div(norm);
240
- boneVectors.push(normalizedBoneVector);
241
- });
242
-
243
- // Calculates relative hand bone angles.
244
- const relativeHandBoneAngles = [];
245
- xb.HAND_BONE_IDX_CONNECTION_MAP.forEach(([bone1, bone2]) => {
246
- const angle = boneVectors[bone1].dot(boneVectors[bone2]);
247
- relativeHandBoneAngles.push(angle);
248
- });
249
-
250
- // Stacks the angles into a tensor.
251
- return tf.stack(relativeHandBoneAngles);
252
- }
253
-
254
- /**
255
- * Runs inference on hand joints.
256
- * @param {Array<number>} handJoints Flat array of joint positions.
257
- * @return {Promise<number>} The detected gesture ID.
258
- */
259
- async detectGesture(handJoints) {
260
- if (!this.model || !handJoints || handJoints.length !== 25 * 3) {
261
- console.log('Invalid hand joints or model load error.');
262
- return GESTURE_RETURN_LABELS_MAX_ENUM;
263
- }
264
-
265
- try {
266
- // Use tidy to clean up intermediate tensors.
267
- const tensor = tf.tidy(() =>
268
- this.calculateRelativeHandBoneAngles(tf.tensor1d(handJoints))
269
- );
270
-
271
- // Detect "Paper" gesture for 2 seconds.
272
- if (this.enablePaperDetection && this.isPaperGesture(tensor)) {
273
- // Paper gesture - GESTURE_RETURN_LABELS
274
- return 2;
275
- }
276
-
277
- const tensorReshaped = tf.tidy(() =>
278
- tensor.reshape([1, xb.HAND_BONE_IDX_CONNECTION_MAP.length, 1])
279
- );
280
- tensor.dispose(); // Disposes original tensor after reshape.
281
-
282
- const result = runWithTfjsTensors(this.model, tensorReshaped);
283
- tensorReshaped.dispose();
284
-
285
- const integerLabel = result[0].as1D().arraySync();
286
- if (integerLabel.length == 7) {
287
- let maxScore = integerLabel[0];
288
-
289
- let idx = 0;
290
- for (let t = 0; t < 7; ++t) {
291
- if (integerLabel[t] > maxScore) {
292
- idx = t;
293
- maxScore = integerLabel[t];
294
- }
295
- }
296
-
297
- // No need to detect thumb up for a play mode
298
- if (idx === 2) {
299
- if (
300
- this.enablePaperDetection ||
301
- !this.isThumbUp(handJoints, 2, 3, 4)
302
- ) {
303
- return 0;
304
- }
305
- }
306
-
307
- //
308
- // Maps model result to the return index for the game.
309
- //
310
-
311
- // Thumb Up Logic check (Model Index 2)
312
- if (idx == 2) {
313
- return 4; // Thumb Up
314
- }
315
-
316
- // Fist (Model Index 4)
317
- if (idx == 4) {
318
- return 2; // Rock
319
- }
320
-
321
- // Victory / Scissors (Model Index 1)
322
- else if (idx == 1) {
323
- return 1; // Scissors
324
- }
325
-
326
- // OTHER gesture
327
- return 0;
328
- }
329
- } catch (error) {
330
- console.error('Error:', error);
331
- }
332
- return GESTURE_RETURN_LABELS_MAX_ENUM;
333
- }
334
-
335
- /**
336
- * Wrapper for thumb up detection.
337
- */
338
- isThumbUp(d1, p1, p2, p3) {
339
- return this.isThumbUpSimple(d1, p1, p3);
340
- }
341
-
342
- /**
343
- * Advanced vector calculation for thumb up.
344
- * @param {Array<number>} data Flat array of joints.
345
- * @param {number} p1 Base index.
346
- * @param {number} p2 Knuckle index.
347
- * @param {number} p3 Tip index.
348
- * @return {boolean}
349
- */
350
- isThumbUpAdvanced(data, p1, p2, p3) {
351
- const v1 = {
352
- x: data[p2 * 3] - data[p1 * 3],
353
- y: data[p2 * 3 + 1] - data[p1 * 3 + 1],
354
- z: data[p2 * 3 + 2] - data[p1 * 3 + 2],
355
- };
356
-
357
- const v2 = {
358
- x: data[p3 * 3] - data[p2 * 3],
359
- y: data[p3 * 3 + 1] - data[p2 * 3 + 1],
360
- z: data[p3 * 3 + 2] - data[p2 * 3 + 2],
361
- };
362
-
363
- const dotProduct = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
364
-
365
- const magnitudeV1 = Math.sqrt(v1.x * v1.x + v1.y * v1.y + v1.z * v1.z);
366
- const magnitudeV2 = Math.sqrt(v2.x * v2.x + v2.y * v2.y + v2.z * v2.z);
367
-
368
- if (magnitudeV1 === 0 || magnitudeV2 === 0) {
369
- return false;
370
- }
371
-
372
- const cosAngle = dotProduct / (magnitudeV1 * magnitudeV2);
373
- const angleRadians = Math.acos(Math.max(-1, Math.min(1, cosAngle)));
374
- const angleDegrees = angleRadians * (180 / Math.PI);
375
- const thumbUpThreshold = 90;
376
-
377
- return angleDegrees > thumbUpThreshold;
378
- }
379
-
380
- /**
381
- * Detects if the hand is showing a "paper" (open palm) gesture.
382
- * @param {tf.Tensor} relativeHandBoneAngles A TensorFlow.js tensor of bone angles.
383
- * @returns {boolean} True if the gesture is "paper", false otherwise.
384
- */
385
- isPaperGesture(relativeHandBoneAngles) {
386
- if (!relativeHandBoneAngles || relativeHandBoneAngles.size === 0) {
387
- return false;
388
- }
389
-
390
- const angles = relativeHandBoneAngles.arraySync();
391
-
392
- const areSegmentsStraight = (...angleIndices) => {
393
- return angleIndices.every(
394
- (idx) => angles[idx] > STRAIGHT_FINGER_THRESHOLD
395
- );
396
- };
397
-
398
- const isThumbStraight = areSegmentsStraight(
399
- ANGLE_INDICES.THUMB_SEGMENT_ANGLE_1,
400
- ANGLE_INDICES.THUMB_SEGMENT_ANGLE_2
401
- );
402
-
403
- const isIndexStraight = areSegmentsStraight(
404
- ANGLE_INDICES.INDEX_SEGMENT_ANGLE_1,
405
- ANGLE_INDICES.INDEX_SEGMENT_ANGLE_2,
406
- ANGLE_INDICES.INDEX_SEGMENT_ANGLE_3
407
- );
408
-
409
- const isMiddleStraight = areSegmentsStraight(
410
- ANGLE_INDICES.MIDDLE_SEGMENT_ANGLE_1,
411
- ANGLE_INDICES.MIDDLE_SEGMENT_ANGLE_2,
412
- ANGLE_INDICES.MIDDLE_SEGMENT_ANGLE_3
413
- );
414
-
415
- const isRingStraight = areSegmentsStraight(
416
- ANGLE_INDICES.RING_SEGMENT_ANGLE_1,
417
- ANGLE_INDICES.RING_SEGMENT_ANGLE_2,
418
- ANGLE_INDICES.RING_SEGMENT_ANGLE_3
419
- );
420
-
421
- const isPinkyStraight = areSegmentsStraight(
422
- ANGLE_INDICES.PINKY_SEGMENT_ANGLE_1,
423
- ANGLE_INDICES.PINKY_SEGMENT_ANGLE_2,
424
- ANGLE_INDICES.PINKY_SEGMENT_ANGLE_3
425
- );
426
-
427
- const isPaper =
428
- isThumbStraight &&
429
- isIndexStraight &&
430
- isMiddleStraight &&
431
- isRingStraight &&
432
- isPinkyStraight;
433
-
434
- return isPaper;
435
- }
436
-
437
- //
438
- // Clone joints and post queue task
439
- //
440
- postTask(joints, handIndex) {
441
- if (Object.keys(joints).length !== 25) {
442
- return UNKNOWN_GESTURE;
443
- }
444
-
445
- let handJointPositions = [];
446
- for (const i in joints) {
447
- handJointPositions.push(joints[i].position.x);
448
- handJointPositions.push(joints[i].position.y);
449
- handJointPositions.push(joints[i].position.z);
450
- }
451
-
452
- if (handJointPositions.length !== 25 * 3) {
453
- return UNKNOWN_GESTURE;
454
- }
455
-
456
- if (handIndex >= 0 && handIndex < this.queue.length) {
457
- this.queue[handIndex].enqueue(async () => {
458
- let result = await this.detectGesture(handJointPositions);
459
-
460
- if (this.observer && this.observer.onGestureDetected) {
461
- this.observer.onGestureDetected(handIndex, result);
462
- }
463
- });
464
- }
465
- }
466
-
467
- isThumbUpSimple(data, p1, p2) {
468
- const vector = {
469
- x: data[p2 * 3] - data[p1 * 3],
470
- y: data[p2 * 3 + 1] - data[p1 * 3 + 1],
471
- z: data[p2 * 3 + 2] - data[p1 * 3 + 2],
472
- };
473
-
474
- const magnitude = Math.sqrt(
475
- vector.x * vector.x + vector.y * vector.y + vector.z * vector.z
476
- );
477
-
478
- if (magnitude < 0.001) {
479
- return false;
480
- }
481
-
482
- const normalizedVector = {
483
- x: vector.x / magnitude,
484
- y: vector.y / magnitude,
485
- z: vector.z / magnitude,
486
- };
487
-
488
- const upVector = {x: 0, y: 1, z: 0};
489
-
490
- const dotProduct =
491
- normalizedVector.x * upVector.x +
492
- normalizedVector.y * upVector.y +
493
- normalizedVector.z * upVector.z;
494
-
495
- const cos45Degrees = Math.cos((45 * Math.PI) / 180); // Approximately 0.707
496
-
497
- return dotProduct >= cos45Degrees;
498
- }
499
-
500
- registerObserver(observer) {
501
- this.observer = observer;
502
- }
503
- }
504
-
505
- // --- Inlined: GameRps.js ---
506
- const LEFT_HAND_INDEX = 0;
507
- const RIGHT_HAND_INDEX = 1;
508
-
509
- const countdownImages = [
510
- 'images/start1.webp',
511
- 'images/start2.webp',
512
- 'images/start3.webp',
513
- 'images/startGo.webp',
514
- ];
515
-
516
- const rpsLeftImages = [
517
- 'images/gestureUnknown.webp',
518
- 'images/gestureFistLeft.webp',
519
- 'images/gestureScissorsLeft.webp',
520
- 'images/gesturePaperLeft.webp',
521
- ];
522
-
523
- const rpsRightImages = [
524
- 'images/gestureUnknown.webp',
525
- 'images/gestureFistRight.webp',
526
- 'images/gestureScissorsRight.webp',
527
- 'images/gesturePaperRight.webp',
528
- ];
529
-
530
- const resultImages = [
531
- 'images/resultTie.webp',
532
- 'images/resultWin.webp',
533
- 'images/resultLose.webp',
534
- ];
535
-
536
- const gameOutcomePhrases = [
537
- // Phrases for a draw
538
- [
539
- 'A draw!',
540
- "We've matched.",
541
- 'Great minds think alike.',
542
- "It's a stalemate.",
543
- "We're even.",
544
- 'Neither of us wins this time.',
545
- "Looks like we're in sync.",
546
- "We'll have to go again.",
547
- 'A perfect match.',
548
- "It's a tie.",
549
- ],
550
- // Phrases for a Gemeni lose
551
- [
552
- 'You got me.',
553
- 'Nicely done, you win.',
554
- 'Ah, you were one step ahead.',
555
- 'The victory is yours.',
556
- "I'll have to get you next time.",
557
- "You've defeated me.",
558
- "I couldn't beat that.",
559
- 'Well played, you earned it.',
560
- 'You read my mind.',
561
- 'The point goes to you.',
562
- ],
563
- // Phrases for a Gemini victory
564
- [
565
- 'Victory is mine!',
566
- 'I got you that time.',
567
- 'Looks like I came out on top.',
568
- "I'll take that win.",
569
- 'Another one for my column.',
570
- "You've been bested.",
571
- 'I predicted your move perfectly.',
572
- "That's how it's done.",
573
- 'I have the winning strategy.',
574
- 'The round goes to me.',
575
- ],
576
- ];
577
-
578
- class GameRps extends xb.Script {
579
- constructor() {
580
- super();
581
- // List of detected gestures for the left and right hands.
582
- this.handGesture = [[], []];
583
- this.isDebug = false;
584
-
585
- //
586
- // Initializes UI.
587
- //
588
- {
589
- // Makes a root panel > grid > row > controlPanel > grid.
590
- const panel = new xb.SpatialPanel({
591
- backgroundColor: '#00000000',
592
- useDefaultPosition: false,
593
- showEdge: false,
594
- });
595
- panel.scale.set(panel.width, panel.height, 1);
596
- panel.isRoot = true;
597
- this.add(panel);
598
-
599
- const grid = panel.addGrid();
600
- // Adds blank space on top of the ctrlPanel.
601
- this.startImageRow = grid.addRow({weight: 0.4});
602
- this.startImageRow.addCol({weight: 0.3});
603
- this.startImage = this.startImageRow.addCol({weight: 0.4}).addImage({
604
- src: 'images/startStart.webp',
605
- });
606
- this.startImageRow.addCol({weight: 0.3});
607
-
608
- if (this.isDebug) {
609
- //
610
- // UI to debug user gestures.
611
- //
612
- this.playImageRow = grid.addRow({weight: 0.2});
613
- this.playImageRow.addCol({weight: 0.2});
614
- this.imageGesture1 = this.playImageRow.addCol({weight: 0.2}).addImage({
615
- src: 'images/gestureEmpty.webp',
616
- });
617
- // VS text
618
- this.playImageRow.addCol({weight: 0.2}).addImage({
619
- src: 'images/resultVS.webp',
620
- });
621
- this.imageGesture2 = this.playImageRow.addCol({weight: 0.2}).addImage({
622
- src: 'images/gestureEmpty.webp',
623
- });
624
- this.playImageRow.addCol({weight: 0.3});
625
- this.playImageRow.hide();
626
-
627
- this.resultImageRow = grid.addRow({weight: 0.2});
628
- this.resultImageRow.addCol({weight: 0.4});
629
- this.resultImage = this.resultImageRow.addCol({weight: 0.2}).addImage({
630
- src: 'images/gestureEmpty.webp',
631
- });
632
- this.resultImageRow.hide();
633
- }
634
-
635
- // Space for orbiter
636
- grid.addRow({weight: 0.1});
637
- // control row
638
- const controlRow = grid.addRow({weight: 0.5});
639
- const ctrlPanel = controlRow.addPanel({backgroundColor: '#000000bb'});
640
-
641
- const ctrlGrid = ctrlPanel.addGrid();
642
- {
643
- // middle column
644
- const midColumn = ctrlGrid.addCol({weight: 0.9});
645
-
646
- // top indentation
647
- midColumn.addRow({weight: 0.1});
648
-
649
- const gesturesRow = midColumn.addRow({weight: 0.4});
650
-
651
- // left indentation
652
- gesturesRow.addCol({weight: 0.05});
653
-
654
- const textCol = gesturesRow.addCol({weight: 1.0});
655
- this.textField1 = textCol.addRow({weight: 1.0}).addText({
656
- text: "Let's play Rock-Paper-Scissors!",
657
- fontColor: '#ffffff',
658
- fontSize: 0.045,
659
- });
660
- this.textField2 = textCol.addRow({weight: 1.0}).addText({
661
- text: 'Do a thumbs-up gesture to get started!',
662
- fontColor: '#ffffff',
663
- fontSize: 0.045,
664
- });
665
-
666
- // right indentation
667
- gesturesRow.addCol({weight: 0.01});
668
-
669
- // bottom indentation
670
- midColumn.addRow({weight: 0.1});
671
- }
672
-
673
- const orbiter = ctrlGrid.addOrbiter();
674
- orbiter.addExitButton();
675
-
676
- panel.updateLayouts();
677
-
678
- this.panel = panel;
679
-
680
- // Gesture detector
681
- this.gestureDetectionHandler = new GestureDetectionHandler();
682
- this.gestureDetectionHandler.registerObserver(this);
683
- }
684
-
685
- // state for 1-2-3-GO
686
- this.state = 0;
687
- // delay for 1-2-3-GO
688
- this.delayMs = this.isDebug ? 400 : 800;
689
- // Wait for 2.5 sec to enable game restart
690
- this.gameRestartTimeout = this.isDebug ? 1000 : 2500;
691
- this.gameGestureDetectionTimeout = this.isDebug ? 900 : 2500;
692
-
693
- // The gesture detection start time
694
- this.gestureStartTime = 0;
695
-
696
- // Frame counter
697
- this.frameId = 0;
698
-
699
- // Play states for both left and right hands
700
- this.handRpsStates = [[], []];
701
-
702
- // 1-2-3-GO
703
- this.displayNextImage = this.displayNextImage.bind(this);
704
- }
705
-
706
- displayNextImage() {
707
- if (this.state < countdownImages.length) {
708
- // Load and display the current image
709
- this.startImage.load(countdownImages[this.state]);
710
- this.state++; // Move to the next image for the next call
711
-
712
- // Schedule the next image display after the delay
713
- setTimeout(this.displayNextImage, this.delayMs);
714
- } else {
715
- this.gestureStartTime = Date.now();
716
- // Enable the papter detection/disable thumb up
717
- this.gestureDetectionHandler.enablePaperGesture(true);
718
-
719
- setTimeout(() => {
720
- this.displayRandomGesture();
721
- }, this.delayMs);
722
- }
723
- }
724
-
725
- displayRandomGesture() {
726
- this.randomGesture = Math.floor(Math.random() * 3) + 1;
727
- if (this.isDebug) {
728
- this.startImageRow.hide();
729
- this.playImageRow.show();
730
- this.resultImageRow.show();
731
- // Load random gesture
732
- this.imageGesture1.load(rpsLeftImages[this.randomGesture]);
733
- } else {
734
- this.startImage.load(rpsLeftImages[this.randomGesture]);
735
- }
736
- }
737
-
738
- displayGameSummary(idx) {
739
- let result = 2;
740
- if (idx == 0) {
741
- this.textField1.setText("I didn't catch what you threw! Let's retry!");
742
- this.textField2.setText('Do a thumbs-up gesture when you are ready!');
743
- } else {
744
- let result = this.getRPSOutcome(this.randomGesture, idx);
745
- this.textField1.setText(this.getRandomPhrase(result));
746
- this.textField2.setText('Do a thumbs-up gesture to play more!');
747
- }
748
-
749
- //
750
- // Debug UI - show game summary and user gesture
751
- //
752
- if (this.isDebug) {
753
- // 'OTHER', 'FIST', 'VICTORY', 'PAPER'
754
- this.imageGesture2.load(rpsRightImages[idx < 4 ? idx : 0]);
755
- // Show user gesture
756
- this.resultImage.load(resultImages[result]);
757
- }
758
- }
759
-
760
- /**
761
- * Determines the outcome of a Rock-Paper-Scissors game.
762
- * @param {number} playerChoice - The player's choice (1: Rock, 2: Paper, 3: Scissors).
763
- * @param {number} opponentChoice - The opponent's choice (1: Rock, 2: Paper, 3: Scissors).
764
- * @returns {number} The outcome (0: Match, 1: Win, 2: Lose).
765
- */
766
- getRPSOutcome(playerChoice, opponentChoice) {
767
- if (playerChoice === opponentChoice) {
768
- return 0; // Match (Tie)
769
- }
770
-
771
- if (
772
- (playerChoice === 1 && opponentChoice === 3) || // Rock vs Scissors
773
- (playerChoice === 2 && opponentChoice === 1) || // Paper vs Rock
774
- (playerChoice === 3 && opponentChoice === 2) // Scissors vs Paper
775
- ) {
776
- return 1; // Win
777
- } else {
778
- return 2; // Lose (all other cases are losses)
779
- }
780
- }
781
-
782
- startGame() {
783
- if (this.isDebug) {
784
- // Show 1-2-3-GO
785
- this.startImageRow.show();
786
- // Reset result images
787
- this.imageGesture1.load('images/gestureEmpty.webp');
788
- this.imageGesture2.load('images/gestureEmpty.webp');
789
- this.resultImage.load('images/gestureEmpty.webp');
790
- // hide result
791
- this.playImageRow.hide();
792
- this.resultImageRow.hide();
793
- }
794
-
795
- this.textField1.setText('');
796
-
797
- // start timer for the 1-2-3-go
798
- this.displayNextImage();
799
- }
800
-
801
- onGestureDetected(handIndex, result) {
802
- // Thumb up
803
- if (result == 4) {
804
- // Ensure the game isn't currently active.
805
- if (this.state === 0) {
806
- this.startGame();
807
- }
808
- return;
809
- }
810
-
811
- if (this.gestureStartTime === 0) {
812
- // skip all late gesture detections
813
- return;
814
- }
815
-
816
- // Record all gesture changes for the
817
- let delta = Date.now() - this.gestureStartTime;
818
- //
819
- // Gesture detection time interval exceeded
820
- //
821
- if (delta > this.gameGestureDetectionTimeout) {
822
- this.gestureStartTime = 0;
823
- this.detectFinalGestures();
824
- } else {
825
- let len = this.handRpsStates[handIndex].length;
826
- if (
827
- len == 0 ||
828
- (this.handGesture[handIndex][len - 1] &&
829
- this.handGesture[handIndex][len - 1].gesture !== result)
830
- ) {
831
- // Save gesture-duration for each hand
832
- this.handRpsStates[handIndex].push({gesture: result, delta: delta});
833
- this.handGesture[handIndex] = result;
834
- }
835
- }
836
- }
837
-
838
- detectFinalGestures() {
839
- this.rightHandGesture = this.detectRPSGesture(
840
- this.handRpsStates[RIGHT_HAND_INDEX]
841
- );
842
-
843
- this.displayGameSummary(this.rightHandGesture);
844
-
845
- // Reset cached states
846
- this.handRpsStates = [[], []];
847
-
848
- // Disable the papter detection. Enable thumb up
849
- this.gestureDetectionHandler.enablePaperGesture(false);
850
-
851
- // add delay before user could re-start the game
852
- setTimeout(() => {
853
- // Ready to play again
854
- this.state = 0;
855
- }, this.gameRestartTimeout);
856
- }
857
-
858
- detectRPSGesture(handRpsStates, options = {}) {
859
- const defaultOptions = {
860
- stabilityDurationMs: 300,
861
- initialRockIgnoreDurationMs: 300,
862
- maxDetectionWindowMs: this.gameGestureDetectionTimeout,
863
- };
864
- const {maxDetectionWindowMs} = {...defaultOptions, ...options};
865
-
866
- if (!handRpsStates || handRpsStates.length === 0) {
867
- return 0; // Other
868
- }
869
-
870
- handRpsStates.sort((a, b) => a.delta - b.delta);
871
-
872
- const firstPhaseEndMs = maxDetectionWindowMs * (2 / 5);
873
- const secondPhaseStartMs = maxDetectionWindowMs * (2 / 5);
874
-
875
- let rockStartedInFirstPhase = false;
876
- const gestureDurations = new Map();
877
-
878
- let prevDelta = 0;
879
- let prevGesture = 0;
880
-
881
- for (let i = 0; i < handRpsStates.length; i++) {
882
- const {gesture, delta} = handRpsStates[i];
883
-
884
- if (delta > maxDetectionWindowMs) {
885
- break;
886
- }
887
-
888
- const duration = delta - prevDelta;
889
-
890
- if (prevGesture !== 0 && prevGesture > 0 && prevGesture < 4) {
891
- gestureDurations.set(
892
- prevGesture,
893
- (gestureDurations.get(prevGesture) || 0) + duration
894
- );
895
- }
896
-
897
- if (gesture === 1 && delta <= firstPhaseEndMs) {
898
- rockStartedInFirstPhase = true;
899
- }
900
-
901
- prevDelta = delta;
902
- prevGesture = gesture;
903
- }
904
-
905
- if (
906
- prevDelta < maxDetectionWindowMs &&
907
- prevGesture !== 0 &&
908
- prevGesture > 0 &&
909
- prevGesture < 4
910
- ) {
911
- const remainingDuration = maxDetectionWindowMs - prevDelta;
912
- gestureDurations.set(
913
- prevGesture,
914
- (gestureDurations.get(prevGesture) || 0) + remainingDuration
915
- );
916
- }
917
-
918
- let longestGesture = 0;
919
- let maxDuration = 0;
920
- const minRequiredDuration = 300;
921
-
922
- let effectiveStartTime = 0;
923
- let effectiveEndTime = maxDetectionWindowMs;
924
-
925
- if (rockStartedInFirstPhase) {
926
- effectiveStartTime = secondPhaseStartMs;
927
- }
928
-
929
- const filteredGestureDurations = new Map();
930
- let currentWindowPrevDelta = effectiveStartTime;
931
- let currentWindowPrevGesture = 0;
932
-
933
- for (let i = 0; i < handRpsStates.length; i++) {
934
- const {gesture, delta} = handRpsStates[i];
935
- if (delta >= effectiveStartTime) {
936
- currentWindowPrevGesture = prevGesture;
937
- if (i > 0) {
938
- currentWindowPrevGesture = handRpsStates[i - 1].gesture;
939
- } else {
940
- currentWindowPrevGesture = gesture;
941
- }
942
- break;
943
- }
944
- prevGesture = gesture;
945
- }
946
-
947
- for (let i = 0; i < handRpsStates.length; i++) {
948
- const {gesture, delta} = handRpsStates[i];
949
-
950
- if (delta < effectiveStartTime) {
951
- currentWindowPrevDelta = delta;
952
- currentWindowPrevGesture = gesture;
953
- continue;
954
- }
955
-
956
- if (delta > effectiveEndTime) {
957
- const duration = Math.max(
958
- 0,
959
- effectiveEndTime -
960
- Math.max(currentWindowPrevDelta, effectiveStartTime)
961
- );
962
- if (
963
- currentWindowPrevGesture !== 0 &&
964
- currentWindowPrevGesture > 0 &&
965
- currentWindowPrevGesture < 4
966
- ) {
967
- filteredGestureDurations.set(
968
- currentWindowPrevGesture,
969
- (filteredGestureDurations.get(currentWindowPrevGesture) || 0) +
970
- duration
971
- );
972
- }
973
- break;
974
- }
975
-
976
- const segmentStart = Math.max(currentWindowPrevDelta, effectiveStartTime);
977
- const segmentEnd = delta;
978
- const duration = segmentEnd - segmentStart;
979
-
980
- if (
981
- currentWindowPrevGesture !== 0 &&
982
- currentWindowPrevGesture > 0 &&
983
- currentWindowPrevGesture < 4
984
- ) {
985
- filteredGestureDurations.set(
986
- currentWindowPrevGesture,
987
- (filteredGestureDurations.get(currentWindowPrevGesture) || 0) +
988
- duration
989
- );
990
- }
991
-
992
- currentWindowPrevDelta = delta;
993
- currentWindowPrevGesture = gesture;
994
- }
995
-
996
- if (
997
- currentWindowPrevDelta < effectiveEndTime &&
998
- currentWindowPrevGesture !== 0 &&
999
- currentWindowPrevGesture > 0 &&
1000
- currentWindowPrevGesture < 4
1001
- ) {
1002
- const remainingDuration =
1003
- effectiveEndTime - Math.max(currentWindowPrevDelta, effectiveStartTime);
1004
- filteredGestureDurations.set(
1005
- currentWindowPrevGesture,
1006
- (filteredGestureDurations.get(currentWindowPrevGesture) || 0) +
1007
- remainingDuration
1008
- );
1009
- }
1010
-
1011
- for (const [gesture, duration] of filteredGestureDurations.entries()) {
1012
- if (gesture !== 0 && duration >= minRequiredDuration) {
1013
- if (duration > maxDuration) {
1014
- maxDuration = duration;
1015
- longestGesture = gesture;
1016
- }
1017
- }
1018
- }
1019
-
1020
- return longestGesture;
1021
- }
1022
-
1023
- /**
1024
- * Initializes the PaintScript.
1025
- */
1026
- init() {
1027
- xb.core.renderer.localClippingEnabled = true;
1028
-
1029
- this.add(new THREE.HemisphereLight(0x888877, 0x777788, 3));
1030
- const light = new THREE.DirectionalLight(0xffffff, 5.0);
1031
- light.position.set(-0.5, 4, 1.0);
1032
- this.add(light);
1033
-
1034
- this.panel.position.set(0, xb.core.user.height, -1.0);
1035
- }
1036
-
1037
- async update() {
1038
- //
1039
- // Run gesture detection 12 times per second for ~60fps
1040
- // But an avg fps for webxr is 30-60
1041
- //
1042
- if (this.frameId % 5 === 0) {
1043
- const hands = xb.core.user.hands;
1044
- if (hands.isValid()) {
1045
- this.gestureDetectionHandler.postTask(
1046
- hands.hands[LEFT_HAND_INDEX].joints,
1047
- LEFT_HAND_INDEX
1048
- );
1049
- this.gestureDetectionHandler.postTask(
1050
- hands.hands[RIGHT_HAND_INDEX].joints,
1051
- RIGHT_HAND_INDEX
1052
- );
1053
- }
1054
- }
1055
-
1056
- this.frameId++;
1057
- }
1058
-
1059
- /**
1060
- * Selects a random phrase from a specified category of game outcomes.
1061
- * @param {number} categoryIndex - The index of the category.
1062
- * @returns {string|null} A randomly selected phrase.
1063
- */
1064
- getRandomPhrase(categoryIndex) {
1065
- if (categoryIndex < 0 || categoryIndex >= gameOutcomePhrases.length) {
1066
- console.error(
1067
- 'Invalid category index provided. Please use 0 for victory, 1 for loss, or 2 for draw.'
1068
- );
1069
- return null;
1070
- }
1071
- const phrasesArray = gameOutcomePhrases[categoryIndex];
1072
- const randomIndex = Math.floor(Math.random() * phrasesArray.length);
1073
- return phrasesArray[randomIndex];
1074
- }
1075
- }
1076
-
1077
- // --- Main entry point ---
1078
- const options = new xb.Options();
1079
- options.simulator.instructions.enabled = false;
1080
- options.enableReticles();
1081
- options.enableHands();
1082
- options.simulator.defaultMode = xb.SimulatorMode.POSE;
1083
- options.simulator.defaultHand = xb.Handedness.RIGHT;
1084
- options.setAppTitle('Rock Paper Scissors');
1085
-
1086
- async function start() {
1087
- xb.add(new GameRps());
1088
- await xb.init(options);
1089
- }
1090
-
1091
- document.addEventListener('DOMContentLoaded', function () {
1092
- setTimeout(function () {
1093
- start();
1094
- }, 200);
1095
- });
1096
-
1097
- </script>
1098
- </body>
1099
- </html>