stereoDrift commited on
Commit
f8b493b
·
verified ·
1 Parent(s): c83ab87

Upload 14 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/demo.png filter=lfs diff=lfs merge=lfs -text
37
+ assets/kick.wav filter=lfs diff=lfs merge=lfs -text
38
+ assets/siteOGImage.webp filter=lfs diff=lfs merge=lfs -text
39
+ assets/snare.wav filter=lfs diff=lfs merge=lfs -text
DrumManager.js ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function _array_like_to_array(arr, len) {
2
+ if (len == null || len > arr.length) len = arr.length;
3
+ for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
4
+ return arr2;
5
+ }
6
+ function _array_with_holes(arr) {
7
+ if (Array.isArray(arr)) return arr;
8
+ }
9
+ function _iterable_to_array_limit(arr, i) {
10
+ var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"];
11
+ if (_i == null) return;
12
+ var _arr = [];
13
+ var _n = true;
14
+ var _d = false;
15
+ var _s, _e;
16
+ try {
17
+ for(_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true){
18
+ _arr.push(_s.value);
19
+ if (i && _arr.length === i) break;
20
+ }
21
+ } catch (err) {
22
+ _d = true;
23
+ _e = err;
24
+ } finally{
25
+ try {
26
+ if (!_n && _i["return"] != null) _i["return"]();
27
+ } finally{
28
+ if (_d) throw _e;
29
+ }
30
+ }
31
+ return _arr;
32
+ }
33
+ function _non_iterable_rest() {
34
+ throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
35
+ }
36
+ function _sliced_to_array(arr, i) {
37
+ return _array_with_holes(arr) || _iterable_to_array_limit(arr, i) || _unsupported_iterable_to_array(arr, i) || _non_iterable_rest();
38
+ }
39
+ function _unsupported_iterable_to_array(o, minLen) {
40
+ if (!o) return;
41
+ if (typeof o === "string") return _array_like_to_array(o, minLen);
42
+ var n = Object.prototype.toString.call(o).slice(8, -1);
43
+ if (n === "Object" && o.constructor) n = o.constructor.name;
44
+ if (n === "Map" || n === "Set") return Array.from(n);
45
+ if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array(o, minLen);
46
+ }
47
+ import * as Tone from 'https://esm.sh/tone';
48
+ // --- Module State ---
49
+ var players = null;
50
+ var isLoaded = false;
51
+ var sequence = null;
52
+ var beatIndex = 0;
53
+ var activeDrums = new Set();
54
+ // More varied and syncopated drum patterns
55
+ var drumPattern = {
56
+ // Syncopated kick pattern
57
+ //'kick': [true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false],
58
+ 'kick': [
59
+ true,
60
+ false,
61
+ false,
62
+ false,
63
+ false,
64
+ true,
65
+ false,
66
+ false,
67
+ true,
68
+ false,
69
+ false,
70
+ true,
71
+ false,
72
+ true,
73
+ false,
74
+ false
75
+ ],
76
+ // Snare on the backbeat (beats 2 and 4)
77
+ 'snare': [
78
+ false,
79
+ false,
80
+ false,
81
+ false,
82
+ true,
83
+ false,
84
+ false,
85
+ false,
86
+ false,
87
+ false,
88
+ false,
89
+ false,
90
+ true,
91
+ false,
92
+ false,
93
+ false
94
+ ],
95
+ // Open hi-hat feel on the off-beats
96
+ 'hihat': [
97
+ false,
98
+ true,
99
+ false,
100
+ true,
101
+ false,
102
+ true,
103
+ false,
104
+ true,
105
+ false,
106
+ true,
107
+ false,
108
+ true,
109
+ false,
110
+ true,
111
+ false,
112
+ true
113
+ ],
114
+ // Clap layered with snare, but with an extra syncopated hit
115
+ 'clap': [
116
+ false,
117
+ false,
118
+ false,
119
+ false,
120
+ true,
121
+ false,
122
+ false,
123
+ true,
124
+ false,
125
+ false,
126
+ false,
127
+ false,
128
+ true,
129
+ false,
130
+ false,
131
+ false
132
+ ]
133
+ };
134
+ var fingerToDrumMap = {
135
+ 'index': 'kick',
136
+ 'middle': 'snare',
137
+ 'ring': 'hihat',
138
+ 'pinky': 'clap'
139
+ };
140
+ // --- Exported Functions ---
141
+ /**
142
+ * Loads all drum samples and returns a promise that resolves when loading is complete
143
+ */ export function loadSamples() {
144
+ return new Promise(function(resolve, reject) {
145
+ if (isLoaded) {
146
+ resolve();
147
+ return;
148
+ }
149
+ players = new Tone.Players({
150
+ urls: {
151
+ kick: 'assets/kick.wav',
152
+ snare: 'assets/snare.wav',
153
+ hihat: 'assets/hihat.wav',
154
+ clap: 'assets/clap.wav'
155
+ },
156
+ onload: function() {
157
+ isLoaded = true;
158
+ // Set volumes after loading
159
+ players.player('kick').volume.value = -6; // Lowered kick volume
160
+ players.player('snare').volume.value = 0;
161
+ players.player('hihat').volume.value = -2; // Softer hi-hat
162
+ players.player('clap').volume.value = 0;
163
+ console.log("Drum samples loaded successfully.");
164
+ resolve();
165
+ },
166
+ onerror: function(error) {
167
+ console.error("Error loading drum samples:", error);
168
+ reject(error);
169
+ }
170
+ }).toDestination();
171
+ });
172
+ }
173
+ /**
174
+ * Creates and starts the main 16-step drum loop.
175
+ * Assumes Tone.Transport has been started elsewhere.
176
+ */ export function startSequence() {
177
+ if (!isLoaded || sequence) {
178
+ console.warn("Drums not loaded or sequence already started. Cannot start sequence.");
179
+ return;
180
+ }
181
+ sequence = new Tone.Sequence(function(time, step) {
182
+ beatIndex = step; // Update for visualization
183
+ Object.entries(drumPattern).forEach(function(param) {
184
+ var _param = _sliced_to_array(param, 2), drum = _param[0], pattern = _param[1];
185
+ // If the drum is active AND its pattern has a note on this step...
186
+ if (activeDrums.has(drum) && pattern[step]) {
187
+ players.player(drum).start(time);
188
+ }
189
+ });
190
+ }, Array.from({
191
+ length: 16
192
+ }, function(_, i) {
193
+ return i;
194
+ }), "16n").start(0);
195
+ console.log("Drum sequence started.");
196
+ }
197
+ /**
198
+ * Updates which drums are active based on finger positions.
199
+ * @param {object} fingerStates - An object with finger names as keys and boolean `isUp` as values.
200
+ */ export function updateActiveDrums(fingerStates) {
201
+ activeDrums.clear();
202
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
203
+ try {
204
+ for(var _iterator = Object.entries(fingerStates)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
205
+ var _step_value = _sliced_to_array(_step.value, 2), finger = _step_value[0], isUp = _step_value[1];
206
+ if (isUp) {
207
+ var drum = fingerToDrumMap[finger];
208
+ if (drum) {
209
+ activeDrums.add(drum);
210
+ }
211
+ }
212
+ }
213
+ } catch (err) {
214
+ _didIteratorError = true;
215
+ _iteratorError = err;
216
+ } finally{
217
+ try {
218
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
219
+ _iterator.return();
220
+ }
221
+ } finally{
222
+ if (_didIteratorError) {
223
+ throw _iteratorError;
224
+ }
225
+ }
226
+ }
227
+ }
228
+ /**
229
+ * Returns the set of currently active drums.
230
+ * @returns {Set<string>} A set of active drum names.
231
+ */ export function getActiveDrums() {
232
+ return activeDrums;
233
+ }
234
+ /**
235
+ * Returns the mapping of fingers to drums.
236
+ * @returns {object} The finger-to-drum map.
237
+ */ export function getFingerToDrumMap() {
238
+ return fingerToDrumMap;
239
+ }
240
+ /**
241
+ * Returns the current beat index for external use (like visualization).
242
+ * @returns {number} The current beat index (0-15).
243
+ */ export function getCurrentBeat() {
244
+ return beatIndex;
245
+ }
246
+ /**
247
+ * Returns the master drum pattern object.
248
+ * @returns {object} The drum pattern.
249
+ */ export function getDrumPattern() {
250
+ return drumPattern;
251
+ }
MusicManager.js ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
2
+ try {
3
+ var info = gen[key](arg);
4
+ var value = info.value;
5
+ } catch (error) {
6
+ reject(error);
7
+ return;
8
+ }
9
+ if (info.done) {
10
+ resolve(value);
11
+ } else {
12
+ Promise.resolve(value).then(_next, _throw);
13
+ }
14
+ }
15
+ function _async_to_generator(fn) {
16
+ return function() {
17
+ var self = this, args = arguments;
18
+ return new Promise(function(resolve, reject) {
19
+ var gen = fn.apply(self, args);
20
+ function _next(value) {
21
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
22
+ }
23
+ function _throw(err) {
24
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
25
+ }
26
+ _next(undefined);
27
+ });
28
+ };
29
+ }
30
+ function _class_call_check(instance, Constructor) {
31
+ if (!(instance instanceof Constructor)) {
32
+ throw new TypeError("Cannot call a class as a function");
33
+ }
34
+ }
35
+ function _defineProperties(target, props) {
36
+ for(var i = 0; i < props.length; i++){
37
+ var descriptor = props[i];
38
+ descriptor.enumerable = descriptor.enumerable || false;
39
+ descriptor.configurable = true;
40
+ if ("value" in descriptor) descriptor.writable = true;
41
+ Object.defineProperty(target, descriptor.key, descriptor);
42
+ }
43
+ }
44
+ function _create_class(Constructor, protoProps, staticProps) {
45
+ if (protoProps) _defineProperties(Constructor.prototype, protoProps);
46
+ if (staticProps) _defineProperties(Constructor, staticProps);
47
+ return Constructor;
48
+ }
49
+ function _ts_generator(thisArg, body) {
50
+ var f, y, t, g, _ = {
51
+ label: 0,
52
+ sent: function() {
53
+ if (t[0] & 1) throw t[1];
54
+ return t[1];
55
+ },
56
+ trys: [],
57
+ ops: []
58
+ };
59
+ return g = {
60
+ next: verb(0),
61
+ "throw": verb(1),
62
+ "return": verb(2)
63
+ }, typeof Symbol === "function" && (g[Symbol.iterator] = function() {
64
+ return this;
65
+ }), g;
66
+ function verb(n) {
67
+ return function(v) {
68
+ return step([
69
+ n,
70
+ v
71
+ ]);
72
+ };
73
+ }
74
+ function step(op) {
75
+ if (f) throw new TypeError("Generator is already executing.");
76
+ while(_)try {
77
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
78
+ if (y = 0, t) op = [
79
+ op[0] & 2,
80
+ t.value
81
+ ];
82
+ switch(op[0]){
83
+ case 0:
84
+ case 1:
85
+ t = op;
86
+ break;
87
+ case 4:
88
+ _.label++;
89
+ return {
90
+ value: op[1],
91
+ done: false
92
+ };
93
+ case 5:
94
+ _.label++;
95
+ y = op[1];
96
+ op = [
97
+ 0
98
+ ];
99
+ continue;
100
+ case 7:
101
+ op = _.ops.pop();
102
+ _.trys.pop();
103
+ continue;
104
+ default:
105
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
106
+ _ = 0;
107
+ continue;
108
+ }
109
+ if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {
110
+ _.label = op[1];
111
+ break;
112
+ }
113
+ if (op[0] === 6 && _.label < t[1]) {
114
+ _.label = t[1];
115
+ t = op;
116
+ break;
117
+ }
118
+ if (t && _.label < t[2]) {
119
+ _.label = t[2];
120
+ _.ops.push(op);
121
+ break;
122
+ }
123
+ if (t[2]) _.ops.pop();
124
+ _.trys.pop();
125
+ continue;
126
+ }
127
+ op = body.call(thisArg, _);
128
+ } catch (e) {
129
+ op = [
130
+ 6,
131
+ e
132
+ ];
133
+ y = 0;
134
+ } finally{
135
+ f = t = 0;
136
+ }
137
+ if (op[0] & 5) throw op[1];
138
+ return {
139
+ value: op[0] ? op[1] : void 0,
140
+ done: true
141
+ };
142
+ }
143
+ }
144
+ import * as Tone from 'https://esm.sh/tone';
145
+ // A simple manager for our Tone.js based music generation
146
+ export var MusicManager = /*#__PURE__*/ function() {
147
+ "use strict";
148
+ function MusicManager() {
149
+ _class_call_check(this, MusicManager);
150
+ this.polySynth = null;
151
+ this.reverb = null;
152
+ this.stereoDelay = null; // Add a delay effect property
153
+ this.analyser = null; // For waveform visualization
154
+ this.isStarted = false;
155
+ // Use a Map to store the active arpeggio pattern for each hand
156
+ this.activePatterns = new Map();
157
+ // Use a Map to store the current volume (velocity) for each hand's arpeggio
158
+ this.handVolumes = new Map();
159
+ this.synthPresets = [
160
+ // Preset 1: Clean Sine Wave (Default)
161
+ {
162
+ harmonicity: 4,
163
+ modulationIndex: 3,
164
+ oscillator: {
165
+ type: 'sine'
166
+ },
167
+ envelope: {
168
+ attack: 0.01,
169
+ decay: 0.2,
170
+ sustain: 0.5,
171
+ release: 1.0
172
+ },
173
+ modulation: {
174
+ type: 'sine'
175
+ },
176
+ modulationEnvelope: {
177
+ attack: 0.1,
178
+ decay: 0.01,
179
+ sustain: 1,
180
+ release: 0.5
181
+ }
182
+ },
183
+ // Preset 2: Buzzy Sawtooth
184
+ {
185
+ harmonicity: 1,
186
+ modulationIndex: 8,
187
+ oscillator: {
188
+ type: 'sawtooth'
189
+ },
190
+ // More staccato/plucky envelope
191
+ envelope: {
192
+ attack: 0.01,
193
+ decay: 0.15,
194
+ sustain: 0.05,
195
+ release: 0.2
196
+ },
197
+ modulation: {
198
+ type: 'square'
199
+ },
200
+ modulationEnvelope: {
201
+ attack: 0.05,
202
+ decay: 0.2,
203
+ sustain: 0.4,
204
+ release: 0.6
205
+ }
206
+ },
207
+ // Preset 3: Funk Electric Piano (Rhodes-like)
208
+ {
209
+ harmonicity: 2,
210
+ modulationIndex: 12,
211
+ oscillator: {
212
+ type: 'sine'
213
+ },
214
+ envelope: {
215
+ attack: 0.02,
216
+ decay: 0.3,
217
+ sustain: 0.2,
218
+ release: 0.8
219
+ },
220
+ modulation: {
221
+ type: 'sine'
222
+ },
223
+ modulationEnvelope: {
224
+ attack: 0.05,
225
+ decay: 0.2,
226
+ sustain: 0.1,
227
+ release: 0.8
228
+ },
229
+ effects: {
230
+ reverbWet: 0.3,
231
+ delayWet: 0.1 // A touch of delay
232
+ }
233
+ }
234
+ ];
235
+ this.currentSynthIndex = 0;
236
+ }
237
+ _create_class(MusicManager, [
238
+ {
239
+ key: "start",
240
+ value: // Must be called after a user interaction
241
+ function start() {
242
+ var _this = this;
243
+ return _async_to_generator(function() {
244
+ return _ts_generator(this, function(_state) {
245
+ switch(_state.label){
246
+ case 0:
247
+ if (_this.isStarted) return [
248
+ 2
249
+ ];
250
+ return [
251
+ 4,
252
+ Tone.start()
253
+ ];
254
+ case 1:
255
+ _state.sent();
256
+ _this.reverb = new Tone.Reverb({
257
+ decay: 5,
258
+ preDelay: 0.0,
259
+ wet: 0.8
260
+ }).toDestination();
261
+ // Create a stereo delay and connect it to the reverb
262
+ _this.stereoDelay = new Tone.FeedbackDelay("8n", 0.5).connect(_this.reverb);
263
+ _this.stereoDelay.wet.value = 0; // Start with no delay effect
264
+ // Create an analyser for the waveform visualizer
265
+ _this.analyser = new Tone.Analyser('waveform', 1024);
266
+ // Use PolySynth to allow multiple arpeggios (one per hand) to play simultaneously.
267
+ // The synth now connects to the analyser, then to the delay, which then connects to the reverb.
268
+ _this.polySynth = new Tone.PolySynth(Tone.FMSynth, _this.synthPresets[_this.currentSynthIndex]);
269
+ _this.polySynth.connect(_this.analyser);
270
+ _this.analyser.connect(_this.stereoDelay);
271
+ // Set a low volume to avoid clipping and create a more ambient feel
272
+ _this.polySynth.volume.value = 0;
273
+ _this.isStarted = true;
274
+ // Set the master tempo to 100 BPM
275
+ Tone.Transport.bpm.value = 100;
276
+ // Start the master transport
277
+ Tone.Transport.start();
278
+ console.log("Tone.js AudioContext started and PolySynth is ready.");
279
+ return [
280
+ 2
281
+ ];
282
+ }
283
+ });
284
+ })();
285
+ }
286
+ },
287
+ {
288
+ // Starts an arpeggio for a specific hand
289
+ key: "startArpeggio",
290
+ value: function startArpeggio(handId, rootNote) {
291
+ var _this = this;
292
+ if (!this.polySynth || this.activePatterns.has(handId)) return;
293
+ // Create a richer arpeggio (Major 7th + Octave)
294
+ // C Minor Pentatonic scale intervals: Root, Minor Third, Perfect Fourth, Perfect Fifth, Minor Seventh
295
+ var chord = Tone.Frequency(rootNote).harmonize([
296
+ 0,
297
+ 3,
298
+ 5,
299
+ 7,
300
+ 10,
301
+ 12
302
+ ]);
303
+ var arpeggioNotes = chord.map(function(freq) {
304
+ return Tone.Frequency(freq).toNote();
305
+ });
306
+ // Tone.Pattern cycles through an array of values
307
+ var pattern = new Tone.Pattern(function(time, note) {
308
+ // Get the latest volume (velocity) for this hand, defaulting to a soft 0.2 if not set.
309
+ var velocity = _this.handVolumes.get(handId) || 0.2;
310
+ // The time argument is the precise time the note should be played
311
+ _this.polySynth.triggerAttackRelease(note, "16n", time, velocity);
312
+ }, arpeggioNotes, "upDown");
313
+ pattern.interval = "16n"; // The time between notes in the pattern (faster)
314
+ pattern.start(0); // Start the pattern immediately
315
+ // Store the pattern and its root note so we can update/stop it later
316
+ this.activePatterns.set(handId, {
317
+ pattern: pattern,
318
+ currentRoot: rootNote
319
+ });
320
+ }
321
+ },
322
+ {
323
+ // Updates the volume (velocity) of an existing arpeggio
324
+ key: "updateArpeggioVolume",
325
+ value: function updateArpeggioVolume(handId, velocity) {
326
+ // Only update if an arpeggio is active for this hand
327
+ if (this.polySynth && this.activePatterns.has(handId)) {
328
+ // Clamp the velocity to be safe
329
+ var clampedVelocity = Math.max(0, Math.min(1, velocity));
330
+ this.handVolumes.set(handId, clampedVelocity);
331
+ // IMPORTANT FIX: Also set the synth's overall volume.
332
+ // Since we only have one arpeggio at a time, we can map this directly.
333
+ // Using logarithmic scaling for a more natural volume control.
334
+ var volumeInDb = Tone.gainToDb(clampedVelocity);
335
+ this.polySynth.volume.value = volumeInDb;
336
+ }
337
+ }
338
+ },
339
+ {
340
+ // Updates the notes in an existing arpeggio
341
+ key: "updateArpeggio",
342
+ value: function updateArpeggio(handId, newRootNote) {
343
+ var activePattern = this.activePatterns.get(handId);
344
+ if (!this.polySynth || !activePattern || activePattern.currentRoot === newRootNote) {
345
+ return; // No need to update if the note hasn't changed
346
+ }
347
+ // Create new notes for the pattern
348
+ var newChord = Tone.Frequency(newRootNote).harmonize([
349
+ 0,
350
+ 3,
351
+ 5,
352
+ 7,
353
+ 10,
354
+ 12
355
+ ]);
356
+ activePattern.pattern.values = newChord.map(function(freq) {
357
+ return Tone.Frequency(freq).toNote();
358
+ });
359
+ activePattern.currentRoot = newRootNote;
360
+ }
361
+ },
362
+ {
363
+ // Stops and cleans up an arpeggio for a specific hand
364
+ key: "stopArpeggio",
365
+ value: function stopArpeggio(handId) {
366
+ var activePattern = this.activePatterns.get(handId);
367
+ if (activePattern) {
368
+ activePattern.pattern.stop(0); // Stop the pattern
369
+ activePattern.pattern.dispose(); // Clean up Tone.js objects
370
+ this.activePatterns.delete(handId); // Remove from our map
371
+ this.handVolumes.delete(handId); // Clean up the stored volume
372
+ // If no other hands are playing, silence the synth.
373
+ if (this.activePatterns.size === 0) {
374
+ this.polySynth.volume.value = -Infinity;
375
+ }
376
+ }
377
+ }
378
+ },
379
+ {
380
+ // Cycles to the next synth preset
381
+ key: "cycleSynth",
382
+ value: function cycleSynth() {
383
+ var _this = this;
384
+ var _newPreset_effects, _newPreset_effects1;
385
+ if (!this.polySynth) return;
386
+ // Stop all currently playing notes/arpeggios before swapping
387
+ this.activePatterns.forEach(function(value, key) {
388
+ _this.stopArpeggio(key);
389
+ });
390
+ // Dispose the old synth to free up resources
391
+ this.polySynth.dispose();
392
+ // Cycle to the next preset
393
+ this.currentSynthIndex = (this.currentSynthIndex + 1) % this.synthPresets.length;
394
+ var newPreset = this.synthPresets[this.currentSynthIndex];
395
+ // Create the new synth but don't connect it yet
396
+ this.polySynth = new Tone.PolySynth(Tone.FMSynth, newPreset);
397
+ // Re-establish the audio chain: synth -> analyser -> delay
398
+ this.polySynth.connect(this.analyser);
399
+ this.polySynth.volume.value = 0; // Reset volume
400
+ var _newPreset_effects_reverbWet;
401
+ // Adjust global effects based on the new preset's settings
402
+ // Use optional chaining `?.` to safely access `effects` property
403
+ this.reverb.wet.value = (_newPreset_effects_reverbWet = (_newPreset_effects = newPreset.effects) === null || _newPreset_effects === void 0 ? void 0 : _newPreset_effects.reverbWet) !== null && _newPreset_effects_reverbWet !== void 0 ? _newPreset_effects_reverbWet : 0.8; // Default to 0.8 if not specified
404
+ var _newPreset_effects_delayWet;
405
+ this.stereoDelay.wet.value = (_newPreset_effects_delayWet = (_newPreset_effects1 = newPreset.effects) === null || _newPreset_effects1 === void 0 ? void 0 : _newPreset_effects1.delayWet) !== null && _newPreset_effects_delayWet !== void 0 ? _newPreset_effects_delayWet : 0; // Default to 0 if not specified
406
+ console.log("Switched to synth preset: ".concat(this.currentSynthIndex));
407
+ }
408
+ },
409
+ {
410
+ // Getter for the analyser so the game can use it
411
+ key: "getAnalyser",
412
+ value: function getAnalyser() {
413
+ return this.analyser;
414
+ }
415
+ }
416
+ ]);
417
+ return MusicManager;
418
+ }();
README.md CHANGED
@@ -1,12 +1,80 @@
1
- ---
2
- title: Arpeggiator
3
- emoji: 🌍
4
- colorFrom: purple
5
- colorTo: gray
6
- sdk: static
7
- pinned: false
8
- license: mit
9
- short_description: Hand-controlled arpeggiator, drum machine, and visualizer
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hand Gesture Arpeggiator
2
+
3
+ Hand-controlled arpeggiator, drum machine, and audio reactive visualizer. Raise your hands to raise the roof!
4
+
5
+ An interactive web app built with threejs, mediapipe computer vision, rosebud AI, and tone.js.
6
+
7
+ - Hand #1 controls the arpeggios (raise hand to raise pitch, pinch to change volume)
8
+ - Hand #2 controls the drums (raise different fingers to change the pattern)
9
+
10
+ [Video](https://youtu.be/JepIs-DTBgk?si=4Y-FrQDF6KNy662C) | [Live Demo](https://collidingscopes.github.io/arpeggiator/) | [More Code & Tutorials](https://funwithcomputervision.com/)
11
+
12
+ <img src="assets/demo.png">
13
+
14
+ ## Requirements
15
+
16
+ - Modern web browser with WebGL support
17
+ - Camera access enabled for hand tracking
18
+
19
+ ## Technologies
20
+
21
+ - **MediaPipe** for hand tracking and gesture recognition
22
+ - **Three.js** for audio reactive visual rendering
23
+ - **Tone.js** for synthesizer sounds
24
+ - **HTML5 Canvas** for visual feedback
25
+ - **JavaScript** for real-time interaction
26
+
27
+ ## Setup for Development
28
+
29
+ ```bash
30
+ # Clone this repository
31
+ git clone https://github.com/collidingScopes/arpeggiator
32
+
33
+ # Navigate to the project directory
34
+ cd arpeggiator
35
+
36
+ # Serve with your preferred method (example using Python)
37
+ python -m http.server
38
+ ```
39
+
40
+ Then navigate to `http://localhost:8000` in your browser.
41
+
42
+ ## License
43
+
44
+ MIT License
45
+
46
+ ## Credits
47
+
48
+ - Three.js - https://threejs.org/
49
+ - MediaPipe - https://mediapipe.dev/
50
+ - Rosebud AI - https://rosebud.ai/
51
+ - Tone.js - https://tonejs.github.io/
52
+
53
+ ## Related Projects
54
+
55
+ I've released several computer vision projects (with code + tutorials) here:
56
+ [Fun With Computer Vision](https://www.funwithcomputervision.com/)
57
+
58
+ You can purchase lifetime access and receive the full project files and tutorials. I'm adding more content regularly 🪬
59
+
60
+ You might also like some of my other open source projects:
61
+
62
+ - [3D Model Playground](https://collidingScopes.github.io/3d-model-playground) - control 3D models with voice and hand gestures
63
+ - [Threejs hand tracking tutorial](https://collidingScopes.github.io/threejs-handtracking-101) - Basic hand tracking setup with threejs and MediaPipe computer vision
64
+ - [Particular Drift](https://collidingScopes.github.io/particular-drift) - Turn photos into flowing particle animations
65
+ - [Video-to-ASCII](https://collidingScopes.github.io/ascii) - Convert videos into ASCII pixel art
66
+
67
+ ## Contact
68
+
69
+ - Instagram: [@stereo.drift](https://www.instagram.com/stereo.drift/)
70
+ - Twitter/X: [@measure_plan](https://x.com/measure_plan)
71
+ - Email: [[email protected]](mailto:[email protected])
72
+ - GitHub: [collidingScopes](https://github.com/collidingScopes)
73
+
74
+ ## Donations
75
+
76
+ If you found this tool useful, feel free to buy me a coffee.
77
+
78
+ My name is Alan, and I enjoy building open source software for computer vision, games, and more. This would be much appreciated during late-night coding sessions!
79
+
80
+ [![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/stereoDrift)
WaveformVisualizer.js ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function _class_call_check(instance, Constructor) {
2
+ if (!(instance instanceof Constructor)) {
3
+ throw new TypeError("Cannot call a class as a function");
4
+ }
5
+ }
6
+ function _defineProperties(target, props) {
7
+ for(var i = 0; i < props.length; i++){
8
+ var descriptor = props[i];
9
+ descriptor.enumerable = descriptor.enumerable || false;
10
+ descriptor.configurable = true;
11
+ if ("value" in descriptor) descriptor.writable = true;
12
+ Object.defineProperty(target, descriptor.key, descriptor);
13
+ }
14
+ }
15
+ function _create_class(Constructor, protoProps, staticProps) {
16
+ if (protoProps) _defineProperties(Constructor.prototype, protoProps);
17
+ if (staticProps) _defineProperties(Constructor, staticProps);
18
+ return Constructor;
19
+ }
20
+ function _instanceof(left, right) {
21
+ if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
22
+ return !!right[Symbol.hasInstance](left);
23
+ } else {
24
+ return left instanceof right;
25
+ }
26
+ }
27
+ import * as THREE from 'three';
28
+ // A class to create and manage a waveform visualizer using Tone.Analyser
29
+ export var WaveformVisualizer = /*#__PURE__*/ function() {
30
+ "use strict";
31
+ function WaveformVisualizer(scene, analyser, canvasWidth, canvasHeight) {
32
+ _class_call_check(this, WaveformVisualizer);
33
+ this.scene = scene;
34
+ this.analyser = analyser;
35
+ this.mesh = null;
36
+ this.bufferLength = this.analyser.size;
37
+ this.dataArray = new Float32Array(this.bufferLength);
38
+ this.smoothedDataArray = new Float32Array(this.bufferLength); // For smoothing
39
+ // Visual properties
40
+ this.smoothingFactor = 0.4; // How much to smooth the wave (0.0 - 1.0)
41
+ this.width = canvasWidth * 0.8; // Occupy 80% of the screen width
42
+ this.height = 450; // The vertical amplitude of the wave
43
+ this.yPosition = 0; // The vertical center of the wave
44
+ this.thickness = 30.0; // The thickness of the line mesh
45
+ this.currentColor = new THREE.Color('#7B4394');
46
+ this.targetColor = new THREE.Color('#7B4394');
47
+ this.uniforms = {
48
+ solidColor: {
49
+ value: this.currentColor
50
+ }
51
+ };
52
+ this._createVisualizer();
53
+ }
54
+ _create_class(WaveformVisualizer, [
55
+ {
56
+ key: "_createVisualizer",
57
+ value: function _createVisualizer() {
58
+ var material = new THREE.ShaderMaterial({
59
+ uniforms: this.uniforms,
60
+ vertexShader: "\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n ",
61
+ fragmentShader: "\n uniform vec3 solidColor;\n void main() {\n gl_FragColor = vec4(solidColor, 0.9);\n }\n ",
62
+ transparent: true,
63
+ side: THREE.DoubleSide
64
+ });
65
+ var geometry = new THREE.BufferGeometry();
66
+ var positions = new Float32Array(this.bufferLength * 2 * 3);
67
+ var uvs = new Float32Array(this.bufferLength * 2 * 2);
68
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
69
+ geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
70
+ var indices = [];
71
+ for(var i = 0; i < this.bufferLength - 1; i++){
72
+ var p1 = i * 2; // top-left
73
+ var p2 = p1 + 1; // bottom-left
74
+ var p3 = (i + 1) * 2; // top-right
75
+ var p4 = p3 + 1; // bottom-right
76
+ indices.push(p1, p2, p3);
77
+ indices.push(p2, p4, p3);
78
+ }
79
+ geometry.setIndex(indices);
80
+ this.mesh = new THREE.Mesh(geometry, material);
81
+ this.scene.add(this.mesh);
82
+ this.updatePosition(window.innerWidth, window.innerHeight);
83
+ }
84
+ },
85
+ {
86
+ // Call this from the main animation loop
87
+ key: "update",
88
+ value: function update() {
89
+ if (!this.analyser || !this.mesh) return;
90
+ // Smoothly interpolate the current color towards the target color
91
+ this.currentColor.lerp(this.targetColor, 0.05);
92
+ var newArray = this.analyser.getValue();
93
+ if (_instanceof(newArray, Float32Array)) {
94
+ this.dataArray.set(newArray);
95
+ }
96
+ var positions = this.mesh.geometry.attributes.position.array;
97
+ var uvs = this.mesh.geometry.attributes.uv.array;
98
+ var startX = -this.width / 2;
99
+ var xStep = this.width / (this.bufferLength - 1);
100
+ var halfThickness = this.thickness / 2;
101
+ for(var i = 0; i < this.bufferLength; i++){
102
+ // Apply exponential smoothing
103
+ this.smoothedDataArray[i] = this.smoothingFactor * this.dataArray[i] + (1 - this.smoothingFactor) * this.smoothedDataArray[i];
104
+ var x = startX + i * xStep;
105
+ var y = this.yPosition + this.smoothedDataArray[i] * this.height;
106
+ // Set top and bottom vertices for the ribbon
107
+ var vertexIndex = i * 2 * 3;
108
+ positions[vertexIndex] = x;
109
+ positions[vertexIndex + 1] = y + halfThickness;
110
+ positions[vertexIndex + 2] = 2;
111
+ positions[vertexIndex + 3] = x;
112
+ positions[vertexIndex + 4] = y - halfThickness;
113
+ positions[vertexIndex + 5] = 2;
114
+ // Set UVs
115
+ var uvIndex = i * 2 * 2;
116
+ uvs[uvIndex] = i / (this.bufferLength - 1); // U coordinate
117
+ uvs[uvIndex + 1] = 1.0; // V for top vertex
118
+ uvs[uvIndex + 2] = i / (this.bufferLength - 1); // U coordinate
119
+ uvs[uvIndex + 3] = 0.0; // V for bottom vertex
120
+ }
121
+ this.mesh.geometry.attributes.position.needsUpdate = true;
122
+ this.mesh.geometry.attributes.uv.needsUpdate = true;
123
+ this.mesh.geometry.computeBoundingSphere();
124
+ }
125
+ },
126
+ {
127
+ key: "updateColor",
128
+ value: function updateColor(newColor) {
129
+ if (this.uniforms) {
130
+ this.targetColor.set(newColor);
131
+ }
132
+ }
133
+ },
134
+ {
135
+ // Call this on window resize
136
+ key: "updatePosition",
137
+ value: function updatePosition(canvasWidth, canvasHeight) {
138
+ this.width = canvasWidth * 0.8;
139
+ this.yPosition = -canvasHeight / 2 + 250; // Position it higher, above the drum beat indicators
140
+ }
141
+ },
142
+ {
143
+ // Clean up Three.js resources
144
+ key: "dispose",
145
+ value: function dispose() {
146
+ if (this.mesh) {
147
+ this.scene.remove(this.mesh);
148
+ if (this.mesh.geometry) this.mesh.geometry.dispose();
149
+ if (this.mesh.material) this.mesh.material.dispose();
150
+ }
151
+ }
152
+ }
153
+ ]);
154
+ return WaveformVisualizer;
155
+ }();
assets/clap.wav ADDED
Binary file (93.3 kB). View file
 
assets/demo.png ADDED

Git LFS Details

  • SHA256: a87d6ba857ed9ebc414fd4ff6ca445f7d234379672571661acc57e4f55dba881
  • Pointer size: 132 Bytes
  • Size of remote file: 2.42 MB
assets/hihat.wav ADDED
Binary file (35.6 kB). View file
 
assets/kick.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0f6aca362d354edda241a23ebd0721b3a6d3a75009a983920b9bd6ff5d551065
3
+ size 344364
assets/siteOGImage.webp ADDED

Git LFS Details

  • SHA256: 37381d0b03a61163ea089e8701b42d39d3a5cc7d756173a1ef9a8cecac6fad0a
  • Pointer size: 131 Bytes
  • Size of remote file: 154 kB
assets/snare.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8972ccc2d9cc63130ca95e4c55819dc7b27041ac6ac35bb8f362ba2c25e5a4c6
3
+ size 176478
game.js ADDED
@@ -0,0 +1,1381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function _array_like_to_array(arr, len) {
2
+ if (len == null || len > arr.length) len = arr.length;
3
+ for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
4
+ return arr2;
5
+ }
6
+ function _array_with_holes(arr) {
7
+ if (Array.isArray(arr)) return arr;
8
+ }
9
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
10
+ try {
11
+ var info = gen[key](arg);
12
+ var value = info.value;
13
+ } catch (error) {
14
+ reject(error);
15
+ return;
16
+ }
17
+ if (info.done) {
18
+ resolve(value);
19
+ } else {
20
+ Promise.resolve(value).then(_next, _throw);
21
+ }
22
+ }
23
+ function _async_to_generator(fn) {
24
+ return function() {
25
+ var self = this, args = arguments;
26
+ return new Promise(function(resolve, reject) {
27
+ var gen = fn.apply(self, args);
28
+ function _next(value) {
29
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
30
+ }
31
+ function _throw(err) {
32
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
33
+ }
34
+ _next(undefined);
35
+ });
36
+ };
37
+ }
38
+ function _class_call_check(instance, Constructor) {
39
+ if (!(instance instanceof Constructor)) {
40
+ throw new TypeError("Cannot call a class as a function");
41
+ }
42
+ }
43
+ function _defineProperties(target, props) {
44
+ for(var i = 0; i < props.length; i++){
45
+ var descriptor = props[i];
46
+ descriptor.enumerable = descriptor.enumerable || false;
47
+ descriptor.configurable = true;
48
+ if ("value" in descriptor) descriptor.writable = true;
49
+ Object.defineProperty(target, descriptor.key, descriptor);
50
+ }
51
+ }
52
+ function _create_class(Constructor, protoProps, staticProps) {
53
+ if (protoProps) _defineProperties(Constructor.prototype, protoProps);
54
+ if (staticProps) _defineProperties(Constructor, staticProps);
55
+ return Constructor;
56
+ }
57
+ function _define_property(obj, key, value) {
58
+ if (key in obj) {
59
+ Object.defineProperty(obj, key, {
60
+ value: value,
61
+ enumerable: true,
62
+ configurable: true,
63
+ writable: true
64
+ });
65
+ } else {
66
+ obj[key] = value;
67
+ }
68
+ return obj;
69
+ }
70
+ function _iterable_to_array_limit(arr, i) {
71
+ var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"];
72
+ if (_i == null) return;
73
+ var _arr = [];
74
+ var _n = true;
75
+ var _d = false;
76
+ var _s, _e;
77
+ try {
78
+ for(_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true){
79
+ _arr.push(_s.value);
80
+ if (i && _arr.length === i) break;
81
+ }
82
+ } catch (err) {
83
+ _d = true;
84
+ _e = err;
85
+ } finally{
86
+ try {
87
+ if (!_n && _i["return"] != null) _i["return"]();
88
+ } finally{
89
+ if (_d) throw _e;
90
+ }
91
+ }
92
+ return _arr;
93
+ }
94
+ function _non_iterable_rest() {
95
+ throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
96
+ }
97
+ function _object_spread(target) {
98
+ for(var i = 1; i < arguments.length; i++){
99
+ var source = arguments[i] != null ? arguments[i] : {};
100
+ var ownKeys = Object.keys(source);
101
+ if (typeof Object.getOwnPropertySymbols === "function") {
102
+ ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
103
+ return Object.getOwnPropertyDescriptor(source, sym).enumerable;
104
+ }));
105
+ }
106
+ ownKeys.forEach(function(key) {
107
+ _define_property(target, key, source[key]);
108
+ });
109
+ }
110
+ return target;
111
+ }
112
+ function _sliced_to_array(arr, i) {
113
+ return _array_with_holes(arr) || _iterable_to_array_limit(arr, i) || _unsupported_iterable_to_array(arr, i) || _non_iterable_rest();
114
+ }
115
+ function _unsupported_iterable_to_array(o, minLen) {
116
+ if (!o) return;
117
+ if (typeof o === "string") return _array_like_to_array(o, minLen);
118
+ var n = Object.prototype.toString.call(o).slice(8, -1);
119
+ if (n === "Object" && o.constructor) n = o.constructor.name;
120
+ if (n === "Map" || n === "Set") return Array.from(n);
121
+ if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array(o, minLen);
122
+ }
123
+ function _ts_generator(thisArg, body) {
124
+ var f, y, t, g, _ = {
125
+ label: 0,
126
+ sent: function() {
127
+ if (t[0] & 1) throw t[1];
128
+ return t[1];
129
+ },
130
+ trys: [],
131
+ ops: []
132
+ };
133
+ return g = {
134
+ next: verb(0),
135
+ "throw": verb(1),
136
+ "return": verb(2)
137
+ }, typeof Symbol === "function" && (g[Symbol.iterator] = function() {
138
+ return this;
139
+ }), g;
140
+ function verb(n) {
141
+ return function(v) {
142
+ return step([
143
+ n,
144
+ v
145
+ ]);
146
+ };
147
+ }
148
+ function step(op) {
149
+ if (f) throw new TypeError("Generator is already executing.");
150
+ while(_)try {
151
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
152
+ if (y = 0, t) op = [
153
+ op[0] & 2,
154
+ t.value
155
+ ];
156
+ switch(op[0]){
157
+ case 0:
158
+ case 1:
159
+ t = op;
160
+ break;
161
+ case 4:
162
+ _.label++;
163
+ return {
164
+ value: op[1],
165
+ done: false
166
+ };
167
+ case 5:
168
+ _.label++;
169
+ y = op[1];
170
+ op = [
171
+ 0
172
+ ];
173
+ continue;
174
+ case 7:
175
+ op = _.ops.pop();
176
+ _.trys.pop();
177
+ continue;
178
+ default:
179
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
180
+ _ = 0;
181
+ continue;
182
+ }
183
+ if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {
184
+ _.label = op[1];
185
+ break;
186
+ }
187
+ if (op[0] === 6 && _.label < t[1]) {
188
+ _.label = t[1];
189
+ t = op;
190
+ break;
191
+ }
192
+ if (t && _.label < t[2]) {
193
+ _.label = t[2];
194
+ _.ops.push(op);
195
+ break;
196
+ }
197
+ if (t[2]) _.ops.pop();
198
+ _.trys.pop();
199
+ continue;
200
+ }
201
+ op = body.call(thisArg, _);
202
+ } catch (e) {
203
+ op = [
204
+ 6,
205
+ e
206
+ ];
207
+ y = 0;
208
+ } finally{
209
+ f = t = 0;
210
+ }
211
+ if (op[0] & 5) throw op[1];
212
+ return {
213
+ value: op[0] ? op[1] : void 0,
214
+ done: true
215
+ };
216
+ }
217
+ }
218
+ import * as THREE from 'three';
219
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; // Import GLTFLoader
220
+ import { HandLandmarker, FilesetResolver } from 'https://esm.sh/@mediapipe/[email protected]';
221
+ import { MusicManager } from './MusicManager.js'; // Import the MusicManager
222
+ import * as Tone from 'https://esm.sh/tone'; // Import Tone to access Transport
223
+ import * as drumManager from './DrumManager.js'; // Import the new drum manager module
224
+ import { WaveformVisualizer } from './WaveformVisualizer.js'; // Import the new waveform visualizer
225
+ export var Game = /*#__PURE__*/ function() {
226
+ "use strict";
227
+ function Game(renderDiv) {
228
+ var _this = this;
229
+ _class_call_check(this, Game);
230
+ this.renderDiv = renderDiv;
231
+ this.scene = null;
232
+ this.camera = null;
233
+ this.renderer = null;
234
+ this.videoElement = null;
235
+ this.handLandmarker = null;
236
+ this.lastVideoTime = -1;
237
+ this.hands = []; // Stores data about detected hands (landmarks, anchor position, line group)
238
+ this.handLineMaterial = null; // Material for hand lines
239
+ this.fingertipMaterialHand1 = null; // Material for first hand's fingertip circles (blue)
240
+ this.fingertipMaterialHand2 = null; // Material for second hand's fingertip circles (green)
241
+ this.fingertipLandmarkIndices = [
242
+ 0,
243
+ 4,
244
+ 8,
245
+ 12,
246
+ 16,
247
+ 20
248
+ ]; // WRIST + TIP landmarks
249
+ this.handConnections = null; // Landmark connection definitions
250
+ // this.handCollisionRadius = 30; // Conceptual radius for hand collision, was 25 (sphere radius) - Not needed for template
251
+ this.gameState = 'loading'; // loading, ready, tracking, error
252
+ this.gameOverText = null; // Will be repurposed or simplified
253
+ this.clock = new THREE.Clock();
254
+ this.musicManager = new MusicManager(); // Create an instance of MusicManager
255
+ this.waveformVisualizer = null; // To be initialized
256
+ // this.drumManager = new DrumManager(); // DrumManager is now a static module, no instance needed
257
+ this.lastLandmarkPositions = [
258
+ [],
259
+ []
260
+ ]; // Store last known smoothed positions for each hand's landmarks
261
+ this.smoothingFactor = 0.4; // Alpha for exponential smoothing (0 < alpha <= 1). Smaller = more smoothing.
262
+ this.loadedModels = {}; // To store loaded models if any (e.g. a generic hand model in future)
263
+ this.beatIndicators = []; // Array to hold the 16 beat indicator meshes
264
+ this.beatIndicatorMaterials = []; // Array to hold the base material for each indicator
265
+ this.beatIndicatorColors = {
266
+ kick: new THREE.Color("#D72828"),
267
+ snare: new THREE.Color("#F36E2F"),
268
+ clap: new THREE.Color("#7B4394"),
269
+ hihat: new THREE.Color("#84C34E"),
270
+ off: new THREE.Color("#ffffff") // Off state remains white
271
+ };
272
+ this.beatIndicatorGroup = null; // Group to hold all indicators for easy repositioning
273
+ this.labelColors = {
274
+ evaPurple: {
275
+ r: 123,
276
+ g: 67,
277
+ b: 148,
278
+ a: 0.9
279
+ },
280
+ evaGreen: {
281
+ r: 132,
282
+ g: 195,
283
+ b: 78,
284
+ a: 0.9
285
+ },
286
+ evaOrange: {
287
+ r: 243,
288
+ g: 110,
289
+ b: 47,
290
+ a: 0.9
291
+ },
292
+ evaRed: {
293
+ r: 215,
294
+ g: 40,
295
+ b: 40,
296
+ a: 0.9
297
+ },
298
+ white: {
299
+ r: 255,
300
+ g: 255,
301
+ b: 255,
302
+ a: 1.0
303
+ },
304
+ black: {
305
+ r: 0,
306
+ g: 0,
307
+ b: 0,
308
+ a: 1.0
309
+ }
310
+ };
311
+ this.waveformColors = [
312
+ new THREE.Color("#7B4394"),
313
+ new THREE.Color("#84C34E"),
314
+ new THREE.Color("#F36E2F"),
315
+ new THREE.Color("#D72828"),
316
+ new THREE.Color("#66ffff")
317
+ ];
318
+ // Initialize asynchronously
319
+ this._init().catch(function(error) {
320
+ console.error("Initialization failed:", error);
321
+ _this._showError("Initialization failed. Check console.");
322
+ });
323
+ }
324
+ _create_class(Game, [
325
+ {
326
+ key: "_init",
327
+ value: function _init() {
328
+ var _this = this;
329
+ return _async_to_generator(function() {
330
+ return _ts_generator(this, function(_state) {
331
+ switch(_state.label){
332
+ case 0:
333
+ _this._setupDOM(); // Sets up basic DOM, including speech bubble container
334
+ _this._setupThree();
335
+ return [
336
+ 4,
337
+ _this._loadAssets()
338
+ ];
339
+ case 1:
340
+ _state.sent(); // Add asset loading step
341
+ return [
342
+ 4,
343
+ _this._setupHandTracking()
344
+ ];
345
+ case 2:
346
+ _state.sent(); // This needs to complete before we can proceed
347
+ // Ensure webcam is playing before starting game logic dependent on it
348
+ return [
349
+ 4,
350
+ _this.videoElement.play()
351
+ ];
352
+ case 3:
353
+ _state.sent();
354
+ window.addEventListener('resize', _this._onResize.bind(_this));
355
+ _this._startGame(); // Start the game directly
356
+ _this._setupEventListeners(); // Set up interaction listeners
357
+ _this._animate(); // Start the animation loop (it will check state)
358
+ return [
359
+ 2
360
+ ];
361
+ }
362
+ });
363
+ })();
364
+ }
365
+ },
366
+ {
367
+ key: "_setupDOM",
368
+ value: function _setupDOM() {
369
+ this.renderDiv.style.position = 'relative';
370
+ this.renderDiv.style.width = '100vw'; // Use viewport units for fullscreen
371
+ this.renderDiv.style.height = '100vh';
372
+ this.renderDiv.style.overflow = 'hidden';
373
+ this.renderDiv.style.background = '#111'; // Fallback background
374
+ this.videoElement = document.createElement('video');
375
+ this.videoElement.style.position = 'absolute';
376
+ this.videoElement.style.top = '0';
377
+ this.videoElement.style.left = '0';
378
+ this.videoElement.style.width = '100%';
379
+ this.videoElement.style.height = '100%';
380
+ this.videoElement.style.objectFit = 'cover';
381
+ this.videoElement.style.transform = 'scaleX(-1)'; // Mirror view for intuitive control
382
+ this.videoElement.style.filter = 'grayscale(100%)'; // Make it black and white
383
+ this.videoElement.autoplay = true;
384
+ this.videoElement.muted = true; // Mute video to avoid feedback loops if audio was captured
385
+ this.videoElement.playsInline = true;
386
+ this.videoElement.style.zIndex = '0'; // Ensure video is behind THREE canvas
387
+ this.renderDiv.appendChild(this.videoElement);
388
+ // Container for Status text (formerly Game Over) and restart hint
389
+ this.gameOverContainer = document.createElement('div');
390
+ this.gameOverContainer.style.position = 'absolute';
391
+ this.gameOverContainer.style.top = '50%';
392
+ this.gameOverContainer.style.left = '50%';
393
+ this.gameOverContainer.style.transform = 'translate(-50%, -50%)';
394
+ this.gameOverContainer.style.zIndex = '10';
395
+ this.gameOverContainer.style.display = 'none'; // Hidden initially
396
+ this.gameOverContainer.style.pointerEvents = 'none'; // Don't block clicks
397
+ this.gameOverContainer.style.textAlign = 'center'; // Center text elements within
398
+ this.gameOverContainer.style.color = 'white'; // Default color, can be changed by _showError
399
+ this.gameOverContainer.style.textShadow = '2px 2px 4px black';
400
+ this.gameOverContainer.style.fontFamily = '"Arial Black", Gadget, sans-serif';
401
+ // Main Status Text (formerly Game Over Text)
402
+ this.gameOverText = document.createElement('div'); // Will be 'gameOverText' internally
403
+ this.gameOverText.innerText = 'STATUS'; // Generic placeholder
404
+ this.gameOverText.style.fontSize = 'clamp(36px, 10vw, 72px)'; // Responsive font size
405
+ this.gameOverText.style.fontWeight = 'bold';
406
+ this.gameOverText.style.marginBottom = '10px'; // Space below main text
407
+ this.gameOverContainer.appendChild(this.gameOverText);
408
+ // Restart Hint Text (may or may not be shown depending on context)
409
+ this.restartHintText = document.createElement('div');
410
+ this.restartHintText.innerText = '(click to restart tracking)';
411
+ this.restartHintText.style.fontSize = 'clamp(16px, 3vw, 24px)';
412
+ this.restartHintText.style.fontWeight = 'normal';
413
+ this.restartHintText.style.opacity = '0.8'; // Slightly faded
414
+ this.gameOverContainer.appendChild(this.restartHintText);
415
+ this.renderDiv.appendChild(this.gameOverContainer);
416
+ // ScoreDisplay removed
417
+ // Watermelon (Center Emoji Marker) setup removed
418
+ // Chad Image Marker setup removed
419
+ }
420
+ },
421
+ {
422
+ key: "_setupThree",
423
+ value: function _setupThree() {
424
+ var width = this.renderDiv.clientWidth;
425
+ var height = this.renderDiv.clientHeight;
426
+ this.scene = new THREE.Scene();
427
+ // Using OrthographicCamera for a 2D-like overlay effect
428
+ this.camera = new THREE.OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 1, 1000);
429
+ this.camera.position.z = 100; // Position along Z doesn't change scale in Ortho
430
+ this.renderer = new THREE.WebGLRenderer({
431
+ alpha: true,
432
+ antialias: true
433
+ });
434
+ this.renderer.setSize(width, height);
435
+ this.renderer.setPixelRatio(window.devicePixelRatio);
436
+ this.renderer.domElement.style.position = 'absolute';
437
+ this.renderer.domElement.style.top = '0';
438
+ this.renderer.domElement.style.left = '0';
439
+ this.renderer.domElement.style.zIndex = '1'; // Canvas on top of video
440
+ this.renderDiv.appendChild(this.renderer.domElement);
441
+ var ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
442
+ this.scene.add(ambientLight);
443
+ var directionalLight = new THREE.DirectionalLight(0xffffff, 0.9);
444
+ directionalLight.position.set(0, 0, 100); // Pointing from behind camera
445
+ this.scene.add(directionalLight);
446
+ // Setup hand visualization (palm circles removed, lines will be added later)
447
+ for(var i = 0; i < 2; i++){
448
+ var lineGroup = new THREE.Group();
449
+ lineGroup.visible = false;
450
+ this.scene.add(lineGroup);
451
+ this.hands.push({
452
+ landmarks: null,
453
+ anchorPos: new THREE.Vector3(),
454
+ lineGroup: lineGroup,
455
+ isFist: false // Track if the hand is currently in a fist
456
+ });
457
+ }
458
+ this.handLineMaterial = new THREE.LineBasicMaterial({
459
+ color: 0x00ccff,
460
+ linewidth: 8
461
+ });
462
+ this.fingertipMaterialHand1 = new THREE.MeshBasicMaterial({
463
+ color: 0xffffff,
464
+ side: THREE.DoubleSide
465
+ }); // White
466
+ this.fingertipMaterialHand2 = new THREE.MeshBasicMaterial({
467
+ color: 0xffffff,
468
+ side: THREE.DoubleSide
469
+ }); // White
470
+ // Define connections for MediaPipe hand landmarks
471
+ // See: https://developers.google.com/mediapipe/solutions/vision/hand_landmarker#hand_landmarks
472
+ this.handConnections = [
473
+ // Thumb
474
+ [
475
+ 0,
476
+ 1
477
+ ],
478
+ [
479
+ 1,
480
+ 2
481
+ ],
482
+ [
483
+ 2,
484
+ 3
485
+ ],
486
+ [
487
+ 3,
488
+ 4
489
+ ],
490
+ // Index finger
491
+ [
492
+ 0,
493
+ 5
494
+ ],
495
+ [
496
+ 5,
497
+ 6
498
+ ],
499
+ [
500
+ 6,
501
+ 7
502
+ ],
503
+ [
504
+ 7,
505
+ 8
506
+ ],
507
+ // Middle finger
508
+ [
509
+ 0,
510
+ 9
511
+ ],
512
+ [
513
+ 9,
514
+ 10
515
+ ],
516
+ [
517
+ 10,
518
+ 11
519
+ ],
520
+ [
521
+ 11,
522
+ 12
523
+ ],
524
+ // Ring finger
525
+ [
526
+ 0,
527
+ 13
528
+ ],
529
+ [
530
+ 13,
531
+ 14
532
+ ],
533
+ [
534
+ 14,
535
+ 15
536
+ ],
537
+ [
538
+ 15,
539
+ 16
540
+ ],
541
+ // Pinky
542
+ [
543
+ 0,
544
+ 17
545
+ ],
546
+ [
547
+ 17,
548
+ 18
549
+ ],
550
+ [
551
+ 18,
552
+ 19
553
+ ],
554
+ [
555
+ 19,
556
+ 20
557
+ ],
558
+ // Palm
559
+ [
560
+ 5,
561
+ 9
562
+ ],
563
+ [
564
+ 9,
565
+ 13
566
+ ],
567
+ [
568
+ 13,
569
+ 17
570
+ ] // Connect base of fingers
571
+ ];
572
+ // Particle resources removed
573
+ // Ground line removed
574
+ // --- Beat Indicator ---
575
+ this.beatIndicatorGroup = new THREE.Group();
576
+ this.scene.add(this.beatIndicatorGroup);
577
+ this._setupBeatIndicatorMaterials(); // Create materials based on drum pattern
578
+ var indicatorSize = 20;
579
+ var indicatorGeometry = new THREE.PlaneGeometry(indicatorSize, indicatorSize);
580
+ for(var i1 = 0; i1 < 16; i1++){
581
+ // Use the pre-calculated material for this beat index
582
+ var indicator = new THREE.Mesh(indicatorGeometry, this.beatIndicatorMaterials[i1]);
583
+ this.beatIndicatorGroup.add(indicator);
584
+ this.beatIndicators.push(indicator);
585
+ }
586
+ this._positionBeatIndicators(); // Position them right after creation
587
+ }
588
+ },
589
+ {
590
+ key: "_loadAssets",
591
+ value: function _loadAssets() {
592
+ var _this = this;
593
+ return _async_to_generator(function() {
594
+ var error;
595
+ return _ts_generator(this, function(_state) {
596
+ switch(_state.label){
597
+ case 0:
598
+ console.log("Loading assets...");
599
+ _state.label = 1;
600
+ case 1:
601
+ _state.trys.push([
602
+ 1,
603
+ 3,
604
+ ,
605
+ 4
606
+ ]);
607
+ // Ghost Textures loading removed
608
+ // Ghost GLTF Model loading removed (was already commented out)
609
+ return [
610
+ 4,
611
+ drumManager.loadSamples()
612
+ ];
613
+ case 2:
614
+ _state.sent(); // Load drum sounds
615
+ console.log("No game-specific assets to load for template.");
616
+ return [
617
+ 3,
618
+ 4
619
+ ];
620
+ case 3:
621
+ error = _state.sent();
622
+ console.error("Error loading assets:", error);
623
+ _this._showError("Failed to load assets."); // Generic message
624
+ throw error; // Stop initialization
625
+ case 4:
626
+ return [
627
+ 2
628
+ ];
629
+ }
630
+ });
631
+ })();
632
+ }
633
+ },
634
+ {
635
+ key: "_setupHandTracking",
636
+ value: function _setupHandTracking() {
637
+ var _this = this;
638
+ return _async_to_generator(function() {
639
+ var vision, stream, error;
640
+ return _ts_generator(this, function(_state) {
641
+ switch(_state.label){
642
+ case 0:
643
+ _state.trys.push([
644
+ 0,
645
+ 4,
646
+ ,
647
+ 5
648
+ ]);
649
+ console.log("Setting up Hand Tracking...");
650
+ return [
651
+ 4,
652
+ FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/wasm')
653
+ ];
654
+ case 1:
655
+ vision = _state.sent();
656
+ return [
657
+ 4,
658
+ HandLandmarker.createFromOptions(vision, {
659
+ baseOptions: {
660
+ modelAssetPath: "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task",
661
+ delegate: 'GPU'
662
+ },
663
+ numHands: 2,
664
+ runningMode: 'VIDEO'
665
+ })
666
+ ];
667
+ case 2:
668
+ _this.handLandmarker = _state.sent();
669
+ console.log("HandLandmarker created.");
670
+ console.log("Requesting webcam access...");
671
+ return [
672
+ 4,
673
+ navigator.mediaDevices.getUserMedia({
674
+ video: {
675
+ facingMode: 'user',
676
+ width: {
677
+ ideal: 1280
678
+ },
679
+ height: {
680
+ ideal: 720
681
+ }
682
+ },
683
+ audio: false
684
+ })
685
+ ];
686
+ case 3:
687
+ stream = _state.sent();
688
+ _this.videoElement.srcObject = stream;
689
+ console.log("Webcam stream obtained.");
690
+ // Wait for video metadata to load to ensure dimensions are available
691
+ return [
692
+ 2,
693
+ new Promise(function(resolve) {
694
+ _this.videoElement.onloadedmetadata = function() {
695
+ console.log("Webcam metadata loaded.");
696
+ // Adjust video size slightly after metadata is loaded if needed, but CSS handles most
697
+ _this.videoElement.style.width = _this.renderDiv.clientWidth + 'px';
698
+ _this.videoElement.style.height = _this.renderDiv.clientHeight + 'px';
699
+ resolve();
700
+ };
701
+ })
702
+ ];
703
+ case 4:
704
+ error = _state.sent();
705
+ console.error('Error setting up Hand Tracking or Webcam:', error);
706
+ _this._showError("Webcam/Hand Tracking Error: ".concat(error.message, ". Please allow camera access."));
707
+ throw error; // Re-throw to stop initialization
708
+ case 5:
709
+ return [
710
+ 2
711
+ ];
712
+ }
713
+ });
714
+ })();
715
+ }
716
+ },
717
+ {
718
+ // _startSpawning, _scheduleNextSpawn, _stopSpawning, _spawnGhost methods removed.
719
+ key: "_updateHands",
720
+ value: function _updateHands() {
721
+ var _this = this;
722
+ if (!this.handLandmarker || !this.videoElement.srcObject || this.videoElement.readyState < 2 || this.videoElement.videoWidth === 0) return;
723
+ var videoTime = this.videoElement.currentTime;
724
+ if (videoTime > this.lastVideoTime) {
725
+ this.lastVideoTime = videoTime;
726
+ try {
727
+ var _this1, _loop = function(i) {
728
+ var hand = _this1.hands[i];
729
+ var wasVisible = hand.landmarks !== null;
730
+ if (results.landmarks && results.landmarks[i]) {
731
+ var currentRawLandmarks = results.landmarks[i];
732
+ if (!_this1.lastLandmarkPositions[i] || _this1.lastLandmarkPositions[i].length !== currentRawLandmarks.length) {
733
+ _this1.lastLandmarkPositions[i] = currentRawLandmarks.map(function(lm) {
734
+ return _object_spread({}, lm);
735
+ });
736
+ }
737
+ var smoothedLandmarks = currentRawLandmarks.map(function(lm, lmIndex) {
738
+ var prevLm = _this.lastLandmarkPositions[i][lmIndex];
739
+ return {
740
+ x: _this.smoothingFactor * lm.x + (1 - _this.smoothingFactor) * prevLm.x,
741
+ y: _this.smoothingFactor * lm.y + (1 - _this.smoothingFactor) * prevLm.y,
742
+ z: _this.smoothingFactor * lm.z + (1 - _this.smoothingFactor) * prevLm.z
743
+ };
744
+ });
745
+ _this1.lastLandmarkPositions[i] = smoothedLandmarks.map(function(lm) {
746
+ return _object_spread({}, lm);
747
+ });
748
+ hand.landmarks = smoothedLandmarks;
749
+ var palm = smoothedLandmarks[9]; // MIDDLE_FINGER_MCP
750
+ var lmOriginalX = palm.x * videoParams.videoNaturalWidth;
751
+ var lmOriginalY = palm.y * videoParams.videoNaturalHeight;
752
+ var normX_visible = (lmOriginalX - videoParams.offsetX) / videoParams.visibleWidth;
753
+ var normY_visible = (lmOriginalY - videoParams.offsetY) / videoParams.visibleHeight;
754
+ var handX = (1 - normX_visible) * canvasWidth - canvasWidth / 2;
755
+ var handY = (1 - normY_visible) * canvasHeight - canvasHeight / 2;
756
+ hand.anchorPos.set(handX, handY, 1);
757
+ if (i === 0) {
758
+ // --- Music & Gesture Control ---
759
+ var isFistNow = _this1._isFist(smoothedLandmarks);
760
+ if (isFistNow && !hand.isFist) {
761
+ // Fist gesture was just made
762
+ _this1.musicManager.cycleSynth();
763
+ _this1.musicManager.stopArpeggio(i); // Stop any old arpeggio
764
+ }
765
+ hand.isFist = isFistNow;
766
+ var noteIndex = Math.floor((1 - normY_visible) * scale.length);
767
+ var note = scale[Math.max(0, Math.min(scale.length - 1, noteIndex))];
768
+ if (_this1.waveformVisualizer) {
769
+ var colorIndex = noteIndex % _this1.waveformColors.length;
770
+ var newColor = _this1.waveformColors[colorIndex];
771
+ _this1.waveformVisualizer.updateColor(newColor);
772
+ }
773
+ var thumbTip = smoothedLandmarks[4];
774
+ var indexTip = smoothedLandmarks[8];
775
+ var dx = thumbTip.x - indexTip.x;
776
+ var dy = thumbTip.y - indexTip.y;
777
+ var distance = Math.sqrt(dx * dx + dy * dy);
778
+ var velocity = Math.max(0, Math.min(1.0, distance * 5));
779
+ _this1._updateHandLines(i, smoothedLandmarks, videoParams, canvasWidth, canvasHeight, {
780
+ note: note,
781
+ velocity: velocity,
782
+ isFist: isFistNow
783
+ });
784
+ if (!isFistNow) {
785
+ // Start/Restart arpeggio if the hand just appeared OR if it just opened from a fist.
786
+ var arpeggioIsActive = _this1.musicManager.activePatterns.has(i);
787
+ if (!wasVisible || !arpeggioIsActive) {
788
+ _this1.musicManager.startArpeggio(i, note);
789
+ } else {
790
+ _this1.musicManager.updateArpeggio(i, note);
791
+ }
792
+ _this1.musicManager.updateArpeggioVolume(i, velocity);
793
+ } else {
794
+ // If it is a fist, make sure the arpeggio is stopped
795
+ _this1.musicManager.stopArpeggio(i);
796
+ }
797
+ } else if (i === 1) {
798
+ var fingerStates = _this1._getFingerStates(smoothedLandmarks);
799
+ drumManager.updateActiveDrums(fingerStates);
800
+ _this1._updateHandLines(i, smoothedLandmarks, videoParams, canvasWidth, canvasHeight, {
801
+ fingerStates: fingerStates
802
+ });
803
+ }
804
+ hand.lineGroup.visible = true;
805
+ } else {
806
+ if (wasVisible) {
807
+ if (i === 0) {
808
+ _this1.musicManager.stopArpeggio(i);
809
+ } else if (i === 1) {
810
+ // Disable all drums when hand is gone
811
+ drumManager.updateActiveDrums({});
812
+ }
813
+ }
814
+ hand.landmarks = null;
815
+ if (hand.lineGroup) hand.lineGroup.visible = false;
816
+ }
817
+ };
818
+ var results = this.handLandmarker.detectForVideo(this.videoElement, performance.now());
819
+ var videoParams = this._getVisibleVideoParameters();
820
+ if (!videoParams) return;
821
+ var canvasWidth = this.renderDiv.clientWidth;
822
+ var canvasHeight = this.renderDiv.clientHeight;
823
+ // C Minor Pentatonic Scale
824
+ var scale = [
825
+ 'C3',
826
+ 'Eb3',
827
+ 'F3',
828
+ 'G3',
829
+ 'Bb3',
830
+ 'C4',
831
+ 'Eb4',
832
+ 'F4',
833
+ 'G4',
834
+ 'Bb4',
835
+ 'C5',
836
+ 'Eb5'
837
+ ];
838
+ for(var i = 0; i < this.hands.length; i++)_this1 = this, _loop(i);
839
+ } catch (error) {
840
+ console.error("Error during hand detection:", error);
841
+ }
842
+ }
843
+ }
844
+ },
845
+ {
846
+ key: "_getVisibleVideoParameters",
847
+ value: function _getVisibleVideoParameters() {
848
+ if (!this.videoElement || this.videoElement.videoWidth === 0 || this.videoElement.videoHeight === 0) {
849
+ return null;
850
+ }
851
+ var vNatW = this.videoElement.videoWidth;
852
+ var vNatH = this.videoElement.videoHeight;
853
+ var rW = this.renderDiv.clientWidth;
854
+ var rH = this.renderDiv.clientHeight;
855
+ if (vNatW === 0 || vNatH === 0 || rW === 0 || rH === 0) return null;
856
+ var videoAR = vNatW / vNatH;
857
+ var renderDivAR = rW / rH;
858
+ var finalVideoPixelX, finalVideoPixelY;
859
+ var visibleVideoPixelWidth, visibleVideoPixelHeight;
860
+ if (videoAR > renderDivAR) {
861
+ // Video is wider than renderDiv, scaled to fit renderDiv height, cropped horizontally.
862
+ var scale = rH / vNatH; // Scale factor based on height.
863
+ var scaledVideoWidth = vNatW * scale; // Width of video if scaled to fit renderDiv height.
864
+ // Total original video pixels cropped horizontally (from both sides combined).
865
+ var totalCroppedPixelsX = (scaledVideoWidth - rW) / scale;
866
+ finalVideoPixelX = totalCroppedPixelsX / 2; // Pixels cropped from the left of original video.
867
+ finalVideoPixelY = 0; // No vertical cropping.
868
+ visibleVideoPixelWidth = vNatW - totalCroppedPixelsX; // Width of the visible part in original video pixels.
869
+ visibleVideoPixelHeight = vNatH; // Full height is visible.
870
+ } else {
871
+ // Video is taller than renderDiv (or same AR), scaled to fit renderDiv width, cropped vertically.
872
+ var scale1 = rW / vNatW; // Scale factor based on width.
873
+ var scaledVideoHeight = vNatH * scale1; // Height of video if scaled to fit renderDiv width.
874
+ // Total original video pixels cropped vertically (from top and bottom combined).
875
+ var totalCroppedPixelsY = (scaledVideoHeight - rH) / scale1;
876
+ finalVideoPixelX = 0; // No horizontal cropping.
877
+ finalVideoPixelY = totalCroppedPixelsY / 2; // Pixels cropped from the top of original video.
878
+ visibleVideoPixelWidth = vNatW; // Full width is visible.
879
+ visibleVideoPixelHeight = vNatH - totalCroppedPixelsY; // Height of the visible part in original video pixels.
880
+ }
881
+ // Safety check for degenerate cases (e.g., extreme aspect ratios leading to zero visible dimension)
882
+ if (visibleVideoPixelWidth <= 0 || visibleVideoPixelHeight <= 0) {
883
+ // Fallback or log error, this shouldn't happen in normal scenarios
884
+ console.warn("Calculated visible video dimension is zero or negative.", {
885
+ visibleVideoPixelWidth: visibleVideoPixelWidth,
886
+ visibleVideoPixelHeight: visibleVideoPixelHeight
887
+ });
888
+ return {
889
+ offsetX: 0,
890
+ offsetY: 0,
891
+ visibleWidth: vNatW,
892
+ visibleHeight: vNatH,
893
+ videoNaturalWidth: vNatW,
894
+ videoNaturalHeight: vNatH
895
+ };
896
+ }
897
+ return {
898
+ offsetX: finalVideoPixelX,
899
+ offsetY: finalVideoPixelY,
900
+ visibleWidth: visibleVideoPixelWidth,
901
+ visibleHeight: visibleVideoPixelHeight,
902
+ videoNaturalWidth: vNatW,
903
+ videoNaturalHeight: vNatH
904
+ };
905
+ }
906
+ },
907
+ {
908
+ // _updateGhosts method removed.
909
+ key: "_showStatusScreen",
910
+ value: function _showStatusScreen(message) {
911
+ var color = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 'white', showRestartHint = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : false;
912
+ this.gameOverContainer.style.display = 'block';
913
+ this.gameOverText.innerText = message;
914
+ this.gameOverText.style.color = color;
915
+ this.restartHintText.style.display = showRestartHint ? 'block' : 'none';
916
+ // No spawning to stop for template
917
+ }
918
+ },
919
+ {
920
+ key: "_showError",
921
+ value: function _showError(message) {
922
+ this.gameOverContainer.style.display = 'block';
923
+ this.gameOverText.innerText = "ERROR: ".concat(message);
924
+ this.gameOverText.style.color = 'orange';
925
+ this.restartHintText.style.display = 'true'; // Show restart hint on error
926
+ this.gameState = 'error';
927
+ // No spawning to stop
928
+ this.hands.forEach(function(hand) {
929
+ if (hand.lineGroup) hand.lineGroup.visible = false;
930
+ });
931
+ // if (this.startButton) this.startButton.style.display = 'none'; // No longer exists
932
+ }
933
+ },
934
+ {
935
+ key: "_startGame",
936
+ value: function _startGame() {
937
+ var _this = this;
938
+ console.log("Starting tracking...");
939
+ // This is now called automatically, so no need to check gameState
940
+ this.musicManager.start().then(function() {
941
+ drumManager.startSequence(); // Start drums *after* audio context is ready.
942
+ // Setup the waveform visualizer after the music manager is ready
943
+ var analyser = _this.musicManager.getAnalyser();
944
+ if (analyser) {
945
+ _this.waveformVisualizer = new WaveformVisualizer(_this.scene, analyser, _this.renderDiv.clientWidth, _this.renderDiv.clientHeight);
946
+ }
947
+ });
948
+ this.gameState = 'tracking'; // Changed from 'playing'
949
+ this.lastVideoTime = -1;
950
+ this.clock.start();
951
+ // Removed display of score, castle, chad
952
+ // Removed _startSpawning()
953
+ }
954
+ },
955
+ {
956
+ key: "_restartGame",
957
+ value: function _restartGame() {
958
+ console.log("Restarting tracking...");
959
+ this.gameOverContainer.style.display = 'none';
960
+ this.hands.forEach(function(hand) {
961
+ if (hand.lineGroup) {
962
+ hand.lineGroup.visible = false;
963
+ }
964
+ });
965
+ // Ghost removal removed
966
+ // Score reset removed
967
+ // Visibility of game elements removed
968
+ this.gameState = 'tracking'; // Changed from 'playing'
969
+ this.lastVideoTime = -1;
970
+ this.clock.start();
971
+ // Removed _startSpawning()
972
+ }
973
+ },
974
+ {
975
+ // _updateScoreDisplay method removed.
976
+ key: "_onResize",
977
+ value: function _onResize() {
978
+ var width = this.renderDiv.clientWidth;
979
+ var height = this.renderDiv.clientHeight;
980
+ // Update camera perspective
981
+ this.camera.left = width / -2;
982
+ this.camera.right = width / 2;
983
+ this.camera.top = height / 2;
984
+ this.camera.bottom = height / -2;
985
+ this.camera.updateProjectionMatrix();
986
+ // Update renderer size
987
+ this.renderer.setSize(width, height);
988
+ // Update video element size
989
+ this.videoElement.style.width = width + 'px';
990
+ this.videoElement.style.height = height + 'px';
991
+ // Watermelon, Chad, GroundLine updates removed.
992
+ this._positionBeatIndicators();
993
+ if (this.waveformVisualizer) {
994
+ this.waveformVisualizer.updatePosition(width, height);
995
+ }
996
+ }
997
+ },
998
+ {
999
+ key: "_positionBeatIndicators",
1000
+ value: function _positionBeatIndicators() {
1001
+ var width = this.renderDiv.clientWidth;
1002
+ var height = this.renderDiv.clientHeight;
1003
+ var totalWidth = width * 0.8; // Occupy 80% of screen width to match the waveform
1004
+ var spacing = totalWidth / 16;
1005
+ var startX = -totalWidth / 2 + spacing / 2;
1006
+ var yPos = -height / 2 + 150; // Positioned a bit higher from the bottom
1007
+ this.beatIndicators.forEach(function(indicator, i) {
1008
+ indicator.position.set(startX + i * spacing, yPos, 1);
1009
+ });
1010
+ }
1011
+ },
1012
+ {
1013
+ key: "_setupBeatIndicatorMaterials",
1014
+ value: function _setupBeatIndicatorMaterials() {
1015
+ // All indicators start as 'off' (white)
1016
+ for(var i = 0; i < 16; i++){
1017
+ // We just need one material definition now and will copy it.
1018
+ this.beatIndicatorMaterials[i] = new THREE.MeshBasicMaterial({
1019
+ color: this.beatIndicatorColors.off,
1020
+ transparent: true,
1021
+ opacity: 0.5
1022
+ });
1023
+ }
1024
+ }
1025
+ },
1026
+ {
1027
+ key: "_createTextSprite",
1028
+ value: function _createTextSprite(message, parameters) {
1029
+ parameters = parameters || {};
1030
+ var fontface = parameters.fontface || 'Arial';
1031
+ var fontsize = parameters.fontsize || 24;
1032
+ // borderColor is no longer needed
1033
+ var backgroundColor = parameters.backgroundColor || {
1034
+ r: 255,
1035
+ g: 255,
1036
+ b: 255,
1037
+ a: 0.8
1038
+ };
1039
+ var textColor = parameters.textColor || {
1040
+ r: 0,
1041
+ g: 0,
1042
+ b: 0,
1043
+ a: 1.0
1044
+ };
1045
+ var canvas = document.createElement('canvas');
1046
+ var context = canvas.getContext('2d');
1047
+ context.font = "Bold ".concat(fontsize, "px ").concat(fontface);
1048
+ // get size data (height depends only on font size)
1049
+ var metrics = context.measureText(message);
1050
+ var textWidth = metrics.width;
1051
+ var padding = 10;
1052
+ var canvasWidth = textWidth + padding * 2;
1053
+ var canvasHeight = fontsize * 1.4 + padding;
1054
+ canvas.width = canvasWidth;
1055
+ canvas.height = canvasHeight;
1056
+ // Font needs to be re-applied after resizing canvas
1057
+ context.font = "Bold ".concat(fontsize, "px ").concat(fontface);
1058
+ // background color
1059
+ context.fillStyle = "rgba(".concat(backgroundColor.r, ",").concat(backgroundColor.g, ",").concat(backgroundColor.b, ",").concat(backgroundColor.a, ")");
1060
+ context.fillRect(0, 0, canvasWidth, canvasHeight);
1061
+ // text color and position
1062
+ context.fillStyle = "rgba(".concat(textColor.r, ", ").concat(textColor.g, ", ").concat(textColor.b, ", 1.0)");
1063
+ context.textAlign = 'center';
1064
+ context.textBaseline = 'middle';
1065
+ context.fillText(message, canvasWidth / 2, canvasHeight / 2);
1066
+ // canvas contents will be used for a texture
1067
+ var texture = new THREE.CanvasTexture(canvas);
1068
+ texture.needsUpdate = true;
1069
+ var spriteMaterial = new THREE.SpriteMaterial({
1070
+ map: texture
1071
+ });
1072
+ var sprite = new THREE.Sprite(spriteMaterial);
1073
+ sprite.scale.set(canvas.width, canvas.height, 1.0);
1074
+ return sprite;
1075
+ }
1076
+ },
1077
+ {
1078
+ key: "_getFingerStates",
1079
+ value: function _getFingerStates(landmarks) {
1080
+ // Landmark indices for fingertips
1081
+ var fingertips = {
1082
+ index: 8,
1083
+ middle: 12,
1084
+ ring: 16,
1085
+ pinky: 20
1086
+ };
1087
+ // Stricter check using the joint below the tip (PIP joint) to avoid false positives.
1088
+ var fingerJointsBelowTip = {
1089
+ index: 6,
1090
+ middle: 10,
1091
+ ring: 14,
1092
+ pinky: 18
1093
+ };
1094
+ var states = {};
1095
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
1096
+ try {
1097
+ for(var _iterator = Object.entries(fingertips)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
1098
+ var _step_value = _sliced_to_array(_step.value, 2), finger = _step_value[0], tipIndex = _step_value[1];
1099
+ var jointIndex = fingerJointsBelowTip[finger];
1100
+ if (landmarks[tipIndex] && landmarks[jointIndex]) {
1101
+ // A finger is "up" if its tip is higher than the joint just below it.
1102
+ states[finger] = landmarks[tipIndex].y < landmarks[jointIndex].y;
1103
+ } else {
1104
+ states[finger] = false;
1105
+ }
1106
+ }
1107
+ } catch (err) {
1108
+ _didIteratorError = true;
1109
+ _iteratorError = err;
1110
+ } finally{
1111
+ try {
1112
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
1113
+ _iterator.return();
1114
+ }
1115
+ } finally{
1116
+ if (_didIteratorError) {
1117
+ throw _iteratorError;
1118
+ }
1119
+ }
1120
+ }
1121
+ return states;
1122
+ }
1123
+ },
1124
+ {
1125
+ key: "_isFist",
1126
+ value: function _isFist(landmarks) {
1127
+ if (!landmarks || landmarks.length < 21) return false;
1128
+ // Use the middle finger's MCP joint as a proxy for the palm center
1129
+ var palmCenter = landmarks[9];
1130
+ var fingertipsIndices = [
1131
+ 4,
1132
+ 8,
1133
+ 12,
1134
+ 16,
1135
+ 20
1136
+ ]; // Thumb, Index, Middle, Ring, Pinky
1137
+ // Threshold for normalized landmark distance. If fingertips are further than this from palm, it's not a fist.
1138
+ // This value may need tuning. A smaller value makes the fist detection stricter.
1139
+ var fistThreshold = 0.1;
1140
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
1141
+ try {
1142
+ for(var _iterator = fingertipsIndices[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
1143
+ var tipIndex = _step.value;
1144
+ var tip = landmarks[tipIndex];
1145
+ var dx = tip.x - palmCenter.x;
1146
+ var dy = tip.y - palmCenter.y;
1147
+ var distance = Math.sqrt(dx * dx + dy * dy);
1148
+ if (distance > fistThreshold) {
1149
+ return false; // At least one finger is open
1150
+ }
1151
+ }
1152
+ } catch (err) {
1153
+ _didIteratorError = true;
1154
+ _iteratorError = err;
1155
+ } finally{
1156
+ try {
1157
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
1158
+ _iterator.return();
1159
+ }
1160
+ } finally{
1161
+ if (_didIteratorError) {
1162
+ throw _iteratorError;
1163
+ }
1164
+ }
1165
+ }
1166
+ return true; // All fingertips are close to the palm
1167
+ }
1168
+ },
1169
+ {
1170
+ key: "_updateHandLines",
1171
+ value: function _updateHandLines(handIndex, landmarks, videoParams, canvasWidth, canvasHeight, controlData) {
1172
+ var _this = this;
1173
+ var hand = this.hands[handIndex];
1174
+ var lineGroup = hand.lineGroup;
1175
+ // Clean up previous frame's objects
1176
+ while(lineGroup.children.length){
1177
+ var child = lineGroup.children[0];
1178
+ lineGroup.remove(child);
1179
+ if (child.geometry) child.geometry.dispose();
1180
+ if (child.material) {
1181
+ // For sprites, we need to dispose the texture map as well
1182
+ if (child.material.map) child.material.map.dispose();
1183
+ child.material.dispose();
1184
+ }
1185
+ }
1186
+ if (!landmarks || landmarks.length === 0 || !videoParams) {
1187
+ lineGroup.visible = false;
1188
+ return;
1189
+ }
1190
+ var points3D = landmarks.map(function(lm) {
1191
+ var lmOriginalX = lm.x * videoParams.videoNaturalWidth;
1192
+ var lmOriginalY = lm.y * videoParams.videoNaturalHeight;
1193
+ var normX_visible = (lmOriginalX - videoParams.offsetX) / videoParams.visibleWidth;
1194
+ var normY_visible = (lmOriginalY - videoParams.offsetY) / videoParams.visibleHeight;
1195
+ normX_visible = Math.max(0, Math.min(1, normX_visible));
1196
+ normY_visible = Math.max(0, Math.min(1, normY_visible));
1197
+ var x = (1 - normX_visible) * canvasWidth - canvasWidth / 2;
1198
+ var y = (1 - normY_visible) * canvasHeight - canvasHeight / 2;
1199
+ return new THREE.Vector3(x, y, 1.1); // Z for fingertip circles
1200
+ });
1201
+ // --- Draw Skeleton Lines ---
1202
+ var lineZ = 1;
1203
+ this.handConnections.forEach(function(conn) {
1204
+ var p1 = points3D[conn[0]];
1205
+ var p2 = points3D[conn[1]];
1206
+ if (p1 && p2) {
1207
+ var lineP1 = p1.clone().setZ(lineZ);
1208
+ var lineP2 = p2.clone().setZ(lineZ);
1209
+ var geometry = new THREE.BufferGeometry().setFromPoints([
1210
+ lineP1,
1211
+ lineP2
1212
+ ]);
1213
+ var line = new THREE.Line(geometry, _this.handLineMaterial);
1214
+ lineGroup.add(line);
1215
+ }
1216
+ });
1217
+ // --- Draw Fingertip & Wrist Circles ---
1218
+ var fingertipRadius = 8, wristRadius = 12, circleSegments = 16;
1219
+ this.fingertipLandmarkIndices.forEach(function(index) {
1220
+ var landmarkPosition = points3D[index];
1221
+ if (landmarkPosition) {
1222
+ var radius = index === 0 ? wristRadius : fingertipRadius;
1223
+ var circleGeometry = new THREE.CircleGeometry(radius, circleSegments);
1224
+ var material = handIndex === 0 ? _this.fingertipMaterialHand1 : _this.fingertipMaterialHand2;
1225
+ var landmarkCircle = new THREE.Mesh(circleGeometry, material);
1226
+ landmarkCircle.position.copy(landmarkPosition);
1227
+ lineGroup.add(landmarkCircle);
1228
+ }
1229
+ });
1230
+ // --- Draw Thumb-to-Index line and Labels ---
1231
+ var thumbPos = points3D[4];
1232
+ var indexPos = points3D[8];
1233
+ var wristPos = points3D[0];
1234
+ if (wristPos) {
1235
+ // Labels depend on which hand it is
1236
+ if (handIndex === 0 && thumbPos && indexPos) {
1237
+ // Connecting line
1238
+ var lineGeom = new THREE.BufferGeometry().setFromPoints([
1239
+ thumbPos,
1240
+ indexPos
1241
+ ]);
1242
+ var line = new THREE.Line(lineGeom, new THREE.LineBasicMaterial({
1243
+ color: 0xffffff,
1244
+ linewidth: 3
1245
+ }));
1246
+ lineGroup.add(line);
1247
+ // Volume and Pitch labels
1248
+ var note = controlData.note, velocity = controlData.velocity, isFist = controlData.isFist;
1249
+ if (isFist) {
1250
+ var fistLabel = this._createTextSprite("SYNTH ".concat(this.musicManager.currentSynthIndex + 1), {
1251
+ fontsize: 22,
1252
+ backgroundColor: this.labelColors.evaPurple,
1253
+ textColor: this.labelColors.evaGreen
1254
+ });
1255
+ fistLabel.position.set(wristPos.x, wristPos.y + 60, 2);
1256
+ lineGroup.add(fistLabel);
1257
+ } else {
1258
+ var midPoint = new THREE.Vector3().lerpVectors(thumbPos, indexPos, 0.5);
1259
+ var volumeLabel = this._createTextSprite("Volume: ".concat(velocity.toFixed(2)), {
1260
+ fontsize: 18,
1261
+ backgroundColor: this.labelColors.evaOrange,
1262
+ textColor: this.labelColors.white
1263
+ });
1264
+ volumeLabel.position.set(midPoint.x, midPoint.y, 2);
1265
+ lineGroup.add(volumeLabel);
1266
+ var pitchLabel = this._createTextSprite("Pitch: ".concat(note), {
1267
+ fontsize: 18,
1268
+ backgroundColor: this.labelColors.evaGreen,
1269
+ textColor: this.labelColors.black
1270
+ });
1271
+ pitchLabel.position.set(wristPos.x, wristPos.y + 60, 2); // Position above the wrist
1272
+ lineGroup.add(pitchLabel);
1273
+ }
1274
+ } else if (handIndex === 1) {
1275
+ var fingerStates = controlData.fingerStates;
1276
+ var activeDrums = Object.entries(fingerStates).filter(function(param) {
1277
+ var _param = _sliced_to_array(param, 2), _ = _param[0], isUp = _param[1];
1278
+ return isUp;
1279
+ }).map(function(param) {
1280
+ var _param = _sliced_to_array(param, 2), finger = _param[0], _ = _param[1];
1281
+ return drumManager.getFingerToDrumMap()[finger];
1282
+ }).join(', ');
1283
+ var drumLabel = this._createTextSprite("Drums: ".concat(activeDrums || 'None'), {
1284
+ fontsize: 18,
1285
+ backgroundColor: this.labelColors.evaRed,
1286
+ textColor: this.labelColors.white
1287
+ });
1288
+ drumLabel.position.set(wristPos.x, wristPos.y + 60, 2);
1289
+ lineGroup.add(drumLabel);
1290
+ }
1291
+ }
1292
+ lineGroup.visible = true;
1293
+ }
1294
+ },
1295
+ {
1296
+ key: "_animate",
1297
+ value: function _animate() {
1298
+ requestAnimationFrame(this._animate.bind(this));
1299
+ if (this.gameState === 'tracking') {
1300
+ var deltaTime = this.clock.getDelta();
1301
+ this._updateHands();
1302
+ this._updateBeatIndicator();
1303
+ if (this.waveformVisualizer) {
1304
+ this.waveformVisualizer.update();
1305
+ }
1306
+ }
1307
+ this.renderer.render(this.scene, this.camera);
1308
+ }
1309
+ },
1310
+ {
1311
+ key: "_updateBeatIndicator",
1312
+ value: function _updateBeatIndicator() {
1313
+ var _this = this;
1314
+ var currentBeat = drumManager.getCurrentBeat();
1315
+ var progress = Tone.Transport.progress;
1316
+ var beatProgress = progress * 16 % 1;
1317
+ var pulse = 1.5 + 0.5 * Math.cos(beatProgress * Math.PI * 2);
1318
+ var activeDrums = drumManager.getActiveDrums();
1319
+ var drumPattern = drumManager.getDrumPattern();
1320
+ var drumPriority = [
1321
+ 'kick',
1322
+ 'snare',
1323
+ 'clap',
1324
+ 'hihat'
1325
+ ];
1326
+ this.beatIndicators.forEach(function(indicator, i) {
1327
+ // Determine the color for this step based on active drums
1328
+ var stepColor = _this.beatIndicatorColors.off;
1329
+ var isHit = false;
1330
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
1331
+ try {
1332
+ for(var _iterator = drumPriority[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
1333
+ var drum = _step.value;
1334
+ if (activeDrums.has(drum) && drumPattern[drum][i]) {
1335
+ stepColor = _this.beatIndicatorColors[drum];
1336
+ isHit = true;
1337
+ break;
1338
+ }
1339
+ }
1340
+ } catch (err) {
1341
+ _didIteratorError = true;
1342
+ _iteratorError = err;
1343
+ } finally{
1344
+ try {
1345
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
1346
+ _iterator.return();
1347
+ }
1348
+ } finally{
1349
+ if (_didIteratorError) {
1350
+ throw _iteratorError;
1351
+ }
1352
+ }
1353
+ }
1354
+ indicator.material.color.set(stepColor);
1355
+ indicator.material.opacity = isHit ? 0.9 : 0.5;
1356
+ // Apply pulse only to the current beat marker
1357
+ if (i === currentBeat) {
1358
+ indicator.scale.set(pulse, pulse, 1);
1359
+ } else {
1360
+ indicator.scale.set(1, 1, 1);
1361
+ }
1362
+ });
1363
+ }
1364
+ },
1365
+ {
1366
+ key: "_setupEventListeners",
1367
+ value: function _setupEventListeners() {
1368
+ var _this = this;
1369
+ // Add click listener for resuming audio context and potentially restarting on error
1370
+ this.renderDiv.addEventListener('click', function() {
1371
+ _this.musicManager.start(); // Resume audio context on any click
1372
+ if (_this.gameState === 'error') {
1373
+ _this._restartGame();
1374
+ }
1375
+ });
1376
+ console.log('Game event listeners set up.');
1377
+ }
1378
+ }
1379
+ ]);
1380
+ return Game;
1381
+ }();
index.html CHANGED
@@ -1,19 +1,57 @@
1
- <!doctype html>
2
  <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
  <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Arpeggiator</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+
9
+ <!-- Primary Meta Tags -->
10
+ <meta name="title" content="Arpeggiator">
11
+ <meta name="description" content="hand-controlled arp, drum machine, real-time visualizer">
12
+
13
+ <!-- Open Graph / Facebook -->
14
+ <meta property="og:type" content="website">
15
+ <meta property="og:url" content="https://collidingscopes.github.io/arpeggiator/">
16
+ <meta property="og:title" content="Arpeggiator">
17
+ <meta property="og:description" content="hand-controlled arp, drum machine, real-time visualizer">
18
+ <meta property="og:image" content="https://raw.githubusercontent.com/collidingScopes/arpeggiator/main/assets/siteOGImage.webp">
19
+
20
+ <!-- Twitter -->
21
+ <meta property="twitter:card" content="summary_large_image">
22
+ <meta property="twitter:url" content="https://collidingscopes.github.io/arpeggiator/">
23
+ <meta property="twitter:title" content="Arpeggiator">
24
+ <meta property="twitter:description" content="hand-controlled arp, drum machine, real-time visualizer">
25
+ <meta property="twitter:image" content="https://raw.githubusercontent.com/collidingScopes/arpeggiator/main/assets/siteOGImage.webp">
26
+
27
+ <script defer src="https://cloud.umami.is/script.js" data-website-id="eb59c81c-27cb-4e1d-9e8c-bfbe70c48cd9"></script>
28
+ <script type="importmap">
29
+ {
30
+ "imports": {
31
+ "three": "https://esm.sh/[email protected]?dev",
32
+ "three/": "https://esm.sh/[email protected]&dev/"
33
+ }
34
+ }
35
+ </script>
36
+ </head>
37
+ <body style="width: 100%; height: 100%; overflow: hidden; margin: 0;">
38
+ <div id="renderDiv" style="width: 100%; height: 100%; margin: 0;">
39
+
40
+ <span id="info-text">raise your hands to raise the roof</span>
41
+ <div id="social-links" class="text-box">
42
+ <a href="https://www.x.com/measure_plan/" target="_blank">> Twitter</a><br>
43
+ <a href="https://www.instagram.com/stereo.drift/" target="_blank">> Instagram</a><br>
44
+ <a href="https://youtube.com/@funwithcomputervision" target="_blank">> Youtube</a>
45
+ </div>
46
+ <div id="coffee-link" class="text-box">
47
+ <a href="https://buymeacoffee.com/stereodrift" target="_blank">Buy me a coffee 💛</a>
48
+ </div>
49
+ <div id="logo-container" class="text-box">
50
+ <span id="logo">🪬</span><br>
51
+ <a href="https://funwithcomputervision.com/" target="_blank">code & tutorials here</a>
52
+ </div>
53
+
54
+ </div>
55
+ <script type="module" src="main.js"></script>
56
+ </body>
57
+ </html>
main.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Game } from './game.js';
2
+ // Get the render target div
3
+ var renderDiv = document.getElementById('renderDiv');
4
+ // Check if renderDiv exists
5
+ if (!renderDiv) {
6
+ console.error('Fatal Error: renderDiv element not found.');
7
+ } else {
8
+ // Initialize the game with the render target
9
+ var game = new Game(renderDiv);
10
+ // The game now initializes and starts automatically from its constructor.
11
+ }
styles.css ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .text-box {
2
+ padding: 8px 15px;
3
+ background-color: rgba(255, 255, 255, 0.6);
4
+ color: black;
5
+ border-radius: 4px;
6
+ font-family: "Helvetica Neue", Helvetica, sans-serif;
7
+ border: 2px solid rgb(53, 47, 108);
8
+ box-shadow: 3px 3px 0px rgb(23, 17, 77);
9
+ font-size: clamp(13px, 2vw, 15px);
10
+ text-align: center;
11
+ z-index: 200;
12
+ opacity: 1;
13
+ transition: opacity 0.3s ease-in-out, bottom 0.3s ease-in-out, box-shadow 0.2s ease;
14
+ }
15
+
16
+ #info-text {
17
+ position: absolute;
18
+ top: 10px;
19
+ left: 50%;
20
+ transform: translateX(-50%);
21
+ padding: 8px 15px;
22
+ background-color: rgba(255, 255, 255, 0.6);
23
+ color: black;
24
+ border-radius: 4px;
25
+ font-family: "Helvetica Neue", Helvetica, sans-serif;
26
+ border: 2px solid rgb(53, 47, 108);
27
+ box-shadow: 3px 3px 0px rgb(23, 17, 77);
28
+ font-size: clamp(20px, 4vw, 30px);
29
+ text-align: center;
30
+ z-index: 200;
31
+ }
32
+
33
+ #instruction-text {
34
+ position: absolute;
35
+ bottom: 10px;
36
+ left: 50%;
37
+ transform: translateX(-50%);
38
+ pointer-events: none;
39
+ }
40
+
41
+ #social-links {
42
+ position: absolute;
43
+ bottom: 10px;
44
+ left: 10px;
45
+ text-align: left;
46
+ }
47
+
48
+ #coffee-link {
49
+ position: absolute;
50
+ bottom: 10px;
51
+ right: 10px;
52
+ }
53
+
54
+ #video-link {
55
+ position: absolute;
56
+ top: 10px;
57
+ left: 10px;
58
+ }
59
+
60
+ #logo-container {
61
+ position: absolute;
62
+ top: 10px;
63
+ right: 10px;
64
+ }
65
+
66
+ #logo {
67
+ font-size: 2em;
68
+ }