mudra-skills 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/.claude-plugin/plugin.json +19 -0
  2. package/CLAUDE.md +35 -0
  3. package/bin/install.js +25 -0
  4. package/package.json +28 -0
  5. package/skills/mudra-master/SKILL.md +202 -0
  6. package/skills/mudra-master/mudra-preview/SKILL.md +151 -0
  7. package/skills/mudra-master/mudra-preview/assets/ar-menu.html +382 -0
  8. package/skills/mudra-master/mudra-preview/assets/document-scroller.html +468 -0
  9. package/skills/mudra-master/mudra-preview/assets/drum-machine.html +788 -0
  10. package/skills/mudra-master/mudra-preview/assets/emg-visualizer.html +390 -0
  11. package/skills/mudra-master/mudra-preview/assets/generative-art.html +408 -0
  12. package/skills/mudra-master/mudra-preview/assets/gesture-assistant.html +342 -0
  13. package/skills/mudra-master/mudra-preview/assets/gesture-speech.html +444 -0
  14. package/skills/mudra-master/mudra-preview/assets/hands-free-desktop.html +376 -0
  15. package/skills/mudra-master/mudra-preview/assets/model-rotator.html +365 -0
  16. package/skills/mudra-master/mudra-preview/assets/mudra-duel.html +3200 -0
  17. package/skills/mudra-master/mudra-preview/assets/mudra-monitor.html +1451 -0
  18. package/skills/mudra-master/mudra-preview/assets/mudra-ultimate-template.html +891 -0
  19. package/skills/mudra-master/mudra-preview/assets/music-sequencer.html +455 -0
  20. package/skills/mudra-master/mudra-preview/assets/neural-pong.html +380 -0
  21. package/skills/mudra-master/mudra-preview/assets/neural-snake.html +364 -0
  22. package/skills/mudra-master/mudra-preview/assets/presentation-controller.html +345 -0
  23. package/skills/mudra-master/mudra-preview/assets/pressure-painter.html +285 -0
  24. package/skills/mudra-master/mudra-preview/assets/runner.html +1812 -0
  25. package/skills/mudra-master/mudra-preview/assets/smart-home.html +461 -0
  26. package/skills/mudra-master/mudra-preview/assets/space-invaders.html +1154 -0
  27. package/skills/mudra-master/mudra-preview/assets/waterful-ring-toss.html +1624 -0
  28. package/skills/mudra-master/mudra-preview/references/agent_protocol.json +646 -0
  29. package/skills/mudra-master/mudra-preview/references/promt.md +16829 -0
  30. package/skills/mudra-master/mudra-xr/SKILL.md +245 -0
  31. package/skills/mudra-master/mudra-xr/assets/demos/3dgs-walkthrough.html +242 -0
  32. package/skills/mudra-master/mudra-xr/assets/demos/aisimulator.html +792 -0
  33. package/skills/mudra-master/mudra-xr/assets/demos/balloonpop.html +422 -0
  34. package/skills/mudra-master/mudra-xr/assets/demos/ballpit.html +284 -0
  35. package/skills/mudra-master/mudra-xr/assets/demos/drone.html +41 -0
  36. package/skills/mudra-master/mudra-xr/assets/demos/gemini-icebreakers.html +338 -0
  37. package/skills/mudra-master/mudra-xr/assets/demos/gemini-xrobject.html +302 -0
  38. package/skills/mudra-master/mudra-xr/assets/demos/math3d.html +188 -0
  39. package/skills/mudra-master/mudra-xr/assets/demos/measure.html +247 -0
  40. package/skills/mudra-master/mudra-xr/assets/demos/occlusion.html +251 -0
  41. package/skills/mudra-master/mudra-xr/assets/demos/rain.html +498 -0
  42. package/skills/mudra-master/mudra-xr/assets/demos/screenwiper.html +432 -0
  43. package/skills/mudra-master/mudra-xr/assets/demos/splash.html +519 -0
  44. package/skills/mudra-master/mudra-xr/assets/demos/webcam_gestures.html +253 -0
  45. package/skills/mudra-master/mudra-xr/assets/demos/xremoji.html +833 -0
  46. package/skills/mudra-master/mudra-xr/assets/demos/xrpoet.html +151 -0
  47. package/skills/mudra-master/mudra-xr/assets/samples/depthmap.html +266 -0
  48. package/skills/mudra-master/mudra-xr/assets/samples/depthmesh.html +100 -0
  49. package/skills/mudra-master/mudra-xr/assets/samples/game_rps.html +1099 -0
  50. package/skills/mudra-master/mudra-xr/assets/samples/gestures_custom.html +467 -0
  51. package/skills/mudra-master/mudra-xr/assets/samples/gestures_heuristic.html +256 -0
  52. package/skills/mudra-master/mudra-xr/assets/samples/lighting.html +163 -0
  53. package/skills/mudra-master/mudra-xr/assets/samples/mesh_detection.html +46 -0
  54. package/skills/mudra-master/mudra-xr/assets/samples/modelviewer.html +183 -0
  55. package/skills/mudra-master/mudra-xr/assets/samples/paint.html +133 -0
  56. package/skills/mudra-master/mudra-xr/assets/samples/planar-vst.html +45 -0
  57. package/skills/mudra-master/mudra-xr/assets/samples/reticle.html +95 -0
  58. package/skills/mudra-master/mudra-xr/assets/samples/skybox_agent.html +384 -0
  59. package/skills/mudra-master/mudra-xr/assets/samples/sound.html +390 -0
  60. package/skills/mudra-master/mudra-xr/assets/samples/ui.html +496 -0
  61. package/skills/mudra-master/mudra-xr/assets/samples/virtual-screens.html +808 -0
  62. package/skills/mudra-master/mudra-xr/assets/templates/0_basic.html +64 -0
  63. package/skills/mudra-master/mudra-xr/assets/templates/1_ui.html +107 -0
  64. package/skills/mudra-master/mudra-xr/assets/templates/2_hands.html +183 -0
  65. package/skills/mudra-master/mudra-xr/assets/templates/3_depth.html +89 -0
  66. package/skills/mudra-master/mudra-xr/assets/templates/4_stereo.html +84 -0
  67. package/skills/mudra-master/mudra-xr/assets/templates/5_camera.html +117 -0
  68. package/skills/mudra-master/mudra-xr/assets/templates/6_ai.html +154 -0
  69. package/skills/mudra-master/mudra-xr/assets/templates/7_ai_live.html +253 -0
  70. package/skills/mudra-master/mudra-xr/assets/templates/8_objects.html +95 -0
  71. package/skills/mudra-master/mudra-xr/assets/templates/9_xr-toggle.html +90 -0
  72. package/skills/mudra-master/mudra-xr/assets/templates/heuristic_hand_gestures.html +102 -0
  73. package/skills/mudra-master/mudra-xr/assets/templates/meshes.html +47 -0
  74. package/skills/mudra-master/mudra-xr/assets/templates/planes.html +47 -0
  75. package/skills/mudra-master/mudra-xr/assets/templates/uikit.html +187 -0
  76. package/skills/mudra-master/mudra-xr/references/agent_protocol.json +878 -0
  77. package/skills/mudra-master/mudra-xr/references/promt.md +1516 -0
  78. package/skills/mudra-preview/SKILL.md +151 -0
  79. package/skills/mudra-preview/assets/ar-menu.html +382 -0
  80. package/skills/mudra-preview/assets/document-scroller.html +468 -0
  81. package/skills/mudra-preview/assets/drum-machine.html +788 -0
  82. package/skills/mudra-preview/assets/emg-visualizer.html +390 -0
  83. package/skills/mudra-preview/assets/generative-art.html +408 -0
  84. package/skills/mudra-preview/assets/gesture-assistant.html +342 -0
  85. package/skills/mudra-preview/assets/gesture-speech.html +444 -0
  86. package/skills/mudra-preview/assets/hands-free-desktop.html +376 -0
  87. package/skills/mudra-preview/assets/model-rotator.html +365 -0
  88. package/skills/mudra-preview/assets/mudra-duel.html +3200 -0
  89. package/skills/mudra-preview/assets/mudra-monitor.html +1451 -0
  90. package/skills/mudra-preview/assets/mudra-ultimate-template.html +891 -0
  91. package/skills/mudra-preview/assets/music-sequencer.html +455 -0
  92. package/skills/mudra-preview/assets/neural-pong.html +380 -0
  93. package/skills/mudra-preview/assets/neural-snake.html +364 -0
  94. package/skills/mudra-preview/assets/presentation-controller.html +345 -0
  95. package/skills/mudra-preview/assets/pressure-painter.html +285 -0
  96. package/skills/mudra-preview/assets/runner.html +1812 -0
  97. package/skills/mudra-preview/assets/smart-home.html +461 -0
  98. package/skills/mudra-preview/assets/space-invaders.html +1154 -0
  99. package/skills/mudra-preview/assets/waterful-ring-toss.html +1624 -0
  100. package/skills/mudra-preview/references/agent_protocol.json +646 -0
  101. package/skills/mudra-preview/references/promt.md +16829 -0
  102. package/skills/mudra-xr/SKILL.md +245 -0
  103. package/skills/mudra-xr/assets/demos/3dgs-walkthrough.html +242 -0
  104. package/skills/mudra-xr/assets/demos/aisimulator.html +792 -0
  105. package/skills/mudra-xr/assets/demos/balloonpop.html +422 -0
  106. package/skills/mudra-xr/assets/demos/ballpit.html +284 -0
  107. package/skills/mudra-xr/assets/demos/drone.html +41 -0
  108. package/skills/mudra-xr/assets/demos/gemini-icebreakers.html +338 -0
  109. package/skills/mudra-xr/assets/demos/gemini-xrobject.html +302 -0
  110. package/skills/mudra-xr/assets/demos/math3d.html +188 -0
  111. package/skills/mudra-xr/assets/demos/measure.html +247 -0
  112. package/skills/mudra-xr/assets/demos/occlusion.html +251 -0
  113. package/skills/mudra-xr/assets/demos/rain.html +498 -0
  114. package/skills/mudra-xr/assets/demos/screenwiper.html +432 -0
  115. package/skills/mudra-xr/assets/demos/splash.html +519 -0
  116. package/skills/mudra-xr/assets/demos/webcam_gestures.html +253 -0
  117. package/skills/mudra-xr/assets/demos/xremoji.html +833 -0
  118. package/skills/mudra-xr/assets/demos/xrpoet.html +151 -0
  119. package/skills/mudra-xr/assets/samples/depthmap.html +266 -0
  120. package/skills/mudra-xr/assets/samples/depthmesh.html +100 -0
  121. package/skills/mudra-xr/assets/samples/game_rps.html +1099 -0
  122. package/skills/mudra-xr/assets/samples/gestures_custom.html +467 -0
  123. package/skills/mudra-xr/assets/samples/gestures_heuristic.html +256 -0
  124. package/skills/mudra-xr/assets/samples/lighting.html +163 -0
  125. package/skills/mudra-xr/assets/samples/mesh_detection.html +46 -0
  126. package/skills/mudra-xr/assets/samples/modelviewer.html +183 -0
  127. package/skills/mudra-xr/assets/samples/paint.html +133 -0
  128. package/skills/mudra-xr/assets/samples/planar-vst.html +45 -0
  129. package/skills/mudra-xr/assets/samples/reticle.html +95 -0
  130. package/skills/mudra-xr/assets/samples/skybox_agent.html +384 -0
  131. package/skills/mudra-xr/assets/samples/sound.html +390 -0
  132. package/skills/mudra-xr/assets/samples/ui.html +496 -0
  133. package/skills/mudra-xr/assets/samples/virtual-screens.html +808 -0
  134. package/skills/mudra-xr/assets/templates/0_basic.html +64 -0
  135. package/skills/mudra-xr/assets/templates/1_ui.html +107 -0
  136. package/skills/mudra-xr/assets/templates/2_hands.html +183 -0
  137. package/skills/mudra-xr/assets/templates/3_depth.html +89 -0
  138. package/skills/mudra-xr/assets/templates/4_stereo.html +84 -0
  139. package/skills/mudra-xr/assets/templates/5_camera.html +117 -0
  140. package/skills/mudra-xr/assets/templates/6_ai.html +154 -0
  141. package/skills/mudra-xr/assets/templates/7_ai_live.html +253 -0
  142. package/skills/mudra-xr/assets/templates/8_objects.html +95 -0
  143. package/skills/mudra-xr/assets/templates/9_xr-toggle.html +90 -0
  144. package/skills/mudra-xr/assets/templates/heuristic_hand_gestures.html +102 -0
  145. package/skills/mudra-xr/assets/templates/meshes.html +47 -0
  146. package/skills/mudra-xr/assets/templates/planes.html +47 -0
  147. package/skills/mudra-xr/assets/templates/uikit.html +187 -0
  148. package/skills/mudra-xr/references/agent_protocol.json +878 -0
  149. package/skills/mudra-xr/references/promt.md +1516 -0
@@ -0,0 +1,1624 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Waterful Ring Toss</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Poppins:wght@300;400;600;700&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --bg: #e0f2fe;
11
+ --card: #f0f9ff;
12
+ --primary: #0ea5e9;
13
+ --accent: #38bdf8;
14
+ --text: #0c4a6e;
15
+ --text-secondary: #0369a1;
16
+ --success: #16a34a;
17
+ --warning: #ca8a04;
18
+ --error: #dc2626;
19
+ --red-plastic: #e63946;
20
+ --red-plastic-dark: #cc2233;
21
+ --red-plastic-light: #ff5566;
22
+ }
23
+
24
+ * { margin: 0; padding: 0; box-sizing: border-box; }
25
+
26
+ body {
27
+ font-family: 'Poppins', system-ui, sans-serif;
28
+ background: radial-gradient(ellipse at top, #bae6fd 0%, #e0f2fe 60%);
29
+ color: var(--text);
30
+ display: flex;
31
+ flex-direction: column;
32
+ align-items: center;
33
+ min-height: 100vh;
34
+ overflow: hidden;
35
+ user-select: none;
36
+ position: relative;
37
+ }
38
+
39
+ .game-container {
40
+ position: relative;
41
+ z-index: 2;
42
+ display: flex;
43
+ flex-direction: column;
44
+ align-items: center;
45
+ }
46
+
47
+ .game-header {
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: space-between;
51
+ width: 100%;
52
+ max-width: 380px;
53
+ padding: 20px 20px 12px;
54
+ }
55
+
56
+ .game-title {
57
+ font-family: 'Fredoka', sans-serif;
58
+ font-size: 18px;
59
+ font-weight: 700;
60
+ color: var(--primary);
61
+ letter-spacing: 0.05em;
62
+ text-shadow: 0 0 20px rgba(119, 234, 233, 0.5);
63
+ }
64
+
65
+ .score-display {
66
+ font-size: 16px;
67
+ color: var(--text-secondary);
68
+ font-weight: 500;
69
+ background: rgba(14,165,233,0.1);
70
+ padding: 6px 14px;
71
+ border-radius: 12px;
72
+ border: 1px solid rgba(14,165,233,0.2);
73
+ }
74
+
75
+ .score-display span {
76
+ color: var(--primary);
77
+ font-weight: 700;
78
+ font-size: 18px;
79
+ }
80
+
81
+ .toy-frame {
82
+ position: relative;
83
+ width: 360px;
84
+ height: 540px;
85
+ border-radius: 24px;
86
+ overflow: visible;
87
+ filter: drop-shadow(0 20px 60px rgba(0,0,0,0.6));
88
+ transform-style: preserve-3d;
89
+ }
90
+
91
+ .toy-shell {
92
+ position: absolute;
93
+ top: 0;
94
+ left: 0;
95
+ right: 0;
96
+ bottom: 0;
97
+ border-radius: 24px;
98
+ background:
99
+ linear-gradient(135deg,
100
+ var(--red-plastic-light) 0%,
101
+ var(--red-plastic) 30%,
102
+ var(--red-plastic-dark) 100%);
103
+ box-shadow:
104
+ inset -2px -2px 4px rgba(255, 255, 255, 0.2),
105
+ inset 2px 2px 6px rgba(0, 0, 0, 0.4);
106
+ }
107
+
108
+ .toy-shell::before {
109
+ content: '';
110
+ position: absolute;
111
+ top: 0;
112
+ left: 0;
113
+ right: 0;
114
+ bottom: 0;
115
+ border-radius: 24px;
116
+ background:
117
+ repeating-linear-gradient(
118
+ 45deg,
119
+ transparent,
120
+ transparent 1px,
121
+ rgba(255, 255, 255, 0.02) 1px,
122
+ rgba(255, 255, 255, 0.02) 2px
123
+ );
124
+ pointer-events: none;
125
+ }
126
+
127
+ .toy-cap-top {
128
+ position: absolute;
129
+ top: 0;
130
+ left: 0;
131
+ right: 0;
132
+ height: 48px;
133
+ background:
134
+ linear-gradient(180deg,
135
+ var(--red-plastic-light) 0%,
136
+ var(--red-plastic) 60%,
137
+ var(--red-plastic-dark) 100%);
138
+ border-radius: 24px 24px 0 0;
139
+ z-index: 10;
140
+ box-shadow:
141
+ inset 0 2px 4px rgba(255, 255, 255, 0.3),
142
+ inset 0 -2px 6px rgba(0, 0, 0, 0.3),
143
+ 0 2px 8px rgba(0,0,0,0.3);
144
+ }
145
+
146
+ .screw {
147
+ position: absolute;
148
+ width: 10px;
149
+ height: 10px;
150
+ border-radius: 50%;
151
+ background:
152
+ radial-gradient(circle at 30% 30%,
153
+ #666 0%,
154
+ #444 50%,
155
+ #222 100%);
156
+ box-shadow:
157
+ inset 1px 1px 2px rgba(0,0,0,0.5),
158
+ 0 1px 2px rgba(255,255,255,0.2);
159
+ }
160
+
161
+ .screw::before {
162
+ content: '';
163
+ position: absolute;
164
+ top: 50%;
165
+ left: 50%;
166
+ transform: translate(-50%, -50%);
167
+ width: 6px;
168
+ height: 1px;
169
+ background: #111;
170
+ box-shadow: 0 0 1px rgba(0,0,0,0.8);
171
+ }
172
+
173
+ .screw-tl { top: 12px; left: 20px; }
174
+ .screw-tr { top: 12px; right: 20px; }
175
+
176
+ .toy-base {
177
+ position: absolute;
178
+ bottom: 0;
179
+ left: 0;
180
+ right: 0;
181
+ height: 90px;
182
+ background:
183
+ linear-gradient(180deg,
184
+ var(--red-plastic) 0%,
185
+ var(--red-plastic-dark) 60%,
186
+ #aa1122 100%);
187
+ border-radius: 0 0 24px 24px;
188
+ z-index: 10;
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: space-between;
192
+ padding: 0 24px;
193
+ box-shadow:
194
+ inset 0 4px 8px rgba(0, 0, 0, 0.4),
195
+ inset 0 -2px 4px rgba(255, 255, 255, 0.2),
196
+ 0 -2px 8px rgba(0,0,0,0.3);
197
+ }
198
+
199
+ .screw-bl { bottom: 12px; left: 20px; }
200
+ .screw-br { bottom: 12px; right: 20px; }
201
+
202
+ .toy-label {
203
+ font-family: 'Fredoka', sans-serif;
204
+ background:
205
+ linear-gradient(135deg, #1a1a2e 0%, #0f0f1a 100%);
206
+ border: 2px solid #333;
207
+ border-radius: 6px;
208
+ padding: 6px 14px;
209
+ font-size: 12px;
210
+ font-weight: 700;
211
+ letter-spacing: 0.1em;
212
+ color: #f0f0f0;
213
+ text-transform: uppercase;
214
+ line-height: 1.3;
215
+ box-shadow:
216
+ inset 1px 1px 2px rgba(255,255,255,0.1),
217
+ inset -1px -1px 2px rgba(0,0,0,0.5),
218
+ 0 2px 4px rgba(0,0,0,0.3);
219
+ text-shadow: 0 1px 2px rgba(0,0,0,0.8);
220
+ }
221
+
222
+ .toy-label small {
223
+ display: block;
224
+ font-size: 9px;
225
+ color: var(--accent);
226
+ font-weight: 500;
227
+ letter-spacing: 0.15em;
228
+ }
229
+
230
+ .emg-button {
231
+ width: 54px;
232
+ height: 54px;
233
+ border-radius: 50%;
234
+ background:
235
+ radial-gradient(circle at 35% 35%,
236
+ #ffffff 0%,
237
+ #e8e8e8 40%,
238
+ #d0d0d0 70%,
239
+ #b8b8b8 100%);
240
+ border: 3px solid #999;
241
+ cursor: pointer;
242
+ transition: all 0.08s cubic-bezier(0.4, 0, 0.2, 1);
243
+ box-shadow:
244
+ 0 4px 8px rgba(0,0,0,0.4),
245
+ inset 0 1px 2px rgba(255,255,255,0.8),
246
+ inset 0 -2px 4px rgba(0,0,0,0.15);
247
+ position: relative;
248
+ }
249
+
250
+ .emg-button::after {
251
+ content: '';
252
+ position: absolute;
253
+ top: 50%;
254
+ left: 50%;
255
+ transform: translate(-50%, -50%);
256
+ width: 24px;
257
+ height: 24px;
258
+ border-radius: 50%;
259
+ background: radial-gradient(circle at 40% 40%,
260
+ rgba(119, 234, 233, 0.2) 0%,
261
+ rgba(119, 234, 233, 0.05) 100%);
262
+ opacity: 0;
263
+ transition: opacity 0.1s;
264
+ }
265
+
266
+ .emg-button.active {
267
+ transform: scale(0.9);
268
+ box-shadow:
269
+ 0 1px 3px rgba(0,0,0,0.4),
270
+ inset 0 2px 6px rgba(0,0,0,0.3),
271
+ inset 0 -1px 2px rgba(255,255,255,0.4);
272
+ background:
273
+ radial-gradient(circle at 65% 65%,
274
+ #ffffff 0%,
275
+ #e0e0e0 40%,
276
+ #c8c8c8 70%,
277
+ #b0b0b0 100%);
278
+ }
279
+
280
+ .emg-button.active::after {
281
+ opacity: 1;
282
+ }
283
+
284
+ .water-chamber {
285
+ position: absolute;
286
+ top: 48px;
287
+ left: 12px;
288
+ right: 12px;
289
+ bottom: 90px;
290
+ background:
291
+ linear-gradient(180deg,
292
+ rgba(173, 216, 230, 0.12) 0%,
293
+ rgba(100, 180, 220, 0.18) 50%,
294
+ rgba(80, 160, 200, 0.24) 100%);
295
+ border: 3px solid rgba(255, 255, 255, 0.2);
296
+ border-radius: 8px;
297
+ overflow: hidden;
298
+ box-shadow:
299
+ inset 2px 2px 8px rgba(255, 255, 255, 0.15),
300
+ inset -2px -2px 8px rgba(0, 0, 0, 0.2),
301
+ inset 0 0 40px rgba(100, 180, 220, 0.08);
302
+ }
303
+
304
+ .water-chamber::before {
305
+ content: '';
306
+ position: absolute;
307
+ top: 0;
308
+ left: 0;
309
+ width: 40%;
310
+ height: 100%;
311
+ background: linear-gradient(90deg,
312
+ rgba(255, 255, 255, 0.15) 0%,
313
+ rgba(255, 255, 255, 0.05) 30%,
314
+ transparent 100%);
315
+ pointer-events: none;
316
+ z-index: 100;
317
+ }
318
+
319
+ .water-chamber::after {
320
+ content: '';
321
+ position: absolute;
322
+ top: 0;
323
+ left: 0;
324
+ right: 0;
325
+ bottom: 0;
326
+ background:
327
+ radial-gradient(ellipse at 30% 20%, rgba(173, 216, 230, 0.15) 0%, transparent 50%),
328
+ radial-gradient(ellipse at 70% 60%, rgba(100, 180, 220, 0.1) 0%, transparent 40%);
329
+ pointer-events: none;
330
+ z-index: 1;
331
+ animation: caustics 8s ease-in-out infinite;
332
+ }
333
+
334
+ @keyframes caustics {
335
+ 0%, 100% { opacity: 0.6; transform: translateY(0) scale(1); }
336
+ 50% { opacity: 0.9; transform: translateY(-5px) scale(1.05); }
337
+ }
338
+
339
+ canvas#gameCanvas {
340
+ width: 100%;
341
+ height: 100%;
342
+ display: block;
343
+ position: relative;
344
+ z-index: 2;
345
+ }
346
+
347
+ .emg-meter {
348
+ position: absolute;
349
+ bottom: 98px;
350
+ left: 50%;
351
+ transform: translateX(-50%);
352
+ width: 220px;
353
+ height: 8px;
354
+ background:
355
+ linear-gradient(90deg,
356
+ rgba(0,0,0,0.4) 0%,
357
+ rgba(0,0,0,0.3) 100%);
358
+ border-radius: 4px;
359
+ z-index: 20;
360
+ overflow: hidden;
361
+ border: 1px solid rgba(0,0,0,0.3);
362
+ box-shadow:
363
+ inset 0 1px 3px rgba(0,0,0,0.5),
364
+ 0 1px 2px rgba(255,255,255,0.1);
365
+ }
366
+
367
+ .emg-meter-fill {
368
+ height: 100%;
369
+ width: 0%;
370
+ background:
371
+ linear-gradient(90deg,
372
+ var(--accent) 0%,
373
+ var(--primary) 50%,
374
+ #fff 100%);
375
+ border-radius: 4px;
376
+ transition: width 0.05s;
377
+ box-shadow:
378
+ 0 0 8px rgba(119, 234, 233, 0.6),
379
+ 0 0 4px rgba(119, 234, 233, 0.8);
380
+ position: relative;
381
+ }
382
+
383
+ .emg-meter-fill::after {
384
+ content: '';
385
+ position: absolute;
386
+ top: 0;
387
+ left: 0;
388
+ right: 0;
389
+ height: 50%;
390
+ background: linear-gradient(180deg,
391
+ rgba(255,255,255,0.6) 0%,
392
+ transparent 100%);
393
+ border-radius: 4px 4px 0 0;
394
+ }
395
+
396
+ .status-bar {
397
+ display: flex;
398
+ align-items: center;
399
+ gap: 8px;
400
+ padding: 12px 16px;
401
+ margin-top: 12px;
402
+ background: rgba(255,255,255,0.03);
403
+ border-radius: 12px;
404
+ border: 1px solid rgba(255,255,255,0.06);
405
+ }
406
+
407
+ .status-dot {
408
+ width: 8px;
409
+ height: 8px;
410
+ border-radius: 50%;
411
+ background: var(--error);
412
+ transition: background 0.3s;
413
+ box-shadow: 0 0 8px currentColor;
414
+ }
415
+
416
+ .status-dot.connected {
417
+ background: var(--success);
418
+ animation: pulse-dot 2s ease-in-out infinite;
419
+ }
420
+
421
+ @keyframes pulse-dot {
422
+ 0%, 100% { opacity: 1; }
423
+ 50% { opacity: 0.6; }
424
+ }
425
+
426
+ .status-text {
427
+ font-size: 12px;
428
+ color: var(--text-secondary);
429
+ font-weight: 500;
430
+ }
431
+
432
+ .instructions {
433
+ max-width: 360px;
434
+ text-align: center;
435
+ padding: 12px;
436
+ font-size: 12px;
437
+ color: var(--text-secondary);
438
+ line-height: 1.6;
439
+ background: rgba(255,255,255,0.02);
440
+ border-radius: 12px;
441
+ margin-top: 4px;
442
+ }
443
+
444
+ .instructions kbd {
445
+ background: var(--card);
446
+ border: 1px solid #333;
447
+ border-radius: 4px;
448
+ padding: 2px 8px;
449
+ font-size: 11px;
450
+ font-family: 'Poppins', monospace;
451
+ color: var(--primary);
452
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
453
+ }
454
+
455
+ .mudra-badge {
456
+ position: fixed;
457
+ bottom: 12px;
458
+ right: 12px;
459
+ font-size: 11px;
460
+ color: var(--text-secondary);
461
+ opacity: 0.5;
462
+ font-family: inherit;
463
+ letter-spacing: 0.02em;
464
+ z-index: 100;
465
+ }
466
+
467
+ </style>
468
+ </head>
469
+ <body>
470
+ <a href="/gallery" style="position:fixed;top:12px;left:12px;z-index:9999;padding:6px 14px;background:rgba(0,0,0,0.6);color:#77eae9;text-decoration:none;font-size:13px;font-family:system-ui,sans-serif;border-radius:8px;backdrop-filter:blur(8px);border:1px solid rgba(119,234,233,0.2);transition:background 0.2s;"onmouseenter="this.style.background='rgba(0,0,0,0.8)'"onmouseleave="this.style.background='rgba(0,0,0,0.6)'">&larr; Gallery</a>
471
+ <div class="game-container">
472
+ <div class="game-header">
473
+ <div class="game-title">WATERFUL RING TOSS</div>
474
+ <div class="score-display">Score: <span id="scoreValue">0</span> / <span id="totalPegs">3</span></div>
475
+ </div>
476
+
477
+ <div class="toy-frame" id="toyFrame">
478
+ <div class="toy-shell"></div>
479
+ <div class="toy-cap-top">
480
+ <div class="screw screw-tl"></div>
481
+ <div class="screw screw-tr"></div>
482
+ </div>
483
+ <div class="water-chamber">
484
+ <canvas id="gameCanvas"></canvas>
485
+ </div>
486
+ <div class="emg-meter">
487
+ <div class="emg-meter-fill" id="emgMeterFill"></div>
488
+ </div>
489
+ <div class="toy-base">
490
+ <div class="screw screw-bl"></div>
491
+ <div class="screw screw-br"></div>
492
+ <div class="toy-label">
493
+ <small>MUDRA</small>
494
+ RING TOSS
495
+ </div>
496
+ <div class="emg-button" id="emgButton"></div>
497
+ </div>
498
+ </div>
499
+
500
+ <div class="status-bar">
501
+ <div class="status-dot" id="statusDot"></div>
502
+ <div class="status-text" id="statusText">Connecting to Mudra...</div>
503
+ </div>
504
+
505
+ <div class="instructions">
506
+ Squeeze to jet water. Land rings on pegs to score!<br>
507
+ <kbd>Space</kbd> = full jet, <kbd>[</kbd><kbd>]</kbd> = adjust pressure, <kbd>R</kbd> = reset.
508
+ </div>
509
+ </div>
510
+
511
+ <div class="mudra-badge">Created by Mudra</div>
512
+
513
+ <script>
514
+ // ==========================================
515
+ // WATERFUL RING TOSS v2 — Pressure + Gesture
516
+ // Uses Mudra pressure signal for analog jet control
517
+ // and gesture signal for twist/shake mechanic.
518
+ // ==========================================
519
+
520
+ const WS_URL = "ws://127.0.0.1:8766";
521
+ const PRESSURE_WINDOW = 5; // smoothing window for pressure samples
522
+
523
+ const canvas = document.getElementById('gameCanvas');
524
+ const ctx = canvas.getContext('2d');
525
+ const emgMeterFill = document.getElementById('emgMeterFill');
526
+ const emgButton = document.getElementById('emgButton');
527
+ const statusDot = document.getElementById('statusDot');
528
+ const statusText = document.getElementById('statusText');
529
+ const scoreValue = document.getElementById('scoreValue');
530
+ const toyFrame = document.getElementById('toyFrame');
531
+
532
+ // High-DPI canvas setup
533
+ function resizeCanvas() {
534
+ const container = canvas.parentElement;
535
+ const dpr = window.devicePixelRatio || 1;
536
+ canvas.width = container.clientWidth * dpr;
537
+ canvas.height = container.clientHeight * dpr;
538
+ ctx.scale(dpr, dpr);
539
+ canvas.style.width = container.clientWidth + 'px';
540
+ canvas.style.height = container.clientHeight + 'px';
541
+ }
542
+ resizeCanvas();
543
+ window.addEventListener('resize', resizeCanvas);
544
+
545
+ // ==========================================
546
+ // Game Constants
547
+ // ==========================================
548
+ const W = () => canvas.width / (window.devicePixelRatio || 1);
549
+ const H = () => canvas.height / (window.devicePixelRatio || 1);
550
+
551
+ const RING_COLORS = ['#ef4444', '#22c55e', '#3b82f6', '#eab308', '#a855f7', '#ec4899'];
552
+
553
+ // ==========================================
554
+ // Water Velocity Field
555
+ // ==========================================
556
+ const NOZZLE_X_RATIO = 0.75;
557
+ const NOZZLE_DIAMETER = 0.08;
558
+ const JET_SPREAD_RATE = 0.12;
559
+ const GRAVITY_ACCEL = 0.02;
560
+ const DRAG_COEFF = 0.06;
561
+ const ADDED_MASS_RATIO = 1.2;
562
+
563
+ function noise2d(x, y) {
564
+ const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;
565
+ return n - Math.floor(n);
566
+ }
567
+ function smoothNoise(x, y) {
568
+ const ix = Math.floor(x), iy = Math.floor(y);
569
+ const fx = x - ix, fy = y - iy;
570
+ const sx = fx * fx * (3 - 2 * fx), sy = fy * fy * (3 - 2 * fy);
571
+ const a = noise2d(ix, iy), b = noise2d(ix + 1, iy);
572
+ const c = noise2d(ix, iy + 1), d = noise2d(ix + 1, iy + 1);
573
+ return a + (b - a) * sx + (c - a) * sy + (a - b - c + d) * sx * sy;
574
+ }
575
+ function fbmNoise(x, y) {
576
+ return smoothNoise(x, y) * 0.5 + smoothNoise(x * 2, y * 2) * 0.3 + smoothNoise(x * 4, y * 4) * 0.2;
577
+ }
578
+
579
+ let frameCount = 0;
580
+
581
+ function getFlowVelocity(px, py, jetStrength) {
582
+ const w = W(), h = H();
583
+ if (jetStrength < 0.01) return { vx: 0, vy: 0 };
584
+
585
+ const nozzleX = w * NOZZLE_X_RATIO;
586
+ const nozzleY = h;
587
+ const nozzleD = w * NOZZLE_DIAMETER;
588
+
589
+ const dy = nozzleY - py;
590
+ const dx = px - nozzleX;
591
+
592
+ if (dy < 1) return { vx: 0, vy: 0 };
593
+
594
+ const potentialCore = nozzleD * 6;
595
+ let centerlineSpeed;
596
+ if (dy < potentialCore) {
597
+ centerlineSpeed = jetStrength * 3.5;
598
+ } else {
599
+ centerlineSpeed = jetStrength * 3.5 * potentialCore / dy;
600
+ }
601
+
602
+ const sigma = Math.max(nozzleD, JET_SPREAD_RATE * dy + w * 0.08);
603
+ const gaussianFalloff = Math.exp(-(dx * dx) / (2 * sigma * sigma));
604
+
605
+ let flowVy = -centerlineSpeed * gaussianFalloff;
606
+
607
+ const entrainmentStrength = centerlineSpeed * 0.08;
608
+ const inwardPull = Math.max(0, 1 - gaussianFalloff) * Math.sign(-dx || 1);
609
+ let flowVx = inwardPull * entrainmentStrength * (1 - Math.abs(dx) / (w * 0.6));
610
+
611
+ // Smooth recirculation cell
612
+ const cellCenterX = w * 0.45;
613
+ const cellCenterY = h * 0.45;
614
+ const cellRadiusX = w * 0.45;
615
+ const cellRadiusY = h * 0.48;
616
+
617
+ const nx2 = (px - cellCenterX) / cellRadiusX;
618
+ const ny2 = (py - cellCenterY) / cellRadiusY;
619
+ const distFromCenter = Math.sqrt(nx2 * nx2 + ny2 * ny2);
620
+
621
+ const circulationEnvelope = Math.min(1, distFromCenter * 1.5) *
622
+ Math.exp(-Math.max(0, distFromCenter - 1.2) * 3);
623
+ const circStrength = jetStrength * 2.5 * circulationEnvelope;
624
+
625
+ flowVx += circStrength * (ny2);
626
+ flowVy += circStrength * (-nx2);
627
+
628
+ if (px < w * 0.15) {
629
+ flowVy += jetStrength * 1.0;
630
+ }
631
+ if (py > h * 0.82) {
632
+ flowVx += jetStrength * 1.4 * (1 - (py - h * 0.82) / (h * 0.18));
633
+ }
634
+ if (py < h * 0.15) {
635
+ const topProximity = 1 - py / (h * 0.15);
636
+ flowVx -= jetStrength * 1.0 * topProximity;
637
+ flowVy += jetStrength * 0.7 * topProximity;
638
+ }
639
+
640
+ const time = frameCount * 0.03;
641
+ const localSpeed = Math.sqrt(flowVx * flowVx + flowVy * flowVy);
642
+ const turbScale = 0.18 * localSpeed;
643
+ const tnx = (fbmNoise(px * 0.015, py * 0.015 + time) - 0.5) * 2;
644
+ const tny = (fbmNoise(px * 0.015 + 50, py * 0.015 + 50 + time) - 0.5) * 2;
645
+ flowVx += tnx * turbScale;
646
+ flowVy += tny * turbScale;
647
+
648
+ return { vx: flowVx, vy: flowVy };
649
+ }
650
+
651
+ // ==========================================
652
+ // Pressure Signal State (replaces EMG pipeline)
653
+ // ==========================================
654
+ let pressureValue = 0; // smoothed 0-1 from Mudra pressure signal
655
+ let smoothedEMG = 0; // game-facing variable (kept for compatibility with physics)
656
+ let isConnected = false;
657
+ let ws = null;
658
+ let reconnectTimer = null;
659
+
660
+ // Pressure smoothing (5-sample moving average, same as template)
661
+ const pressureSamples = [];
662
+
663
+ const PRESSURE_DEAD_ZONE = 0.18; // ignore resting baseline (~15%)
664
+
665
+ function handlePressure(data) {
666
+ const raw = clamp01(Number(data.normalized ?? 0));
667
+ // Dead zone: cut off resting noise, rescale remainder to 0-1
668
+ const adjusted = raw < PRESSURE_DEAD_ZONE ? 0 : (raw - PRESSURE_DEAD_ZONE) / (1 - PRESSURE_DEAD_ZONE);
669
+ pressureSamples.push(adjusted);
670
+ if (pressureSamples.length > PRESSURE_WINDOW) pressureSamples.shift();
671
+ pressureValue = pressureSamples.reduce((sum, s) => sum + s, 0) / pressureSamples.length;
672
+ }
673
+
674
+ function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
675
+ function clamp01(v) { return Number.isFinite(v) ? clamp(v, 0, 1) : 0; }
676
+
677
+ // ==========================================
678
+ // Mudra WebSocket Connection
679
+ // (Protocol-safe: reconnect, parseMessage, per-signal subscribe)
680
+ // ==========================================
681
+ function connect() {
682
+ try {
683
+ ws = new WebSocket(WS_URL);
684
+
685
+ ws.onopen = () => {
686
+ setStatus(true, "Connected to Mudra Companion");
687
+ // Subscribe to pressure (analog jet control)
688
+ send({ command: "subscribe", signal: "pressure" });
689
+ };
690
+
691
+ ws.onmessage = (event) => {
692
+ const msg = parseMessage(event.data);
693
+ if (!msg) return;
694
+
695
+ if (msg.type === "connection_status") {
696
+ const connected = msg.data?.status === "connected";
697
+ setStatus(connected, connected ? "Mudra Band connected" : "Connect your Mudra Band");
698
+ return;
699
+ }
700
+
701
+ if (msg.type === "pressure" && msg.data) {
702
+ handlePressure(msg.data);
703
+ }
704
+ };
705
+
706
+ ws.onerror = () => {
707
+ setStatus(false, "Connection error — use keyboard to play");
708
+ };
709
+
710
+ ws.onclose = () => {
711
+ setStatus(false, "Disconnected — reconnecting...");
712
+ scheduleReconnect();
713
+ };
714
+ } catch (e) {
715
+ setStatus(false, "No Mudra — use keyboard to play");
716
+ }
717
+ }
718
+
719
+ function send(payload) {
720
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
721
+ ws.send(JSON.stringify(payload));
722
+ }
723
+
724
+ function parseMessage(raw) {
725
+ try { return JSON.parse(raw); }
726
+ catch { return null; }
727
+ }
728
+
729
+ function setStatus(connected, text) {
730
+ isConnected = connected;
731
+ statusDot.classList.toggle("connected", connected);
732
+ statusText.textContent = text;
733
+ }
734
+
735
+ function scheduleReconnect() {
736
+ clearTimeout(reconnectTimer);
737
+ reconnectTimer = setTimeout(connect, 1200);
738
+ }
739
+
740
+ connect();
741
+
742
+ // ==========================================
743
+ // Keyboard/Mouse Fallback
744
+ // ==========================================
745
+ let keyHeld = false;
746
+ let keyPressure = 0; // keyboard-simulated pressure (0-1)
747
+
748
+ document.addEventListener('keydown', (e) => {
749
+ if (e.code === 'Space' && !e.repeat) {
750
+ e.preventDefault();
751
+ keyHeld = true;
752
+ keyPressure = 1.0;
753
+ emgButton.classList.add('active');
754
+ }
755
+ // [ and ] adjust pressure in steps
756
+ if (e.key === ']') {
757
+ keyPressure = clamp01(keyPressure + 0.08);
758
+ keyHeld = keyPressure > 0;
759
+ if (keyHeld) emgButton.classList.add('active');
760
+ }
761
+ if (e.key === '[') {
762
+ keyPressure = clamp01(keyPressure - 0.08);
763
+ keyHeld = keyPressure > 0;
764
+ if (!keyHeld) emgButton.classList.remove('active');
765
+ }
766
+ // R key = reset
767
+ if (e.code === 'KeyR') initGame();
768
+ });
769
+
770
+ document.addEventListener('keyup', (e) => {
771
+ if (e.code === 'Space') {
772
+ keyHeld = false;
773
+ keyPressure = 0;
774
+ emgButton.classList.remove('active');
775
+ }
776
+ });
777
+
778
+ emgButton.addEventListener('mousedown', () => {
779
+ keyHeld = true;
780
+ keyPressure = 1.0;
781
+ emgButton.classList.add('active');
782
+ });
783
+
784
+ document.addEventListener('mouseup', () => {
785
+ keyHeld = false;
786
+ keyPressure = 0;
787
+ emgButton.classList.remove('active');
788
+ });
789
+
790
+ emgButton.addEventListener('touchstart', (e) => {
791
+ e.preventDefault();
792
+ keyHeld = true;
793
+ keyPressure = 1.0;
794
+ emgButton.classList.add('active');
795
+ });
796
+
797
+ document.addEventListener('touchend', () => {
798
+ keyHeld = false;
799
+ keyPressure = 0;
800
+ emgButton.classList.remove('active');
801
+ });
802
+
803
+ // ==========================================
804
+ // Particle Effects System
805
+ // ==========================================
806
+ class SplashParticle {
807
+ constructor(x, y, vx, vy) {
808
+ this.x = x;
809
+ this.y = y;
810
+ this.vx = vx + (Math.random() - 0.5) * 2;
811
+ this.vy = vy - Math.random() * 3;
812
+ this.size = Math.random() * 3 + 1;
813
+ this.alpha = 0.8;
814
+ this.gravity = 0.15;
815
+ }
816
+
817
+ update() {
818
+ this.vy += this.gravity;
819
+ this.x += this.vx;
820
+ this.y += this.vy;
821
+ this.alpha -= 0.02;
822
+ this.size *= 0.97;
823
+ return this.alpha > 0;
824
+ }
825
+
826
+ draw() {
827
+ ctx.save();
828
+ ctx.globalAlpha = this.alpha;
829
+ ctx.beginPath();
830
+ ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
831
+ const grad = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.size);
832
+ grad.addColorStop(0, 'rgba(200, 230, 255, 0.9)');
833
+ grad.addColorStop(1, 'rgba(150, 210, 240, 0.3)');
834
+ ctx.fillStyle = grad;
835
+ ctx.fill();
836
+ ctx.restore();
837
+ }
838
+ }
839
+
840
+ let splashParticles = [];
841
+
842
+ function createSplash(x, y, intensity = 1) {
843
+ const count = Math.floor(5 + intensity * 10);
844
+ for (let i = 0; i < count; i++) {
845
+ const angle = Math.random() * Math.PI * 2;
846
+ const speed = Math.random() * 3 * intensity;
847
+ splashParticles.push(new SplashParticle(
848
+ x, y,
849
+ Math.cos(angle) * speed,
850
+ Math.sin(angle) * speed
851
+ ));
852
+ }
853
+ }
854
+
855
+ // ==========================================
856
+ // Game Objects
857
+ // ==========================================
858
+ class Peg {
859
+ constructor(x, y) {
860
+ this.x = x;
861
+ this.y = y;
862
+ this.width = 5;
863
+ this.height = 35;
864
+ this.ringCount = 0;
865
+ this.maxRings = 3;
866
+ this.glowAlpha = 0;
867
+ this.celebrationPhase = 0;
868
+ }
869
+
870
+ draw() {
871
+ const w = W(), h = H();
872
+ const px = this.x * w;
873
+ const py = this.y * h;
874
+
875
+ ctx.save();
876
+
877
+ const postGrad = ctx.createLinearGradient(
878
+ px - this.width / 2, py - this.height,
879
+ px + this.width / 2, py - this.height
880
+ );
881
+ postGrad.addColorStop(0, 'rgba(150, 170, 190, 0.9)');
882
+ postGrad.addColorStop(0.4, 'rgba(200, 220, 240, 1)');
883
+ postGrad.addColorStop(0.6, 'rgba(220, 235, 250, 1)');
884
+ postGrad.addColorStop(1, 'rgba(160, 180, 200, 0.85)');
885
+
886
+ ctx.fillStyle = postGrad;
887
+ ctx.fillRect(px - this.width / 2, py - this.height, this.width, this.height);
888
+
889
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
890
+ ctx.fillRect(px - this.width / 2, py - this.height, 1, this.height);
891
+
892
+ const ballRadius = 6;
893
+ const ballGrad = ctx.createRadialGradient(
894
+ px - 2, py - this.height - 2, 0,
895
+ px, py - this.height, ballRadius
896
+ );
897
+ ballGrad.addColorStop(0, 'rgba(255, 255, 255, 1)');
898
+ ballGrad.addColorStop(0.3, 'rgba(230, 240, 250, 1)');
899
+ ballGrad.addColorStop(0.7, 'rgba(180, 210, 230, 1)');
900
+ ballGrad.addColorStop(1, 'rgba(140, 170, 190, 0.9)');
901
+
902
+ ctx.beginPath();
903
+ ctx.arc(px, py - this.height, ballRadius, 0, Math.PI * 2);
904
+ ctx.fillStyle = ballGrad;
905
+ ctx.fill();
906
+
907
+ ctx.beginPath();
908
+ ctx.arc(px - 1.5, py - this.height - 1.5, 2, 0, Math.PI * 2);
909
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
910
+ ctx.fill();
911
+
912
+ if (this.ringCount > 0) {
913
+ this.glowAlpha = Math.min(1, this.glowAlpha + 0.05);
914
+ this.celebrationPhase += 0.1;
915
+ } else {
916
+ this.glowAlpha = Math.max(0, this.glowAlpha - 0.05);
917
+ }
918
+
919
+ if (this.glowAlpha > 0) {
920
+ const pulseSize = 14 + Math.sin(this.celebrationPhase) * 3;
921
+ ctx.beginPath();
922
+ ctx.arc(px, py - this.height, pulseSize, 0, Math.PI * 2);
923
+ const grd = ctx.createRadialGradient(px, py - this.height, 2, px, py - this.height, pulseSize);
924
+ grd.addColorStop(0, `rgba(119, 234, 233, ${this.glowAlpha * 0.7})`);
925
+ grd.addColorStop(0.5, `rgba(119, 234, 233, ${this.glowAlpha * 0.3})`);
926
+ grd.addColorStop(1, 'rgba(119, 234, 233, 0)');
927
+ ctx.fillStyle = grd;
928
+ ctx.fill();
929
+ }
930
+
931
+ ctx.restore();
932
+ }
933
+ }
934
+
935
+ class Ring {
936
+ constructor(x, y, color) {
937
+ this.x = x;
938
+ this.y = y;
939
+ this.vx = (Math.random() - 0.5) * 0.3;
940
+ this.vy = 0;
941
+ this.radius = 12;
942
+ this.innerRadius = 7;
943
+ this.color = color;
944
+ this.angle = Math.random() * Math.PI * 2;
945
+ this.angularVel = (Math.random() - 0.5) * 0.02;
946
+ this.scored = false;
947
+ this.onPeg = null;
948
+ this.settled = false;
949
+ this.settleTimer = 0;
950
+ this.mass = 0.8 + Math.random() * 0.6;
951
+ this.dragArea = 0.8 + Math.random() * 0.4;
952
+ this.vortexPhase = Math.random() * Math.PI * 2;
953
+ this.lastY = y;
954
+ }
955
+
956
+ update(jetForce) {
957
+ const w = W(), h = H();
958
+ const floorY = h - this.radius - 2;
959
+ const prevY = this.y;
960
+
961
+ if (this.settled) {
962
+ const flow = getFlowVelocity(this.x, this.y, jetForce);
963
+ const flowSpeed = Math.sqrt(flow.vx * flow.vx + flow.vy * flow.vy);
964
+ if (flowSpeed > 1.5 + this.mass * 0.8) {
965
+ this.settled = false;
966
+ this.scored = false;
967
+ if (this.onPeg) {
968
+ this.onPeg.ringCount = Math.max(0, this.onPeg.ringCount - 1);
969
+ this.onPeg = null;
970
+ }
971
+ this.settleTimer = 0;
972
+ this.vx = flow.vx * 0.3;
973
+ this.vy = flow.vy * 0.3;
974
+ updateScore();
975
+ createSplash(this.x, this.y, 0.8);
976
+ }
977
+ return;
978
+ }
979
+
980
+ const flow = getFlowVelocity(this.x, this.y, jetForce);
981
+
982
+ const relVx = flow.vx - this.vx;
983
+ const relVy = flow.vy - this.vy;
984
+ const relSpeed = Math.sqrt(relVx * relVx + relVy * relVy);
985
+
986
+ const flowAngle = Math.atan2(relVy, relVx);
987
+ const attackAngle = Math.abs(this.angle - flowAngle) % Math.PI;
988
+ const effectiveCd = 0.6 + 0.7 * Math.abs(Math.sin(attackAngle));
989
+
990
+ const dragMag = DRAG_COEFF * effectiveCd * this.dragArea * relSpeed;
991
+ const effectiveMass = this.mass * (1 + ADDED_MASS_RATIO);
992
+
993
+ this.vx += (dragMag * relVx) / effectiveMass;
994
+ this.vy += (dragMag * relVy) / effectiveMass;
995
+
996
+ this.vy += GRAVITY_ACCEL * this.mass;
997
+
998
+ if (relSpeed > 0.3) {
999
+ const diameter = this.radius * 2;
1000
+ const sheddingFreq = 0.2 * relSpeed / (diameter * 0.05);
1001
+ this.vortexPhase += sheddingFreq * 0.016;
1002
+
1003
+ const lateralMag = Math.sin(this.vortexPhase) * relSpeed * 0.04 / effectiveMass;
1004
+ if (relSpeed > 0.01) {
1005
+ this.vx += lateralMag * (-relVy / relSpeed);
1006
+ this.vy += lateralMag * (relVx / relSpeed);
1007
+ }
1008
+ }
1009
+
1010
+ const froudeNum = relSpeed / Math.sqrt(Math.max(GRAVITY_ACCEL, 0.01) * this.radius * 2);
1011
+
1012
+ if (froudeNum > 0.67) {
1013
+ const torque = relSpeed * 0.008 * Math.sign(relVx);
1014
+ this.angularVel += torque / effectiveMass;
1015
+ } else {
1016
+ if (relSpeed > 0.05) {
1017
+ const angleDiff = flowAngle - this.angle;
1018
+ const normDiff = Math.atan2(Math.sin(angleDiff), Math.cos(angleDiff));
1019
+ this.angularVel += normDiff * 0.015;
1020
+ }
1021
+ this.angularVel += Math.sin(frameCount * 0.08 + this.vortexPhase) * 0.003;
1022
+ }
1023
+ this.angularVel = Math.max(-0.15, Math.min(0.15, this.angularVel));
1024
+ this.angularVel *= 0.92;
1025
+ this.angle += this.angularVel;
1026
+
1027
+ this.vx *= 0.995;
1028
+ this.vy *= 0.995;
1029
+
1030
+ this.vy = Math.max(-8, Math.min(8, this.vy));
1031
+ this.vx = Math.max(-6, Math.min(6, this.vx));
1032
+
1033
+ this.x += this.vx;
1034
+ this.y += this.vy;
1035
+
1036
+ if (this.x < this.radius) {
1037
+ this.x = this.radius;
1038
+ this.vx = Math.abs(this.vx) * 0.3;
1039
+ this.angularVel *= 0.5;
1040
+ createSplash(this.x, this.y, 0.5);
1041
+ }
1042
+ if (this.x > w - this.radius) {
1043
+ this.x = w - this.radius;
1044
+ this.vx = -Math.abs(this.vx) * 0.3;
1045
+ this.angularVel *= 0.5;
1046
+ createSplash(this.x, this.y, 0.5);
1047
+ }
1048
+ if (this.y < this.radius + 2) {
1049
+ this.y = this.radius + 2;
1050
+ this.vy = Math.abs(this.vy) * 0.2;
1051
+ this.angularVel *= 0.5;
1052
+ createSplash(this.x, this.y, 0.6);
1053
+ }
1054
+ if (this.y > floorY) {
1055
+ this.y = floorY;
1056
+ this.vy = -Math.abs(this.vy) * 0.15;
1057
+ this.vx *= 0.9;
1058
+ this.angularVel *= 0.4;
1059
+ if (Math.abs(this.vy) > 1) {
1060
+ createSplash(this.x, this.y, Math.abs(this.vy) * 0.2);
1061
+ }
1062
+ }
1063
+
1064
+ this.lastY = prevY;
1065
+ this.checkPegCollisions();
1066
+ }
1067
+
1068
+ checkPegCollisions() {
1069
+ const w = W(), h = H();
1070
+
1071
+ for (const peg of pegs) {
1072
+ if (peg.ringCount >= peg.maxRings) continue;
1073
+
1074
+ const pegX = peg.x * w;
1075
+ const pegTopY = peg.y * h - peg.height;
1076
+ const pegBottomY = peg.y * h;
1077
+
1078
+ const stackY = pegTopY + 8 - peg.ringCount * 7;
1079
+
1080
+ const dx = this.x - pegX;
1081
+ const dy = this.y - stackY;
1082
+ const dist = Math.sqrt(dx * dx + dy * dy);
1083
+
1084
+ const isNearPeg = Math.abs(dx) < this.innerRadius + 2;
1085
+ const isAbovePeg = this.y < stackY + 5;
1086
+ const isFalling = this.vy > 0;
1087
+
1088
+ if (isNearPeg && isAbovePeg && isFalling && dist < this.radius + 8) {
1089
+ this.onPeg = peg;
1090
+ this.vx *= 0.8;
1091
+ this.x += (pegX - this.x) * 0.05;
1092
+ this.settleTimer++;
1093
+
1094
+ if (this.settleTimer > 20) {
1095
+ this.settled = true;
1096
+ peg.ringCount++;
1097
+ this.scored = true;
1098
+ this.x = pegX;
1099
+ this.y = stackY;
1100
+ this.vx = 0;
1101
+ this.vy = 0;
1102
+ this.angularVel = 0;
1103
+ updateScore();
1104
+ createSplash(pegX, stackY, 1.2);
1105
+ screenShake(3);
1106
+ }
1107
+ } else if (this.onPeg === peg && !isNearPeg) {
1108
+ this.onPeg = null;
1109
+ this.settleTimer = 0;
1110
+ } else {
1111
+ if (Math.abs(dx) < (this.radius + peg.width / 2 + 2) &&
1112
+ this.y > pegTopY && this.y < pegBottomY) {
1113
+ this.vx = dx > 0 ? 1.5 : -1.5;
1114
+ this.vy -= 0.2;
1115
+ this.angularVel *= 0.3;
1116
+ createSplash(this.x, this.y, 0.4);
1117
+ }
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ draw() {
1123
+ ctx.save();
1124
+ ctx.translate(this.x, this.y);
1125
+ const maxTilt = Math.PI * 0.19;
1126
+ const visualAngle = this.settled ? 0 : Math.sin(this.angle) * maxTilt;
1127
+ ctx.rotate(visualAngle);
1128
+
1129
+ ctx.globalAlpha = 0.2;
1130
+ ctx.beginPath();
1131
+ ctx.ellipse(3, 3, this.radius, this.radius * 0.4, 0, 0, Math.PI * 2);
1132
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
1133
+ ctx.fill();
1134
+ ctx.globalAlpha = 1;
1135
+
1136
+ const outerGrad = ctx.createLinearGradient(
1137
+ -this.radius, -this.radius * 0.4,
1138
+ this.radius, this.radius * 0.4
1139
+ );
1140
+
1141
+ const baseColor = this.color;
1142
+ const darkColor = this.adjustColorBrightness(baseColor, -40);
1143
+ const lightColor = this.adjustColorBrightness(baseColor, 40);
1144
+
1145
+ outerGrad.addColorStop(0, darkColor);
1146
+ outerGrad.addColorStop(0.4, baseColor);
1147
+ outerGrad.addColorStop(0.6, lightColor);
1148
+ outerGrad.addColorStop(1, darkColor);
1149
+
1150
+ ctx.beginPath();
1151
+ ctx.ellipse(0, 0, this.radius, this.radius * 0.4, 0, 0, Math.PI * 2);
1152
+ ctx.fillStyle = outerGrad;
1153
+ ctx.fill();
1154
+
1155
+ ctx.save();
1156
+ ctx.globalAlpha = 0.6;
1157
+ ctx.beginPath();
1158
+ ctx.ellipse(-this.radius * 0.5, -this.radius * 0.25, this.radius * 0.4, this.radius * 0.15, 0, 0, Math.PI * 2);
1159
+ const specGrad = ctx.createRadialGradient(
1160
+ -this.radius * 0.5, -this.radius * 0.25, 0,
1161
+ -this.radius * 0.5, -this.radius * 0.25, this.radius * 0.4
1162
+ );
1163
+ specGrad.addColorStop(0, 'rgba(255, 255, 255, 0.8)');
1164
+ specGrad.addColorStop(1, 'rgba(255, 255, 255, 0)');
1165
+ ctx.fillStyle = specGrad;
1166
+ ctx.fill();
1167
+ ctx.restore();
1168
+
1169
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
1170
+ ctx.lineWidth = 1;
1171
+ ctx.beginPath();
1172
+ ctx.ellipse(0, 0, this.radius, this.radius * 0.4, 0, 0, Math.PI * 2);
1173
+ ctx.stroke();
1174
+
1175
+ const holeGrad = ctx.createRadialGradient(0, 0, this.innerRadius * 0.3, 0, 0, this.innerRadius);
1176
+ holeGrad.addColorStop(0, 'rgba(60, 120, 160, 0.3)');
1177
+ holeGrad.addColorStop(0.7, 'rgba(100, 160, 200, 0.2)');
1178
+ holeGrad.addColorStop(1, 'rgba(140, 200, 230, 0.1)');
1179
+
1180
+ ctx.beginPath();
1181
+ ctx.ellipse(0, 0, this.innerRadius, this.innerRadius * 0.35, 0, 0, Math.PI * 2);
1182
+ ctx.fillStyle = holeGrad;
1183
+ ctx.fill();
1184
+
1185
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
1186
+ ctx.lineWidth = 1;
1187
+ ctx.beginPath();
1188
+ ctx.ellipse(0, 0, this.innerRadius, this.innerRadius * 0.35, 0, 0, Math.PI * 2);
1189
+ ctx.stroke();
1190
+
1191
+ ctx.globalAlpha = 0.9;
1192
+ ctx.restore();
1193
+ }
1194
+
1195
+ adjustColorBrightness(color, amount) {
1196
+ const hex = color.replace('#', '');
1197
+ const r = Math.max(0, Math.min(255, parseInt(hex.substr(0, 2), 16) + amount));
1198
+ const g = Math.max(0, Math.min(255, parseInt(hex.substr(2, 2), 16) + amount));
1199
+ const b = Math.max(0, Math.min(255, parseInt(hex.substr(4, 2), 16) + amount));
1200
+ return `rgb(${r}, ${g}, ${b})`;
1201
+ }
1202
+ }
1203
+
1204
+ class Bubble {
1205
+ constructor(x, y, size) {
1206
+ this.x = x;
1207
+ this.y = y;
1208
+ this.size = size || (Math.random() * 4 + 1);
1209
+ this.vy = -(Math.random() * 2 + 1);
1210
+ this.vx = (Math.random() - 0.5) * 0.5;
1211
+ this.alpha = 0.6;
1212
+ this.wobble = Math.random() * Math.PI * 2;
1213
+ }
1214
+
1215
+ update() {
1216
+ this.y += this.vy;
1217
+ this.x += this.vx + Math.sin(this.wobble) * 0.3;
1218
+ this.wobble += 0.08;
1219
+ this.alpha -= 0.008;
1220
+ this.size *= 0.998;
1221
+ return this.alpha > 0 && this.y > 0;
1222
+ }
1223
+
1224
+ draw() {
1225
+ ctx.save();
1226
+ ctx.globalAlpha = this.alpha;
1227
+
1228
+ ctx.beginPath();
1229
+ ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
1230
+ const bubbleGrad = ctx.createRadialGradient(
1231
+ this.x - this.size * 0.3, this.y - this.size * 0.3, 0,
1232
+ this.x, this.y, this.size
1233
+ );
1234
+ bubbleGrad.addColorStop(0, 'rgba(255, 255, 255, 0.5)');
1235
+ bubbleGrad.addColorStop(0.5, 'rgba(200, 230, 255, 0.3)');
1236
+ bubbleGrad.addColorStop(1, 'rgba(173, 216, 230, 0.2)');
1237
+ ctx.fillStyle = bubbleGrad;
1238
+ ctx.fill();
1239
+
1240
+ ctx.strokeStyle = `rgba(200, 230, 255, ${this.alpha * 0.6})`;
1241
+ ctx.lineWidth = 0.5;
1242
+ ctx.stroke();
1243
+
1244
+ ctx.beginPath();
1245
+ ctx.arc(this.x - this.size * 0.35, this.y - this.size * 0.35, this.size * 0.3, 0, Math.PI * 2);
1246
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
1247
+ ctx.fill();
1248
+
1249
+ ctx.restore();
1250
+ }
1251
+ }
1252
+
1253
+ // ==========================================
1254
+ // Screen Shake Effect
1255
+ // ==========================================
1256
+ let shakeIntensity = 0;
1257
+ let shakeDecay = 0.9;
1258
+
1259
+ function screenShake(intensity) {
1260
+ shakeIntensity = intensity;
1261
+ }
1262
+
1263
+ // ==========================================
1264
+ // Game State
1265
+ // ==========================================
1266
+ let pegs = [];
1267
+ let rings = [];
1268
+ let bubbles = [];
1269
+ let score = 0;
1270
+ let jetStrength = 0;
1271
+
1272
+ // Air pump tank
1273
+ let airTank = 1.0;
1274
+ const AIR_DRAIN_RATE = 0.002;
1275
+ const AIR_REFILL_RATE = 0.008;
1276
+
1277
+ function initGame() {
1278
+ const w = W(), h = H();
1279
+ pegs = [];
1280
+ rings = [];
1281
+ bubbles = [];
1282
+ splashParticles = [];
1283
+ score = 0;
1284
+ scoreValue.textContent = '0';
1285
+
1286
+ pegs.push(new Peg(0.3, 0.35));
1287
+ pegs.push(new Peg(0.7, 0.28));
1288
+ pegs.push(new Peg(0.5, 0.45));
1289
+
1290
+ const colors = [...RING_COLORS];
1291
+ const masses = [0.3, 0.35, 0.4, 0.45, 0.5, 0.55];
1292
+ const dragAreas = [0.8, 0.85, 0.9, 0.95, 1.0, 1.05];
1293
+ for (let i = masses.length - 1; i > 0; i--) {
1294
+ const j = Math.floor(Math.random() * (i + 1));
1295
+ [masses[i], masses[j]] = [masses[j], masses[i]];
1296
+ [dragAreas[i], dragAreas[j]] = [dragAreas[j], dragAreas[i]];
1297
+ }
1298
+ for (let i = 0; i < 6; i++) {
1299
+ const rx = w * 0.15 + Math.random() * w * 0.7;
1300
+ const ry = h * 0.75 + Math.random() * h * 0.15;
1301
+ const ring = new Ring(rx, ry, colors[i % colors.length]);
1302
+ ring.mass = masses[i];
1303
+ ring.dragArea = dragAreas[i];
1304
+ rings.push(ring);
1305
+ }
1306
+
1307
+ document.getElementById('totalPegs').textContent = rings.length;
1308
+ }
1309
+
1310
+ function updateScore() {
1311
+ score = pegs.reduce((sum, p) => sum + p.ringCount, 0);
1312
+ scoreValue.textContent = score;
1313
+ }
1314
+
1315
+ // ==========================================
1316
+ // Water Jet Rendering
1317
+ // ==========================================
1318
+ function drawWaterJets(force) {
1319
+ if (force < 0.02) return;
1320
+
1321
+ const w = W(), h = H();
1322
+ const bottomY = h;
1323
+ const nozzleX = w * NOZZLE_X_RATIO;
1324
+ const nozzleD = w * NOZZLE_DIAMETER;
1325
+ const jetHeight = force * h * 0.95;
1326
+
1327
+ if (force > 0.1) {
1328
+ ctx.save();
1329
+ ctx.globalAlpha = force * 0.6;
1330
+ const foamGrad = ctx.createRadialGradient(nozzleX, bottomY, 0, nozzleX, bottomY, nozzleD * 2);
1331
+ foamGrad.addColorStop(0, 'rgba(200, 230, 255, 0.8)');
1332
+ foamGrad.addColorStop(0.5, 'rgba(150, 210, 240, 0.4)');
1333
+ foamGrad.addColorStop(1, 'rgba(150, 210, 240, 0)');
1334
+ ctx.fillStyle = foamGrad;
1335
+ ctx.beginPath();
1336
+ ctx.arc(nozzleX, bottomY, nozzleD * 2, 0, Math.PI * 2);
1337
+ ctx.fill();
1338
+ ctx.restore();
1339
+ }
1340
+
1341
+ ctx.save();
1342
+ ctx.globalAlpha = force * 0.3;
1343
+
1344
+ const gradient = ctx.createLinearGradient(nozzleX, bottomY, nozzleX, bottomY - jetHeight);
1345
+ gradient.addColorStop(0, 'rgba(150, 210, 240, 0.7)');
1346
+ gradient.addColorStop(0.2, 'rgba(150, 210, 240, 0.5)');
1347
+ gradient.addColorStop(0.5, 'rgba(150, 210, 240, 0.25)');
1348
+ gradient.addColorStop(0.8, 'rgba(150, 210, 240, 0.08)');
1349
+ gradient.addColorStop(1, 'rgba(150, 210, 240, 0)');
1350
+
1351
+ ctx.beginPath();
1352
+ const baseHalfWidth = nozzleD * 0.5 * force;
1353
+ ctx.moveTo(nozzleX - baseHalfWidth, bottomY);
1354
+
1355
+ const steps = 28;
1356
+ const time = Date.now() / 180;
1357
+
1358
+ for (let i = 0; i <= steps; i++) {
1359
+ const t = i / steps;
1360
+ const y = bottomY - jetHeight * t;
1361
+ const spread = baseHalfWidth + JET_SPREAD_RATE * jetHeight * t * force;
1362
+ const wave = Math.sin(t * 7 + time) * spread * 0.2 * force;
1363
+ ctx.lineTo(nozzleX - spread + wave, y);
1364
+ }
1365
+ for (let i = steps; i >= 0; i--) {
1366
+ const t = i / steps;
1367
+ const y = bottomY - jetHeight * t;
1368
+ const spread = baseHalfWidth + JET_SPREAD_RATE * jetHeight * t * force;
1369
+ const wave = Math.sin(t * 7 + time + 1.5) * spread * 0.2 * force;
1370
+ ctx.lineTo(nozzleX + spread + wave, y);
1371
+ }
1372
+
1373
+ ctx.closePath();
1374
+ ctx.fillStyle = gradient;
1375
+ ctx.fill();
1376
+ ctx.restore();
1377
+
1378
+ if (force > 0.3) {
1379
+ ctx.save();
1380
+ ctx.globalAlpha = force * 0.15;
1381
+ for (let i = 0; i < 3; i++) {
1382
+ const shimmerY = bottomY - jetHeight * (0.3 + i * 0.2);
1383
+ const shimmerGrad = ctx.createRadialGradient(nozzleX, shimmerY, 0, nozzleX, shimmerY, baseHalfWidth * 3);
1384
+ shimmerGrad.addColorStop(0, 'rgba(200, 240, 255, 0.6)');
1385
+ shimmerGrad.addColorStop(1, 'rgba(200, 240, 255, 0)');
1386
+ ctx.fillStyle = shimmerGrad;
1387
+ ctx.fillRect(nozzleX - baseHalfWidth * 3, shimmerY - 3, baseHalfWidth * 6, 6);
1388
+ }
1389
+ ctx.restore();
1390
+ }
1391
+
1392
+ if (force > 0.3) {
1393
+ ctx.save();
1394
+ ctx.globalAlpha = force * 0.08;
1395
+ for (let side = 0; side < 2; side++) {
1396
+ const sx = side === 0 ? w * 0.05 : w * 0.95;
1397
+ const streakGrad = ctx.createLinearGradient(sx, h * 0.1, sx, h * 0.8);
1398
+ streakGrad.addColorStop(0, 'rgba(130, 190, 220, 0)');
1399
+ streakGrad.addColorStop(0.3, 'rgba(130, 190, 220, 0.3)');
1400
+ streakGrad.addColorStop(1, 'rgba(130, 190, 220, 0)');
1401
+ ctx.fillStyle = streakGrad;
1402
+ ctx.fillRect(sx - 3, h * 0.1, 6, h * 0.7);
1403
+ }
1404
+ ctx.restore();
1405
+ }
1406
+
1407
+ const bubbleCount = Math.floor(force * 8);
1408
+ for (let b = 0; b < bubbleCount; b++) {
1409
+ if (Math.random() < 0.7) {
1410
+ const t = Math.random();
1411
+ const spreadAtT = baseHalfWidth + JET_SPREAD_RATE * jetHeight * t * force;
1412
+ const jx = nozzleX + (Math.random() - 0.5) * spreadAtT * 2;
1413
+ const jy = bottomY - t * jetHeight * 0.8;
1414
+ bubbles.push(new Bubble(jx, jy, Math.random() * 2.5 + 0.8));
1415
+ }
1416
+ }
1417
+ }
1418
+
1419
+ // ==========================================
1420
+ // Water Surface
1421
+ // ==========================================
1422
+ function drawWaterSurface() {
1423
+ const w = W(), h = H();
1424
+ const waterY = h * 0.06;
1425
+ const time = Date.now() / 1000;
1426
+
1427
+ ctx.save();
1428
+
1429
+ ctx.beginPath();
1430
+ ctx.moveTo(0, h);
1431
+ ctx.lineTo(0, waterY);
1432
+
1433
+ for (let x = 0; x <= w; x += 3) {
1434
+ const wave = Math.sin(x * 0.03 + time * 2) * 1.5 +
1435
+ Math.sin(x * 0.05 + time * 3) * 0.8 +
1436
+ (jetStrength > 0.1 ? Math.sin(x * 0.08 + time * 5) * jetStrength * 3 : 0);
1437
+ ctx.lineTo(x, waterY + wave);
1438
+ }
1439
+
1440
+ ctx.lineTo(w, h);
1441
+ ctx.closePath();
1442
+
1443
+ const gradient = ctx.createLinearGradient(0, waterY, 0, h);
1444
+ gradient.addColorStop(0, 'rgba(80, 160, 220, 0.12)');
1445
+ gradient.addColorStop(0.5, 'rgba(60, 130, 200, 0.08)');
1446
+ gradient.addColorStop(1, 'rgba(40, 100, 180, 0.15)');
1447
+ ctx.fillStyle = gradient;
1448
+ ctx.fill();
1449
+
1450
+ ctx.globalAlpha = 0.4;
1451
+ ctx.strokeStyle = 'rgba(150, 210, 255, 0.6)';
1452
+ ctx.lineWidth = 1.5;
1453
+ ctx.beginPath();
1454
+ ctx.moveTo(0, waterY);
1455
+ for (let x = 0; x <= w; x += 3) {
1456
+ const wave = Math.sin(x * 0.03 + time * 2) * 1.5 +
1457
+ Math.sin(x * 0.05 + time * 3) * 0.8 +
1458
+ (jetStrength > 0.1 ? Math.sin(x * 0.08 + time * 5) * jetStrength * 3 : 0);
1459
+ ctx.lineTo(x, waterY + wave);
1460
+ }
1461
+ ctx.stroke();
1462
+
1463
+ ctx.globalAlpha = 0.04;
1464
+ for (let i = 0; i < 5; i++) {
1465
+ const cx = w * (0.15 + i * 0.18);
1466
+ const cy = h * (0.2 + Math.sin(time * 0.5 + i) * 0.1);
1467
+ const r = w * 0.12;
1468
+ const causticGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
1469
+ causticGrad.addColorStop(0, 'rgba(150, 220, 255, 1)');
1470
+ causticGrad.addColorStop(1, 'transparent');
1471
+ ctx.fillStyle = causticGrad;
1472
+ ctx.fillRect(cx - r, cy - r, r * 2, r * 2);
1473
+ }
1474
+
1475
+ ctx.restore();
1476
+ }
1477
+
1478
+ // ==========================================
1479
+ // Main Game Loop
1480
+ // ==========================================
1481
+ const FIXED_DT = 1 / 60;
1482
+ const MAX_SUBSTEPS = 60;
1483
+ let lastTime = 0;
1484
+
1485
+ function gameLoop(timestamp) {
1486
+ if (!timestamp) timestamp = performance.now();
1487
+ const w = W(), h = H();
1488
+
1489
+ let realDt = lastTime ? (timestamp - lastTime) / 1000 : FIXED_DT;
1490
+ lastTime = timestamp;
1491
+ realDt = Math.min(realDt, MAX_SUBSTEPS * FIXED_DT);
1492
+
1493
+ const subSteps = Math.max(1, Math.round(realDt / FIXED_DT));
1494
+
1495
+ let shakeX = 0, shakeY = 0;
1496
+ if (shakeIntensity > 0.1) {
1497
+ shakeX = (Math.random() - 0.5) * shakeIntensity;
1498
+ shakeY = (Math.random() - 0.5) * shakeIntensity;
1499
+ shakeIntensity *= shakeDecay;
1500
+ } else {
1501
+ shakeIntensity = 0;
1502
+ }
1503
+
1504
+ ctx.save();
1505
+ ctx.translate(shakeX, shakeY);
1506
+ ctx.clearRect(-shakeX, -shakeY, w + Math.abs(shakeX) * 2, h + Math.abs(shakeY) * 2);
1507
+
1508
+ const waterBg = ctx.createLinearGradient(0, 0, 0, h);
1509
+ waterBg.addColorStop(0, '#0a1628');
1510
+ waterBg.addColorStop(0.3, '#0d1f3c');
1511
+ waterBg.addColorStop(0.7, '#0f2847');
1512
+ waterBg.addColorStop(1, '#132d4f');
1513
+ ctx.fillStyle = waterBg;
1514
+ ctx.fillRect(-shakeX, -shakeY, w + Math.abs(shakeX) * 2, h + Math.abs(shakeY) * 2);
1515
+
1516
+ // --- Physics sub-stepping ---
1517
+ for (let step = 0; step < subSteps; step++) {
1518
+ frameCount++;
1519
+
1520
+ // Determine effective pressure input:
1521
+ // Mudra pressure signal takes priority; keyboard fallback when no signal
1522
+ const mudraPressure = pressureValue; // from Mudra pressure signal (0-1)
1523
+ const effectiveInput = mudraPressure > 0.01 ? mudraPressure : keyPressure;
1524
+
1525
+ // Smooth the input for game use
1526
+ if (effectiveInput > 0.01) {
1527
+ smoothedEMG += (effectiveInput - smoothedEMG) * 0.2;
1528
+ } else {
1529
+ smoothedEMG *= 0.93;
1530
+ if (smoothedEMG < 0.005) smoothedEMG = 0;
1531
+ }
1532
+
1533
+ // Air pump tank mechanic
1534
+ const releaseAmount = 1.0 - smoothedEMG;
1535
+ airTank -= smoothedEMG * AIR_DRAIN_RATE;
1536
+ airTank += releaseAmount * AIR_REFILL_RATE;
1537
+ airTank = Math.max(0, Math.min(1.0, airTank));
1538
+
1539
+ jetStrength = smoothedEMG * airTank;
1540
+
1541
+ // Ring-ring repulsion
1542
+ for (let i = 0; i < rings.length; i++) {
1543
+ for (let j = i + 1; j < rings.length; j++) {
1544
+ const a = rings[i], b = rings[j];
1545
+ if (a.settled || b.settled) continue;
1546
+ const dx = b.x - a.x;
1547
+ const dy = b.y - a.y;
1548
+ const dist = Math.sqrt(dx * dx + dy * dy);
1549
+ const minDist = a.radius + b.radius;
1550
+ if (dist < minDist && dist > 0) {
1551
+ const push = (minDist - dist) * 0.15 / dist;
1552
+ a.vx -= dx * push;
1553
+ a.vy -= dy * push;
1554
+ b.vx += dx * push;
1555
+ b.vy += dy * push;
1556
+ }
1557
+ }
1558
+ }
1559
+
1560
+ for (const ring of rings) {
1561
+ ring.update(jetStrength);
1562
+ }
1563
+ }
1564
+
1565
+ // Update meter (shows air tank level)
1566
+ emgMeterFill.style.width = (airTank * 100) + '%';
1567
+
1568
+ // Button visual feedback
1569
+ if (smoothedEMG > 0.05) {
1570
+ emgButton.classList.add('active');
1571
+ } else if (!keyHeld) {
1572
+ emgButton.classList.remove('active');
1573
+ }
1574
+
1575
+ // --- Rendering ---
1576
+ drawWaterSurface();
1577
+ drawWaterJets(jetStrength);
1578
+
1579
+ splashParticles = splashParticles.filter(p => p.update());
1580
+ for (const particle of splashParticles) {
1581
+ particle.draw();
1582
+ }
1583
+
1584
+ bubbles = bubbles.filter(b => b.update());
1585
+ for (const bubble of bubbles) {
1586
+ bubble.draw();
1587
+ }
1588
+
1589
+ for (const peg of pegs) {
1590
+ peg.draw();
1591
+ }
1592
+
1593
+ for (const ring of rings) {
1594
+ ring.draw();
1595
+ }
1596
+
1597
+ if (Math.random() < 0.03) {
1598
+ const bx = Math.random() * w;
1599
+ const by = h * 0.85 + Math.random() * h * 0.15;
1600
+ bubbles.push(new Bubble(bx, by, Math.random() * 2 + 0.5));
1601
+ }
1602
+
1603
+ ctx.restore();
1604
+
1605
+ requestAnimationFrame(gameLoop);
1606
+ }
1607
+
1608
+ // ==========================================
1609
+ // Start
1610
+ // ==========================================
1611
+ initGame();
1612
+ gameLoop(performance.now());
1613
+
1614
+ // Fallback for backgrounded tabs
1615
+ let lastKnownFrame = 0;
1616
+ setInterval(() => {
1617
+ if (frameCount === lastKnownFrame) {
1618
+ gameLoop(performance.now());
1619
+ }
1620
+ lastKnownFrame = frameCount;
1621
+ }, 32);
1622
+ </script>
1623
+ </body>
1624
+ </html>