File size: 10,345 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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
//    abc_tie_element.js: Definition of the TieElement class.

var TieElem = function TieElem(options) {
	this.type = "TieElem";
	//	console.log("constructor", options.anchor1 ? options.anchor1.pitch : "N/A", options.anchor2 ? options.anchor2.pitch : "N/A", options.isTie, options.isGrace);
	this.anchor1 = options.anchor1; // must have a .x and a .pitch, and a .parent property or be null (means starts at the "beginning" of the line - after keysig)
	this.anchor2 = options.anchor2; // must have a .x and a .pitch property or be null (means ends at the end of the line)
	if (options.isGrace)
		this.isGrace = true;
	if (options.fixedY)
		this.fixedY = true;
	if (options.stemDir)
		this.stemDir = options.stemDir;
	if (options.voiceNumber !== undefined)
		this.voiceNumber = options.voiceNumber;
	if (options.style !== undefined)
		this.dotted = true;
	this.internalNotes = [];
};

TieElem.prototype.addInternalNote = function (note) {
	this.internalNotes.push(note);
};

TieElem.prototype.setEndAnchor = function (anchor2) {
	//	console.log("end", this.anchor1 ? this.anchor1.pitch : "N/A", anchor2 ? anchor2.pitch : "N/A", this.isTie, this.isGrace);
	this.anchor2 = anchor2; // must have a .x and a .pitch property or be null (means ends at the end of the line)

	// we don't really have enough info to know what the vertical extent is yet and we won't until drawing. This will just give it enough
	// room on either side (we don't even know if the slur will be above yet). We need to set this so that we can make sure the voice has
	// at least enough room that the line doesn't get cut off if the tie or slur is the lowest thing.
	if (this.anchor1) {
		this.top = Math.max(this.anchor1.pitch, this.anchor2.pitch) + 4
		this.bottom = Math.min(this.anchor1.pitch, this.anchor2.pitch) - 4
	} else {
		this.top = this.anchor2.pitch + 4
		this.bottom = this.anchor2.pitch - 4
	}
};

// If we encounter a repeat sign, then we don't want to extend either a tie or a slur past it, so these are called to be a limit.
TieElem.prototype.setStartX = function (startLimitElem) {
	this.startLimitX = startLimitElem;
};

TieElem.prototype.setEndX = function (endLimitElem) {
	this.endLimitX = endLimitElem;
};

TieElem.prototype.setHint = function () {
	this.hint = true;
};

TieElem.prototype.calcTieDirection = function () {
	// The rules:
	// 1) If it is in a grace note group, then the direction is always BELOW.
	// 2) If it is in a single voice, then the direction is always OPPOSITE of the stem (or where the stem would have been in the case of whole notes.)
	// 3) If the stem direction is forced (probably because there are two voices on the same line), then the direction is the SAME as the stem direction.

	if (this.isGrace)
		this.above = false;
	else if (this.voiceNumber === 0)
		this.above = true;
	else if (this.voiceNumber > 0)
		this.above = false;
	else {
		var referencePitch;
		if (this.anchor1)
			referencePitch = this.anchor1.pitch;
		else if (this.anchor2)
			referencePitch = this.anchor2.pitch;
		else
			referencePitch = 14; // TODO-PER: this can't really happen normally. This would imply that a tie crossed over three lines, something like "C-\nz\nC"
		// Put the arc in the opposite direction of the stem. That isn't always the pitch if one or both of the notes are beamed with something that affects its stem.
		if ((this.anchor1 && this.anchor1.stemDir === 'down') && (this.anchor2 && this.anchor2.stemDir === "down"))
			this.above = true;
		else if ((this.anchor1 && this.anchor1.stemDir === 'up') && (this.anchor2 && this.anchor2.stemDir === "up"))
			this.above = false;
		else if (this.anchor1 && this.anchor2)
			this.above = referencePitch >= 6;
		else if (this.anchor1)
			this.above = this.anchor1.stemDir === "down";
		else if (this.anchor2)
			this.above = this.anchor2.stemDir === "down";
		else
			this.above = referencePitch >= 6;
	}
};

// From "standard music notation practice" by Music Publishers’ Association:
// 1) Slurs are placed under the note heads if all stems go up.
// 2) Slurs are placed over the note heads if all stems go down.
// 3) If there are both up stems and down stems, prefer placing the slur over.
// 4) When the staff has opposite stemmed voices, all slurs should be on the stemmed side.

TieElem.prototype.calcSlurDirection = function () {
	if (this.isGrace)
		this.above = false;
	else if (this.voiceNumber === 0)
		this.above = true;
	else if (this.voiceNumber > 0)
		this.above = false;
	else {
		var hasDownStem = false;
		if (this.anchor1 && this.anchor1.stemDir === "down")
			hasDownStem = true;
		if (this.anchor2 && this.anchor2.stemDir === "down")
			hasDownStem = true;
		for (var i = 0; i < this.internalNotes.length; i++) {
			var n = this.internalNotes[i];
			if (n.stemDir === "down")
				hasDownStem = true;
		}
		this.above = hasDownStem;
	}
};

TieElem.prototype.calcX = function (lineStartX, lineEndX) {
	if (this.anchor1) {
		this.startX = this.anchor1.x; // The normal case where there is a starting element to attach to.
		if (this.anchor1.scalex < 1) // this is a grace note - don't offset the tie as much.
			this.startX -= 3;
	} else if (this.startLimitX)
		this.startX = this.startLimitX.x + this.startLimitX.w; // if there is no start element, but there is a repeat mark before the start of the line.
	else {
		if (this.anchor2)
			this.startX = this.anchor2.x - 20; // There is no element and no repeat mark: make a small arc
		else
			this.startX = lineStartX; // Don't have any guidance, so extend to beginning of line
	}
	if (!this.anchor1 && this.dotted)
		this.startX -= 3; // The arc needs to be long enough to tell that it is dotted.

	if (this.anchor2)
		this.endX = this.anchor2.x; // The normal case where there is a starting element to attach to.
	else if (this.endLimitX)
		this.endX = this.endLimitX.x; // if there is no start element, but there is a repeat mark before the start of the line.
	else
		this.endX = lineEndX; // There is no element and no repeat mark: extend to the beginning of the line.
};

TieElem.prototype.calcTieY = function () {
	// If the tie comes from another line, then one or both anchors will be missing.
	if (this.anchor1)
		this.startY = this.anchor1.pitch;
	else if (this.anchor2)
		this.startY = this.anchor2.pitch;
	else
		this.startY = this.above ? 14 : 0;

	if (this.anchor2)
		this.endY = this.anchor2.pitch;
	else if (this.anchor1)
		this.endY = this.anchor1.pitch;
	else
		this.endY = this.above ? 14 : 0;
};

// From "standard music notation practice" by Music Publishers’ Association:
// 1) If the anchor note is down stem, the slur points to the note head.
// 2) If the anchor note is up stem, and the slur is over, then point to middle of stem.

TieElem.prototype.calcSlurY = function () {
	if (this.anchor1 && this.anchor2) {
		if (this.above && this.anchor1.stemDir === "up" && !this.fixedY) {
			this.startY = (this.anchor1.highestVert + this.anchor1.pitch) / 2;
			this.startX += this.anchor1.w / 2; // When going to the middle of the stem, bump the line to the right a little bit to make it look right.
		} else
			this.startY = this.anchor1.pitch;

		// If the closing note has an up stem, and it is beamed, and it isn't the first note in the beam, then the beam will get in the way.
		var beamInterferes = this.anchor2.parent.beam && this.anchor2.parent.beam.stemsUp && this.anchor2.parent.beam.elems[0] !== this.anchor2.parent;
		var midPoint = (this.anchor2.highestVert + this.anchor2.pitch) / 2;
		if (this.above && this.anchor2.stemDir === "up" && !this.fixedY && !beamInterferes && (midPoint < this.startY)) {
			this.endY = midPoint;
			this.endX += Math.round(this.anchor2.w / 2); // When going to the middle of the stem, bump the line to the right a little bit to make it look right.
		} else
			this.endY = this.above && beamInterferes ? this.anchor2.highestVert : this.anchor2.pitch;

		if (this.anchor1.scalex === 1) { // Need a way to tell if this is a grace note - if so then keep the slur as close as possible. TODO-PER-HACK: this should be more declaratively determined.
			var hasBeam1 = !!this.anchor1.parent.beam
			var hasBeam2 = !!this.anchor2.parent.beam
			if (hasBeam1) {
				var isLastInBeam = this.anchor1.parent === this.anchor1.parent.beam.elems[this.anchor1.parent.beam.elems.length-1]
				if (!isLastInBeam) {
						if (this.above)
						this.startY = this.anchor1.parent.fixed.t
					else
						this.startY = this.anchor1.parent.fixed.b
				}
			}

			if (hasBeam2) {
				var isFirstInBeam = this.anchor2.parent === this.anchor2.parent.beam.elems[0]
				if (!isFirstInBeam) {
					if (this.above)
						this.endY = this.anchor2.parent.fixed.t
					else
						this.endY = this.anchor2.parent.fixed.b
				}
			}
		}
	} else if (this.anchor1) {
		this.startY = this.endY = this.anchor1.pitch;
	} else if (this.anchor2) {
		this.startY = this.endY = this.anchor2.pitch;
	} else {
		// This is the case where the slur covers the entire line.
		// TODO-PER: figure out where the real top and bottom of the line are.
		this.startY = this.above ? 14 : 0;
		this.endY = this.above ? 14 : 0;
	}
};

TieElem.prototype.avoidCollisionAbove = function () {
	// Double check that an interior note in the slur isn't so high that it interferes.
	if (this.above) {
		var maxInnerHeight = -50;
		for (var i = 0; i < this.internalNotes.length; i++) {
			if (this.internalNotes[i].highestVert > maxInnerHeight)
				maxInnerHeight = this.internalNotes[i].highestVert;
		}
		if (maxInnerHeight > this.startY && maxInnerHeight > this.endY)
			this.startY = this.endY = maxInnerHeight - 1;
	}
};

TieElem.prototype.getYBounds = function () {
	var lineStartX = 10 // TODO-PER: I'm not sure where to get this number from but it probably doesn't matter much
	var lineEndX = 1000 // TODO-PER: I'm not sure where to get this number from but it probably doesn't matter much
	if (this.isTie) {
		this.calcTieDirection();
		this.calcX(lineStartX, lineEndX);
		this.calcTieY();

	} else {
		this.calcSlurDirection();
		this.calcX(lineStartX, lineEndX);
		this.calcSlurY();
	}
	var top;
	var bottom;
	// TODO-PER: It's hard to tell how far the arc is, so I'm just using 3 as the max
	if (this.above) {
		bottom = Math.min(this.startY, this.endY)
		top = bottom + 3
	} else {
		top = Math.min(this.startY, this.endY)
		bottom = top - 3
	}
	return [ top, bottom ]
};

module.exports = TieElem;