arpeggiator / game.js
stereoDrift's picture
Upload 14 files
f8b493b verified
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;
}();