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;