File size: 8,673 Bytes
af6912c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
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;
|