abcjs
Version:
Renderer for abc music notation
369 lines (350 loc) • 12 kB
JavaScript
// abc_decoration.js: Creates a data structure suitable for printing a line of abc
var DynamicDecoration = require('./elements/dynamic-decoration');
var CrescendoElem = require('./elements/crescendo-element');
var GlissandoElem = require('./elements/glissando-element');
var glyphs = require('./glyphs');
var RelativeElement = require('./elements/relative-element');
var TieElem = require('./elements/tie-element');
var Decoration = function Decoration() {
this.startDiminuendoX = undefined;
this.startCrescendoX = undefined;
this.minTop = 12; // TODO-PER: this is assuming a 5-line staff. Pass that info in.
this.minBottom = 0;
};
var closeDecoration = function (voice, decoration, pitch, width, abselem, roomtaken, dir, minPitch, accentAbove) {
var yPos;
for (var i = 0; i < decoration.length; i++) {
if (decoration[i] === "staccato" || decoration[i] === "tenuto" || (decoration[i] === "accent" && !accentAbove)) {
var symbol = "scripts." + decoration[i];
if (decoration[i] === "accent") symbol = "scripts.sforzato";
if (yPos === undefined)
yPos = (dir === "down") ? pitch + 2 : minPitch - 2;
else
yPos = (dir === "down") ? yPos + 2 : yPos - 2;
if (decoration[i] === "accent") {
// Always place the accent three pitches away, no matter whether that is a line or space.
if (dir === "up") yPos--;
else yPos++;
} else {
// don't place on a stave line. The stave lines are 2,4,6,8,10
switch (yPos) {
case 2:
case 4:
case 6:
case 8:
case 10:
if (dir === "up") yPos--;
else yPos++;
break;
}
}
if (pitch > 9) yPos++; // take up some room of those that are above
var deltaX = width / 2;
if (glyphs.getSymbolAlign(symbol) !== "center") {
deltaX -= (glyphs.getSymbolWidth(symbol) / 2);
}
abselem.addFixedX(new RelativeElement(symbol, deltaX, glyphs.getSymbolWidth(symbol), yPos));
}
if (decoration[i] === "slide" && abselem.heads[0]) {
var yPos2 = abselem.heads[0].pitch;
yPos2 -= 2; // TODO-PER: not sure what this fudge factor is.
var blank1 = new RelativeElement("", -roomtaken - 15, 0, yPos2 - 1);
var blank2 = new RelativeElement("", -roomtaken - 5, 0, yPos2 + 1);
abselem.addFixedX(blank1);
abselem.addFixedX(blank2);
voice.addOther(new TieElem({ anchor1: blank1, anchor2: blank2, fixedY: true }));
}
}
if (yPos === undefined)
yPos = pitch;
return { above: yPos, below: abselem.bottom };
};
var volumeDecoration = function (voice, decoration, abselem, positioning) {
for (var i = 0; i < decoration.length; i++) {
switch (decoration[i]) {
case "p":
case "mp":
case "pp":
case "ppp":
case "pppp":
case "f":
case "ff":
case "fff":
case "ffff":
case "sfz":
case "mf":
var elem = new DynamicDecoration(abselem, decoration[i], positioning);
voice.addOther(elem);
}
}
};
var compoundDecoration = function (decoration, pitch, width, abselem, dir) {
function highestPitch() {
if (abselem.heads.length === 0)
return 10; // TODO-PER: I don't know if this can happen, but we'll return the top of the staff if so.
var pitch = abselem.heads[0].pitch;
for (var i = 1; i < abselem.heads.length; i++)
pitch = Math.max(pitch, abselem.heads[i].pitch);
return pitch;
}
function lowestPitch() {
if (abselem.heads.length === 0)
return 2; // TODO-PER: I don't know if this can happen, but we'll return the bottom of the staff if so.
var pitch = abselem.heads[0].pitch;
for (var i = 1; i < abselem.heads.length; i++)
pitch = Math.min(pitch, abselem.heads[i].pitch);
return pitch;
}
function compoundDecoration(symbol, count) {
var placement = (dir === 'down') ? lowestPitch() + 1 : highestPitch() + 9;
if (dir !== 'down' && count === 1)
placement--;
var deltaX = width / 2;
deltaX += (dir === 'down') ? -5 : 3;
for (var i = 0; i < count; i++) {
placement -= 1;
abselem.addFixedX(new RelativeElement(symbol, deltaX, glyphs.getSymbolWidth(symbol), placement));
}
}
for (var i = 0; i < decoration.length; i++) {
switch (decoration[i]) {
case "/": compoundDecoration("flags.ugrace", 1); break;
case "//": compoundDecoration("flags.ugrace", 2); break;
case "///": compoundDecoration("flags.ugrace", 3); break;
case "////": compoundDecoration("flags.ugrace", 4); break;
}
}
};
var stackedDecoration = function (decoration, width, abselem, yPos, positioning, minTop, minBottom, accentAbove) {
function incrementPlacement(placement, height) {
if (placement === 'above')
yPos.above += height;
else
yPos.below -= height;
}
function getPlacement(placement) {
var y;
if (placement === 'above') {
y = yPos.above;
if (y < minTop)
y = minTop;
} else {
y = yPos.below;
if (y > minBottom)
y = minBottom;
}
return y;
}
function textDecoration(text, placement, anchor) {
var y = getPlacement(placement);
var textFudge = 2;
var textHeight = 5;
// TODO-PER: Get the height of the current font and use that for the thickness.
abselem.addFixedX(new RelativeElement(text, width / 2, 0, y + textFudge, { type: "decoration", klass: 'ornament', thickness: 3, anchor: anchor }));
incrementPlacement(placement, textHeight);
}
function symbolDecoration(symbol, placement) {
var deltaX = width / 2;
if (glyphs.getSymbolAlign(symbol) !== "center") {
deltaX -= (glyphs.getSymbolWidth(symbol) / 2);
}
var height = glyphs.symbolHeightInPitches(symbol) + 1; // adding a little padding so nothing touches.
var y = getPlacement(placement);
y = (placement === 'above') ? y + height / 2 : y - height / 2;// Center the element vertically.
abselem.addFixedX(new RelativeElement(symbol, deltaX, glyphs.getSymbolWidth(symbol), y, { klass: 'ornament', thickness: glyphs.symbolHeightInPitches(symbol), position: placement }));
incrementPlacement(placement, height);
}
var symbolList = {
"+": "scripts.stopped",
"open": "scripts.open",
"snap": "scripts.snap",
"wedge": "scripts.wedge",
"thumb": "scripts.thumb",
"shortphrase": "scripts.shortphrase",
"mediumphrase": "scripts.mediumphrase",
"longphrase": "scripts.longphrase",
"trill": "scripts.trill",
"trillh": "scripts.trill",
"roll": "scripts.roll",
"irishroll": "scripts.roll",
"marcato": "scripts.umarcato",
"dmarcato": "scripts.dmarcato",
"umarcato": "scripts.umarcato",
"turn": "scripts.turn",
"uppermordent": "scripts.prall",
"pralltriller": "scripts.prall",
"mordent": "scripts.mordent",
"lowermordent": "scripts.mordent",
"downbow": "scripts.downbow",
"upbow": "scripts.upbow",
"fermata": "scripts.ufermata",
"invertedfermata": "scripts.dfermata",
"breath": ",",
"coda": "scripts.coda",
"segno": "scripts.segno"
};
var hasOne = false;
for (var i = 0; i < decoration.length; i++) {
switch (decoration[i]) {
case "0":
case "1":
case "2":
case "3":
case "4":
case "5":
case "D.C.":
case "D.S.":
textDecoration(decoration[i], positioning, 'middle');
hasOne = true;
break;
case "D.C.alcoda":
textDecoration("D.C. al coda", positioning, 'end');
hasOne = true;
break;
case "D.C.alfine":
textDecoration("D.C. al fine", positioning, 'end');
hasOne = true;
break;
case "D.S.alcoda":
textDecoration("D.S. al coda", positioning, 'end');
hasOne = true;
break;
case "D.S.alfine":
textDecoration("D.S. al fine", positioning, 'end');
hasOne = true;
break;
case "fine":
textDecoration("FINE", positioning, 'middle');
hasOne = true;
break;
case "+":
case "open":
case "snap":
case "wedge":
case "thumb":
case "shortphrase":
case "mediumphrase":
case "longphrase":
case "trill":
case "trillh":
case "roll":
case "irishroll":
case "marcato":
case "dmarcato":
case "turn":
case "uppermordent":
case "pralltriller":
case "mordent":
case "lowermordent":
case "downbow":
case "upbow":
case "fermata":
case "breath":
case "umarcato":
case "coda":
case "segno":
symbolDecoration(symbolList[decoration[i]], positioning);
hasOne = true;
break;
case "invertedfermata":
symbolDecoration(symbolList[decoration[i]], 'below');
hasOne = true;
break;
case "mark":
abselem.klass = "mark";
break;
case "accent":
if (accentAbove) {
symbolDecoration("scripts.sforzato", positioning);
hasOne = true;
}
break;
}
}
return hasOne;
};
function leftDecoration(decoration, abselem, roomtaken) {
for (var i = 0; i < decoration.length; i++) {
switch (decoration[i]) {
case "arpeggio":
// The arpeggio symbol is the height of a note (that is, two Y units). This stacks as many as we need to go from the
// top note to the bottom note. The arpeggio should also be a little taller than the stacked notes, so there is an extra
// one drawn and it is offset by half of a note height (that is, one Y unit).
for (var j = abselem.abcelem.minpitch - 1; j <= abselem.abcelem.maxpitch; j += 2) {
abselem.addExtra(
new RelativeElement(
"scripts.arpeggio",
-glyphs.getSymbolWidth("scripts.arpeggio") * 2 - roomtaken,
0,
j + 2,
{ klass: 'ornament', thickness: glyphs.symbolHeightInPitches("scripts.arpeggio") }
)
);
}
break;
}
}
}
Decoration.prototype.dynamicDecoration = function (voice, decoration, abselem, positioning) {
var diminuendo;
var crescendo;
var glissando;
for (var i = 0; i < decoration.length; i++) {
switch (decoration[i]) {
case "diminuendo(":
this.startDiminuendoX = abselem;
diminuendo = undefined;
break;
case "diminuendo)":
diminuendo = { start: this.startDiminuendoX, stop: abselem };
this.startDiminuendoX = undefined;
break;
case "crescendo(":
this.startCrescendoX = abselem;
crescendo = undefined;
break;
case "crescendo)":
crescendo = { start: this.startCrescendoX, stop: abselem };
this.startCrescendoX = undefined;
break;
case '~(':
case "glissando(":
this.startGlissandoX = abselem;
glissando = undefined;
break;
case '~)':
case "glissando)":
glissando = { start: this.startGlissandoX, stop: abselem };
this.startGlissandoX = undefined;
break;
}
}
if (diminuendo) {
voice.addOther(new CrescendoElem(diminuendo.start, diminuendo.stop, ">", positioning));
}
if (crescendo) {
voice.addOther(new CrescendoElem(crescendo.start, crescendo.stop, "<", positioning));
}
if (glissando) {
voice.addOther(new GlissandoElem(glissando.start, glissando.stop));
}
};
Decoration.prototype.createDecoration = function (voice, decoration, pitch, width, abselem, roomtaken, dir, minPitch, positioning, hasVocals, accentAbove) {
if (!positioning)
positioning = { ornamentPosition: 'above', volumePosition: hasVocals ? 'above' : 'below', dynamicPosition: hasVocals ? 'above' : 'below' };
// These decorations don't affect the placement of other decorations
volumeDecoration(voice, decoration, abselem, positioning.volumePosition);
this.dynamicDecoration(voice, decoration, abselem, positioning.dynamicPosition);
compoundDecoration(decoration, pitch, width, abselem, dir);
// treat staccato, accent, and tenuto first (may need to shift other markers)
var yPos = closeDecoration(voice, decoration, pitch, width, abselem, roomtaken, dir, minPitch, accentAbove);
// yPos is an object containing 'above' and 'below'. That is the placement of the next symbol on either side.
yPos.above = Math.max(yPos.above, this.minTop);
yPos.below = Math.min(yPos.below, minPitch);
var hasOne = stackedDecoration(decoration, width, abselem, yPos, positioning.ornamentPosition, this.minTop, minPitch, accentAbove);
//if (hasOne) {
// abselem.top = Math.max(yPos.above + 3, abselem.top); // TODO-PER: Not sure why we need this fudge factor.
//}
leftDecoration(decoration, abselem, roomtaken);
};
module.exports = Decoration;