abcjs
Version:
Renderer for abc music notation
214 lines (189 loc) • 8.67 kB
JavaScript
var RelativeElement = require('../creation/elements/relative-element');
var spacing = require('../helpers/spacing');
var getBarYAt = require('./get-bar-y-at');
var layoutBeam = function (beam) {
if (beam.elems.length === 0 || beam.allrests) return;
var dy = calcDy(beam.stemsUp, beam.isgrace); // This is the width of the beam line.
// create the main beam
var firstElement = beam.elems[0];
var lastElement = beam.elems[beam.elems.length - 1];
var minStemHeight = 0; // The following is to leave space for "!///!" marks.
var referencePitch = beam.stemsUp ? firstElement.abcelem.maxpitch : firstElement.abcelem.minpitch;
minStemHeight = minStem(firstElement, beam.stemsUp, referencePitch, minStemHeight);
minStemHeight = minStem(lastElement, beam.stemsUp, referencePitch, minStemHeight);
minStemHeight = Math.max(beam.stemHeight, minStemHeight + 3); // TODO-PER: The 3 is the width of a 16th beam. The actual height of the beam should be used instead.
var yPos = calcYPos(beam.average, beam.elems.length, minStemHeight, beam.stemsUp, firstElement.abcelem.averagepitch, lastElement.abcelem.averagepitch, beam.isflat, beam.min, beam.max, beam.isgrace);
var xPos = calcXPos(beam.stemsUp, firstElement, lastElement);
beam.addBeam({ startX: xPos[0], endX: xPos[1], startY: yPos[0], endY: yPos[1], dy: dy });
// create the rest of the beams (in the case of 1/16th notes, etc.
var beams = createAdditionalBeams(beam.elems, beam.stemsUp, beam.beams[0], beam.isgrace, dy);
for (var i = 0; i < beams.length; i++)
beam.addBeam(beams[i]);
// Now that the main beam is defined, we know how tall the stems should be, so create them and attach them to the original notes.
createStems(beam.elems, beam.stemsUp, beam.beams[0], dy, beam.mainNote);
};
var getDurlog = function (duration) {
// TODO-PER: This is a hack to prevent a Chrome lockup. Duration should have been defined already,
// but there's definitely a case where it isn't. [Probably something to do with triplets.]
if (duration === undefined) {
return 0;
}
// console.log("getDurlog: " + duration);
return Math.floor(Math.log(duration) / Math.log(2));
};
//
// private functions
//
function minStem(element, stemsUp, referencePitch, minStemHeight) {
if (!element.children)
return minStemHeight;
for (var i = 0; i < element.children.length; i++) {
var elem = element.children[i];
if (stemsUp && elem.top !== undefined && elem.c === "flags.ugrace")
minStemHeight = Math.max(minStemHeight, elem.top - referencePitch);
else if (!stemsUp && elem.bottom !== undefined && elem.c === "flags.ugrace")
minStemHeight = Math.max(minStemHeight, referencePitch - elem.bottom + 7); // The extra 7 is because we are measuring the slash from the top.
}
return minStemHeight;
}
function calcSlant(leftAveragePitch, rightAveragePitch, numStems, isFlat) {
if (isFlat)
return 0;
var slant = leftAveragePitch - rightAveragePitch;
var maxSlant = numStems / 2;
if (slant > maxSlant) slant = maxSlant;
if (slant < -maxSlant) slant = -maxSlant;
return slant;
}
function calcDy(asc, isGrace) {
var dy = (asc) ? spacing.STEP : -spacing.STEP;
if (isGrace) dy = dy * 0.4;
return dy;
}
function calcXPos(asc, firstElement, lastElement) {
var starthead = firstElement.heads[asc ? 0 : firstElement.heads.length - 1];
var endhead = lastElement.heads[asc ? 0 : lastElement.heads.length - 1];
var startX = starthead.x;
if (asc) startX += starthead.w - 0.6;
var endX = endhead.x;
endX += (asc) ? endhead.w : 0.6;
return [startX, endX];
}
function calcYPos(average, numElements, stemHeight, asc, firstAveragePitch, lastAveragePitch, isFlat, minPitch, maxPitch, isGrace) {
var barpos = stemHeight - 2; // (isGrace)? 5:7;
var barminpos = stemHeight - 2;
var pos = Math.round(asc ? Math.max(average + barpos, maxPitch + barminpos) : Math.min(average - barpos, minPitch - barminpos));
var slant = calcSlant(firstAveragePitch, lastAveragePitch, numElements, isFlat);
var startY = pos + Math.floor(slant / 2);
var endY = pos + Math.floor(-slant / 2);
// If the notes are too high or too low, make the beam go down to the middle
if (!isGrace) {
if (asc && pos < 6) {
startY = 6;
endY = 6;
} else if (!asc && pos > 6) {
startY = 6;
endY = 6;
}
}
return [startY, endY];
}
function createStems(elems, asc, beam, dy, mainNote) {
for (var i = 0; i < elems.length; i++) {
var elem = elems[i];
if (elem.abcelem.rest)
continue;
// TODO-PER: This is odd. If it is a regular beam then elems is an array of AbsoluteElements, if it is a grace beam then it is an array of objects , so we directly attach the element to the parent. We tell it if is a grace note because they are passed in as a generic object instead of an AbsoluteElement.
var isGrace = elem.addExtra ? false : true;
var parent = isGrace ? mainNote : elem;
var furthestHead = elem.heads[(asc) ? 0 : elem.heads.length - 1];
var ovalDelta = 1 / 5;//(isGrace)?1/3:1/5;
var pitch = furthestHead.pitch + ((asc) ? ovalDelta : -ovalDelta);
var dx = asc ? furthestHead.w : 0; // down-pointing stems start on the left side of the note, up-pointing stems start on the right side, so we offset by the note width.
if (!isGrace)
dx += furthestHead.dx;
var x = furthestHead.x + dx; // this is now the actual x location in pixels.
var bary = getBarYAt(beam.startX, beam.startY, beam.endX, beam.endY, x);
var lineWidth = (asc) ? -0.6 : 0.6;
if (!asc)
bary -= (dy / 2) / spacing.STEP; // TODO-PER: This is just a fudge factor so the down-pointing stems don't overlap.
if (isGrace)
dx += elem.heads[0].dx;
// TODO-PER-HACK: One type of note head has a different placement of the stem. This should be more generically calculated:
if (furthestHead.c === 'noteheads.slash.quarter') {
if (asc)
pitch += 1;
else
pitch -= 1;
}
var stem = new RelativeElement(null, dx, 0, pitch, {
"type": "stem",
"pitch2": bary,
linewidth: lineWidth
});
stem.setX(parent.x); // This is after the x coordinates were set, so we have to set it directly.
parent.addRight(stem);
}
}
function createAdditionalBeams(elems, asc, beam, isGrace, dy) {
var beams = [];
var auxBeams = []; // auxbeam will be {x, y, durlog, single} auxbeam[0] should match with durlog=-4 (16th) (j=-4-durlog)
for (var i = 0; i < elems.length; i++) {
var elem = elems[i];
if (elem.abcelem.rest)
continue;
var furthestHead = elem.heads[(asc) ? 0 : elem.heads.length - 1];
var x = furthestHead.x + ((asc) ? furthestHead.w : 0);
var bary = getBarYAt(beam.startX, beam.startY, beam.endX, beam.endY, x);
var sy = (asc) ? -1.5 : 1.5;
if (isGrace) sy = sy * 2 / 3; // This makes the second beam on grace notes closer to the first one.
var duration = elem.abcelem.duration; // get the duration via abcelem because of triplets
if (duration === 0) duration = 0.25; // if this is stemless, then we use quarter note as the duration.
for (var durlog = getDurlog(duration); durlog < -3; durlog++) {
var index = -4 - durlog;
if (auxBeams[index]) {
auxBeams[index].single = false;
} else {
auxBeams[index] = {
x: x + ((asc) ? -0.6 : 0), y: bary + sy * (index + 1),
durlog: durlog, single: true
};
}
if (i > 0 && elem.abcelem.beambr && elem.abcelem.beambr <= (index + 1)) {
if (!auxBeams[index].split)
auxBeams[index].split = [auxBeams[index].x];
var xPos = calcXPos(asc, elems[i - 1], elem);
if (auxBeams[index].split[auxBeams[index].split.length - 1] >= xPos[0]) {
// the reduction in beams leaves a note unattached so create a small flag for it.
xPos[0] += elem.w;
}
auxBeams[index].split.push(xPos[0]);
auxBeams[index].split.push(xPos[1]);
}
}
for (var j = auxBeams.length - 1; j >= 0; j--) {
if (i === elems.length - 1 || getDurlog(elems[i + 1].abcelem.duration) > (-j - 4)) {
var auxBeamEndX = x;
var auxBeamEndY = bary + sy * (j + 1);
if (auxBeams[j].single) {
auxBeamEndX = (i === 0) ? x + 5 : x - 5;
auxBeamEndY = getBarYAt(beam.startX, beam.startY, beam.endX, beam.endY, auxBeamEndX) + sy * (j + 1);
}
var b = { startX: auxBeams[j].x, endX: auxBeamEndX, startY: auxBeams[j].y, endY: auxBeamEndY, dy: dy }
if (auxBeams[j].split !== undefined) {
var split = auxBeams[j].split;
if (b.endX <= split[split.length - 1]) {
// the reduction in beams leaves the last note by itself, so create a little flag for it
split[split.length - 1] -= elem.w;
}
split.push(b.endX);
b.split = auxBeams[j].split;
}
beams.push(b);
auxBeams = auxBeams.slice(0, j);
}
}
}
return beams;
}
module.exports = layoutBeam;