VexFlow - Copyright (c) Mohit Muthanna 2010.
This file implements the formatting and layout algorithms that are used to position notes in a voice. The algorithm can align multiple voices both within a stave, and across multiple staves.
To do this, the formatter breaks up voices into a grid of rational-valued
ticks
, to which each note is assigned. Then, minimum widths are assigned
to each tick based on the widths of the notes and modifiers in that tick. This
establishes the smallest amount of space required for each tick.
Finally, the formatter distributes the left over space proportionally to
all the ticks, setting the x
values of the notes in each tick.
See tests/formatter_tests.js
for usage examples. The helper functions included
here (FormatAndDraw
, FormatAndDrawTab
) also serve as useful usage examples.
import { Vex } from './vex';
import { Beam } from './beam';
import { Flow } from './tables';
import { Fraction } from './fraction';
import { Voice } from './voice';
import { StaveConnector } from './staveconnector';
import { StaveNote } from './stavenote';
import { Note } from './note';
import { ModifierContext } from './modifiercontext';
import { TickContext } from './tickcontext';
To enable logging for this class. Set Vex.Flow.Formatter.DEBUG
to true
.
function L(...args) { if (Formatter.DEBUG) Vex.L('Vex.Flow.Formatter', args); }
Helper function to locate the next non-rest note(s).
function lookAhead(notes, restLine, i, compare) {
If no valid next note group, nextRestLine is same as current.
let nextRestLine = restLine;
Get the rest line for next valid non-rest note group.
for (i += 1; i < notes.length; i += 1) {
const note = notes[i];
if (!note.isRest() && !note.shouldIgnoreTicks()) {
nextRestLine = note.getLineForRest();
break;
}
}
Locate the mid point between two lines.
if (compare && restLine !== nextRestLine) {
const top = Math.max(restLine, nextRestLine);
const bot = Math.min(restLine, nextRestLine);
nextRestLine = Vex.MidLine(top, bot);
}
return nextRestLine;
}
Take an array of voices
and place aligned tickables in the same context. Returns
a mapping from tick
to ContextType
, a list of tick
s, and the resolution
multiplier.
Params:
voices
: Array of Voice
instances.ContextType
: A context class (e.g., ModifierContext
, TickContext
)addToContext
: Function to add tickable to context.function createContexts(voices, ContextType, addToContext) {
if (!voices || !voices.length) {
throw new Vex.RERR('BadArgument', 'No voices to format');
}
Find out highest common multiple of resolution multipliers. The purpose of this is to find out a common denominator for all fractional tick values in all tickables of all voices, so that the values can be expanded and the numerator used as an integer tick value.
const totalTicks = voices[0].getTotalTicks();
const resolutionMultiplier = voices.reduce((resolutionMultiplier, voice) => {
if (!voice.getTotalTicks().equals(totalTicks)) {
throw new Vex.RERR(
'TickMismatch', 'Voices should have same total note duration in ticks.'
);
}
if (voice.getMode() === Voice.Mode.STRICT && !voice.isComplete()) {
throw new Vex.RERR(
'IncompleteVoice', 'Voice does not have enough notes.'
);
}
return Math.max(
resolutionMultiplier,
Fraction.LCM(resolutionMultiplier, voice.getResolutionMultiplier())
);
}, 1);
Initialize tick maps.
const tickToContextMap = {};
const tickList = [];
const contexts = [];
For each voice, extract notes and create a context for every new tick that hasn’t been seen before.
voices.forEach(voice => {
Use resolution multiplier as denominator to expand ticks to suitable integer values, so that no additional expansion of fractional tick values is needed.
const ticksUsed = new Fraction(0, resolutionMultiplier);
voice.getTickables().forEach(tickable => {
const integerTicks = ticksUsed.numerator;
If we have no tick context for this tick, create one.
if (!tickToContextMap[integerTicks]) {
const newContext = new ContextType();
contexts.push(newContext);
tickToContextMap[integerTicks] = newContext;
}
Add this tickable to the TickContext.
addToContext(tickable, tickToContextMap[integerTicks]);
Maintain a sorted list of tick contexts.
tickList.push(integerTicks);
ticksUsed.add(tickable.getTicks());
});
});
return {
map: tickToContextMap,
array: contexts,
list: Vex.SortAndUnique(tickList, (a, b) => a - b, (a, b) => a === b),
resolutionMultiplier,
};
}
export class Formatter {
Helper function to layout “notes” one after the other without regard for proportions. Useful for tests and debugging.
static SimpleFormat(notes, x = 0) {
notes.reduce((x, note) => {
note.addToModifierContext(new ModifierContext());
const tick = new TickContext().addTickable(note).preFormat();
const extra = tick.getExtraPx();
tick.setX(x + extra.left);
return x + tick.getWidth() + extra.right + 10;
}, x);
}
Helper function to plot formatter debug info.
static plotDebugging(ctx, formatter, xPos, y1, y2) {
const x = xPos + Note.STAVEPADDING;
const contextGaps = formatter.contextGaps;
function stroke(x1, x2, color) {
ctx.beginPath();
ctx.setStrokeStyle(color);
ctx.setFillStyle(color);
ctx.setLineWidth(1);
ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
}
ctx.save();
ctx.setFont('Arial', 8, '');
contextGaps.gaps.forEach(gap => {
stroke(x + gap.x1, x + gap.x2, '#aaa');
Vex.drawDot(ctx, xPos + gap.x1, yPos, ‘blue’);
ctx.fillText(Math.round(gap.x2 - gap.x1), x + gap.x1, y2 + 12);
});
ctx.fillText(Math.round(contextGaps.total) + 'px', x - 20, y2 + 12);
ctx.setFillStyle('red');
ctx.fillText('Loss: ' +
formatter.lossHistory.map(loss => Math.round(loss)), x - 20, y2 + 22);
ctx.restore();
}
Helper function to format and draw a single voice. Returns a bounding box for the notation.
Parameters:
ctx
- The rendering contextstave
- The stave to which to draw (Stave
or TabStave
)notes
- Array of Note
instances (StaveNote
, TextNote
, TabNote
, etc.)params
- One of below:autobeam
only (context, stave, notes, true)
or
(ctx, stave, notes, {autobeam: true})
align_rests
a struct is needed (context, stave, notes, {align_rests: true})
(context, stave, notes, {
autobeam: true, align_rests: true})
autobeam
automatically generates beams for the notes.
align_rests
aligns rests with nearby notes.
static FormatAndDraw(ctx, stave, notes, params) {
const options = {
auto_beam: false,
align_rests: false,
};
if (typeof params === 'object') {
Vex.Merge(options, params);
} else if (typeof params === 'boolean') {
options.auto_beam = params;
}
Start by creating a voice and adding all the notes to it.
const voice = new Voice(Flow.TIME4_4)
.setMode(Voice.Mode.SOFT)
.addTickables(notes);
Then create beams, if requested.
const beams = options.auto_beam ? Beam.applyAndGetBeams(voice) : [];
Instantiate a Formatter
and format the notes.
new Formatter()
.joinVoices([voice], { align_rests: options.align_rests })
.formatToStave([voice], stave, { align_rests: options.align_rests, stave });
Render the voice and beams to the stave.
voice.setStave(stave).draw(ctx, stave);
beams.forEach(beam => beam.setContext(ctx).draw());
Return the bounding box of the voice.
return voice.getBoundingBox();
}
Helper function to format and draw aligned tab and stave notes in two separate staves.
Parameters:
ctx
- The rendering contexttabstave
- A TabStave
instance on which to render TabNote
s.stave
- A Stave
instance on which to render Note
s.notes
- Array of Note
instances for the stave (StaveNote
, BarNote
, etc.)tabnotes
- Array of Note
instances for the tab stave (TabNote
, BarNote
, etc.)autobeam
- Automatically generate beams.params
- A configuration object:autobeam
automatically generates beams for the notes.align_rests
aligns rests with nearby notes. static FormatAndDrawTab(ctx, tabstave, stave, tabnotes, notes, autobeam, params) {
const opts = {
auto_beam: autobeam,
align_rests: false,
};
if (typeof params === 'object') {
Vex.Merge(opts, params);
} else if (typeof params === 'boolean') {
opts.auto_beam = params;
}
Create a 4/4
voice for notes
.
const notevoice = new Voice(Flow.TIME4_4)
.setMode(Voice.Mode.SOFT)
.addTickables(notes);
Create a 4/4
voice for tabnotes
.
const tabvoice = new Voice(Flow.TIME4_4)
.setMode(Voice.Mode.SOFT)
.addTickables(tabnotes);
Then create beams, if requested.
const beams = opts.auto_beam ? Beam.applyAndGetBeams(notevoice) : [];
Instantiate a Formatter
and align tab and stave notes.
new Formatter()
.joinVoices([notevoice], { align_rests: opts.align_rests })
.joinVoices([tabvoice])
.formatToStave([notevoice, tabvoice], stave, { align_rests: opts.align_rests });
Render voices and beams to staves.
notevoice.draw(ctx, stave);
tabvoice.draw(ctx, tabstave);
beams.forEach(beam => beam.setContext(ctx).draw());
Draw a connector between tab and note staves.
new StaveConnector(stave, tabstave).setContext(ctx).draw();
}
Auto position rests based on previous/next note positions.
Params:
notes
: An array of notes.alignAllNotes
: If set to false, only aligns non-beamed notes.alignTuplets
: If set to false, ignores tuplets. static AlignRestsToNotes(notes, alignAllNotes, alignTuplets) {
notes.forEach((note, index) => {
if (note instanceof StaveNote && note.isRest()) {
if (note.tuplet && !alignTuplets) return;
If activated rests not on default can be rendered as specified.
const position = note.getGlyph().position.toUpperCase();
if (position !== 'R/4' && position !== 'B/4') return;
if (alignAllNotes || note.beam != null) {
Align rests with previous/next notes.
const props = note.getKeyProps()[0];
if (index === 0) {
props.line = lookAhead(notes, props.line, index, false);
note.setKeyLine(0, props.line);
} else if (index > 0 && index < notes.length) {
If previous note is a rest, use its line number.
let restLine;
if (notes[index - 1].isRest()) {
restLine = notes[index - 1].getKeyProps()[0].line;
props.line = restLine;
} else {
restLine = notes[index - 1].getLineForRest();
Get the rest line for next valid non-rest note group.
props.line = lookAhead(notes, restLine, index, true);
}
note.setKeyLine(0, props.line);
}
}
}
});
return this;
}
constructor() {
Minimum width required to render all the notes in the voices.
this.minTotalWidth = 0;
This is set to true
after minTotalWidth
is calculated.
this.hasMinTotalWidth = false;
Total number of ticks in the voice.
this.totalTicks = new Fraction(0, 1);
Arrays of tick and modifier contexts.
this.tickContexts = null;
this.modiferContexts = null;
Gaps between contexts, for free movement of notes post formatting.
this.contextGaps = {
total: 0,
gaps: [],
};
this.voices = [];
}
Find all the rests in each of the voices
and align them
to neighboring notes. If alignAllNotes
is false
, then only
align non-beamed notes.
alignRests(voices, alignAllNotes) {
if (!voices || !voices.length) {
throw new Vex.RERR('BadArgument', 'No voices to format rests');
}
voices.forEach(voice =>
Formatter.AlignRestsToNotes(voice.getTickables(), alignAllNotes));
}
Calculate the minimum width required to align and format voices
.
preCalculateMinTotalWidth(voices) {
Cache results.
if (this.hasMinTotalWidth) return this.minTotalWidth;
Create tick contexts if not already created.
if (!this.tickContexts) {
if (!voices) {
throw new Vex.RERR(
'BadArgument', "'voices' required to run preCalculateMinTotalWidth"
);
}
this.createTickContexts(voices);
}
const { list: contextList, map: contextMap } = this.tickContexts;
Go through each tick context and calculate total width.
this.minTotalWidth = contextList
.map(tick => {
const context = contextMap[tick];
context.preFormat();
return context.getWidth();
})
.reduce((a, b) => a + b, 0);
this.hasMinTotalWidth = true;
return this.minTotalWidth;
}
Get minimum width required to render all voices. Either format
or
preCalculateMinTotalWidth
must be called before this method.
getMinTotalWidth() {
if (!this.hasMinTotalWidth) {
throw new Vex.RERR(
'NoMinTotalWidth',
"Call 'preCalculateMinTotalWidth' or 'preFormat' before calling 'getMinTotalWidth'"
);
}
return this.minTotalWidth;
}
Create ModifierContext
s for each tick in voices
.
createModifierContexts(voices) {
const contexts = createContexts(
voices,
ModifierContext,
(tickable, context) => tickable.addToModifierContext(context)
);
this.modiferContexts = contexts;
return contexts;
}
Create TickContext
s for each tick in voices
. Also calculate the
total number of ticks in voices.
createTickContexts(voices) {
const contexts = createContexts(
voices,
TickContext,
(tickable, context) => context.addTickable(tickable)
);
contexts.array.forEach(context => {
context.tContexts = contexts.array;
});
this.totalTicks = voices[0].getTicksUsed().clone();
this.tickContexts = contexts;
return contexts;
}
This is the core formatter logic. Format voices and justify them
to justifyWidth
pixels. renderingContext
is required to justify elements
that can’t retreive widths without a canvas. This method sets the x
positions
of all the tickables/notes in the formatter.
preFormat(justifyWidth = 0, renderingContext, voices, stave) {
Initialize context maps.
const contexts = this.tickContexts;
const { list: contextList, map: contextMap, resolutionMultiplier } = contexts;
If voices and a stave were provided, set the Stave for each voice and preFormat to apply Y values to the notes;
if (voices && stave) {
voices.forEach(voice => voice.setStave(stave).preFormat());
}
Now distribute the ticks to each tick context, and assign them their own X positions.
let x = 0;
let shift = 0;
const centerX = justifyWidth / 2;
this.minTotalWidth = 0;
Pass 1: Give each note maximum width requested by context.
contextList.forEach((tick) => {
const context = contextMap[tick];
if (renderingContext) context.setContext(renderingContext);
Make sure that all tickables in this context have calculated their space requirements.
context.preFormat();
const width = context.getWidth();
this.minTotalWidth += width;
const metrics = context.getMetrics();
x = x + shift + metrics.extraLeftPx;
context.setX(x);
Calculate shift for the next tick.
shift = width - metrics.extraLeftPx;
});
this.minTotalWidth = x + shift;
this.hasMinTotalWidth = true;
No justification needed. End formatting.
if (justifyWidth <= 0) return;
Pass 2: Take leftover width, and distribute it to proportionately to all notes.
const remainingX = justifyWidth - this.minTotalWidth;
const leftoverPxPerTick = remainingX / (this.totalTicks.value() * resolutionMultiplier);
let spaceAccum = 0;
contextList.forEach((tick, index) => {
const prevTick = contextList[index - 1] || 0;
const context = contextMap[tick];
const tickSpace = (tick - prevTick) * leftoverPxPerTick;
spaceAccum += tickSpace;
context.setX(context.getX() + spaceAccum);
Move center aligned tickables to middle
context
.getCenterAlignedTickables()
.forEach(tickable => { // eslint-disable-line
tickable.center_x_shift = centerX - context.getX();
});
});
Just one context. Done formatting.
if (contextList.length === 1) return;
this.justifyWidth = justifyWidth;
this.lossHistory = [];
this.evaluate();
}
Calculate the total cost of this formatting decision.
evaluate() {
const justifyWidth = this.justifyWidth;
Calculate available slack per tick context. This works out how much freedom to move a context has in either direction, without affecting other notes.
this.contextGaps = { total: 0, gaps: [] };
this.tickContexts.list.forEach((tick, index) => {
if (index === 0) return;
const prevTick = this.tickContexts.list[index - 1];
const prevContext = this.tickContexts.map[prevTick];
const context = this.tickContexts.map[tick];
const prevMetrics = prevContext.getMetrics();
const insideRightEdge = prevContext.getX() + prevMetrics.width;
const insideLeftEdge = context.getX();
const gap = insideLeftEdge - insideRightEdge;
this.contextGaps.total += gap;
this.contextGaps.gaps.push({ x1: insideRightEdge, x2: insideLeftEdge });
Tell the tick contexts how much they can reposition themselves.
context.getFormatterMetrics().freedom.left = gap;
prevContext.getFormatterMetrics().freedom.right = gap;
});
Calculate mean distance in each voice for each duration type, then calculate how far each note is from the mean.
const durationStats = this.durationStats = {};
function updateStats(duration, space) {
const stats = durationStats[duration];
if (stats === undefined) {
durationStats[duration] = { mean: space, count: 1 };
} else {
stats.count += 1;
stats.mean = (stats.mean + space) / 2;
}
}
this.voices.forEach(voice => {
voice.getTickables().forEach((note, i, notes) => {
const duration = note.getTicks().clone().simplify().toString();
const metrics = note.getMetrics();
const formatterMetrics = note.getFormatterMetrics();
const leftNoteEdge = note.getX() + metrics.noteWidth +
metrics.modRightPx + metrics.extraRightPx;
let space = 0;
if (i < (notes.length - 1)) {
const rightNote = notes[i + 1];
const rightMetrics = rightNote.getMetrics();
const rightNoteEdge = rightNote.getX() -
rightMetrics.modLeftPx - rightMetrics.extraLeftPx;
space = rightNoteEdge - leftNoteEdge;
formatterMetrics.space.used = rightNote.getX() - note.getX();
rightNote.getFormatterMetrics().freedom.left = space;
} else {
space = justifyWidth - leftNoteEdge;
formatterMetrics.space.used = justifyWidth - note.getX();
}
formatterMetrics.freedom.right = space;
updateStats(duration, formatterMetrics.space.used);
});
});
Calculate how much each note deviates from the mean. Loss function is square root of the sum of squared deviations.
let totalDeviation = 0;
this.voices.forEach(voice => {
voice.getTickables().forEach((note) => {
const duration = note.getTicks().clone().simplify().toString();
const metrics = note.getFormatterMetrics();
metrics.iterations += 1;
metrics.space.deviation = metrics.space.used - durationStats[duration].mean;
metrics.duration = duration;
metrics.space.mean = durationStats[duration].mean;
totalDeviation += Math.pow(durationStats[duration].mean, 2);
});
});
this.totalCost = Math.sqrt(totalDeviation);
this.lossHistory.push(this.totalCost);
return this;
}
Run a single iteration of rejustification. At a high level, this method calculates the overall “loss” (or cost) of this layout, and repositions tickcontexts in an attempt to reduce the cost. You can call this method multiple times until it finds and oscillates around a global minimum.
tune() {
const sum = (means) => means.reduce((a, b) => a + b);
Move current
tickcontext by shift
pixels, and adjust the freedom
on adjacent tickcontexts.
function move(current, prev, next, shift) {
current.setX(current.getX() + shift);
current.getFormatterMetrics().freedom.left += shift;
current.getFormatterMetrics().freedom.right -= shift;
if (prev) prev.getFormatterMetrics().freedom.right += shift;
if (next) next.getFormatterMetrics().freedom.left -= shift;
}
let shift = 0;
this.tickContexts.list.forEach((tick, index, list) => {
const context = this.tickContexts.map[tick];
const prevContext = (index > 0) ? this.tickContexts.map[list[index - 1]] : null;
const nextContext = (index < list.length - 1) ? this.tickContexts.map[list[index + 1]] : null;
move(context, prevContext, nextContext, shift);
const cost = -sum(
context.getTickables().map(t => t.getFormatterMetrics().space.deviation));
if (cost > 0) {
shift = -Math.min(context.getFormatterMetrics().freedom.right, Math.abs(cost));
} else if (cost < 0) {
if (nextContext) {
shift = Math.min(nextContext.getFormatterMetrics().freedom.right, Math.abs(cost));
} else {
shift = 0;
}
}
const minShift = Math.min(5, Math.abs(shift));
shift = shift > 0 ? minShift : -minShift;
});
return this.evaluate();
}
This is the top-level call for all formatting logic completed
after x
and y
values have been computed for the notes
in the voices.
postFormat() {
const postFormatContexts = (contexts) =>
contexts.list.forEach(tick => contexts.map[tick].postFormat());
postFormatContexts(this.modiferContexts);
postFormatContexts(this.tickContexts);
return this;
}
Take all voices
and create ModifierContext
s out of them. This tells
the formatters that the voices belong on a single stave.
joinVoices(voices) {
this.createModifierContexts(voices);
this.hasMinTotalWidth = false;
return this;
}
Align rests in voices, justify the contexts, and position the notes
so voices are aligned and ready to render onto the stave. This method
mutates the x
positions of all tickables in voices
.
Voices are full justified to fit in justifyWidth
pixels.
Set options.context
to the rendering context. Set options.align_rests
to true to enable rest alignment.
format(voices, justifyWidth, options) {
const opts = {
align_rests: false,
context: null,
stave: null,
};
Vex.Merge(opts, options);
this.voices = voices;
this.alignRests(voices, opts.align_rests);
this.createTickContexts(voices);
this.preFormat(justifyWidth, opts.context, voices, opts.stave);
Only postFormat if a stave was supplied for y value formatting
if (opts.stave) this.postFormat();
return this;
}
This method is just like format
except that the justifyWidth
is inferred
from the stave
.
formatToStave(voices, stave, options) {
const justifyWidth = stave.getNoteEndX() - stave.getNoteStartX() - 10;
L('Formatting voices to width: ', justifyWidth);
const opts = { context: stave.getContext() };
Vex.Merge(opts, options);
return this.format(voices, justifyWidth, opts);
}
}