VexFlow - Copyright (c) Mohit Muthanna 2010. Author: Larry Kuhns
This file implements the Stroke
class which renders chord strokes
that can be arpeggiated, brushed, rasquedo, etc.
import { Vex } from './vex';
import { Modifier } from './modifier';
import { StaveNote } from './stavenote';
import { Glyph } from './glyph';
export class Stroke extends Modifier {
static get CATEGORY() { return 'strokes'; }
static get Type() {
return {
BRUSH_DOWN: 1,
BRUSH_UP: 2,
ROLL_DOWN: 3, // Arpegiated chord
ROLL_UP: 4, // Arpegiated chord
RASQUEDO_DOWN: 5,
RASQUEDO_UP: 6,
};
}
Arrange strokes inside ModifierContext
static format(strokes, state) {
const left_shift = state.left_shift;
const stroke_spacing = 0;
if (!strokes || strokes.length === 0) return this;
const strokeList = strokes.map((stroke) => {
const note = stroke.getNote();
if (note instanceof StaveNote) {
const { line, displaced } = note.getKeyProps()[stroke.getIndex()];
const shift = displaced ? note.getExtraLeftPx() : 0;
return { line, shift, stroke };
} else {
const { str: string } = note.getPositions()[stroke.getIndex()];
return { line: string, shift: 0, stroke };
}
});
const strokeShift = left_shift;
There can only be one stroke .. if more than one, they overlay each other
const xShift = strokeList.reduce((xShift, { stroke, shift }) => {
stroke.setXShift(strokeShift + shift);
return Math.max(stroke.getWidth() + stroke_spacing, xShift);
}, 0);
state.left_shift += xShift;
return true;
}
constructor(type, options) {
super();
this.setAttribute('type', 'Stroke');
this.note = null;
this.options = Vex.Merge({}, options);
multi voice - span stroke across all voices if true
this.all_voices = 'all_voices' in this.options ? this.options.all_voices : true;
multi voice - end note of stroke, set in draw()
this.note_end = null;
this.index = null;
this.type = type;
this.position = Modifier.Position.LEFT;
this.render_options = {
font_scale: 38,
stroke_px: 3,
stroke_spacing: 10,
};
this.font = {
family: 'serif',
size: 10,
weight: 'bold italic',
};
this.setXShift(0);
this.setWidth(10);
}
getCategory() { return Stroke.CATEGORY; }
getPosition() { return this.position; }
addEndNote(note) { this.note_end = note; return this; }
draw() {
this.checkContext();
if (!(this.note && (this.index != null))) {
throw new Vex.RERR('NoAttachedNote', "Can't draw stroke without a note and index.");
}
const start = this.note.getModifierStartXY(this.position, this.index);
let ys = this.note.getYs();
let topY = start.y;
let botY = start.y;
const x = start.x - 5;
const line_space = this.note.stave.options.spacing_between_lines_px;
const notes = this.getModifierContext().getModifiers(this.note.getCategory());
for (let i = 0; i < notes.length; i++) {
ys = notes[i].getYs();
for (let n = 0; n < ys.length; n++) {
if (this.note === notes[i] || this.all_voices) {
topY = Vex.Min(topY, ys[n]);
botY = Vex.Max(botY, ys[n]);
}
}
}
let arrow;
let arrow_shift_x;
let arrow_y;
let text_shift_x;
let text_y;
switch (this.type) {
case Stroke.Type.BRUSH_DOWN:
arrow = 'vc3';
arrow_shift_x = -3;
arrow_y = topY - (line_space / 2) + 10;
botY += (line_space / 2);
break;
case Stroke.Type.BRUSH_UP:
arrow = 'v11';
arrow_shift_x = 0.5;
arrow_y = botY + (line_space / 2);
topY -= (line_space / 2);
break;
case Stroke.Type.ROLL_DOWN:
case Stroke.Type.RASQUEDO_DOWN:
arrow = 'vc3';
arrow_shift_x = -3;
text_shift_x = this.x_shift + arrow_shift_x - 2;
if (this.note instanceof StaveNote) {
topY += 1.5 * line_space;
if ((botY - topY) % 2 !== 0) {
botY += 0.5 * line_space;
} else {
botY += line_space;
}
arrow_y = topY - line_space;
text_y = botY + line_space + 2;
} else {
topY += 1.5 * line_space;
botY += line_space;
arrow_y = topY - 0.75 * line_space;
text_y = botY + 0.25 * line_space;
}
break;
case Stroke.Type.ROLL_UP:
case Stroke.Type.RASQUEDO_UP:
arrow = 'v52';
arrow_shift_x = -4;
text_shift_x = this.x_shift + arrow_shift_x - 1;
if (this.note instanceof StaveNote) {
arrow_y = line_space / 2;
topY += 0.5 * line_space;
if ((botY - topY) % 2 === 0) {
botY += line_space / 2;
}
arrow_y = botY + 0.5 * line_space;
text_y = topY - 1.25 * line_space;
} else {
topY += 0.25 * line_space;
botY += 0.5 * line_space;
arrow_y = botY + 0.25 * line_space;
text_y = topY - line_space;
}
break;
default:
throw new Vex.RERR('InvalidType', `The stroke type ${this.type} does not exist`);
}
Draw the stroke
if (this.type === Stroke.Type.BRUSH_DOWN || this.type === Stroke.Type.BRUSH_UP) {
this.context.fillRect(x + this.x_shift, topY, 1, botY - topY);
} else {
if (this.note instanceof StaveNote) {
for (let i = topY; i <= botY; i += line_space) {
Glyph.renderGlyph(
this.context,
x + this.x_shift - 4,
i,
this.render_options.font_scale,
'va3'
);
}
} else {
let i;
for (i = topY; i <= botY; i += 10) {
Glyph.renderGlyph(
this.context,
x + this.x_shift - 4,
i,
this.render_options.font_scale,
'va3'
);
}
if (this.type === Stroke.Type.RASQUEDO_DOWN) {
text_y = i + 0.25 * line_space;
}
}
}
Draw the arrow head
Glyph.renderGlyph(
this.context,
x + this.x_shift + arrow_shift_x,
arrow_y,
this.render_options.font_scale,
arrow
);
Draw the rasquedo “R”
if (this.type === Stroke.Type.RASQUEDO_DOWN || this.type === Stroke.Type.RASQUEDO_UP) {
this.context.save();
this.context.setFont(this.font.family, this.font.size, this.font.weight);
this.context.fillText('R', x + text_shift_x, text_y);
this.context.restore();
}
}
}