Spaces:
Running
Running
| function _array_like_to_array(arr, len) { | |
| if (len == null || len > arr.length) len = arr.length; | |
| for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i]; | |
| return arr2; | |
| } | |
| function _array_with_holes(arr) { | |
| if (Array.isArray(arr)) return arr; | |
| } | |
| function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { | |
| try { | |
| var info = gen[key](arg); | |
| var value = info.value; | |
| } catch (error) { | |
| reject(error); | |
| return; | |
| } | |
| if (info.done) { | |
| resolve(value); | |
| } else { | |
| Promise.resolve(value).then(_next, _throw); | |
| } | |
| } | |
| function _async_to_generator(fn) { | |
| return function() { | |
| var self = this, args = arguments; | |
| return new Promise(function(resolve, reject) { | |
| var gen = fn.apply(self, args); | |
| function _next(value) { | |
| asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); | |
| } | |
| function _throw(err) { | |
| asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); | |
| } | |
| _next(undefined); | |
| }); | |
| }; | |
| } | |
| function _class_call_check(instance, Constructor) { | |
| if (!(instance instanceof Constructor)) { | |
| throw new TypeError("Cannot call a class as a function"); | |
| } | |
| } | |
| function _defineProperties(target, props) { | |
| for(var i = 0; i < props.length; i++){ | |
| var descriptor = props[i]; | |
| descriptor.enumerable = descriptor.enumerable || false; | |
| descriptor.configurable = true; | |
| if ("value" in descriptor) descriptor.writable = true; | |
| Object.defineProperty(target, descriptor.key, descriptor); | |
| } | |
| } | |
| function _create_class(Constructor, protoProps, staticProps) { | |
| if (protoProps) _defineProperties(Constructor.prototype, protoProps); | |
| if (staticProps) _defineProperties(Constructor, staticProps); | |
| return Constructor; | |
| } | |
| function _define_property(obj, key, value) { | |
| if (key in obj) { | |
| Object.defineProperty(obj, key, { | |
| value: value, | |
| enumerable: true, | |
| configurable: true, | |
| writable: true | |
| }); | |
| } else { | |
| obj[key] = value; | |
| } | |
| return obj; | |
| } | |
| function _iterable_to_array_limit(arr, i) { | |
| var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; | |
| if (_i == null) return; | |
| var _arr = []; | |
| var _n = true; | |
| var _d = false; | |
| var _s, _e; | |
| try { | |
| for(_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true){ | |
| _arr.push(_s.value); | |
| if (i && _arr.length === i) break; | |
| } | |
| } catch (err) { | |
| _d = true; | |
| _e = err; | |
| } finally{ | |
| try { | |
| if (!_n && _i["return"] != null) _i["return"](); | |
| } finally{ | |
| if (_d) throw _e; | |
| } | |
| } | |
| return _arr; | |
| } | |
| function _non_iterable_rest() { | |
| throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); | |
| } | |
| function _object_spread(target) { | |
| for(var i = 1; i < arguments.length; i++){ | |
| var source = arguments[i] != null ? arguments[i] : {}; | |
| var ownKeys = Object.keys(source); | |
| if (typeof Object.getOwnPropertySymbols === "function") { | |
| ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) { | |
| return Object.getOwnPropertyDescriptor(source, sym).enumerable; | |
| })); | |
| } | |
| ownKeys.forEach(function(key) { | |
| _define_property(target, key, source[key]); | |
| }); | |
| } | |
| return target; | |
| } | |
| function _sliced_to_array(arr, i) { | |
| return _array_with_holes(arr) || _iterable_to_array_limit(arr, i) || _unsupported_iterable_to_array(arr, i) || _non_iterable_rest(); | |
| } | |
| function _unsupported_iterable_to_array(o, minLen) { | |
| if (!o) return; | |
| if (typeof o === "string") return _array_like_to_array(o, minLen); | |
| var n = Object.prototype.toString.call(o).slice(8, -1); | |
| if (n === "Object" && o.constructor) n = o.constructor.name; | |
| if (n === "Map" || n === "Set") return Array.from(n); | |
| if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array(o, minLen); | |
| } | |
| function _ts_generator(thisArg, body) { | |
| var f, y, t, g, _ = { | |
| label: 0, | |
| sent: function() { | |
| if (t[0] & 1) throw t[1]; | |
| return t[1]; | |
| }, | |
| trys: [], | |
| ops: [] | |
| }; | |
| return g = { | |
| next: verb(0), | |
| "throw": verb(1), | |
| "return": verb(2) | |
| }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { | |
| return this; | |
| }), g; | |
| function verb(n) { | |
| return function(v) { | |
| return step([ | |
| n, | |
| v | |
| ]); | |
| }; | |
| } | |
| function step(op) { | |
| if (f) throw new TypeError("Generator is already executing."); | |
| while(_)try { | |
| 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; | |
| if (y = 0, t) op = [ | |
| op[0] & 2, | |
| t.value | |
| ]; | |
| switch(op[0]){ | |
| case 0: | |
| case 1: | |
| t = op; | |
| break; | |
| case 4: | |
| _.label++; | |
| return { | |
| value: op[1], | |
| done: false | |
| }; | |
| case 5: | |
| _.label++; | |
| y = op[1]; | |
| op = [ | |
| 0 | |
| ]; | |
| continue; | |
| case 7: | |
| op = _.ops.pop(); | |
| _.trys.pop(); | |
| continue; | |
| default: | |
| if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { | |
| _ = 0; | |
| continue; | |
| } | |
| if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) { | |
| _.label = op[1]; | |
| break; | |
| } | |
| if (op[0] === 6 && _.label < t[1]) { | |
| _.label = t[1]; | |
| t = op; | |
| break; | |
| } | |
| if (t && _.label < t[2]) { | |
| _.label = t[2]; | |
| _.ops.push(op); | |
| break; | |
| } | |
| if (t[2]) _.ops.pop(); | |
| _.trys.pop(); | |
| continue; | |
| } | |
| op = body.call(thisArg, _); | |
| } catch (e) { | |
| op = [ | |
| 6, | |
| e | |
| ]; | |
| y = 0; | |
| } finally{ | |
| f = t = 0; | |
| } | |
| if (op[0] & 5) throw op[1]; | |
| return { | |
| value: op[0] ? op[1] : void 0, | |
| done: true | |
| }; | |
| } | |
| } | |
| import * as THREE from 'three'; | |
| import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; // Import GLTFLoader | |
| import { HandLandmarker, FilesetResolver } from 'https://esm.sh/@mediapipe/[email protected]'; | |
| import { MusicManager } from './MusicManager.js'; // Import the MusicManager | |
| import * as Tone from 'https://esm.sh/tone'; // Import Tone to access Transport | |
| import * as drumManager from './DrumManager.js'; // Import the new drum manager module | |
| import { WaveformVisualizer } from './WaveformVisualizer.js'; // Import the new waveform visualizer | |
| export var Game = /*#__PURE__*/ function() { | |
| "use strict"; | |
| function Game(renderDiv) { | |
| var _this = this; | |
| _class_call_check(this, Game); | |
| this.renderDiv = renderDiv; | |
| this.scene = null; | |
| this.camera = null; | |
| this.renderer = null; | |
| this.videoElement = null; | |
| this.handLandmarker = null; | |
| this.lastVideoTime = -1; | |
| this.hands = []; // Stores data about detected hands (landmarks, anchor position, line group) | |
| this.handLineMaterial = null; // Material for hand lines | |
| this.fingertipMaterialHand1 = null; // Material for first hand's fingertip circles (blue) | |
| this.fingertipMaterialHand2 = null; // Material for second hand's fingertip circles (green) | |
| this.fingertipLandmarkIndices = [ | |
| 0, | |
| 4, | |
| 8, | |
| 12, | |
| 16, | |
| 20 | |
| ]; // WRIST + TIP landmarks | |
| this.handConnections = null; // Landmark connection definitions | |
| // this.handCollisionRadius = 30; // Conceptual radius for hand collision, was 25 (sphere radius) - Not needed for template | |
| this.gameState = 'loading'; // loading, ready, tracking, error | |
| this.gameOverText = null; // Will be repurposed or simplified | |
| this.clock = new THREE.Clock(); | |
| this.musicManager = new MusicManager(); // Create an instance of MusicManager | |
| this.waveformVisualizer = null; // To be initialized | |
| // this.drumManager = new DrumManager(); // DrumManager is now a static module, no instance needed | |
| this.lastLandmarkPositions = [ | |
| [], | |
| [] | |
| ]; // Store last known smoothed positions for each hand's landmarks | |
| this.smoothingFactor = 0.4; // Alpha for exponential smoothing (0 < alpha <= 1). Smaller = more smoothing. | |
| this.loadedModels = {}; // To store loaded models if any (e.g. a generic hand model in future) | |
| this.beatIndicators = []; // Array to hold the 16 beat indicator meshes | |
| this.beatIndicatorMaterials = []; // Array to hold the base material for each indicator | |
| this.beatIndicatorColors = { | |
| kick: new THREE.Color("#D72828"), | |
| snare: new THREE.Color("#F36E2F"), | |
| clap: new THREE.Color("#7B4394"), | |
| hihat: new THREE.Color("#84C34E"), | |
| off: new THREE.Color("#ffffff") // Off state remains white | |
| }; | |
| this.beatIndicatorGroup = null; // Group to hold all indicators for easy repositioning | |
| this.labelColors = { | |
| evaPurple: { | |
| r: 123, | |
| g: 67, | |
| b: 148, | |
| a: 0.9 | |
| }, | |
| evaGreen: { | |
| r: 132, | |
| g: 195, | |
| b: 78, | |
| a: 0.9 | |
| }, | |
| evaOrange: { | |
| r: 243, | |
| g: 110, | |
| b: 47, | |
| a: 0.9 | |
| }, | |
| evaRed: { | |
| r: 215, | |
| g: 40, | |
| b: 40, | |
| a: 0.9 | |
| }, | |
| white: { | |
| r: 255, | |
| g: 255, | |
| b: 255, | |
| a: 1.0 | |
| }, | |
| black: { | |
| r: 0, | |
| g: 0, | |
| b: 0, | |
| a: 1.0 | |
| } | |
| }; | |
| this.waveformColors = [ | |
| new THREE.Color("#7B4394"), | |
| new THREE.Color("#84C34E"), | |
| new THREE.Color("#F36E2F"), | |
| new THREE.Color("#D72828"), | |
| new THREE.Color("#66ffff") | |
| ]; | |
| // Initialize asynchronously | |
| this._init().catch(function(error) { | |
| console.error("Initialization failed:", error); | |
| _this._showError("Initialization failed. Check console."); | |
| }); | |
| } | |
| _create_class(Game, [ | |
| { | |
| key: "_init", | |
| value: function _init() { | |
| var _this = this; | |
| return _async_to_generator(function() { | |
| return _ts_generator(this, function(_state) { | |
| switch(_state.label){ | |
| case 0: | |
| _this._setupDOM(); // Sets up basic DOM, including speech bubble container | |
| _this._setupThree(); | |
| return [ | |
| 4, | |
| _this._loadAssets() | |
| ]; | |
| case 1: | |
| _state.sent(); // Add asset loading step | |
| return [ | |
| 4, | |
| _this._setupHandTracking() | |
| ]; | |
| case 2: | |
| _state.sent(); // This needs to complete before we can proceed | |
| // Ensure webcam is playing before starting game logic dependent on it | |
| return [ | |
| 4, | |
| _this.videoElement.play() | |
| ]; | |
| case 3: | |
| _state.sent(); | |
| window.addEventListener('resize', _this._onResize.bind(_this)); | |
| _this._startGame(); // Start the game directly | |
| _this._setupEventListeners(); // Set up interaction listeners | |
| _this._animate(); // Start the animation loop (it will check state) | |
| return [ | |
| 2 | |
| ]; | |
| } | |
| }); | |
| })(); | |
| } | |
| }, | |
| { | |
| key: "_setupDOM", | |
| value: function _setupDOM() { | |
| this.renderDiv.style.position = 'relative'; | |
| this.renderDiv.style.width = '100vw'; // Use viewport units for fullscreen | |
| this.renderDiv.style.height = '100vh'; | |
| this.renderDiv.style.overflow = 'hidden'; | |
| this.renderDiv.style.background = '#111'; // Fallback background | |
| this.videoElement = document.createElement('video'); | |
| this.videoElement.style.position = 'absolute'; | |
| this.videoElement.style.top = '0'; | |
| this.videoElement.style.left = '0'; | |
| this.videoElement.style.width = '100%'; | |
| this.videoElement.style.height = '100%'; | |
| this.videoElement.style.objectFit = 'cover'; | |
| this.videoElement.style.transform = 'scaleX(-1)'; // Mirror view for intuitive control | |
| this.videoElement.style.filter = 'grayscale(100%)'; // Make it black and white | |
| this.videoElement.autoplay = true; | |
| this.videoElement.muted = true; // Mute video to avoid feedback loops if audio was captured | |
| this.videoElement.playsInline = true; | |
| this.videoElement.style.zIndex = '0'; // Ensure video is behind THREE canvas | |
| this.renderDiv.appendChild(this.videoElement); | |
| // Container for Status text (formerly Game Over) and restart hint | |
| this.gameOverContainer = document.createElement('div'); | |
| this.gameOverContainer.style.position = 'absolute'; | |
| this.gameOverContainer.style.top = '50%'; | |
| this.gameOverContainer.style.left = '50%'; | |
| this.gameOverContainer.style.transform = 'translate(-50%, -50%)'; | |
| this.gameOverContainer.style.zIndex = '10'; | |
| this.gameOverContainer.style.display = 'none'; // Hidden initially | |
| this.gameOverContainer.style.pointerEvents = 'none'; // Don't block clicks | |
| this.gameOverContainer.style.textAlign = 'center'; // Center text elements within | |
| this.gameOverContainer.style.color = 'white'; // Default color, can be changed by _showError | |
| this.gameOverContainer.style.textShadow = '2px 2px 4px black'; | |
| this.gameOverContainer.style.fontFamily = '"Arial Black", Gadget, sans-serif'; | |
| // Main Status Text (formerly Game Over Text) | |
| this.gameOverText = document.createElement('div'); // Will be 'gameOverText' internally | |
| this.gameOverText.innerText = 'STATUS'; // Generic placeholder | |
| this.gameOverText.style.fontSize = 'clamp(36px, 10vw, 72px)'; // Responsive font size | |
| this.gameOverText.style.fontWeight = 'bold'; | |
| this.gameOverText.style.marginBottom = '10px'; // Space below main text | |
| this.gameOverContainer.appendChild(this.gameOverText); | |
| // Restart Hint Text (may or may not be shown depending on context) | |
| this.restartHintText = document.createElement('div'); | |
| this.restartHintText.innerText = '(click to restart tracking)'; | |
| this.restartHintText.style.fontSize = 'clamp(16px, 3vw, 24px)'; | |
| this.restartHintText.style.fontWeight = 'normal'; | |
| this.restartHintText.style.opacity = '0.8'; // Slightly faded | |
| this.gameOverContainer.appendChild(this.restartHintText); | |
| this.renderDiv.appendChild(this.gameOverContainer); | |
| // ScoreDisplay removed | |
| // Watermelon (Center Emoji Marker) setup removed | |
| // Chad Image Marker setup removed | |
| } | |
| }, | |
| { | |
| key: "_setupThree", | |
| value: function _setupThree() { | |
| var width = this.renderDiv.clientWidth; | |
| var height = this.renderDiv.clientHeight; | |
| this.scene = new THREE.Scene(); | |
| // Using OrthographicCamera for a 2D-like overlay effect | |
| this.camera = new THREE.OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 1, 1000); | |
| this.camera.position.z = 100; // Position along Z doesn't change scale in Ortho | |
| this.renderer = new THREE.WebGLRenderer({ | |
| alpha: true, | |
| antialias: true | |
| }); | |
| this.renderer.setSize(width, height); | |
| this.renderer.setPixelRatio(window.devicePixelRatio); | |
| this.renderer.domElement.style.position = 'absolute'; | |
| this.renderer.domElement.style.top = '0'; | |
| this.renderer.domElement.style.left = '0'; | |
| this.renderer.domElement.style.zIndex = '1'; // Canvas on top of video | |
| this.renderDiv.appendChild(this.renderer.domElement); | |
| var ambientLight = new THREE.AmbientLight(0xffffff, 0.7); | |
| this.scene.add(ambientLight); | |
| var directionalLight = new THREE.DirectionalLight(0xffffff, 0.9); | |
| directionalLight.position.set(0, 0, 100); // Pointing from behind camera | |
| this.scene.add(directionalLight); | |
| // Setup hand visualization (palm circles removed, lines will be added later) | |
| for(var i = 0; i < 2; i++){ | |
| var lineGroup = new THREE.Group(); | |
| lineGroup.visible = false; | |
| this.scene.add(lineGroup); | |
| this.hands.push({ | |
| landmarks: null, | |
| anchorPos: new THREE.Vector3(), | |
| lineGroup: lineGroup, | |
| isFist: false // Track if the hand is currently in a fist | |
| }); | |
| } | |
| this.handLineMaterial = new THREE.LineBasicMaterial({ | |
| color: 0x00ccff, | |
| linewidth: 8 | |
| }); | |
| this.fingertipMaterialHand1 = new THREE.MeshBasicMaterial({ | |
| color: 0xffffff, | |
| side: THREE.DoubleSide | |
| }); // White | |
| this.fingertipMaterialHand2 = new THREE.MeshBasicMaterial({ | |
| color: 0xffffff, | |
| side: THREE.DoubleSide | |
| }); // White | |
| // Define connections for MediaPipe hand landmarks | |
| // See: https://developers.google.com/mediapipe/solutions/vision/hand_landmarker#hand_landmarks | |
| this.handConnections = [ | |
| // Thumb | |
| [ | |
| 0, | |
| 1 | |
| ], | |
| [ | |
| 1, | |
| 2 | |
| ], | |
| [ | |
| 2, | |
| 3 | |
| ], | |
| [ | |
| 3, | |
| 4 | |
| ], | |
| // Index finger | |
| [ | |
| 0, | |
| 5 | |
| ], | |
| [ | |
| 5, | |
| 6 | |
| ], | |
| [ | |
| 6, | |
| 7 | |
| ], | |
| [ | |
| 7, | |
| 8 | |
| ], | |
| // Middle finger | |
| [ | |
| 0, | |
| 9 | |
| ], | |
| [ | |
| 9, | |
| 10 | |
| ], | |
| [ | |
| 10, | |
| 11 | |
| ], | |
| [ | |
| 11, | |
| 12 | |
| ], | |
| // Ring finger | |
| [ | |
| 0, | |
| 13 | |
| ], | |
| [ | |
| 13, | |
| 14 | |
| ], | |
| [ | |
| 14, | |
| 15 | |
| ], | |
| [ | |
| 15, | |
| 16 | |
| ], | |
| // Pinky | |
| [ | |
| 0, | |
| 17 | |
| ], | |
| [ | |
| 17, | |
| 18 | |
| ], | |
| [ | |
| 18, | |
| 19 | |
| ], | |
| [ | |
| 19, | |
| 20 | |
| ], | |
| // Palm | |
| [ | |
| 5, | |
| 9 | |
| ], | |
| [ | |
| 9, | |
| 13 | |
| ], | |
| [ | |
| 13, | |
| 17 | |
| ] // Connect base of fingers | |
| ]; | |
| // Particle resources removed | |
| // Ground line removed | |
| // --- Beat Indicator --- | |
| this.beatIndicatorGroup = new THREE.Group(); | |
| this.scene.add(this.beatIndicatorGroup); | |
| this._setupBeatIndicatorMaterials(); // Create materials based on drum pattern | |
| var indicatorSize = 20; | |
| var indicatorGeometry = new THREE.PlaneGeometry(indicatorSize, indicatorSize); | |
| for(var i1 = 0; i1 < 16; i1++){ | |
| // Use the pre-calculated material for this beat index | |
| var indicator = new THREE.Mesh(indicatorGeometry, this.beatIndicatorMaterials[i1]); | |
| this.beatIndicatorGroup.add(indicator); | |
| this.beatIndicators.push(indicator); | |
| } | |
| this._positionBeatIndicators(); // Position them right after creation | |
| } | |
| }, | |
| { | |
| key: "_loadAssets", | |
| value: function _loadAssets() { | |
| var _this = this; | |
| return _async_to_generator(function() { | |
| var error; | |
| return _ts_generator(this, function(_state) { | |
| switch(_state.label){ | |
| case 0: | |
| console.log("Loading assets..."); | |
| _state.label = 1; | |
| case 1: | |
| _state.trys.push([ | |
| 1, | |
| 3, | |
| , | |
| 4 | |
| ]); | |
| // Ghost Textures loading removed | |
| // Ghost GLTF Model loading removed (was already commented out) | |
| return [ | |
| 4, | |
| drumManager.loadSamples() | |
| ]; | |
| case 2: | |
| _state.sent(); // Load drum sounds | |
| console.log("No game-specific assets to load for template."); | |
| return [ | |
| 3, | |
| 4 | |
| ]; | |
| case 3: | |
| error = _state.sent(); | |
| console.error("Error loading assets:", error); | |
| _this._showError("Failed to load assets."); // Generic message | |
| throw error; // Stop initialization | |
| case 4: | |
| return [ | |
| 2 | |
| ]; | |
| } | |
| }); | |
| })(); | |
| } | |
| }, | |
| { | |
| key: "_setupHandTracking", | |
| value: function _setupHandTracking() { | |
| var _this = this; | |
| return _async_to_generator(function() { | |
| var vision, stream, error; | |
| return _ts_generator(this, function(_state) { | |
| switch(_state.label){ | |
| case 0: | |
| _state.trys.push([ | |
| 0, | |
| 4, | |
| , | |
| 5 | |
| ]); | |
| console.log("Setting up Hand Tracking..."); | |
| return [ | |
| 4, | |
| FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/wasm') | |
| ]; | |
| case 1: | |
| vision = _state.sent(); | |
| return [ | |
| 4, | |
| HandLandmarker.createFromOptions(vision, { | |
| baseOptions: { | |
| modelAssetPath: "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task", | |
| delegate: 'GPU' | |
| }, | |
| numHands: 2, | |
| runningMode: 'VIDEO' | |
| }) | |
| ]; | |
| case 2: | |
| _this.handLandmarker = _state.sent(); | |
| console.log("HandLandmarker created."); | |
| console.log("Requesting webcam access..."); | |
| return [ | |
| 4, | |
| navigator.mediaDevices.getUserMedia({ | |
| video: { | |
| facingMode: 'user', | |
| width: { | |
| ideal: 1280 | |
| }, | |
| height: { | |
| ideal: 720 | |
| } | |
| }, | |
| audio: false | |
| }) | |
| ]; | |
| case 3: | |
| stream = _state.sent(); | |
| _this.videoElement.srcObject = stream; | |
| console.log("Webcam stream obtained."); | |
| // Wait for video metadata to load to ensure dimensions are available | |
| return [ | |
| 2, | |
| new Promise(function(resolve) { | |
| _this.videoElement.onloadedmetadata = function() { | |
| console.log("Webcam metadata loaded."); | |
| // Adjust video size slightly after metadata is loaded if needed, but CSS handles most | |
| _this.videoElement.style.width = _this.renderDiv.clientWidth + 'px'; | |
| _this.videoElement.style.height = _this.renderDiv.clientHeight + 'px'; | |
| resolve(); | |
| }; | |
| }) | |
| ]; | |
| case 4: | |
| error = _state.sent(); | |
| console.error('Error setting up Hand Tracking or Webcam:', error); | |
| _this._showError("Webcam/Hand Tracking Error: ".concat(error.message, ". Please allow camera access.")); | |
| throw error; // Re-throw to stop initialization | |
| case 5: | |
| return [ | |
| 2 | |
| ]; | |
| } | |
| }); | |
| })(); | |
| } | |
| }, | |
| { | |
| // _startSpawning, _scheduleNextSpawn, _stopSpawning, _spawnGhost methods removed. | |
| key: "_updateHands", | |
| value: function _updateHands() { | |
| var _this = this; | |
| if (!this.handLandmarker || !this.videoElement.srcObject || this.videoElement.readyState < 2 || this.videoElement.videoWidth === 0) return; | |
| var videoTime = this.videoElement.currentTime; | |
| if (videoTime > this.lastVideoTime) { | |
| this.lastVideoTime = videoTime; | |
| try { | |
| var _this1, _loop = function(i) { | |
| var hand = _this1.hands[i]; | |
| var wasVisible = hand.landmarks !== null; | |
| if (results.landmarks && results.landmarks[i]) { | |
| var currentRawLandmarks = results.landmarks[i]; | |
| if (!_this1.lastLandmarkPositions[i] || _this1.lastLandmarkPositions[i].length !== currentRawLandmarks.length) { | |
| _this1.lastLandmarkPositions[i] = currentRawLandmarks.map(function(lm) { | |
| return _object_spread({}, lm); | |
| }); | |
| } | |
| var smoothedLandmarks = currentRawLandmarks.map(function(lm, lmIndex) { | |
| var prevLm = _this.lastLandmarkPositions[i][lmIndex]; | |
| return { | |
| x: _this.smoothingFactor * lm.x + (1 - _this.smoothingFactor) * prevLm.x, | |
| y: _this.smoothingFactor * lm.y + (1 - _this.smoothingFactor) * prevLm.y, | |
| z: _this.smoothingFactor * lm.z + (1 - _this.smoothingFactor) * prevLm.z | |
| }; | |
| }); | |
| _this1.lastLandmarkPositions[i] = smoothedLandmarks.map(function(lm) { | |
| return _object_spread({}, lm); | |
| }); | |
| hand.landmarks = smoothedLandmarks; | |
| var palm = smoothedLandmarks[9]; // MIDDLE_FINGER_MCP | |
| var lmOriginalX = palm.x * videoParams.videoNaturalWidth; | |
| var lmOriginalY = palm.y * videoParams.videoNaturalHeight; | |
| var normX_visible = (lmOriginalX - videoParams.offsetX) / videoParams.visibleWidth; | |
| var normY_visible = (lmOriginalY - videoParams.offsetY) / videoParams.visibleHeight; | |
| var handX = (1 - normX_visible) * canvasWidth - canvasWidth / 2; | |
| var handY = (1 - normY_visible) * canvasHeight - canvasHeight / 2; | |
| hand.anchorPos.set(handX, handY, 1); | |
| if (i === 0) { | |
| // --- Music & Gesture Control --- | |
| var isFistNow = _this1._isFist(smoothedLandmarks); | |
| if (isFistNow && !hand.isFist) { | |
| // Fist gesture was just made | |
| _this1.musicManager.cycleSynth(); | |
| _this1.musicManager.stopArpeggio(i); // Stop any old arpeggio | |
| } | |
| hand.isFist = isFistNow; | |
| var noteIndex = Math.floor((1 - normY_visible) * scale.length); | |
| var note = scale[Math.max(0, Math.min(scale.length - 1, noteIndex))]; | |
| if (_this1.waveformVisualizer) { | |
| var colorIndex = noteIndex % _this1.waveformColors.length; | |
| var newColor = _this1.waveformColors[colorIndex]; | |
| _this1.waveformVisualizer.updateColor(newColor); | |
| } | |
| var thumbTip = smoothedLandmarks[4]; | |
| var indexTip = smoothedLandmarks[8]; | |
| var dx = thumbTip.x - indexTip.x; | |
| var dy = thumbTip.y - indexTip.y; | |
| var distance = Math.sqrt(dx * dx + dy * dy); | |
| var velocity = Math.max(0, Math.min(1.0, distance * 5)); | |
| _this1._updateHandLines(i, smoothedLandmarks, videoParams, canvasWidth, canvasHeight, { | |
| note: note, | |
| velocity: velocity, | |
| isFist: isFistNow | |
| }); | |
| if (!isFistNow) { | |
| // Start/Restart arpeggio if the hand just appeared OR if it just opened from a fist. | |
| var arpeggioIsActive = _this1.musicManager.activePatterns.has(i); | |
| if (!wasVisible || !arpeggioIsActive) { | |
| _this1.musicManager.startArpeggio(i, note); | |
| } else { | |
| _this1.musicManager.updateArpeggio(i, note); | |
| } | |
| _this1.musicManager.updateArpeggioVolume(i, velocity); | |
| } else { | |
| // If it is a fist, make sure the arpeggio is stopped | |
| _this1.musicManager.stopArpeggio(i); | |
| } | |
| } else if (i === 1) { | |
| var fingerStates = _this1._getFingerStates(smoothedLandmarks); | |
| drumManager.updateActiveDrums(fingerStates); | |
| _this1._updateHandLines(i, smoothedLandmarks, videoParams, canvasWidth, canvasHeight, { | |
| fingerStates: fingerStates | |
| }); | |
| } | |
| hand.lineGroup.visible = true; | |
| } else { | |
| if (wasVisible) { | |
| if (i === 0) { | |
| _this1.musicManager.stopArpeggio(i); | |
| } else if (i === 1) { | |
| // Disable all drums when hand is gone | |
| drumManager.updateActiveDrums({}); | |
| } | |
| } | |
| hand.landmarks = null; | |
| if (hand.lineGroup) hand.lineGroup.visible = false; | |
| } | |
| }; | |
| var results = this.handLandmarker.detectForVideo(this.videoElement, performance.now()); | |
| var videoParams = this._getVisibleVideoParameters(); | |
| if (!videoParams) return; | |
| var canvasWidth = this.renderDiv.clientWidth; | |
| var canvasHeight = this.renderDiv.clientHeight; | |
| // C Minor Pentatonic Scale | |
| var scale = [ | |
| 'C3', | |
| 'Eb3', | |
| 'F3', | |
| 'G3', | |
| 'Bb3', | |
| 'C4', | |
| 'Eb4', | |
| 'F4', | |
| 'G4', | |
| 'Bb4', | |
| 'C5', | |
| 'Eb5' | |
| ]; | |
| for(var i = 0; i < this.hands.length; i++)_this1 = this, _loop(i); | |
| } catch (error) { | |
| console.error("Error during hand detection:", error); | |
| } | |
| } | |
| } | |
| }, | |
| { | |
| key: "_getVisibleVideoParameters", | |
| value: function _getVisibleVideoParameters() { | |
| if (!this.videoElement || this.videoElement.videoWidth === 0 || this.videoElement.videoHeight === 0) { | |
| return null; | |
| } | |
| var vNatW = this.videoElement.videoWidth; | |
| var vNatH = this.videoElement.videoHeight; | |
| var rW = this.renderDiv.clientWidth; | |
| var rH = this.renderDiv.clientHeight; | |
| if (vNatW === 0 || vNatH === 0 || rW === 0 || rH === 0) return null; | |
| var videoAR = vNatW / vNatH; | |
| var renderDivAR = rW / rH; | |
| var finalVideoPixelX, finalVideoPixelY; | |
| var visibleVideoPixelWidth, visibleVideoPixelHeight; | |
| if (videoAR > renderDivAR) { | |
| // Video is wider than renderDiv, scaled to fit renderDiv height, cropped horizontally. | |
| var scale = rH / vNatH; // Scale factor based on height. | |
| var scaledVideoWidth = vNatW * scale; // Width of video if scaled to fit renderDiv height. | |
| // Total original video pixels cropped horizontally (from both sides combined). | |
| var totalCroppedPixelsX = (scaledVideoWidth - rW) / scale; | |
| finalVideoPixelX = totalCroppedPixelsX / 2; // Pixels cropped from the left of original video. | |
| finalVideoPixelY = 0; // No vertical cropping. | |
| visibleVideoPixelWidth = vNatW - totalCroppedPixelsX; // Width of the visible part in original video pixels. | |
| visibleVideoPixelHeight = vNatH; // Full height is visible. | |
| } else { | |
| // Video is taller than renderDiv (or same AR), scaled to fit renderDiv width, cropped vertically. | |
| var scale1 = rW / vNatW; // Scale factor based on width. | |
| var scaledVideoHeight = vNatH * scale1; // Height of video if scaled to fit renderDiv width. | |
| // Total original video pixels cropped vertically (from top and bottom combined). | |
| var totalCroppedPixelsY = (scaledVideoHeight - rH) / scale1; | |
| finalVideoPixelX = 0; // No horizontal cropping. | |
| finalVideoPixelY = totalCroppedPixelsY / 2; // Pixels cropped from the top of original video. | |
| visibleVideoPixelWidth = vNatW; // Full width is visible. | |
| visibleVideoPixelHeight = vNatH - totalCroppedPixelsY; // Height of the visible part in original video pixels. | |
| } | |
| // Safety check for degenerate cases (e.g., extreme aspect ratios leading to zero visible dimension) | |
| if (visibleVideoPixelWidth <= 0 || visibleVideoPixelHeight <= 0) { | |
| // Fallback or log error, this shouldn't happen in normal scenarios | |
| console.warn("Calculated visible video dimension is zero or negative.", { | |
| visibleVideoPixelWidth: visibleVideoPixelWidth, | |
| visibleVideoPixelHeight: visibleVideoPixelHeight | |
| }); | |
| return { | |
| offsetX: 0, | |
| offsetY: 0, | |
| visibleWidth: vNatW, | |
| visibleHeight: vNatH, | |
| videoNaturalWidth: vNatW, | |
| videoNaturalHeight: vNatH | |
| }; | |
| } | |
| return { | |
| offsetX: finalVideoPixelX, | |
| offsetY: finalVideoPixelY, | |
| visibleWidth: visibleVideoPixelWidth, | |
| visibleHeight: visibleVideoPixelHeight, | |
| videoNaturalWidth: vNatW, | |
| videoNaturalHeight: vNatH | |
| }; | |
| } | |
| }, | |
| { | |
| // _updateGhosts method removed. | |
| key: "_showStatusScreen", | |
| value: function _showStatusScreen(message) { | |
| var color = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : 'white', showRestartHint = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : false; | |
| this.gameOverContainer.style.display = 'block'; | |
| this.gameOverText.innerText = message; | |
| this.gameOverText.style.color = color; | |
| this.restartHintText.style.display = showRestartHint ? 'block' : 'none'; | |
| // No spawning to stop for template | |
| } | |
| }, | |
| { | |
| key: "_showError", | |
| value: function _showError(message) { | |
| this.gameOverContainer.style.display = 'block'; | |
| this.gameOverText.innerText = "ERROR: ".concat(message); | |
| this.gameOverText.style.color = 'orange'; | |
| this.restartHintText.style.display = 'true'; // Show restart hint on error | |
| this.gameState = 'error'; | |
| // No spawning to stop | |
| this.hands.forEach(function(hand) { | |
| if (hand.lineGroup) hand.lineGroup.visible = false; | |
| }); | |
| // if (this.startButton) this.startButton.style.display = 'none'; // No longer exists | |
| } | |
| }, | |
| { | |
| key: "_startGame", | |
| value: function _startGame() { | |
| var _this = this; | |
| console.log("Starting tracking..."); | |
| // This is now called automatically, so no need to check gameState | |
| this.musicManager.start().then(function() { | |
| drumManager.startSequence(); // Start drums *after* audio context is ready. | |
| // Setup the waveform visualizer after the music manager is ready | |
| var analyser = _this.musicManager.getAnalyser(); | |
| if (analyser) { | |
| _this.waveformVisualizer = new WaveformVisualizer(_this.scene, analyser, _this.renderDiv.clientWidth, _this.renderDiv.clientHeight); | |
| } | |
| }); | |
| this.gameState = 'tracking'; // Changed from 'playing' | |
| this.lastVideoTime = -1; | |
| this.clock.start(); | |
| // Removed display of score, castle, chad | |
| // Removed _startSpawning() | |
| } | |
| }, | |
| { | |
| key: "_restartGame", | |
| value: function _restartGame() { | |
| console.log("Restarting tracking..."); | |
| this.gameOverContainer.style.display = 'none'; | |
| this.hands.forEach(function(hand) { | |
| if (hand.lineGroup) { | |
| hand.lineGroup.visible = false; | |
| } | |
| }); | |
| // Ghost removal removed | |
| // Score reset removed | |
| // Visibility of game elements removed | |
| this.gameState = 'tracking'; // Changed from 'playing' | |
| this.lastVideoTime = -1; | |
| this.clock.start(); | |
| // Removed _startSpawning() | |
| } | |
| }, | |
| { | |
| // _updateScoreDisplay method removed. | |
| key: "_onResize", | |
| value: function _onResize() { | |
| var width = this.renderDiv.clientWidth; | |
| var height = this.renderDiv.clientHeight; | |
| // Update camera perspective | |
| this.camera.left = width / -2; | |
| this.camera.right = width / 2; | |
| this.camera.top = height / 2; | |
| this.camera.bottom = height / -2; | |
| this.camera.updateProjectionMatrix(); | |
| // Update renderer size | |
| this.renderer.setSize(width, height); | |
| // Update video element size | |
| this.videoElement.style.width = width + 'px'; | |
| this.videoElement.style.height = height + 'px'; | |
| // Watermelon, Chad, GroundLine updates removed. | |
| this._positionBeatIndicators(); | |
| if (this.waveformVisualizer) { | |
| this.waveformVisualizer.updatePosition(width, height); | |
| } | |
| } | |
| }, | |
| { | |
| key: "_positionBeatIndicators", | |
| value: function _positionBeatIndicators() { | |
| var width = this.renderDiv.clientWidth; | |
| var height = this.renderDiv.clientHeight; | |
| var totalWidth = width * 0.8; // Occupy 80% of screen width to match the waveform | |
| var spacing = totalWidth / 16; | |
| var startX = -totalWidth / 2 + spacing / 2; | |
| var yPos = -height / 2 + 150; // Positioned a bit higher from the bottom | |
| this.beatIndicators.forEach(function(indicator, i) { | |
| indicator.position.set(startX + i * spacing, yPos, 1); | |
| }); | |
| } | |
| }, | |
| { | |
| key: "_setupBeatIndicatorMaterials", | |
| value: function _setupBeatIndicatorMaterials() { | |
| // All indicators start as 'off' (white) | |
| for(var i = 0; i < 16; i++){ | |
| // We just need one material definition now and will copy it. | |
| this.beatIndicatorMaterials[i] = new THREE.MeshBasicMaterial({ | |
| color: this.beatIndicatorColors.off, | |
| transparent: true, | |
| opacity: 0.5 | |
| }); | |
| } | |
| } | |
| }, | |
| { | |
| key: "_createTextSprite", | |
| value: function _createTextSprite(message, parameters) { | |
| parameters = parameters || {}; | |
| var fontface = parameters.fontface || 'Arial'; | |
| var fontsize = parameters.fontsize || 24; | |
| // borderColor is no longer needed | |
| var backgroundColor = parameters.backgroundColor || { | |
| r: 255, | |
| g: 255, | |
| b: 255, | |
| a: 0.8 | |
| }; | |
| var textColor = parameters.textColor || { | |
| r: 0, | |
| g: 0, | |
| b: 0, | |
| a: 1.0 | |
| }; | |
| var canvas = document.createElement('canvas'); | |
| var context = canvas.getContext('2d'); | |
| context.font = "Bold ".concat(fontsize, "px ").concat(fontface); | |
| // get size data (height depends only on font size) | |
| var metrics = context.measureText(message); | |
| var textWidth = metrics.width; | |
| var padding = 10; | |
| var canvasWidth = textWidth + padding * 2; | |
| var canvasHeight = fontsize * 1.4 + padding; | |
| canvas.width = canvasWidth; | |
| canvas.height = canvasHeight; | |
| // Font needs to be re-applied after resizing canvas | |
| context.font = "Bold ".concat(fontsize, "px ").concat(fontface); | |
| // background color | |
| context.fillStyle = "rgba(".concat(backgroundColor.r, ",").concat(backgroundColor.g, ",").concat(backgroundColor.b, ",").concat(backgroundColor.a, ")"); | |
| context.fillRect(0, 0, canvasWidth, canvasHeight); | |
| // text color and position | |
| context.fillStyle = "rgba(".concat(textColor.r, ", ").concat(textColor.g, ", ").concat(textColor.b, ", 1.0)"); | |
| context.textAlign = 'center'; | |
| context.textBaseline = 'middle'; | |
| context.fillText(message, canvasWidth / 2, canvasHeight / 2); | |
| // canvas contents will be used for a texture | |
| var texture = new THREE.CanvasTexture(canvas); | |
| texture.needsUpdate = true; | |
| var spriteMaterial = new THREE.SpriteMaterial({ | |
| map: texture | |
| }); | |
| var sprite = new THREE.Sprite(spriteMaterial); | |
| sprite.scale.set(canvas.width, canvas.height, 1.0); | |
| return sprite; | |
| } | |
| }, | |
| { | |
| key: "_getFingerStates", | |
| value: function _getFingerStates(landmarks) { | |
| // Landmark indices for fingertips | |
| var fingertips = { | |
| index: 8, | |
| middle: 12, | |
| ring: 16, | |
| pinky: 20 | |
| }; | |
| // Stricter check using the joint below the tip (PIP joint) to avoid false positives. | |
| var fingerJointsBelowTip = { | |
| index: 6, | |
| middle: 10, | |
| ring: 14, | |
| pinky: 18 | |
| }; | |
| var states = {}; | |
| var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined; | |
| try { | |
| for(var _iterator = Object.entries(fingertips)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){ | |
| var _step_value = _sliced_to_array(_step.value, 2), finger = _step_value[0], tipIndex = _step_value[1]; | |
| var jointIndex = fingerJointsBelowTip[finger]; | |
| if (landmarks[tipIndex] && landmarks[jointIndex]) { | |
| // A finger is "up" if its tip is higher than the joint just below it. | |
| states[finger] = landmarks[tipIndex].y < landmarks[jointIndex].y; | |
| } else { | |
| states[finger] = false; | |
| } | |
| } | |
| } catch (err) { | |
| _didIteratorError = true; | |
| _iteratorError = err; | |
| } finally{ | |
| try { | |
| if (!_iteratorNormalCompletion && _iterator.return != null) { | |
| _iterator.return(); | |
| } | |
| } finally{ | |
| if (_didIteratorError) { | |
| throw _iteratorError; | |
| } | |
| } | |
| } | |
| return states; | |
| } | |
| }, | |
| { | |
| key: "_isFist", | |
| value: function _isFist(landmarks) { | |
| if (!landmarks || landmarks.length < 21) return false; | |
| // Use the middle finger's MCP joint as a proxy for the palm center | |
| var palmCenter = landmarks[9]; | |
| var fingertipsIndices = [ | |
| 4, | |
| 8, | |
| 12, | |
| 16, | |
| 20 | |
| ]; // Thumb, Index, Middle, Ring, Pinky | |
| // Threshold for normalized landmark distance. If fingertips are further than this from palm, it's not a fist. | |
| // This value may need tuning. A smaller value makes the fist detection stricter. | |
| var fistThreshold = 0.1; | |
| var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined; | |
| try { | |
| for(var _iterator = fingertipsIndices[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){ | |
| var tipIndex = _step.value; | |
| var tip = landmarks[tipIndex]; | |
| var dx = tip.x - palmCenter.x; | |
| var dy = tip.y - palmCenter.y; | |
| var distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance > fistThreshold) { | |
| return false; // At least one finger is open | |
| } | |
| } | |
| } catch (err) { | |
| _didIteratorError = true; | |
| _iteratorError = err; | |
| } finally{ | |
| try { | |
| if (!_iteratorNormalCompletion && _iterator.return != null) { | |
| _iterator.return(); | |
| } | |
| } finally{ | |
| if (_didIteratorError) { | |
| throw _iteratorError; | |
| } | |
| } | |
| } | |
| return true; // All fingertips are close to the palm | |
| } | |
| }, | |
| { | |
| key: "_updateHandLines", | |
| value: function _updateHandLines(handIndex, landmarks, videoParams, canvasWidth, canvasHeight, controlData) { | |
| var _this = this; | |
| var hand = this.hands[handIndex]; | |
| var lineGroup = hand.lineGroup; | |
| // Clean up previous frame's objects | |
| while(lineGroup.children.length){ | |
| var child = lineGroup.children[0]; | |
| lineGroup.remove(child); | |
| if (child.geometry) child.geometry.dispose(); | |
| if (child.material) { | |
| // For sprites, we need to dispose the texture map as well | |
| if (child.material.map) child.material.map.dispose(); | |
| child.material.dispose(); | |
| } | |
| } | |
| if (!landmarks || landmarks.length === 0 || !videoParams) { | |
| lineGroup.visible = false; | |
| return; | |
| } | |
| var points3D = landmarks.map(function(lm) { | |
| var lmOriginalX = lm.x * videoParams.videoNaturalWidth; | |
| var lmOriginalY = lm.y * videoParams.videoNaturalHeight; | |
| var normX_visible = (lmOriginalX - videoParams.offsetX) / videoParams.visibleWidth; | |
| var normY_visible = (lmOriginalY - videoParams.offsetY) / videoParams.visibleHeight; | |
| normX_visible = Math.max(0, Math.min(1, normX_visible)); | |
| normY_visible = Math.max(0, Math.min(1, normY_visible)); | |
| var x = (1 - normX_visible) * canvasWidth - canvasWidth / 2; | |
| var y = (1 - normY_visible) * canvasHeight - canvasHeight / 2; | |
| return new THREE.Vector3(x, y, 1.1); // Z for fingertip circles | |
| }); | |
| // --- Draw Skeleton Lines --- | |
| var lineZ = 1; | |
| this.handConnections.forEach(function(conn) { | |
| var p1 = points3D[conn[0]]; | |
| var p2 = points3D[conn[1]]; | |
| if (p1 && p2) { | |
| var lineP1 = p1.clone().setZ(lineZ); | |
| var lineP2 = p2.clone().setZ(lineZ); | |
| var geometry = new THREE.BufferGeometry().setFromPoints([ | |
| lineP1, | |
| lineP2 | |
| ]); | |
| var line = new THREE.Line(geometry, _this.handLineMaterial); | |
| lineGroup.add(line); | |
| } | |
| }); | |
| // --- Draw Fingertip & Wrist Circles --- | |
| var fingertipRadius = 8, wristRadius = 12, circleSegments = 16; | |
| this.fingertipLandmarkIndices.forEach(function(index) { | |
| var landmarkPosition = points3D[index]; | |
| if (landmarkPosition) { | |
| var radius = index === 0 ? wristRadius : fingertipRadius; | |
| var circleGeometry = new THREE.CircleGeometry(radius, circleSegments); | |
| var material = handIndex === 0 ? _this.fingertipMaterialHand1 : _this.fingertipMaterialHand2; | |
| var landmarkCircle = new THREE.Mesh(circleGeometry, material); | |
| landmarkCircle.position.copy(landmarkPosition); | |
| lineGroup.add(landmarkCircle); | |
| } | |
| }); | |
| // --- Draw Thumb-to-Index line and Labels --- | |
| var thumbPos = points3D[4]; | |
| var indexPos = points3D[8]; | |
| var wristPos = points3D[0]; | |
| if (wristPos) { | |
| // Labels depend on which hand it is | |
| if (handIndex === 0 && thumbPos && indexPos) { | |
| // Connecting line | |
| var lineGeom = new THREE.BufferGeometry().setFromPoints([ | |
| thumbPos, | |
| indexPos | |
| ]); | |
| var line = new THREE.Line(lineGeom, new THREE.LineBasicMaterial({ | |
| color: 0xffffff, | |
| linewidth: 3 | |
| })); | |
| lineGroup.add(line); | |
| // Volume and Pitch labels | |
| var note = controlData.note, velocity = controlData.velocity, isFist = controlData.isFist; | |
| if (isFist) { | |
| var fistLabel = this._createTextSprite("SYNTH ".concat(this.musicManager.currentSynthIndex + 1), { | |
| fontsize: 22, | |
| backgroundColor: this.labelColors.evaPurple, | |
| textColor: this.labelColors.evaGreen | |
| }); | |
| fistLabel.position.set(wristPos.x, wristPos.y + 60, 2); | |
| lineGroup.add(fistLabel); | |
| } else { | |
| var midPoint = new THREE.Vector3().lerpVectors(thumbPos, indexPos, 0.5); | |
| var volumeLabel = this._createTextSprite("Volume: ".concat(velocity.toFixed(2)), { | |
| fontsize: 18, | |
| backgroundColor: this.labelColors.evaOrange, | |
| textColor: this.labelColors.white | |
| }); | |
| volumeLabel.position.set(midPoint.x, midPoint.y, 2); | |
| lineGroup.add(volumeLabel); | |
| var pitchLabel = this._createTextSprite("Pitch: ".concat(note), { | |
| fontsize: 18, | |
| backgroundColor: this.labelColors.evaGreen, | |
| textColor: this.labelColors.black | |
| }); | |
| pitchLabel.position.set(wristPos.x, wristPos.y + 60, 2); // Position above the wrist | |
| lineGroup.add(pitchLabel); | |
| } | |
| } else if (handIndex === 1) { | |
| var fingerStates = controlData.fingerStates; | |
| var activeDrums = Object.entries(fingerStates).filter(function(param) { | |
| var _param = _sliced_to_array(param, 2), _ = _param[0], isUp = _param[1]; | |
| return isUp; | |
| }).map(function(param) { | |
| var _param = _sliced_to_array(param, 2), finger = _param[0], _ = _param[1]; | |
| return drumManager.getFingerToDrumMap()[finger]; | |
| }).join(', '); | |
| var drumLabel = this._createTextSprite("Drums: ".concat(activeDrums || 'None'), { | |
| fontsize: 18, | |
| backgroundColor: this.labelColors.evaRed, | |
| textColor: this.labelColors.white | |
| }); | |
| drumLabel.position.set(wristPos.x, wristPos.y + 60, 2); | |
| lineGroup.add(drumLabel); | |
| } | |
| } | |
| lineGroup.visible = true; | |
| } | |
| }, | |
| { | |
| key: "_animate", | |
| value: function _animate() { | |
| requestAnimationFrame(this._animate.bind(this)); | |
| if (this.gameState === 'tracking') { | |
| var deltaTime = this.clock.getDelta(); | |
| this._updateHands(); | |
| this._updateBeatIndicator(); | |
| if (this.waveformVisualizer) { | |
| this.waveformVisualizer.update(); | |
| } | |
| } | |
| this.renderer.render(this.scene, this.camera); | |
| } | |
| }, | |
| { | |
| key: "_updateBeatIndicator", | |
| value: function _updateBeatIndicator() { | |
| var _this = this; | |
| var currentBeat = drumManager.getCurrentBeat(); | |
| var progress = Tone.Transport.progress; | |
| var beatProgress = progress * 16 % 1; | |
| var pulse = 1.5 + 0.5 * Math.cos(beatProgress * Math.PI * 2); | |
| var activeDrums = drumManager.getActiveDrums(); | |
| var drumPattern = drumManager.getDrumPattern(); | |
| var drumPriority = [ | |
| 'kick', | |
| 'snare', | |
| 'clap', | |
| 'hihat' | |
| ]; | |
| this.beatIndicators.forEach(function(indicator, i) { | |
| // Determine the color for this step based on active drums | |
| var stepColor = _this.beatIndicatorColors.off; | |
| var isHit = false; | |
| var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined; | |
| try { | |
| for(var _iterator = drumPriority[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){ | |
| var drum = _step.value; | |
| if (activeDrums.has(drum) && drumPattern[drum][i]) { | |
| stepColor = _this.beatIndicatorColors[drum]; | |
| isHit = true; | |
| break; | |
| } | |
| } | |
| } catch (err) { | |
| _didIteratorError = true; | |
| _iteratorError = err; | |
| } finally{ | |
| try { | |
| if (!_iteratorNormalCompletion && _iterator.return != null) { | |
| _iterator.return(); | |
| } | |
| } finally{ | |
| if (_didIteratorError) { | |
| throw _iteratorError; | |
| } | |
| } | |
| } | |
| indicator.material.color.set(stepColor); | |
| indicator.material.opacity = isHit ? 0.9 : 0.5; | |
| // Apply pulse only to the current beat marker | |
| if (i === currentBeat) { | |
| indicator.scale.set(pulse, pulse, 1); | |
| } else { | |
| indicator.scale.set(1, 1, 1); | |
| } | |
| }); | |
| } | |
| }, | |
| { | |
| key: "_setupEventListeners", | |
| value: function _setupEventListeners() { | |
| var _this = this; | |
| // Add click listener for resuming audio context and potentially restarting on error | |
| this.renderDiv.addEventListener('click', function() { | |
| _this.musicManager.start(); // Resume audio context on any click | |
| if (_this.gameState === 'error') { | |
| _this._restartGame(); | |
| } | |
| }); | |
| console.log('Game event listeners set up.'); | |
| } | |
| } | |
| ]); | |
| return Game; | |
| }(); | |