Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 297 additions & 0 deletions flute-svg.js
Original file line number Diff line number Diff line change
@@ -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);
}
});
}
}
47 changes: 18 additions & 29 deletions game.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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() {
Expand Down
Loading