/**
* @file
* @description Parser and renderer for Music XML files to Vex Flow
* @author {@link mailto:neumann.benni@gmail.com|neumann.benni@gmail.com}
* @version 0.1
*/
import Vex from 'vexflow';
import { MusicXml } from './xml/MusicXml.js';
import { Measure } from './vex/Measure.js';
const { Flow } = Vex;
/**
* MusicXmlRenderer
* @param
*/
export class MusicXmlRenderer {
constructor(data, canvas) {
this.musicXml = new MusicXml(data);
console.profileEnd('parsing');
console.log(this.musicXml);
if (false) {
const part = 1;
const from = 1;
const to = 2;
this.musicXml.Parts = [this.musicXml.Parts[part]];
this.musicXml.Parts[0].Measures = this.musicXml.Parts[0].Measures.slice(from, to);
}
this.mStartMeasure = 0;
this.mStopMeasure = this.musicXml.Parts[0].Measures.length;
this.isSvg = !(canvas instanceof HTMLCanvasElement);
this.canvas = canvas;
// eslint-disable-next-line max-len
this.renderer = new Flow.Renderer(this.canvas, this.isSvg ? Flow.Renderer.Backends.SVG : Flow.Renderer.Backends.CANVAS);
// Properties for rendering
this.ctx = this.renderer.getContext();
this.Drawables = [];
// Some formatting constants
this.staveSpace = 100;
this.staveXOffset = 20;
this.staveYOffset = 20;
this.calculateLayout();
console.time('parse');
this.parse().render();
console.timeEnd('parse');
}
getScoreHeight() {
return this.systemSpace * this.format.linesPerPage;
}
// https://github.com/0xfe/vexflow/blob/1.2.83/tests/formatter_tests.js line 271
parse() {
const allParts = this.musicXml.Parts;
for (const [p] of allParts.entries()) {
const part = allParts[p];
for (let m = this.mStartMeasure; m < this.mStopMeasure; m++) {
const measure = part.Measures[m];
this.Drawables.push(new Measure(measure, this.format, this.ctx));
}
}
// Connect the first measures in a line
const MeasuresFirstInLine = this.Drawables.filter(m => m.firstInLine);
const MeasureNumsFirstInLine = new Set(MeasuresFirstInLine.map(m => m.xmlMeasure.Number));
MeasureNumsFirstInLine.forEach((n) => {
// Get all the measures from all parts with the same starting number.
// Get their stavelist(s) and concatenate them in one array. Now we have an
// array of staves that are in the first line.
const system = [].concat(...MeasuresFirstInLine.filter(m => m.xmlMeasure.Number === n).map(m => m.staveList));
for (let s = 0; s < system.length - 1; s++) {
// It actually doesn't matter which measure we use for the connectors
MeasuresFirstInLine[0].addConnector(system[s], system[s + 1], Flow.StaveConnector.type.SINGLE_LEFT);
}
});
return this;
}
clear() {
$(this.ctx.svg).empty();
}
set StartMeasure(value) {
// TODO: Recalculate layout and rerender
this.mStartMeasure = value;
this.calculateLayout();
this.Drawables = [];
this.clear();
this.parse().render();
}
set StopMeasure(value) {
// TODO: Recalculate layout and rerender
this.mStopMeasure = value;
this.calculateLayout();
this.clear();
this.Drawables = [];
this.parse().render();
}
calculateLayout() {
this.stavesPerSystem = this.musicXml.Parts
.map(p => p.getAllStaves()) // get all the staves in a part
.reduce((e, ne) => e + ne); // sum them up
this.width = this.isSvg ? parseInt(this.canvas.getAttribute('width'), 10) : this.canvas.width;
const startWidth = document.body.clientWidth < 250 ? document.body.clientWidth : 250;
this.systemSpace = this.staveSpace * this.stavesPerSystem + 50;
this.measuresPerStave = Math.floor(this.width / startWidth); // measures per stave
this.staveWidth = Math.round(this.width / this.measuresPerStave) - this.staveXOffset;
this.format = {
staveSpace: this.staveSpace,
staveXOffset: this.staveXOffset,
staveYOffset: this.staveYOffset,
systemSpace: this.systemSpace,
// FIXME: Refactor to stavesPerMeasure
measuresPerStave: this.measuresPerStave,
totalMeasures: (this.mStopMeasure - this.mStartMeasure),
staveWidth: this.staveWidth,
stavesPerSystem: this.musicXml.getStavesPerSystem(),
width: this.width,
linesPerPage: Math.ceil((this.mStopMeasure - this.mStartMeasure) + 1 / this.measuresPerStave),
};
// Set the SVG viewbox according to the calculated layout
const vb = [0, 0, this.width, this.getScoreHeight()];
this.ctx.setViewBox(vb);
}
render() {
this.Drawables.forEach(d => d.draw());
}
}