elements
const noteElements = container.querySelectorAll('.vf-stavenote');
noteElements.forEach((element, index) => {
element.addEventListener('click', () => {
if (onClick) {
onClick(`note-${index}`);
}
// Highlight selected note
element.classList.add('selected');
});
});
}
```
---
## Multi-Staff Rendering
For piano (treble + bass clefs):
```typescript
function renderPianoStaves(notes: { treble: NoteData[], bass: NoteData[] }, context) {
// Treble staff
const trebleStave = new Stave(10, 40, 400);
trebleStave.addClef('treble');
trebleStave.addTimeSignature('4/4');
trebleStave.setContext(context).draw();
const trebleNotes = notes.treble.map(n => new StaveNote({ keys: [n.pitch], duration: n.duration }));
const trebleVoice = new Voice({ num_beats: 4, beat_value: 4 }).addTickables(trebleNotes);
new Formatter().joinVoices([trebleVoice]).format([trebleVoice], 400);
trebleVoice.draw(context, trebleStave);
// Bass staff (connected with brace)
const bassStave = new Stave(10, 140, 400);
bassStave.addClef('bass');
bassStave.setContext(context).draw();
const bassNotes = notes.bass.map(n => new StaveNote({ keys: [n.pitch], duration: n.duration, clef: 'bass' }));
const bassVoice = new Voice({ num_beats: 4, beat_value: 4 }).addTickables(bassNotes);
new Formatter().joinVoices([bassVoice]).format([bassVoice], 400);
bassVoice.draw(context, bassStave);
// Draw brace connecting staves
const brace = new StaveConnector(trebleStave, bassStave).setType('brace');
brace.setContext(context).draw();
}
```
---
## Responsive Layout
### Auto-Resize on Window Resize
```typescript
useEffect(() => {
function handleResize() {
if (containerRef.current && rendererRef.current) {
const width = containerRef.current.offsetWidth;
rendererRef.current.resize(width, 600);
// Re-render notation with new width
reRenderNotation();
}
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
```
---
## Styling & Theming
### CSS for Notation
```css
.notation-canvas {
background: white;
border: 1px solid #ccc;
border-radius: 4px;
}
/* Highlight selected notes */
.vf-stavenote.selected {
fill: #007bff;
opacity: 0.7;
}
/* Hover effect */
.vf-stavenote:hover {
cursor: pointer;
opacity: 0.8;
}
```
---
## Performance Considerations
### Large Scores
- **Virtualization**: Only render visible measures (like react-window)
- **Pagination**: Show one page at a time, lazy-load other pages
- **Canvas vs SVG**: SVG is slower for 100+ measures, consider Canvas backend
```typescript
// Use Canvas backend for better performance
const renderer = new Renderer(container, Renderer.Backends.CANVAS);
```
### Measure-by-Measure Rendering
```typescript
function renderMeasure(measureData: MeasureData, yOffset: number) {
const stave = new Stave(10, yOffset, 400);
// ... render single measure
}
// Render only visible measures
const visibleMeasures = getVisibleMeasures(scrollPosition);
visibleMeasures.forEach((measure, index) => {
renderMeasure(measure, index * 120); // 120px per measure
});
```
---
## Accessibility
- **Keyboard Navigation**: Arrow keys to navigate notes
- **Screen Reader**: Add ARIA labels to SVG elements
- **Zoom**: Ctrl+/- to zoom in/out
```typescript
{/* VexFlow rendering */}
```
---
## Next Steps
1. Implement [Interactive Editor](editor.md) on top of rendered notation
2. Add [Playback System](playback.md) to sync audio with notation
3. Test with diverse MusicXML files (different keys, time signatures, multi-instrument)
See [Data Flow](data-flow.md) for state management of notation data.