Skip to content

Commit ed2ed04

Browse files
authored
Merge pull request #4 from jessephus/copilot/fix-8bc99ef3-2cc5-41f3-a359-b86be5ed5200
Add SVG Flute Fingering Diagram
2 parents 09b053a + 4d07550 commit ed2ed04

File tree

4 files changed

+357
-60
lines changed

4 files changed

+357
-60
lines changed

flute-svg.js

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
// SVG Flute Component Generator
2+
// Creates a detailed SVG representation of an orchestral flute with interactive fingering indicators
3+
4+
class FluteSVG {
5+
constructor(containerId) {
6+
this.container = document.getElementById(containerId);
7+
this.keyStates = {
8+
'A': false,
9+
'S': false,
10+
'D': false,
11+
'F': false,
12+
'G': false,
13+
'H': false,
14+
'J': false
15+
};
16+
// Animation constants
17+
this.ANIMATION_DURATION_MS = 300;
18+
19+
// Cache for DOM elements
20+
this.keyElements = {};
21+
this.previewElements = {};
22+
this.highlightElements = {};
23+
24+
this.createFluteSVG();
25+
this.cacheKeyElements();
26+
}
27+
28+
cacheKeyElements() {
29+
// Cache DOM elements to avoid repeated queries
30+
const keys = ['A', 'S', 'D', 'F', 'G', 'H', 'J'];
31+
keys.forEach(key => {
32+
const keyGroup = this.container.querySelector(`[data-key="${key}"]`);
33+
if (keyGroup) {
34+
this.keyElements[key] = keyGroup;
35+
this.previewElements[key] = keyGroup.querySelector('.key-preview');
36+
this.highlightElements[key] = keyGroup.querySelector('.key-highlight');
37+
}
38+
});
39+
}
40+
41+
createFluteSVG() {
42+
// Create SVG element
43+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
44+
svg.setAttribute('viewBox', '0 0 800 200');
45+
svg.setAttribute('id', 'flute-svg');
46+
svg.setAttribute('role', 'img');
47+
svg.setAttribute('aria-label', 'Interactive flute diagram showing finger positions for notes');
48+
svg.style.width = '100%';
49+
svg.style.height = 'auto';
50+
51+
// Create flute body (main tube)
52+
const fluteBody = this.createFluteBody();
53+
svg.appendChild(fluteBody);
54+
55+
// Create finger holes/keys representing each keyboard key
56+
// Flute keys from left to right: A, S, D, F, G, H, J
57+
const keyPositions = [
58+
{ id: 'A', x: 150, y: 100, label: 'A' }, // Left hand index finger
59+
{ id: 'S', x: 220, y: 100, label: 'S' }, // Left hand middle finger
60+
{ id: 'D', x: 290, y: 100, label: 'D' }, // Left hand ring finger
61+
{ id: 'F', x: 380, y: 100, label: 'F' }, // Right hand index finger
62+
{ id: 'G', x: 450, y: 100, label: 'G' }, // Right hand middle finger
63+
{ id: 'H', x: 520, y: 100, label: 'H' }, // Right hand ring finger
64+
{ id: 'J', x: 590, y: 100, label: 'J' } // Right hand pinky finger
65+
];
66+
67+
keyPositions.forEach(keyPos => {
68+
const keyGroup = this.createKey(keyPos);
69+
svg.appendChild(keyGroup);
70+
});
71+
72+
// Add embouchure (mouthpiece) on the left
73+
const embouchure = this.createEmbouchure();
74+
svg.appendChild(embouchure);
75+
76+
// Clear container and add SVG
77+
this.container.innerHTML = '';
78+
this.container.appendChild(svg);
79+
}
80+
81+
createFluteBody() {
82+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
83+
g.setAttribute('id', 'flute-body');
84+
85+
// Main tube (cylindrical body)
86+
const tube = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
87+
tube.setAttribute('x', '100');
88+
tube.setAttribute('y', '85');
89+
tube.setAttribute('width', '600');
90+
tube.setAttribute('height', '30');
91+
tube.setAttribute('rx', '15');
92+
tube.setAttribute('fill', 'url(#fluteGradient)');
93+
tube.setAttribute('stroke', '#999');
94+
tube.setAttribute('stroke-width', '2');
95+
96+
// Add gradient for metallic look
97+
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
98+
const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
99+
gradient.setAttribute('id', 'fluteGradient');
100+
gradient.setAttribute('x1', '0%');
101+
gradient.setAttribute('y1', '0%');
102+
gradient.setAttribute('x2', '0%');
103+
gradient.setAttribute('y2', '100%');
104+
105+
const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
106+
stop1.setAttribute('offset', '0%');
107+
stop1.setAttribute('style', 'stop-color:#e8e8e8;stop-opacity:1');
108+
109+
const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
110+
stop2.setAttribute('offset', '50%');
111+
stop2.setAttribute('style', 'stop-color:#d4d4d4;stop-opacity:1');
112+
113+
const stop3 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
114+
stop3.setAttribute('offset', '100%');
115+
stop3.setAttribute('style', 'stop-color:#c0c0c0;stop-opacity:1');
116+
117+
gradient.appendChild(stop1);
118+
gradient.appendChild(stop2);
119+
gradient.appendChild(stop3);
120+
defs.appendChild(gradient);
121+
122+
g.appendChild(defs);
123+
g.appendChild(tube);
124+
125+
// Add shine/highlight on top of tube
126+
const highlight = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
127+
highlight.setAttribute('x', '100');
128+
highlight.setAttribute('y', '87');
129+
highlight.setAttribute('width', '600');
130+
highlight.setAttribute('height', '8');
131+
highlight.setAttribute('rx', '4');
132+
highlight.setAttribute('fill', 'rgba(255, 255, 255, 0.4)');
133+
134+
g.appendChild(highlight);
135+
136+
return g;
137+
}
138+
139+
createKey(keyPos) {
140+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
141+
g.setAttribute('class', 'flute-key-svg');
142+
g.setAttribute('data-key', keyPos.id);
143+
144+
// Outer ring (key mechanism)
145+
const outerRing = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
146+
outerRing.setAttribute('cx', keyPos.x);
147+
outerRing.setAttribute('cy', keyPos.y);
148+
outerRing.setAttribute('r', '22');
149+
outerRing.setAttribute('fill', '#888');
150+
outerRing.setAttribute('stroke', '#666');
151+
outerRing.setAttribute('stroke-width', '1.5');
152+
153+
// Inner pad (finger hole cover)
154+
const innerPad = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
155+
innerPad.setAttribute('cx', keyPos.x);
156+
innerPad.setAttribute('cy', keyPos.y);
157+
innerPad.setAttribute('r', '18');
158+
innerPad.setAttribute('fill', '#b8b8b8');
159+
innerPad.setAttribute('stroke', '#999');
160+
innerPad.setAttribute('stroke-width', '1');
161+
innerPad.setAttribute('class', 'key-pad');
162+
163+
// Highlight overlay (shows when key is active)
164+
const highlight = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
165+
highlight.setAttribute('cx', keyPos.x);
166+
highlight.setAttribute('cy', keyPos.y);
167+
highlight.setAttribute('r', '18');
168+
highlight.setAttribute('fill', '#667eea');
169+
highlight.setAttribute('opacity', '0');
170+
highlight.setAttribute('class', 'key-highlight');
171+
172+
// Preview overlay (shows upcoming note)
173+
const preview = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
174+
preview.setAttribute('cx', keyPos.x);
175+
preview.setAttribute('cy', keyPos.y);
176+
preview.setAttribute('r', '18');
177+
preview.setAttribute('fill', '#FFD700');
178+
preview.setAttribute('opacity', '0');
179+
preview.setAttribute('class', 'key-preview');
180+
181+
g.appendChild(outerRing);
182+
g.appendChild(innerPad);
183+
g.appendChild(highlight);
184+
g.appendChild(preview);
185+
186+
return g;
187+
}
188+
189+
createEmbouchure() {
190+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
191+
g.setAttribute('id', 'embouchure');
192+
193+
// Head joint (wider section on left)
194+
const headJoint = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
195+
headJoint.setAttribute('x', '20');
196+
headJoint.setAttribute('y', '82');
197+
headJoint.setAttribute('width', '85');
198+
headJoint.setAttribute('height', '36');
199+
headJoint.setAttribute('rx', '18');
200+
headJoint.setAttribute('fill', 'url(#fluteGradient)');
201+
headJoint.setAttribute('stroke', '#999');
202+
headJoint.setAttribute('stroke-width', '2');
203+
204+
// Embouchure hole (blow hole)
205+
const embouchureHole = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
206+
embouchureHole.setAttribute('cx', '70');
207+
embouchureHole.setAttribute('cy', '100');
208+
embouchureHole.setAttribute('rx', '12');
209+
embouchureHole.setAttribute('ry', '6');
210+
embouchureHole.setAttribute('fill', '#333');
211+
embouchureHole.setAttribute('stroke', '#666');
212+
embouchureHole.setAttribute('stroke-width', '1');
213+
214+
g.appendChild(headJoint);
215+
g.appendChild(embouchureHole);
216+
217+
return g;
218+
}
219+
220+
// Update key states based on fingering
221+
updateFingering(requiredKeys) {
222+
// Reset all keys first
223+
Object.keys(this.keyStates).forEach(key => {
224+
this.keyStates[key] = false;
225+
});
226+
227+
// Set required keys
228+
requiredKeys.forEach(key => {
229+
this.keyStates[key] = true;
230+
});
231+
232+
this.renderKeyStates();
233+
}
234+
235+
// Show preview for upcoming note
236+
showPreview(requiredKeys) {
237+
// Clear all previews first using cached elements
238+
Object.values(this.previewElements).forEach(preview => {
239+
if (preview) {
240+
preview.setAttribute('opacity', '0');
241+
}
242+
});
243+
244+
// Show preview for required keys
245+
requiredKeys.forEach(key => {
246+
const preview = this.previewElements[key];
247+
if (preview) {
248+
preview.setAttribute('opacity', '0.7');
249+
}
250+
});
251+
}
252+
253+
// Clear all previews
254+
clearPreview() {
255+
Object.values(this.previewElements).forEach(preview => {
256+
if (preview) {
257+
preview.setAttribute('opacity', '0');
258+
}
259+
});
260+
}
261+
262+
// Render current key states
263+
renderKeyStates() {
264+
Object.keys(this.keyStates).forEach(key => {
265+
const highlight = this.highlightElements[key];
266+
267+
if (highlight) {
268+
if (this.keyStates[key]) {
269+
// Key is active (pressed)
270+
highlight.setAttribute('opacity', '0.9');
271+
} else {
272+
// Key is inactive
273+
highlight.setAttribute('opacity', '0');
274+
}
275+
}
276+
});
277+
}
278+
279+
// Trigger visual feedback animation for a note hit
280+
triggerHitFeedback(requiredKeys) {
281+
requiredKeys.forEach(key => {
282+
const highlight = this.highlightElements[key];
283+
284+
if (highlight) {
285+
// Use CSS class for animation instead of creating SVG animation elements
286+
highlight.setAttribute('opacity', '1');
287+
highlight.classList.add('hit-pulse');
288+
289+
// Remove class after animation completes
290+
setTimeout(() => {
291+
highlight.classList.remove('hit-pulse');
292+
highlight.setAttribute('opacity', '0.9');
293+
}, this.ANIMATION_DURATION_MS);
294+
}
295+
});
296+
}
297+
}

game.js

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ class FluteGame {
3232
this.targetY = 340; // Y position of the target line
3333
this.previewTime = 1.5; // seconds ahead to preview upcoming note
3434

35+
// Initialize SVG Flute
36+
this.fluteSVG = null;
37+
3538
this.initializeCanvas();
3639
this.setupEventListeners();
3740
this.createSongButtons();
@@ -280,6 +283,11 @@ class FluteGame {
280283
await this.initializeAudio();
281284
}
282285

286+
// Initialize SVG Flute if not already initialized
287+
if (!this.fluteSVG) {
288+
this.fluteSVG = new FluteSVG('flute-diagram');
289+
}
290+
283291
this.state = GameState.PLAYING;
284292
this.notes = this.createNoteObjects(this.currentSong);
285293
this.startTime = Date.now();
@@ -386,40 +394,21 @@ class FluteGame {
386394
(note.time - this.currentTime) <= this.previewTime
387395
);
388396

389-
// Clear all preview highlights
390-
VALID_FLUTE_KEYS.forEach(key => {
391-
const keyElement = document.getElementById(`key-${key.toLowerCase()}`);
392-
if (keyElement) {
393-
keyElement.classList.remove('preview');
397+
// Use SVG flute preview if available
398+
if (this.fluteSVG) {
399+
if (upcomingNote) {
400+
this.fluteSVG.showPreview(upcomingNote.requiredKeys);
401+
} else {
402+
this.fluteSVG.clearPreview();
394403
}
395-
});
396-
397-
// Add preview highlight to upcoming note keys
398-
if (upcomingNote) {
399-
upcomingNote.requiredKeys.forEach(key => {
400-
const keyElement = document.getElementById(`key-${key.toLowerCase()}`);
401-
if (keyElement) {
402-
keyElement.classList.add('preview');
403-
}
404-
});
405404
}
406405
}
407406

408407
showHitFeedback(note) {
409-
// Visual feedback is handled through CSS animations
410-
note.requiredKeys.forEach(key => {
411-
const keyElement = document.getElementById(`key-${key.toLowerCase()}`);
412-
if (keyElement) {
413-
const indicator = keyElement.querySelector('.key-indicator');
414-
if (indicator) {
415-
// Trigger animation by removing and re-adding
416-
indicator.style.animation = 'none';
417-
setTimeout(() => {
418-
indicator.style.animation = 'pulse 0.5s';
419-
}, 10);
420-
}
421-
}
422-
});
408+
// Use SVG flute feedback if available
409+
if (this.fluteSVG) {
410+
this.fluteSVG.triggerHitFeedback(note.requiredKeys);
411+
}
423412
}
424413

425414
updateNotes() {

0 commit comments

Comments
 (0)