UNPKG

35.7 kBJavaScriptView Raw
1// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
2//
3// ## Description
4// This file implements notes for standard notation. This consists of one or
5// more `NoteHeads`, an optional stem, and an optional flag.
6//
7// *Throughout these comments, a "note" refers to the entire `StaveNote`,
8// and a "key" refers to a specific pitch/notehead within a note.*
9//
10// See `tests/stavenote_tests.js` for usage examples.
11
12import { Vex } from './vex';
13import { Flow } from './tables';
14import { BoundingBox } from './boundingbox';
15import { Stem } from './stem';
16import { NoteHead } from './notehead';
17import { StemmableNote } from './stemmablenote';
18import { Modifier } from './modifier';
19import { Dot } from './dot';
20
21// To enable logging for this class. Set `Vex.Flow.StaveNote.DEBUG` to `true`.
22function L(...args) { if (StaveNote.DEBUG) Vex.L('Vex.Flow.StaveNote', args); }
23
24const getStemAdjustment = (note) => Stem.WIDTH / (2 * -note.getStemDirection());
25
26const isInnerNoteIndex = (note, index) =>
27 index === (note.getStemDirection() === Stem.UP ? note.keyProps.length - 1 : 0);
28
29// Helper methods for rest positioning in ModifierContext.
30function shiftRestVertical(rest, note, dir) {
31 const delta = (note.isrest ? 0.0 : 1.0) * dir;
32
33 rest.line += delta;
34 rest.maxLine += delta;
35 rest.minLine += delta;
36 rest.note.setKeyLine(0, rest.note.getKeyLine(0) + (delta));
37}
38
39// Called from formatNotes :: center a rest between two notes
40function centerRest(rest, noteU, noteL) {
41 const delta = rest.line - Vex.MidLine(noteU.minLine, noteL.maxLine);
42 rest.note.setKeyLine(0, rest.note.getKeyLine(0) - delta);
43 rest.line -= delta;
44 rest.maxLine -= delta;
45 rest.minLine -= delta;
46}
47
48export class StaveNote extends StemmableNote {
49 static get CATEGORY() { return 'stavenotes'; }
50 static get STEM_UP() { return Stem.UP; }
51 static get STEM_DOWN() { return Stem.DOWN; }
52 static get DEFAULT_LEDGER_LINE_OFFSET() { return 3; }
53
54 // ## Static Methods
55 //
56 // Format notes inside a ModifierContext.
57 static format(notes, state) {
58 if (!notes || notes.length < 2) return false;
59
60 // FIXME: VexFlow will soon require that a stave be set before formatting.
61 // Which, according to the below condition, means that following branch will
62 // always be taken and the rest of this function is dead code.
63 //
64 // Problematically, `Formatter#formatByY` was not designed to work for more
65 // than 2 voices (although, doesn't throw on this condition, just tries
66 // to power through).
67 //
68 // Based on the above:
69 // * 2 voices can be formatted *with or without* a stave being set but
70 // the output will be different
71 // * 3 voices can only be formatted *without* a stave
72 if (notes[0].getStave()) {
73 return StaveNote.formatByY(notes, state);
74 }
75
76 const notesList = [];
77
78 for (let i = 0; i < notes.length; i++) {
79 const props = notes[i].getKeyProps();
80 const line = props[0].line;
81 let minL = props[props.length - 1].line;
82 const stemDirection = notes[i].getStemDirection();
83 const stemMax = notes[i].getStemLength() / 10;
84 const stemMin = notes[i].getStemMinumumLength() / 10;
85
86 let maxL;
87 if (notes[i].isRest()) {
88 maxL = line + notes[i].glyph.line_above;
89 minL = line - notes[i].glyph.line_below;
90 } else {
91 maxL = stemDirection === 1
92 ? props[props.length - 1].line + stemMax
93 : props[props.length - 1].line;
94
95 minL = stemDirection === 1
96 ? props[0].line
97 : props[0].line - stemMax;
98 }
99
100 notesList.push({
101 line: props[0].line, // note/rest base line
102 maxLine: maxL, // note/rest upper bounds line
103 minLine: minL, // note/rest lower bounds line
104 isrest: notes[i].isRest(),
105 stemDirection,
106 stemMax, // Maximum (default) note stem length;
107 stemMin, // minimum note stem length
108 voice_shift: notes[i].getVoiceShiftWidth(),
109 is_displaced: notes[i].isDisplaced(), // note manually displaced
110 note: notes[i],
111 });
112 }
113
114 const voices = notesList.length;
115
116 let noteU = notesList[0];
117 const noteM = voices > 2 ? notesList[1] : null;
118 let noteL = voices > 2 ? notesList[2] : notesList[1];
119
120 // for two voice backward compatibility, ensure upper voice is stems up
121 // for three voices, the voices must be in order (upper, middle, lower)
122 if (voices === 2 && noteU.stemDirection === -1 && noteL.stemDirection === 1) {
123 noteU = notesList[1];
124 noteL = notesList[0];
125 }
126
127 const voiceXShift = Math.max(noteU.voice_shift, noteL.voice_shift);
128 let xShift = 0;
129 let stemDelta;
130
131 // Test for two voice note intersection
132 if (voices === 2) {
133 const lineSpacing = noteU.stemDirection === noteL.stemDirection ? 0.0 : 0.5;
134 // if top voice is a middle voice, check stem intersection with lower voice
135 if (noteU.stemDirection === noteL.stemDirection &&
136 noteU.minLine <= noteL.maxLine) {
137 if (!noteU.isrest) {
138 stemDelta = Math.abs(noteU.line - (noteL.maxLine + 0.5));
139 stemDelta = Math.max(stemDelta, noteU.stemMin);
140 noteU.minLine = noteU.line - stemDelta;
141 noteU.note.setStemLength(stemDelta * 10);
142 }
143 }
144 if (noteU.minLine <= noteL.maxLine + lineSpacing) {
145 if (noteU.isrest) {
146 // shift rest up
147 shiftRestVertical(noteU, noteL, 1);
148 } else if (noteL.isrest) {
149 // shift rest down
150 shiftRestVertical(noteL, noteU, -1);
151 } else {
152 xShift = voiceXShift;
153 if (noteU.stemDirection === noteL.stemDirection) {
154 // upper voice is middle voice, so shift it right
155 noteU.note.setXShift(xShift + 3);
156 } else {
157 // shift lower voice right
158 noteL.note.setXShift(xShift);
159 }
160 }
161 }
162
163 // format complete
164 return true;
165 }
166
167 // Check middle voice stem intersection with lower voice
168 if (noteM !== null && noteM.minLine < noteL.maxLine + 0.5) {
169 if (!noteM.isrest) {
170 stemDelta = Math.abs(noteM.line - (noteL.maxLine + 0.5));
171 stemDelta = Math.max(stemDelta, noteM.stemMin);
172 noteM.minLine = noteM.line - stemDelta;
173 noteM.note.setStemLength(stemDelta * 10);
174 }
175 }
176
177 // For three voices, test if rests can be repositioned
178 //
179 // Special case 1 :: middle voice rest between two notes
180 //
181 if (noteM.isrest && !noteU.isrest && !noteL.isrest) {
182 if (noteU.minLine <= noteM.maxLine || noteM.minLine <= noteL.maxLine) {
183 const restHeight = noteM.maxLine - noteM.minLine;
184 const space = noteU.minLine - noteL.maxLine;
185 if (restHeight < space) {
186 // center middle voice rest between the upper and lower voices
187 centerRest(noteM, noteU, noteL);
188 } else {
189 xShift = voiceXShift + 3; // shift middle rest right
190 noteM.note.setXShift(xShift);
191 }
192 // format complete
193 return true;
194 }
195 }
196
197 // Special case 2 :: all voices are rests
198 if (noteU.isrest && noteM.isrest && noteL.isrest) {
199 // Shift upper voice rest up
200 shiftRestVertical(noteU, noteM, 1);
201 // Shift lower voice rest down
202 shiftRestVertical(noteL, noteM, -1);
203 // format complete
204 return true;
205 }
206
207 // Test if any other rests can be repositioned
208 if (noteM.isrest && noteU.isrest && noteM.minLine <= noteL.maxLine) {
209 // Shift middle voice rest up
210 shiftRestVertical(noteM, noteL, 1);
211 }
212 if (noteM.isrest && noteL.isrest && noteU.minLine <= noteM.maxLine) {
213 // Shift middle voice rest down
214 shiftRestVertical(noteM, noteU, -1);
215 }
216 if (noteU.isrest && noteU.minLine <= noteM.maxLine) {
217 // shift upper voice rest up;
218 shiftRestVertical(noteU, noteM, 1);
219 }
220 if (noteL.isrest && noteM.minLine <= noteL.maxLine) {
221 // shift lower voice rest down
222 shiftRestVertical(noteL, noteM, -1);
223 }
224
225 // If middle voice intersects upper or lower voice
226 if ((!noteU.isrest && !noteM.isrest && noteU.minLine <= noteM.maxLine + 0.5) ||
227 (!noteM.isrest && !noteL.isrest && noteM.minLine <= noteL.maxLine)) {
228 xShift = voiceXShift + 3; // shift middle note right
229 noteM.note.setXShift(xShift);
230 }
231
232 return true;
233 }
234
235 static formatByY(notes, state) {
236 // NOTE: this function does not support more than two voices per stave
237 // use with care.
238 let hasStave = true;
239
240 for (let i = 0; i < notes.length; i++) {
241 hasStave = hasStave && notes[i].getStave() != null;
242 }
243
244 if (!hasStave) {
245 throw new Vex.RERR(
246 'Stave Missing',
247 'All notes must have a stave - Vex.Flow.ModifierContext.formatMultiVoice!'
248 );
249 }
250
251 let xShift = 0;
252
253 for (let i = 0; i < notes.length - 1; i++) {
254 let topNote = notes[i];
255 let bottomNote = notes[i + 1];
256
257 if (topNote.getStemDirection() === Stem.DOWN) {
258 topNote = notes[i + 1];
259 bottomNote = notes[i];
260 }
261
262 const topKeys = topNote.getKeyProps();
263 const bottomKeys = bottomNote.getKeyProps();
264
265 const HALF_NOTEHEAD_HEIGHT = 0.5;
266
267 // `keyProps` and `stave.getYForLine` have different notions of a `line`
268 // so we have to convert the keyProps value by subtracting 5.
269 // See https://github.com/0xfe/vexflow/wiki/Development-Gotchas
270 //
271 // We also extend the y for each note by a half notehead because the
272 // notehead's origin is centered
273 const topNoteBottomY = topNote
274 .getStave()
275 .getYForLine(5 - topKeys[0].line + HALF_NOTEHEAD_HEIGHT);
276
277 const bottomNoteTopY = bottomNote
278 .getStave()
279 .getYForLine(5 - bottomKeys[bottomKeys.length - 1].line - HALF_NOTEHEAD_HEIGHT);
280
281 const areNotesColliding = bottomNoteTopY - topNoteBottomY < 0;
282
283 if (areNotesColliding) {
284 xShift = topNote.getVoiceShiftWidth() + 2;
285 bottomNote.setXShift(xShift);
286 }
287 }
288
289 state.right_shift += xShift;
290 }
291
292 static postFormat(notes) {
293 if (!notes) return false;
294
295 notes.forEach(note => note.postFormat());
296
297 return true;
298 }
299
300 constructor(noteStruct) {
301 super(noteStruct);
302 this.setAttribute('type', 'StaveNote');
303
304 this.keys = noteStruct.keys;
305 this.clef = noteStruct.clef;
306 this.octave_shift = noteStruct.octave_shift;
307 this.beam = null;
308
309 // Pull note rendering properties
310 this.glyph = Flow.getGlyphProps(this.duration, this.noteType);
311
312 if (!this.glyph) {
313 throw new Vex.RuntimeError(
314 'BadArguments',
315 `Invalid note initialization data (No glyph found): ${JSON.stringify(noteStruct)}`
316 );
317 }
318
319 // if true, displace note to right
320 this.displaced = false;
321 this.dot_shiftY = 0;
322 // per-pitch properties
323 this.keyProps = [];
324 // for displaced ledger lines
325 this.use_default_head_x = false;
326
327 // Drawing
328 this.note_heads = [];
329 this.modifiers = [];
330
331 Vex.Merge(this.render_options, {
332 // font size for note heads and rests
333 glyph_font_scale: noteStruct.glyph_font_scale || Flow.DEFAULT_NOTATION_FONT_SCALE,
334 // number of stroke px to the left and right of head
335 stroke_px: noteStruct.stroke_px || StaveNote.DEFAULT_LEDGER_LINE_OFFSET,
336 });
337
338 this.calculateKeyProps();
339 this.buildStem();
340
341 // Set the stem direction
342 if (noteStruct.auto_stem) {
343 this.autoStem();
344 } else {
345 this.setStemDirection(noteStruct.stem_direction);
346 }
347 this.reset();
348 this.buildFlag();
349 }
350
351 reset() {
352 super.reset();
353
354 // Save prior noteHead styles & reapply them after making new noteheads.
355 const noteHeadStyles = this.note_heads.map(noteHead => noteHead.getStyle());
356 this.buildNoteHeads();
357 this.note_heads.forEach((noteHead, index) => noteHead.setStyle(noteHeadStyles[index]));
358
359 if (this.stave) {
360 this.note_heads.forEach(head => head.setStave(this.stave));
361 }
362 this.calcNoteDisplacements();
363 }
364
365 setBeam(beam) {
366 this.beam = beam;
367 this.calcNoteDisplacements();
368 return this;
369 }
370
371 getCategory() { return StaveNote.CATEGORY; }
372
373 // Builds a `Stem` for the note
374 buildStem() {
375 this.setStem(new Stem({ hide: !!this.isRest(), }));
376 }
377
378 // Builds a `NoteHead` for each key in the note
379 buildNoteHeads() {
380 this.note_heads = [];
381 const stemDirection = this.getStemDirection();
382 const keys = this.getKeys();
383
384 let lastLine = null;
385 let lineDiff = null;
386 let displaced = false;
387
388 // Draw notes from bottom to top.
389
390 // For down-stem notes, we draw from top to bottom.
391 let start;
392 let end;
393 let step;
394 if (stemDirection === Stem.UP) {
395 start = 0;
396 end = keys.length;
397 step = 1;
398 } else if (stemDirection === Stem.DOWN) {
399 start = keys.length - 1;
400 end = -1;
401 step = -1;
402 }
403
404 for (let i = start; i !== end; i += step) {
405 const noteProps = this.keyProps[i];
406 const line = noteProps.line;
407
408 // Keep track of last line with a note head, so that consecutive heads
409 // are correctly displaced.
410 if (lastLine === null) {
411 lastLine = line;
412 } else {
413 lineDiff = Math.abs(lastLine - line);
414 if (lineDiff === 0 || lineDiff === 0.5) {
415 displaced = !displaced;
416 } else {
417 displaced = false;
418 this.use_default_head_x = true;
419 }
420 }
421 lastLine = line;
422
423 const notehead = new NoteHead({
424 duration: this.duration,
425 note_type: this.noteType,
426 displaced,
427 stem_direction: stemDirection,
428 custom_glyph_code: noteProps.code,
429 glyph_font_scale: this.render_options.glyph_font_scale,
430 x_shift: noteProps.shift_right,
431 stem_up_x_offset: noteProps.stem_up_x_offset,
432 stem_down_x_offset: noteProps.stem_down_x_offset,
433 line: noteProps.line,
434 });
435
436 this.note_heads[i] = notehead;
437 }
438 }
439
440 // Automatically sets the stem direction based on the keys in the note
441 autoStem() {
442 // Figure out optimal stem direction based on given notes
443 this.minLine = this.keyProps[0].line;
444 this.maxLine = this.keyProps[this.keyProps.length - 1].line;
445
446 const MIDDLE_LINE = 3;
447 const decider = (this.minLine + this.maxLine) / 2;
448 const stemDirection = decider < MIDDLE_LINE ? Stem.UP : Stem.DOWN;
449
450 this.setStemDirection(stemDirection);
451 }
452
453 // Calculates and stores the properties for each key in the note
454 calculateKeyProps() {
455 let lastLine = null;
456 for (let i = 0; i < this.keys.length; ++i) {
457 const key = this.keys[i];
458
459 // All rests use the same position on the line.
460 // if (this.glyph.rest) key = this.glyph.position;
461 if (this.glyph.rest) this.glyph.position = key;
462
463 const options = { octave_shift: this.octave_shift || 0 };
464 const props = Flow.keyProperties(key, this.clef, options);
465
466 if (!props) {
467 throw new Vex.RuntimeError('BadArguments', `Invalid key for note properties: ${key}`);
468 }
469
470 // Override line placement for default rests
471 if (props.key === 'R') {
472 if (this.duration === '1' || this.duration === 'w') {
473 props.line = 4;
474 } else {
475 props.line = 3;
476 }
477 }
478
479 // Calculate displacement of this note
480 const line = props.line;
481 if (lastLine === null) {
482 lastLine = line;
483 } else {
484 if (Math.abs(lastLine - line) === 0.5) {
485 this.displaced = true;
486 props.displaced = true;
487
488 // Have to mark the previous note as
489 // displaced as well, for modifier placement
490 if (this.keyProps.length > 0) {
491 this.keyProps[i - 1].displaced = true;
492 }
493 }
494 }
495
496 lastLine = line;
497 this.keyProps.push(props);
498 }
499
500 // Sort the notes from lowest line to highest line
501 lastLine = -Infinity;
502 this.keyProps.forEach(key => {
503 if (key.line < lastLine) {
504 Vex.W(
505 'Unsorted keys in note will be sorted. ' +
506 'See https://github.com/0xfe/vexflow/issues/104 for details.'
507 );
508 }
509 lastLine = key.line;
510 });
511 this.keyProps.sort((a, b) => a.line - b.line);
512 }
513
514 // Get the `BoundingBox` for the entire note
515 getBoundingBox() {
516 if (!this.preFormatted) {
517 throw new Vex.RERR('UnformattedNote', "Can't call getBoundingBox on an unformatted note.");
518 }
519
520 const { width: w, modLeftPx, leftDisplacedHeadPx } = this.getMetrics();
521 const x = this.getAbsoluteX() - modLeftPx - leftDisplacedHeadPx;
522
523 let minY = 0;
524 let maxY = 0;
525 const halfLineSpacing = this.getStave().getSpacingBetweenLines() / 2;
526 const lineSpacing = halfLineSpacing * 2;
527
528 if (this.isRest()) {
529 const y = this.ys[0];
530 const frac = Flow.durationToFraction(this.duration);
531 if (frac.equals(1) || frac.equals(2)) {
532 minY = y - halfLineSpacing;
533 maxY = y + halfLineSpacing;
534 } else {
535 minY = y - (this.glyph.line_above * lineSpacing);
536 maxY = y + (this.glyph.line_below * lineSpacing);
537 }
538 } else if (this.glyph.stem) {
539 const ys = this.getStemExtents();
540 ys.baseY += halfLineSpacing * this.stem_direction;
541 minY = Math.min(ys.topY, ys.baseY);
542 maxY = Math.max(ys.topY, ys.baseY);
543 } else {
544 minY = null;
545 maxY = null;
546
547 for (let i = 0; i < this.ys.length; ++i) {
548 const yy = this.ys[i];
549 if (i === 0) {
550 minY = yy;
551 maxY = yy;
552 } else {
553 minY = Math.min(yy, minY);
554 maxY = Math.max(yy, maxY);
555 }
556 }
557 minY -= halfLineSpacing;
558 maxY += halfLineSpacing;
559 }
560
561 return new BoundingBox(x, minY, w, maxY - minY);
562 }
563
564 // Gets the line number of the top or bottom note in the chord.
565 // If `isTopNote` is `true` then get the top note
566 getLineNumber(isTopNote) {
567 if (!this.keyProps.length) {
568 throw new Vex.RERR(
569 'NoKeyProps', "Can't get bottom note line, because note is not initialized properly."
570 );
571 }
572
573 let resultLine = this.keyProps[0].line;
574
575 // No precondition assumed for sortedness of keyProps array
576 for (let i = 0; i < this.keyProps.length; i++) {
577 const thisLine = this.keyProps[i].line;
578 if (isTopNote) {
579 if (thisLine > resultLine) resultLine = thisLine;
580 } else {
581 if (thisLine < resultLine) resultLine = thisLine;
582 }
583 }
584
585 return resultLine;
586 }
587
588 // Determine if current note is a rest
589 isRest() { return this.glyph.rest; }
590
591 // Determine if the current note is a chord
592 isChord() { return !this.isRest() && this.keys.length > 1; }
593
594 // Determine if the `StaveNote` has a stem
595 hasStem() { return this.glyph.stem; }
596
597 hasFlag() {
598 return super.hasFlag() && !this.isRest();
599 }
600
601 getStemX() {
602 if (this.noteType === 'r') {
603 return this.getCenterGlyphX();
604 } else {
605 // We adjust the origin of the stem because we want the stem left-aligned
606 // with the notehead if stemmed-down, and right-aligned if stemmed-up
607 return super.getStemX() + getStemAdjustment(this);
608 }
609 }
610
611 // Get the `y` coordinate for text placed on the top/bottom of a
612 // note at a desired `text_line`
613 getYForTopText(textLine) {
614 const extents = this.getStemExtents();
615 return Math.min(
616 this.stave.getYForTopText(textLine),
617 extents.topY - (this.render_options.annotation_spacing * (textLine + 1))
618 );
619 }
620 getYForBottomText(textLine) {
621 const extents = this.getStemExtents();
622 return Math.max(
623 this.stave.getYForTopText(textLine),
624 extents.baseY + (this.render_options.annotation_spacing * (textLine))
625 );
626 }
627
628 // Sets the current note to the provided `stave`. This applies
629 // `y` values to the `NoteHeads`.
630 setStave(stave) {
631 super.setStave(stave);
632
633 const ys = this.note_heads.map(notehead => {
634 notehead.setStave(stave);
635 return notehead.getY();
636 });
637
638 this.setYs(ys);
639
640 if (this.stem) {
641 const { y_top, y_bottom } = this.getNoteHeadBounds();
642 this.stem.setYBounds(y_top, y_bottom);
643 }
644
645 return this;
646 }
647
648 // Get the pitches in the note
649 getKeys() { return this.keys; }
650
651 // Get the properties for all the keys in the note
652 getKeyProps() {
653 return this.keyProps;
654 }
655
656 // Check if note is shifted to the right
657 isDisplaced() {
658 return this.displaced;
659 }
660
661 // Sets whether shift note to the right. `displaced` is a `boolean`
662 setNoteDisplaced(displaced) {
663 this.displaced = displaced;
664 return this;
665 }
666
667 // Get the starting `x` coordinate for a `StaveTie`
668 getTieRightX() {
669 let tieStartX = this.getAbsoluteX();
670 tieStartX += this.getGlyphWidth() + this.x_shift + this.rightDisplacedHeadPx;
671 if (this.modifierContext) tieStartX += this.modifierContext.getRightShift();
672 return tieStartX;
673 }
674
675 // Get the ending `x` coordinate for a `StaveTie`
676 getTieLeftX() {
677 let tieEndX = this.getAbsoluteX();
678 tieEndX += this.x_shift - this.leftDisplacedHeadPx;
679 return tieEndX;
680 }
681
682 // Get the stave line on which to place a rest
683 getLineForRest() {
684 let restLine = this.keyProps[0].line;
685 if (this.keyProps.length > 1) {
686 const lastLine = this.keyProps[this.keyProps.length - 1].line;
687 const top = Math.max(restLine, lastLine);
688 const bot = Math.min(restLine, lastLine);
689 restLine = Vex.MidLine(top, bot);
690 }
691
692 return restLine;
693 }
694
695 // Get the default `x` and `y` coordinates for the provided `position`
696 // and key `index`
697 getModifierStartXY(position, index, options) {
698 options = options || {};
699 if (!this.preFormatted) {
700 throw new Vex.RERR('UnformattedNote', "Can't call GetModifierStartXY on an unformatted note");
701 }
702
703 if (this.ys.length === 0) {
704 throw new Vex.RERR('NoYValues', 'No Y-Values calculated for this note.');
705 }
706
707 const { ABOVE, BELOW, LEFT, RIGHT } = Modifier.Position;
708 let x = 0;
709 if (position === LEFT) {
710 // FIXME: Left modifier padding, move to font file
711 x = -1 * 2;
712 } else if (position === RIGHT) {
713 // FIXME: Right modifier padding, move to font file
714 x = this.getGlyphWidth() + this.x_shift + 2;
715
716 if (this.stem_direction === Stem.UP && this.hasFlag() &&
717 (options.forceFlagRight || isInnerNoteIndex(this, index))) {
718 x += this.flag.getMetrics().width;
719 }
720 } else if (position === BELOW || position === ABOVE) {
721 x = this.getGlyphWidth() / 2;
722 }
723
724 return {
725 x: this.getAbsoluteX() + x,
726 y: this.ys[index],
727 };
728 }
729
730 // Sets the style of the complete StaveNote, including all keys
731 // and the stem.
732 setStyle(style) {
733 super.setStyle(style);
734 this.note_heads.forEach(notehead => notehead.setStyle(style));
735 this.stem.setStyle(style);
736 }
737
738 setStemStyle(style) {
739 const stem = this.getStem();
740 stem.setStyle(style);
741 }
742 getStemStyle() { return this.stem.getStyle(); }
743
744 setLedgerLineStyle(style) { this.ledgerLineStyle = style; }
745 getLedgerLineStyle() { return this.ledgerLineStyle; }
746
747 setFlagStyle(style) { this.flagStyle = style; }
748 getFlagStyle() { return this.flagStyle; }
749
750 // Sets the notehead at `index` to the provided coloring `style`.
751 //
752 // `style` is an `object` with the following properties: `shadowColor`,
753 // `shadowBlur`, `fillStyle`, `strokeStyle`
754 setKeyStyle(index, style) {
755 this.note_heads[index].setStyle(style);
756 return this;
757 }
758
759 setKeyLine(index, line) {
760 this.keyProps[index].line = line;
761 this.reset();
762 return this;
763 }
764
765 getKeyLine(index) {
766 return this.keyProps[index].line;
767 }
768
769 // Add self to modifier context. `mContext` is the `ModifierContext`
770 // to be added to.
771 addToModifierContext(mContext) {
772 this.setModifierContext(mContext);
773 for (let i = 0; i < this.modifiers.length; ++i) {
774 this.modifierContext.addModifier(this.modifiers[i]);
775 }
776 this.modifierContext.addModifier(this);
777 this.setPreFormatted(false);
778 return this;
779 }
780
781 // Generic function to add modifiers to a note
782 //
783 // Parameters:
784 // * `index`: The index of the key that we're modifying
785 // * `modifier`: The modifier to add
786 addModifier(index, modifier) {
787 modifier.setNote(this);
788 modifier.setIndex(index);
789 this.modifiers.push(modifier);
790 this.setPreFormatted(false);
791 return this;
792 }
793
794 // Helper function to add an accidental to a key
795 addAccidental(index, accidental) {
796 return this.addModifier(index, accidental);
797 }
798
799 // Helper function to add an articulation to a key
800 addArticulation(index, articulation) {
801 return this.addModifier(index, articulation);
802 }
803
804 // Helper function to add an annotation to a key
805 addAnnotation(index, annotation) {
806 return this.addModifier(index, annotation);
807 }
808
809 // Helper function to add a dot on a specific key
810 addDot(index) {
811 const dot = new Dot();
812 dot.setDotShiftY(this.glyph.dot_shiftY);
813 this.dots++;
814 return this.addModifier(index, dot);
815 }
816
817 // Convenience method to add dot to all keys in note
818 addDotToAll() {
819 for (let i = 0; i < this.keys.length; ++i) {
820 this.addDot(i);
821 }
822 return this;
823 }
824
825 // Get all accidentals in the `ModifierContext`
826 getAccidentals() {
827 return this.modifierContext.getModifiers('accidentals');
828 }
829
830 // Get all dots in the `ModifierContext`
831 getDots() {
832 return this.modifierContext.getModifiers('dots');
833 }
834
835 // Get the width of the note if it is displaced. Used for `Voice`
836 // formatting
837 getVoiceShiftWidth() {
838 // TODO: may need to accomodate for dot here.
839 return this.getGlyphWidth() * (this.displaced ? 2 : 1);
840 }
841
842 // Calculates and sets the extra pixels to the left or right
843 // if the note is displaced.
844 calcNoteDisplacements() {
845 this.setLeftDisplacedHeadPx(
846 this.displaced && this.stem_direction === Stem.DOWN
847 ? this.getGlyphWidth()
848 : 0
849 );
850
851 // For upstems with flags, the extra space is unnecessary, since it's taken
852 // up by the flag.
853 this.setRightDisplacedHeadPx(
854 !this.hasFlag() && this.displaced && this.stem_direction === Stem.UP
855 ? this.getGlyphWidth()
856 : 0
857 );
858 }
859
860 // Pre-render formatting
861 preFormat() {
862 if (this.preFormatted) return;
863 if (this.modifierContext) this.modifierContext.preFormat();
864
865 let width = this.getGlyphWidth() + this.leftDisplacedHeadPx + this.rightDisplacedHeadPx;
866
867 // For upward flagged notes, the width of the flag needs to be added
868 if (this.glyph.flag && this.beam === null && this.stem_direction === Stem.UP) {
869 width += this.getGlyphWidth();
870 // TODO: Add flag width as a separate metric
871 }
872
873 this.setWidth(width);
874 this.setPreFormatted(true);
875 }
876
877 /**
878 * @typedef {Object} noteHeadBounds
879 * @property {number} y_top the highest notehead bound
880 * @property {number} y_bottom the lowest notehead bound
881 * @property {number|Null} displaced_x the starting x for displaced noteheads
882 * @property {number|Null} non_displaced_x the starting x for non-displaced noteheads
883 * @property {number} highest_line the highest notehead line in traditional music line
884 * numbering (bottom line = 1, top line = 5)
885 * @property {number} lowest_line the lowest notehead line
886 * @property {number|false} highest_displaced_line the highest staff line number
887 * for a displaced notehead
888 * @property {number|false} lowest_displaced_line
889 * @property {number} highest_non_displaced_line
890 * @property {number} lowest_non_displaced_line
891 */
892
893 /**
894 * Get the staff line and y value for the highest & lowest noteheads
895 * @returns {noteHeadBounds}
896 */
897 getNoteHeadBounds() {
898 // Top and bottom Y values for stem.
899 let yTop = null;
900 let yBottom = null;
901 let nonDisplacedX = null;
902 let displacedX = null;
903
904 let highestLine = this.stave.getNumLines();
905 let lowestLine = 1;
906 let highestDisplacedLine = false;
907 let lowestDisplacedLine = false;
908 let highestNonDisplacedLine = highestLine;
909 let lowestNonDisplacedLine = lowestLine;
910
911 this.note_heads.forEach(notehead => {
912 const line = notehead.getLine();
913 const y = notehead.getY();
914
915 if (yTop === null || y < yTop) {
916 yTop = y;
917 }
918
919 if (yBottom === null || y > yBottom) {
920 yBottom = y;
921 }
922
923 if (displacedX === null && notehead.isDisplaced()) {
924 displacedX = notehead.getAbsoluteX();
925 }
926
927 if (nonDisplacedX === null && !notehead.isDisplaced()) {
928 nonDisplacedX = notehead.getAbsoluteX();
929 }
930
931 highestLine = line > highestLine ? line : highestLine;
932 lowestLine = line < lowestLine ? line : lowestLine;
933
934 if (notehead.isDisplaced()) {
935 highestDisplacedLine = (highestDisplacedLine === false) ?
936 line : Math.max(line, highestDisplacedLine);
937 lowestDisplacedLine = (lowestDisplacedLine === false) ?
938 line : Math.min(line, lowestDisplacedLine);
939 } else {
940 highestNonDisplacedLine = Math.max(line, highestNonDisplacedLine);
941 lowestNonDisplacedLine = Math.min(line, lowestNonDisplacedLine);
942 }
943 }, this);
944
945 return {
946 y_top: yTop,
947 y_bottom: yBottom,
948 displaced_x: displacedX,
949 non_displaced_x: nonDisplacedX,
950 highest_line: highestLine,
951 lowest_line: lowestLine,
952 highest_displaced_line: highestDisplacedLine,
953 lowest_displaced_line: lowestDisplacedLine,
954 highest_non_displaced_line: highestNonDisplacedLine,
955 lowest_non_displaced_line: lowestNonDisplacedLine,
956 };
957 }
958
959 // Get the starting `x` coordinate for the noteheads
960 getNoteHeadBeginX() {
961 return this.getAbsoluteX() + this.x_shift;
962 }
963
964 // Get the ending `x` coordinate for the noteheads
965 getNoteHeadEndX() {
966 const xBegin = this.getNoteHeadBeginX();
967 return xBegin + this.getGlyphWidth();
968 }
969
970 // Draw the ledger lines between the stave and the highest/lowest keys
971 drawLedgerLines() {
972 const {
973 stave, glyph,
974 render_options: { stroke_px },
975 context: ctx,
976 } = this;
977
978 const width = glyph.getWidth() + (stroke_px * 2);
979 const doubleWidth = 2 * (glyph.getWidth() + stroke_px) - (Stem.WIDTH / 2);
980
981 if (this.isRest()) return;
982 if (!ctx) {
983 throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
984 }
985
986 const {
987 highest_line,
988 lowest_line,
989 highest_displaced_line,
990 highest_non_displaced_line,
991 lowest_displaced_line,
992 lowest_non_displaced_line,
993 displaced_x,
994 non_displaced_x,
995 } = this.getNoteHeadBounds();
996
997 const min_x = Math.min(displaced_x, non_displaced_x);
998
999 const drawLedgerLine = (y, normal, displaced) => {
1000 let x;
1001 if (displaced && normal) x = min_x - stroke_px;
1002 else if (normal) x = non_displaced_x - stroke_px;
1003 else x = displaced_x - stroke_px;
1004 const ledgerWidth = (normal && displaced) ? doubleWidth : width;
1005
1006 ctx.beginPath();
1007 ctx.moveTo(x, y);
1008 ctx.lineTo(x + ledgerWidth, y);
1009 ctx.stroke();
1010 };
1011
1012 const style = { ...stave.getStyle() || {}, ...this.getLedgerLineStyle() || {} };
1013 this.applyStyle(ctx, style);
1014
1015 // Draw ledger lines below the staff:
1016 for (let line = 6; line <= highest_line; ++line) {
1017 const normal = (non_displaced_x !== null) && (line <= highest_non_displaced_line);
1018 const displaced = (displaced_x !== null) && (line <= highest_displaced_line);
1019 drawLedgerLine(stave.getYForNote(line), normal, displaced);
1020 }
1021
1022 // Draw ledger lines above the staff:
1023 for (let line = 0; line >= lowest_line; --line) {
1024 const normal = (non_displaced_x !== null) && (line >= lowest_non_displaced_line);
1025 const displaced = (displaced_x !== null) && (line >= lowest_displaced_line);
1026 drawLedgerLine(stave.getYForNote(line), normal, displaced);
1027 }
1028
1029 this.restoreStyle(ctx, style);
1030 }
1031
1032 // Draw all key modifiers
1033 drawModifiers() {
1034 if (!this.context) {
1035 throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
1036 }
1037
1038 const ctx = this.context;
1039 ctx.openGroup('modifiers');
1040 for (let i = 0; i < this.modifiers.length; i++) {
1041 const modifier = this.modifiers[i];
1042 const notehead = this.note_heads[modifier.getIndex()];
1043 const noteheadStyle = notehead.getStyle();
1044 notehead.applyStyle(ctx, noteheadStyle);
1045 modifier.setContext(ctx);
1046 modifier.drawWithStyle();
1047 notehead.restoreStyle(ctx, noteheadStyle);
1048 }
1049 ctx.closeGroup();
1050 }
1051
1052 // Draw the flag for the note
1053 drawFlag() {
1054 const { stem, beam, context: ctx } = this;
1055
1056 if (!ctx) {
1057 throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
1058 }
1059
1060 const shouldRenderFlag = beam === null;
1061 const glyph = this.getGlyph();
1062
1063 if (glyph.flag && shouldRenderFlag) {
1064 const { y_top, y_bottom } = this.getNoteHeadBounds();
1065 const noteStemHeight = stem.getHeight();
1066 const flagX = this.getStemX();
1067 // FIXME: What's with the magic +/- 2
1068 const flagY = this.getStemDirection() === Stem.DOWN
1069 // Down stems have flags on the left
1070 ? y_top - noteStemHeight + 2
1071 // Up stems have flags on the eft.
1072 : y_bottom - noteStemHeight - 2;
1073
1074 // Draw the Flag
1075 ctx.openGroup('flag', null, { pointerBBox: true });
1076 this.applyStyle(ctx, this.getFlagStyle() || false);
1077 this.flag.render(ctx, flagX, flagY);
1078 this.restoreStyle(ctx, this.getFlagStyle() || false);
1079 ctx.closeGroup();
1080 }
1081 }
1082
1083 // Draw the NoteHeads
1084 drawNoteHeads() {
1085 this.note_heads.forEach(notehead => {
1086 this.context.openGroup('notehead', null, { pointerBBox: true });
1087 notehead.setContext(this.context).draw();
1088 this.context.closeGroup();
1089 });
1090 }
1091
1092 drawStem(stemStruct) {
1093 // GCR TODO: I can't find any context in which this is called with the stemStruct
1094 // argument in the codebase or tests. Nor can I find a case where super.drawStem
1095 // is called at all. Perhaps these should be removed?
1096 if (!this.context) {
1097 throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
1098 }
1099
1100 if (stemStruct) {
1101 this.setStem(new Stem(stemStruct));
1102 }
1103
1104 this.context.openGroup('stem', null, { pointerBBox: true });
1105 this.stem.setContext(this.context).draw();
1106 this.context.closeGroup();
1107 }
1108
1109 // Draws all the `StaveNote` parts. This is the main drawing method.
1110 draw() {
1111 if (!this.context) {
1112 throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
1113 }
1114 if (!this.stave) {
1115 throw new Vex.RERR('NoStave', "Can't draw without a stave.");
1116 }
1117 if (this.ys.length === 0) {
1118 throw new Vex.RERR('NoYValues', "Can't draw note without Y values.");
1119 }
1120
1121 const xBegin = this.getNoteHeadBeginX();
1122 const shouldRenderStem = this.hasStem() && !this.beam;
1123
1124 // Format note head x positions
1125 this.note_heads.forEach(notehead => notehead.setX(xBegin));
1126
1127 // Format stem x positions
1128 const stemX = this.getStemX();
1129 this.stem.setNoteHeadXBounds(stemX, stemX);
1130
1131 L('Rendering ', this.isChord() ? 'chord :' : 'note :', this.keys);
1132
1133 // Draw each part of the note
1134 this.drawLedgerLines();
1135
1136 // Apply the overall style -- may be contradicted by local settings:
1137 this.applyStyle();
1138 this.setAttribute('el', this.context.openGroup('stavenote', this.getAttribute('id')));
1139 this.context.openGroup('note', null, { pointerBBox: true });
1140 if (shouldRenderStem) this.drawStem();
1141 this.drawNoteHeads();
1142 this.drawFlag();
1143 this.context.closeGroup();
1144 this.drawModifiers();
1145 this.context.closeGroup();
1146 this.restoreStyle();
1147 this.setRendered();
1148 }
1149}