diff --git a/flute-svg.js b/flute-svg.js new file mode 100644 index 0000000..1a836dc --- /dev/null +++ b/flute-svg.js @@ -0,0 +1,297 @@ +// SVG Flute Component Generator +// Creates a detailed SVG representation of an orchestral flute with interactive fingering indicators + +class FluteSVG { + constructor(containerId) { + this.container = document.getElementById(containerId); + this.keyStates = { + 'A': false, + 'S': false, + 'D': false, + 'F': false, + 'G': false, + 'H': false, + 'J': false + }; + // Animation constants + this.ANIMATION_DURATION_MS = 300; + + // Cache for DOM elements + this.keyElements = {}; + this.previewElements = {}; + this.highlightElements = {}; + + this.createFluteSVG(); + this.cacheKeyElements(); + } + + cacheKeyElements() { + // Cache DOM elements to avoid repeated queries + const keys = ['A', 'S', 'D', 'F', 'G', 'H', 'J']; + keys.forEach(key => { + const keyGroup = this.container.querySelector(`[data-key="${key}"]`); + if (keyGroup) { + this.keyElements[key] = keyGroup; + this.previewElements[key] = keyGroup.querySelector('.key-preview'); + this.highlightElements[key] = keyGroup.querySelector('.key-highlight'); + } + }); + } + + createFluteSVG() { + // Create SVG element + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('viewBox', '0 0 800 200'); + svg.setAttribute('id', 'flute-svg'); + svg.setAttribute('role', 'img'); + svg.setAttribute('aria-label', 'Interactive flute diagram showing finger positions for notes'); + svg.style.width = '100%'; + svg.style.height = 'auto'; + + // Create flute body (main tube) + const fluteBody = this.createFluteBody(); + svg.appendChild(fluteBody); + + // Create finger holes/keys representing each keyboard key + // Flute keys from left to right: A, S, D, F, G, H, J + const keyPositions = [ + { id: 'A', x: 150, y: 100, label: 'A' }, // Left hand index finger + { id: 'S', x: 220, y: 100, label: 'S' }, // Left hand middle finger + { id: 'D', x: 290, y: 100, label: 'D' }, // Left hand ring finger + { id: 'F', x: 380, y: 100, label: 'F' }, // Right hand index finger + { id: 'G', x: 450, y: 100, label: 'G' }, // Right hand middle finger + { id: 'H', x: 520, y: 100, label: 'H' }, // Right hand ring finger + { id: 'J', x: 590, y: 100, label: 'J' } // Right hand pinky finger + ]; + + keyPositions.forEach(keyPos => { + const keyGroup = this.createKey(keyPos); + svg.appendChild(keyGroup); + }); + + // Add embouchure (mouthpiece) on the left + const embouchure = this.createEmbouchure(); + svg.appendChild(embouchure); + + // Clear container and add SVG + this.container.innerHTML = ''; + this.container.appendChild(svg); + } + + createFluteBody() { + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + g.setAttribute('id', 'flute-body'); + + // Main tube (cylindrical body) + const tube = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + tube.setAttribute('x', '100'); + tube.setAttribute('y', '85'); + tube.setAttribute('width', '600'); + tube.setAttribute('height', '30'); + tube.setAttribute('rx', '15'); + tube.setAttribute('fill', 'url(#fluteGradient)'); + tube.setAttribute('stroke', '#999'); + tube.setAttribute('stroke-width', '2'); + + // Add gradient for metallic look + const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); + const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient'); + gradient.setAttribute('id', 'fluteGradient'); + gradient.setAttribute('x1', '0%'); + gradient.setAttribute('y1', '0%'); + gradient.setAttribute('x2', '0%'); + gradient.setAttribute('y2', '100%'); + + const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); + stop1.setAttribute('offset', '0%'); + stop1.setAttribute('style', 'stop-color:#e8e8e8;stop-opacity:1'); + + const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); + stop2.setAttribute('offset', '50%'); + stop2.setAttribute('style', 'stop-color:#d4d4d4;stop-opacity:1'); + + const stop3 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); + stop3.setAttribute('offset', '100%'); + stop3.setAttribute('style', 'stop-color:#c0c0c0;stop-opacity:1'); + + gradient.appendChild(stop1); + gradient.appendChild(stop2); + gradient.appendChild(stop3); + defs.appendChild(gradient); + + g.appendChild(defs); + g.appendChild(tube); + + // Add shine/highlight on top of tube + const highlight = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + highlight.setAttribute('x', '100'); + highlight.setAttribute('y', '87'); + highlight.setAttribute('width', '600'); + highlight.setAttribute('height', '8'); + highlight.setAttribute('rx', '4'); + highlight.setAttribute('fill', 'rgba(255, 255, 255, 0.4)'); + + g.appendChild(highlight); + + return g; + } + + createKey(keyPos) { + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + g.setAttribute('class', 'flute-key-svg'); + g.setAttribute('data-key', keyPos.id); + + // Outer ring (key mechanism) + const outerRing = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + outerRing.setAttribute('cx', keyPos.x); + outerRing.setAttribute('cy', keyPos.y); + outerRing.setAttribute('r', '22'); + outerRing.setAttribute('fill', '#888'); + outerRing.setAttribute('stroke', '#666'); + outerRing.setAttribute('stroke-width', '1.5'); + + // Inner pad (finger hole cover) + const innerPad = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + innerPad.setAttribute('cx', keyPos.x); + innerPad.setAttribute('cy', keyPos.y); + innerPad.setAttribute('r', '18'); + innerPad.setAttribute('fill', '#b8b8b8'); + innerPad.setAttribute('stroke', '#999'); + innerPad.setAttribute('stroke-width', '1'); + innerPad.setAttribute('class', 'key-pad'); + + // Highlight overlay (shows when key is active) + const highlight = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + highlight.setAttribute('cx', keyPos.x); + highlight.setAttribute('cy', keyPos.y); + highlight.setAttribute('r', '18'); + highlight.setAttribute('fill', '#667eea'); + highlight.setAttribute('opacity', '0'); + highlight.setAttribute('class', 'key-highlight'); + + // Preview overlay (shows upcoming note) + const preview = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + preview.setAttribute('cx', keyPos.x); + preview.setAttribute('cy', keyPos.y); + preview.setAttribute('r', '18'); + preview.setAttribute('fill', '#FFD700'); + preview.setAttribute('opacity', '0'); + preview.setAttribute('class', 'key-preview'); + + g.appendChild(outerRing); + g.appendChild(innerPad); + g.appendChild(highlight); + g.appendChild(preview); + + return g; + } + + createEmbouchure() { + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + g.setAttribute('id', 'embouchure'); + + // Head joint (wider section on left) + const headJoint = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + headJoint.setAttribute('x', '20'); + headJoint.setAttribute('y', '82'); + headJoint.setAttribute('width', '85'); + headJoint.setAttribute('height', '36'); + headJoint.setAttribute('rx', '18'); + headJoint.setAttribute('fill', 'url(#fluteGradient)'); + headJoint.setAttribute('stroke', '#999'); + headJoint.setAttribute('stroke-width', '2'); + + // Embouchure hole (blow hole) + const embouchureHole = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse'); + embouchureHole.setAttribute('cx', '70'); + embouchureHole.setAttribute('cy', '100'); + embouchureHole.setAttribute('rx', '12'); + embouchureHole.setAttribute('ry', '6'); + embouchureHole.setAttribute('fill', '#333'); + embouchureHole.setAttribute('stroke', '#666'); + embouchureHole.setAttribute('stroke-width', '1'); + + g.appendChild(headJoint); + g.appendChild(embouchureHole); + + return g; + } + + // Update key states based on fingering + updateFingering(requiredKeys) { + // Reset all keys first + Object.keys(this.keyStates).forEach(key => { + this.keyStates[key] = false; + }); + + // Set required keys + requiredKeys.forEach(key => { + this.keyStates[key] = true; + }); + + this.renderKeyStates(); + } + + // Show preview for upcoming note + showPreview(requiredKeys) { + // Clear all previews first using cached elements + Object.values(this.previewElements).forEach(preview => { + if (preview) { + preview.setAttribute('opacity', '0'); + } + }); + + // Show preview for required keys + requiredKeys.forEach(key => { + const preview = this.previewElements[key]; + if (preview) { + preview.setAttribute('opacity', '0.7'); + } + }); + } + + // Clear all previews + clearPreview() { + Object.values(this.previewElements).forEach(preview => { + if (preview) { + preview.setAttribute('opacity', '0'); + } + }); + } + + // Render current key states + renderKeyStates() { + Object.keys(this.keyStates).forEach(key => { + const highlight = this.highlightElements[key]; + + if (highlight) { + if (this.keyStates[key]) { + // Key is active (pressed) + highlight.setAttribute('opacity', '0.9'); + } else { + // Key is inactive + highlight.setAttribute('opacity', '0'); + } + } + }); + } + + // Trigger visual feedback animation for a note hit + triggerHitFeedback(requiredKeys) { + requiredKeys.forEach(key => { + const highlight = this.highlightElements[key]; + + if (highlight) { + // Use CSS class for animation instead of creating SVG animation elements + highlight.setAttribute('opacity', '1'); + highlight.classList.add('hit-pulse'); + + // Remove class after animation completes + setTimeout(() => { + highlight.classList.remove('hit-pulse'); + highlight.setAttribute('opacity', '0.9'); + }, this.ANIMATION_DURATION_MS); + } + }); + } +} diff --git a/game.js b/game.js index 607081b..0d76a2f 100644 --- a/game.js +++ b/game.js @@ -32,6 +32,9 @@ class FluteGame { this.targetY = 340; // Y position of the target line this.previewTime = 1.5; // seconds ahead to preview upcoming note + // Initialize SVG Flute + this.fluteSVG = null; + this.initializeCanvas(); this.setupEventListeners(); this.createSongButtons(); @@ -280,6 +283,11 @@ class FluteGame { await this.initializeAudio(); } + // Initialize SVG Flute if not already initialized + if (!this.fluteSVG) { + this.fluteSVG = new FluteSVG('flute-diagram'); + } + this.state = GameState.PLAYING; this.notes = this.createNoteObjects(this.currentSong); this.startTime = Date.now(); @@ -386,40 +394,21 @@ class FluteGame { (note.time - this.currentTime) <= this.previewTime ); - // Clear all preview highlights - VALID_FLUTE_KEYS.forEach(key => { - const keyElement = document.getElementById(`key-${key.toLowerCase()}`); - if (keyElement) { - keyElement.classList.remove('preview'); + // Use SVG flute preview if available + if (this.fluteSVG) { + if (upcomingNote) { + this.fluteSVG.showPreview(upcomingNote.requiredKeys); + } else { + this.fluteSVG.clearPreview(); } - }); - - // Add preview highlight to upcoming note keys - if (upcomingNote) { - upcomingNote.requiredKeys.forEach(key => { - const keyElement = document.getElementById(`key-${key.toLowerCase()}`); - if (keyElement) { - keyElement.classList.add('preview'); - } - }); } } showHitFeedback(note) { - // Visual feedback is handled through CSS animations - note.requiredKeys.forEach(key => { - const keyElement = document.getElementById(`key-${key.toLowerCase()}`); - if (keyElement) { - const indicator = keyElement.querySelector('.key-indicator'); - if (indicator) { - // Trigger animation by removing and re-adding - indicator.style.animation = 'none'; - setTimeout(() => { - indicator.style.animation = 'pulse 0.5s'; - }, 10); - } - } - }); + // Use SVG flute feedback if available + if (this.fluteSVG) { + this.fluteSVG.triggerHitFeedback(note.requiredKeys); + } } updateNotes() { diff --git a/index.html b/index.html index 090de30..12c15c8 100644 --- a/index.html +++ b/index.html @@ -41,34 +41,7 @@

How to Use:

-
- A -
-
-
- S -
-
-
- D -
-
-
- F -
-
-
- G -
-
-
- H -
-
-
- J -
-
+
@@ -91,6 +64,7 @@

🎵 Song Complete! 🎵

+ diff --git a/styles.css b/styles.css index 0aeb5d4..c4b613e 100644 --- a/styles.css +++ b/styles.css @@ -184,13 +184,50 @@ header h1 { #flute-diagram { display: flex; justify-content: center; - gap: 15px; + align-items: center; margin-bottom: 30px; - padding: 20px; - background: #f5f5f5; + padding: 30px 20px; + background: linear-gradient(to bottom, #f8f8f8 0%, #e8e8e8 100%); border-radius: 10px; + box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.1); + min-height: 200px; +} + +#flute-svg { + filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2)); +} + +/* SVG Flute Key Animations */ +.flute-key-svg { + transition: all 0.2s ease-in-out; +} + +.flute-key-svg .key-preview { + transition: opacity 0.3s ease-in-out; +} + +.flute-key-svg .key-highlight { + transition: opacity 0.2s ease-in-out; +} + +.flute-key-svg .key-highlight.hit-pulse { + animation: hit-pulse-animation 0.3s ease-out; +} + +@keyframes hit-pulse-animation { + 0% { + opacity: 1; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.9; + } } +/* Legacy flute key styles - kept for backwards compatibility */ + .flute-key { width: 80px; height: 80px;