vexflow
Version:
A JavaScript library for rendering music notation and guitar tablature.
350 lines (349 loc) • 14.9 kB
JavaScript
import { Fraction } from './fraction.js';
import { Glyph } from './glyph.js';
import { Modifier } from './modifier.js';
import { Music } from './music.js';
import { Tables } from './tables.js';
import { isAccidental, isGraceNote, isGraceNoteGroup, isStaveNote } from './typeguard.js';
import { defined, log } from './util.js';
function L(...args) {
if (Accidental.DEBUG)
log('Vex.Flow.Accidental', args);
}
class Accidental extends Modifier {
static get CATEGORY() {
return "Accidental";
}
static format(accidentals, state) {
if (!accidentals || accidentals.length === 0)
return;
const musicFont = Tables.currentMusicFont();
const noteheadAccidentalPadding = musicFont.lookupMetric('accidental.noteheadAccidentalPadding');
const leftShift = state.left_shift + noteheadAccidentalPadding;
const accidentalSpacing = musicFont.lookupMetric('accidental.accidentalSpacing');
const additionalPadding = musicFont.lookupMetric('accidental.leftPadding');
const accList = [];
let prevNote = undefined;
let shiftL = 0;
for (let i = 0; i < accidentals.length; ++i) {
const acc = accidentals[i];
const note = acc.getNote();
const stave = note.getStave();
const index = acc.checkIndex();
const props = note.getKeyProps()[index];
if (note !== prevNote) {
for (let n = 0; n < note.keys.length; ++n) {
shiftL = Math.max(note.getLeftDisplacedHeadPx() - note.getXShift(), shiftL);
}
prevNote = note;
}
if (stave) {
const lineSpace = stave.getSpacingBetweenLines();
const y = stave.getYForLine(props.line);
const accLine = Math.round((y / lineSpace) * 2) / 2;
accList.push({ y, line: accLine, shift: shiftL, acc, lineSpace });
}
else {
accList.push({ line: props.line, shift: shiftL, acc });
}
}
accList.sort((a, b) => b.line - a.line);
const lineList = [];
let accShift = 0;
let previousLine = undefined;
for (let i = 0; i < accList.length; i++) {
const acc = accList[i];
if (previousLine === undefined || previousLine !== acc.line) {
lineList.push({
line: acc.line,
flatLine: true,
dblSharpLine: true,
numAcc: 0,
width: 0,
column: 0,
});
}
if (acc.acc.type !== 'b' && acc.acc.type !== 'bb') {
lineList[lineList.length - 1].flatLine = false;
}
if (acc.acc.type !== '##') {
lineList[lineList.length - 1].dblSharpLine = false;
}
lineList[lineList.length - 1].numAcc++;
lineList[lineList.length - 1].width += acc.acc.getWidth() + accidentalSpacing;
accShift = acc.shift > accShift ? acc.shift : accShift;
previousLine = acc.line;
}
let totalColumns = 0;
for (let i = 0; i < lineList.length; i++) {
let noFurtherConflicts = false;
const groupStart = i;
let groupEnd = i;
while (groupEnd + 1 < lineList.length && !noFurtherConflicts) {
if (this.checkCollision(lineList[groupEnd], lineList[groupEnd + 1])) {
groupEnd++;
}
else {
noFurtherConflicts = true;
}
}
const getGroupLine = (index) => lineList[groupStart + index];
const getGroupLines = (indexes) => indexes.map(getGroupLine);
const lineDifference = (indexA, indexB) => {
const [a, b] = getGroupLines([indexA, indexB]).map((item) => item.line);
return a - b;
};
const notColliding = (...indexPairs) => indexPairs.map(getGroupLines).every(([line1, line2]) => !this.checkCollision(line1, line2));
const groupLength = groupEnd - groupStart + 1;
let endCase = this.checkCollision(lineList[groupStart], lineList[groupEnd]) ? 'a' : 'b';
switch (groupLength) {
case 3:
if (endCase === 'a' && lineDifference(1, 2) === 0.5 && lineDifference(0, 1) !== 0.5) {
endCase = 'second_on_bottom';
}
break;
case 4:
if (notColliding([0, 2], [1, 3])) {
endCase = 'spaced_out_tetrachord';
}
break;
case 5:
if (endCase === 'b' && notColliding([1, 3])) {
endCase = 'spaced_out_pentachord';
if (notColliding([0, 2], [2, 4])) {
endCase = 'very_spaced_out_pentachord';
}
}
break;
case 6:
if (notColliding([0, 3], [1, 4], [2, 5])) {
endCase = 'spaced_out_hexachord';
}
if (notColliding([0, 2], [2, 4], [1, 3], [3, 5])) {
endCase = 'very_spaced_out_hexachord';
}
break;
default:
break;
}
let groupMember;
let column;
if (groupLength >= 7) {
let patternLength = 2;
let collisionDetected = true;
while (collisionDetected === true) {
collisionDetected = false;
for (let line = 0; line + patternLength < lineList.length; line++) {
if (this.checkCollision(lineList[line], lineList[line + patternLength])) {
collisionDetected = true;
patternLength++;
break;
}
}
}
for (groupMember = i; groupMember <= groupEnd; groupMember++) {
column = ((groupMember - i) % patternLength) + 1;
lineList[groupMember].column = column;
totalColumns = totalColumns > column ? totalColumns : column;
}
}
else {
for (groupMember = i; groupMember <= groupEnd; groupMember++) {
column = Tables.accidentalColumnsTable[groupLength][endCase][groupMember - i];
lineList[groupMember].column = column;
totalColumns = totalColumns > column ? totalColumns : column;
}
}
i = groupEnd;
}
const columnWidths = [];
const columnXOffsets = [];
for (let i = 0; i <= totalColumns; i++) {
columnWidths[i] = 0;
columnXOffsets[i] = 0;
}
columnWidths[0] = accShift + leftShift;
columnXOffsets[0] = accShift + leftShift;
lineList.forEach((line) => {
if (line.width > columnWidths[line.column])
columnWidths[line.column] = line.width;
});
for (let i = 1; i < columnWidths.length; i++) {
columnXOffsets[i] = columnWidths[i] + columnXOffsets[i - 1];
}
const totalShift = columnXOffsets[columnXOffsets.length - 1];
let accCount = 0;
lineList.forEach((line) => {
let lineWidth = 0;
const lastAccOnLine = accCount + line.numAcc;
for (accCount; accCount < lastAccOnLine; accCount++) {
const xShift = columnXOffsets[line.column - 1] + lineWidth;
accList[accCount].acc.setXShift(xShift);
lineWidth += accList[accCount].acc.getWidth() + accidentalSpacing;
L('Line, accCount, shift: ', line.line, accCount, xShift);
}
});
state.left_shift += totalShift + additionalPadding;
}
static checkCollision(line1, line2) {
let clearance = line2.line - line1.line;
let clearanceRequired = 3;
if (clearance > 0) {
clearanceRequired = line2.flatLine || line2.dblSharpLine ? 2.5 : 3.0;
if (line1.dblSharpLine)
clearance -= 0.5;
}
else {
clearanceRequired = line1.flatLine || line1.dblSharpLine ? 2.5 : 3.0;
if (line2.dblSharpLine)
clearance -= 0.5;
}
const collision = Math.abs(clearance) < clearanceRequired;
L('Line_1, Line_2, Collision: ', line1.line, line2.line, collision);
return collision;
}
static applyAccidentals(voices, keySignature) {
const tickPositions = [];
const tickNoteMap = {};
voices.forEach((voice) => {
const tickPosition = new Fraction(0, 1);
const tickable = voice.getTickables();
tickable.forEach((t) => {
if (t.shouldIgnoreTicks())
return;
const notesAtPosition = tickNoteMap[tickPosition.value()];
if (!notesAtPosition) {
tickPositions.push(tickPosition.value());
tickNoteMap[tickPosition.value()] = [t];
}
else {
notesAtPosition.push(t);
}
tickPosition.add(t.getTicks());
});
});
const music = new Music();
if (!keySignature)
keySignature = 'C';
const scaleMapKey = music.createScaleMap(keySignature);
const scaleMap = {};
tickPositions.forEach((tickPos) => {
const tickables = tickNoteMap[tickPos];
const modifiedPitches = [];
const processNote = (t) => {
if (!isStaveNote(t) || t.isRest() || t.shouldIgnoreTicks()) {
return;
}
const staveNote = t;
staveNote.keys.forEach((keyString, keyIndex) => {
const key = music.getNoteParts(keyString.split('/')[0]);
const octave = keyString.split('/')[1];
const accidentalString = key.accidental || 'n';
const pitch = key.root + accidentalString;
if (!scaleMap[key.root + octave])
scaleMap[key.root + octave] = scaleMapKey[key.root];
const sameAccidental = scaleMap[key.root + octave] === pitch;
const previouslyModified = modifiedPitches.indexOf(keyString) > -1;
staveNote.getModifiers().forEach((modifier, index) => {
if (isAccidental(modifier) && modifier.type == accidentalString && modifier.getIndex() == keyIndex) {
staveNote.getModifiers().splice(index, 1);
}
});
if (!sameAccidental || (sameAccidental && previouslyModified)) {
scaleMap[key.root + octave] = pitch;
const accidental = new Accidental(accidentalString);
staveNote.addModifier(accidental, keyIndex);
modifiedPitches.push(keyString);
}
});
staveNote.getModifiers().forEach((modifier) => {
if (isGraceNoteGroup(modifier)) {
modifier.getGraceNotes().forEach(processNote);
}
});
};
tickables.forEach(processNote);
});
}
constructor(type) {
super();
L('New accidental: ', type);
this.type = type;
this.position = Modifier.Position.LEFT;
this.render_options = {
font_scale: Tables.NOTATION_FONT_SCALE,
parenLeftPadding: 2,
parenRightPadding: 2,
};
this.accidental = Tables.accidentalCodes(this.type);
defined(this.accidental, 'ArgumentError', `Unknown accidental type: ${type}`);
this.cautionary = false;
this.reset();
}
reset() {
const fontScale = this.render_options.font_scale;
this.glyph = new Glyph(this.accidental.code, fontScale);
this.glyph.setOriginX(1.0);
if (this.cautionary) {
this.parenLeft = new Glyph(Tables.accidentalCodes('{').code, fontScale);
this.parenRight = new Glyph(Tables.accidentalCodes('}').code, fontScale);
this.parenLeft.setOriginX(1.0);
this.parenRight.setOriginX(1.0);
}
}
getWidth() {
if (this.cautionary) {
const parenLeft = defined(this.parenLeft);
const parenRight = defined(this.parenRight);
const parenWidth = parenLeft.getMetrics().width +
parenRight.getMetrics().width +
this.render_options.parenLeftPadding +
this.render_options.parenRightPadding;
return this.glyph.getMetrics().width + parenWidth;
}
else {
return this.glyph.getMetrics().width;
}
}
setNote(note) {
defined(note, 'ArgumentError', `Bad note value: ${note}`);
this.note = note;
if (isGraceNote(note)) {
this.render_options.font_scale = 25;
this.reset();
}
return this;
}
setAsCautionary() {
this.cautionary = true;
this.render_options.font_scale = 28;
this.reset();
return this;
}
draw() {
const { type, position, index, cautionary, x_shift, y_shift, glyph, render_options: { parenLeftPadding, parenRightPadding }, } = this;
const ctx = this.checkContext();
const note = this.checkAttachedNote();
this.setRendered();
const start = note.getModifierStartXY(position, index);
let accX = start.x + x_shift;
const accY = start.y + y_shift;
L('Rendering: ', type, accX, accY);
if (!cautionary) {
glyph.render(ctx, accX, accY);
}
else {
const parenLeft = defined(this.parenLeft);
const parenRight = defined(this.parenRight);
parenRight.render(ctx, accX, accY);
accX -= parenRight.getMetrics().width;
accX -= parenRightPadding;
accX -= this.accidental.parenRightPaddingAdjustment;
glyph.render(ctx, accX, accY);
accX -= glyph.getMetrics().width;
accX -= parenLeftPadding;
parenLeft.render(ctx, accX, accY);
}
}
}
Accidental.DEBUG = false;
export { Accidental };