abcjs 6.5.1 → 6.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/RELEASE.md +44 -0
- package/dist/abcjs-basic-min.js +2 -2
- package/dist/abcjs-basic.js +1198 -150
- package/dist/abcjs-basic.js.map +1 -1
- package/dist/abcjs-plugin-min.js +2 -2
- package/package.json +1 -1
- package/src/api/abc_timing_callbacks.js +88 -13
- package/src/const/relative-major.js +24 -19
- package/src/data/abc_tune.js +12 -1
- package/src/data/deline-tune.js +1 -1
- package/src/edit/abc_editarea.js +53 -50
- package/src/edit/abc_editor.js +176 -157
- package/src/midi/abc_midi_create.js +2 -2
- package/src/parse/abc_parse.js +20 -0
- package/src/parse/chord-grid.js +364 -0
- package/src/parse/tune-builder.js +4 -4
- package/src/str/output.js +97 -48
- package/src/synth/abc_midi_sequencer.js +20 -30
- package/src/synth/create-synth.js +1 -1
- package/src/synth/repeats.js +200 -0
- package/src/write/creation/abstract-engraver.js +4 -1
- package/src/write/draw/chord-grid.js +252 -0
- package/src/write/draw/draw.js +48 -35
- package/src/write/draw/text.js +1 -1
- package/src/write/engraver-controller.js +4 -2
- package/src/write/layout/beam.js +16 -1
- package/src/write/renderer.js +4 -0
- package/src/write/svg.js +8 -1
- package/types/index.d.ts +36 -3
- package/version.js +1 -1
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
var sequence;
|
|
4
4
|
var parseCommon = require("../parse/abc_common");
|
|
5
|
+
var Repeats = require("./repeats");
|
|
5
6
|
|
|
6
7
|
(function() {
|
|
7
8
|
"use strict";
|
|
8
9
|
|
|
9
10
|
var measureLength = 1; // This should be set by the meter, but just in case that is missing, we'll take a guess.
|
|
10
|
-
// The abc is provided to us line by line. It might have repeats in it. We want to
|
|
11
|
+
// The abc is provided to us line by line. It might have repeats in it. We want to rearrange the elements to
|
|
11
12
|
// be an array of voices with all the repeats embedded, and no lines. Then it is trivial to go through the events
|
|
12
13
|
// one at a time and turn it into midi.
|
|
13
14
|
|
|
@@ -142,8 +143,7 @@ var parseCommon = require("../parse/abc_common");
|
|
|
142
143
|
var tempoChanges = {};
|
|
143
144
|
tempoChanges["0"] = { el_type: 'tempo', qpm: qpm, timing: 0 };
|
|
144
145
|
var currentVolume;
|
|
145
|
-
var
|
|
146
|
-
var skipEndingPlaceholder = []; // This is the place where the first ending starts.
|
|
146
|
+
var repeats = []
|
|
147
147
|
var startingDrumSet = false;
|
|
148
148
|
var lines = abctune.lines; //abctune.deline(); TODO-PER: can switch to this, then simplify the loops below.
|
|
149
149
|
for (var i = 0; i < lines.length; i++) {
|
|
@@ -166,6 +166,7 @@ var parseCommon = require("../parse/abc_common");
|
|
|
166
166
|
var voiceName = getTrackTitle(line.staff, voiceNumber);
|
|
167
167
|
if (voiceName)
|
|
168
168
|
voices[voiceNumber].unshift({el_type: "name", trackName: voiceName});
|
|
169
|
+
repeats[voiceNumber] = new Repeats(voices[voiceNumber])
|
|
169
170
|
}
|
|
170
171
|
// Negate any transposition for the percussion staff.
|
|
171
172
|
if (transpose && staff.clef.type === "perc")
|
|
@@ -316,31 +317,7 @@ var parseCommon = require("../parse/abc_common");
|
|
|
316
317
|
voices[voiceNumber].push({ el_type: 'bar' }); // We need the bar marking to reset the accidentals.
|
|
317
318
|
setDynamics(elem);
|
|
318
319
|
noteEventsInBar = 0;
|
|
319
|
-
|
|
320
|
-
// The important part is where there is a start repeat, and end repeat, or a first ending.
|
|
321
|
-
var endRepeat = (elem.type === "bar_right_repeat" || elem.type === "bar_dbl_repeat");
|
|
322
|
-
var startEnding = (elem.startEnding === '1');
|
|
323
|
-
var startRepeat = (elem.type === "bar_left_repeat" || elem.type === "bar_dbl_repeat" || elem.type === "bar_right_repeat");
|
|
324
|
-
if (endRepeat) {
|
|
325
|
-
var s = startRepeatPlaceholder[voiceNumber];
|
|
326
|
-
if (!s) s = 0; // If there wasn't a left repeat, then we repeat from the beginning.
|
|
327
|
-
var e = skipEndingPlaceholder[voiceNumber];
|
|
328
|
-
if (!e) e = voices[voiceNumber].length; // If there wasn't a first ending marker, then we copy everything.
|
|
329
|
-
// duplicate each of the elements - this has to be a deep copy.
|
|
330
|
-
for (var z = s; z < e; z++) {
|
|
331
|
-
var item = Object.assign({},voices[voiceNumber][z]);
|
|
332
|
-
if (item.pitches)
|
|
333
|
-
item.pitches = parseCommon.cloneArray(item.pitches);
|
|
334
|
-
voices[voiceNumber].push(item);
|
|
335
|
-
}
|
|
336
|
-
// reset these in case there is a second repeat later on.
|
|
337
|
-
skipEndingPlaceholder[voiceNumber] = undefined;
|
|
338
|
-
startRepeatPlaceholder[voiceNumber] = undefined;
|
|
339
|
-
}
|
|
340
|
-
if (startEnding)
|
|
341
|
-
skipEndingPlaceholder[voiceNumber] = voices[voiceNumber].length;
|
|
342
|
-
if (startRepeat)
|
|
343
|
-
startRepeatPlaceholder[voiceNumber] = voices[voiceNumber].length;
|
|
320
|
+
repeats[voiceNumber].addBar(elem, voiceNumber)
|
|
344
321
|
rhythmHeadThisBar = false;
|
|
345
322
|
break;
|
|
346
323
|
case 'style':
|
|
@@ -524,6 +501,9 @@ var parseCommon = require("../parse/abc_common");
|
|
|
524
501
|
}
|
|
525
502
|
}
|
|
526
503
|
}
|
|
504
|
+
for (var r = 0; r < repeats.length; r++)
|
|
505
|
+
voices[r] = repeats[r].resolveRepeats()
|
|
506
|
+
|
|
527
507
|
// If there are tempo changes, make sure they are in all the voices. This must be done post process because all the elements in all the voices need to be created first.
|
|
528
508
|
insertTempoChanges(voices, tempoChanges);
|
|
529
509
|
|
|
@@ -647,19 +627,29 @@ var parseCommon = require("../parse/abc_common");
|
|
|
647
627
|
switch (element.type) {
|
|
648
628
|
case "common_time":
|
|
649
629
|
meter = { el_type: 'meter', num: 4, den: 4 };
|
|
630
|
+
measureLength = 4/4
|
|
650
631
|
break;
|
|
651
632
|
case "cut_time":
|
|
652
633
|
meter = { el_type: 'meter', num: 2, den: 2 };
|
|
634
|
+
measureLength = 2/2
|
|
653
635
|
break;
|
|
654
636
|
case "specified":
|
|
655
637
|
// TODO-PER: only taking the first meter, so the complex meters are not handled.
|
|
656
|
-
|
|
638
|
+
let num = 0
|
|
639
|
+
if (element.value && element.value.length > 0 && element.value[0].num.indexOf('+') > 0) {
|
|
640
|
+
var parts = element.value[0].num.split('+')
|
|
641
|
+
for (var i = 0; i < parts.length; i++)
|
|
642
|
+
num += parseInt(parts[i],10)
|
|
643
|
+
} else
|
|
644
|
+
num = parseInt(element.value[0].num, 10);
|
|
645
|
+
meter = { el_type: 'meter', num: num, den: element.value[0].den };
|
|
646
|
+
measureLength = num / parseInt(element.value[0].den,10)
|
|
657
647
|
break;
|
|
658
648
|
default:
|
|
659
649
|
// This should never happen.
|
|
660
650
|
meter = { el_type: 'meter' };
|
|
651
|
+
measureLength = 1
|
|
661
652
|
}
|
|
662
|
-
measureLength = meter.num/meter.den;
|
|
663
653
|
return meter;
|
|
664
654
|
}
|
|
665
655
|
|
|
@@ -116,7 +116,7 @@ function CreateSynth() {
|
|
|
116
116
|
self.flattened = options.visualObj.setUpAudio(params);
|
|
117
117
|
var meter = options.visualObj.getMeterFraction();
|
|
118
118
|
if (meter.den)
|
|
119
|
-
self.meterSize =
|
|
119
|
+
self.meterSize = meter.num / meter.den;
|
|
120
120
|
self.pickupLength = options.visualObj.getPickupLength()
|
|
121
121
|
} else if (options.sequence)
|
|
122
122
|
self.flattened = options.sequence;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const parseCommon = require("../parse/abc_common");
|
|
2
|
+
|
|
3
|
+
function Repeats(voice) {
|
|
4
|
+
this.sections = [{type: 'startRepeat', index: -1}]
|
|
5
|
+
|
|
6
|
+
this.addBar = function(elem) {
|
|
7
|
+
// Record the "interesting" parts for analysis at the end.
|
|
8
|
+
var thisIndex = voice.length - 1
|
|
9
|
+
var isStartRepeat = elem.type === "bar_left_repeat" || elem.type === "bar_dbl_repeat"
|
|
10
|
+
var isEndRepeat = elem.type === "bar_right_repeat" || elem.type === "bar_dbl_repeat"
|
|
11
|
+
var startEnding = elem.startEnding ? startEndingNumbers(elem.startEnding) : undefined
|
|
12
|
+
if (isEndRepeat)
|
|
13
|
+
this.sections.push({type:"endRepeat", index: thisIndex})
|
|
14
|
+
if (isStartRepeat)
|
|
15
|
+
this.sections.push({type:"startRepeat", index: thisIndex})
|
|
16
|
+
if (startEnding)
|
|
17
|
+
this.sections.push({type:"startEnding", index: thisIndex, endings: startEnding})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
this.resolveRepeats = function() {
|
|
21
|
+
// this.sections contain all the interesting bars - start and end repeats.
|
|
22
|
+
var e
|
|
23
|
+
|
|
24
|
+
// There may be one last set of events after the last interesting bar, so capture that now.
|
|
25
|
+
var lastSection = this.sections[this.sections.length-1]
|
|
26
|
+
var lastElement = voice.length-1
|
|
27
|
+
if (lastSection.type === 'startRepeat')
|
|
28
|
+
lastSection.end = lastElement
|
|
29
|
+
else if (lastSection.index+1 < lastElement)
|
|
30
|
+
this.sections.push({type: "startRepeat", index: lastSection.index+1})
|
|
31
|
+
|
|
32
|
+
// console.log(voice.map((el,index) => {
|
|
33
|
+
// return JSON.stringify({i: index, t: el.el_type, p: el.pitches ? el.pitches[0].name: undefined})
|
|
34
|
+
// }).join("\n"))
|
|
35
|
+
|
|
36
|
+
// console.log(this.sections.map(s => JSON.stringify(s)).join("\n"))
|
|
37
|
+
if (this.sections.length < 2)
|
|
38
|
+
return voice // If there are no repeats then don't bother copying anything
|
|
39
|
+
|
|
40
|
+
// Go through all the markers and turn that into an array of sets of sections in order.
|
|
41
|
+
// The output is repeatInstructions. If "endings" is not present, then the common section should just
|
|
42
|
+
// be copied once. If "endings" is present but is empty, that means it is a plain repeat without
|
|
43
|
+
// endings so the common section is copied twice. If "endings" contains items, then copy the
|
|
44
|
+
// common section followed by each ending in turn. If the last item in "endings" is -1, then
|
|
45
|
+
// the common section should be copied one more time but there isn't a corresponding ending for it.
|
|
46
|
+
var repeatInstructions = [] // { common: { start: number, end: number }, endings: Array<{start:number, end:number> }
|
|
47
|
+
var currentRepeat = null
|
|
48
|
+
for (var i = 0; i < this.sections.length; i++) {
|
|
49
|
+
var section = this.sections[i]
|
|
50
|
+
//var end = i < this.sections.length-1 ? this.sections[i+1].index : lastElement
|
|
51
|
+
switch (section.type) {
|
|
52
|
+
case "startRepeat":
|
|
53
|
+
if (currentRepeat) {
|
|
54
|
+
if (!currentRepeat.common.end)
|
|
55
|
+
currentRepeat.common.end = section.index
|
|
56
|
+
if (currentRepeat.endings) {
|
|
57
|
+
for (e = 0; e < currentRepeat.endings.length; e++) {
|
|
58
|
+
if (currentRepeat.endings[e] && !currentRepeat.endings[e].end && currentRepeat.endings[e].start !== section.index)
|
|
59
|
+
currentRepeat.endings[e].end = section.index
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// If the last event was an end repeat, then there is one more repeat of just the common area. (Only when there are ending markers - otherwise it is already taken care of.)
|
|
63
|
+
if (this.sections[i-1].type === 'endRepeat' && currentRepeat.endings && currentRepeat.endings.length)
|
|
64
|
+
currentRepeat.endings[currentRepeat.endings.length] = { start: -1, end: -1}
|
|
65
|
+
|
|
66
|
+
repeatInstructions.push(currentRepeat)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// if there is a gap between the last event and this start, then
|
|
70
|
+
// insert those items.
|
|
71
|
+
if (currentRepeat) {
|
|
72
|
+
var lastUsed = currentRepeat.common.end
|
|
73
|
+
if (currentRepeat.endings) {
|
|
74
|
+
for (e = 0; e < currentRepeat.endings.length; e++) {
|
|
75
|
+
if (currentRepeat.endings[e])
|
|
76
|
+
lastUsed = Math.max(lastUsed, currentRepeat.endings[e].end)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (lastUsed < section.index - 1) {
|
|
81
|
+
console.log("gap", voice.slice(lastUsed+1, section.index))
|
|
82
|
+
repeatInstructions.push({common: {start: lastUsed+1, end: section.index}})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
currentRepeat = { common: { start: section.index} }
|
|
86
|
+
break;
|
|
87
|
+
case "startEnding": {
|
|
88
|
+
if (currentRepeat) {
|
|
89
|
+
if (!currentRepeat.common.end)
|
|
90
|
+
currentRepeat.common.end = section.index
|
|
91
|
+
if (!currentRepeat.endings)
|
|
92
|
+
currentRepeat.endings = []
|
|
93
|
+
for (e = 0; e < section.endings.length; e++)
|
|
94
|
+
currentRepeat.endings[section.endings[e]] = {start: section.index+1}
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case "endRepeat":
|
|
99
|
+
if (currentRepeat) {
|
|
100
|
+
if (!currentRepeat.endings)
|
|
101
|
+
currentRepeat.endings = []
|
|
102
|
+
if (currentRepeat.endings.length > 0) {
|
|
103
|
+
for (e = 0; e < currentRepeat.endings.length; e++) {
|
|
104
|
+
if (currentRepeat.endings[e] && !currentRepeat.endings[e].end)
|
|
105
|
+
currentRepeat.endings[e].end = section.index
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!currentRepeat.common.end)
|
|
109
|
+
// This is a repeat that doesn't have first and second endings
|
|
110
|
+
currentRepeat.common.end = section.index
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (currentRepeat) {
|
|
116
|
+
if (!currentRepeat.common.end)
|
|
117
|
+
currentRepeat.common.end = lastElement
|
|
118
|
+
if (currentRepeat.endings) {
|
|
119
|
+
for (e = 0; e < currentRepeat.endings.length; e++) {
|
|
120
|
+
if (currentRepeat.endings[e] && !currentRepeat.endings[e].end)
|
|
121
|
+
currentRepeat.endings[e].end = lastElement
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
repeatInstructions.push(currentRepeat)
|
|
125
|
+
}
|
|
126
|
+
// for (var x = 0; x < repeatInstructions.length; x++) {
|
|
127
|
+
// console.log(JSON.stringify(repeatInstructions[x]))
|
|
128
|
+
// }
|
|
129
|
+
|
|
130
|
+
var output = []
|
|
131
|
+
var lastEnd = -1
|
|
132
|
+
for (var r = 0; r < repeatInstructions.length; r++) {
|
|
133
|
+
var instructions = repeatInstructions[r]
|
|
134
|
+
if (!instructions.endings) {
|
|
135
|
+
duplicateSpan(voice, output, instructions.common.start, instructions.common.end)
|
|
136
|
+
} else if (instructions.endings.length === 0) {
|
|
137
|
+
// this is when there is no endings specified - it is just a repeat
|
|
138
|
+
duplicateSpan(voice, output, instructions.common.start, instructions.common.end)
|
|
139
|
+
duplicateSpan(voice, output, instructions.common.start, instructions.common.end)
|
|
140
|
+
} else {
|
|
141
|
+
for (e = 0; e < instructions.endings.length; e++) {
|
|
142
|
+
var ending = instructions.endings[e]
|
|
143
|
+
if (ending) { // this is a sparse array so skip the empty ones
|
|
144
|
+
duplicateSpan(voice, output, instructions.common.start, instructions.common.end)
|
|
145
|
+
if (ending.start > 0) {
|
|
146
|
+
duplicateSpan(voice, output, ending.start, ending.end)
|
|
147
|
+
}
|
|
148
|
+
lastEnd = Math.max(lastEnd, ending.end)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return output
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function duplicateSpan(input, output, start, end) {
|
|
158
|
+
//console.log("dup", {start, end})
|
|
159
|
+
for (var i = start; i <= end; i++) {
|
|
160
|
+
output.push(duplicateItem(input[i]))
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function duplicateItem(src) {
|
|
165
|
+
var item = Object.assign({},src);
|
|
166
|
+
if (item.pitches)
|
|
167
|
+
item.pitches = parseCommon.cloneArray(item.pitches);
|
|
168
|
+
return item
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function startEndingNumbers(startEnding) {
|
|
172
|
+
// The ending can be in four different types: "random-string", "number", "number-number", "number,number"
|
|
173
|
+
// If we don't get a number out of it then we will just skip the ending - we don't know what to do with it.
|
|
174
|
+
var nums = []
|
|
175
|
+
var ending, endings, i;
|
|
176
|
+
if (startEnding.indexOf(',') > 0) {
|
|
177
|
+
endings = startEnding.split(',')
|
|
178
|
+
for (i = 0; i < endings.length; i++) {
|
|
179
|
+
ending = parseInt(endings[i],10)
|
|
180
|
+
if (ending > 0) {
|
|
181
|
+
nums.push(ending)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} else if (startEnding.indexOf('-') > 0) {
|
|
185
|
+
endings = startEnding.split('-')
|
|
186
|
+
var se = parseInt(endings[0],10)
|
|
187
|
+
var ee = parseInt(endings[1],10)
|
|
188
|
+
for (i = se; i <= ee; i++) {
|
|
189
|
+
nums.push(i)
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
ending = parseInt(startEnding,10)
|
|
193
|
+
if (ending > 0) {
|
|
194
|
+
nums.push(ending)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return nums
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = Repeats;
|
|
@@ -348,8 +348,11 @@ AbstractEngraver.prototype.createABCElement = function (isFirstStaff, isSingleLi
|
|
|
348
348
|
elemset[0] = abselem;
|
|
349
349
|
break;
|
|
350
350
|
case "tempo":
|
|
351
|
+
// MAE 20 Nov 2025 For %%printtempo after initial header
|
|
351
352
|
var abselem3 = new AbsoluteElement(elem, 0, 0, 'tempo', this.tuneNumber);
|
|
352
|
-
|
|
353
|
+
if (!elem.suppress){
|
|
354
|
+
abselem3.addFixedX(new TempoElement(elem, this.tuneNumber, createNoteHead));
|
|
355
|
+
}
|
|
353
356
|
elemset[0] = abselem3;
|
|
354
357
|
break;
|
|
355
358
|
case "style":
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
const printSymbol = require("./print-symbol");
|
|
2
|
+
const printStem = require("./print-stem");
|
|
3
|
+
|
|
4
|
+
function drawChordGrid(renderer, parts, leftMargin, pageWidth, fonts) {
|
|
5
|
+
const chordFont = fonts.gchordfont
|
|
6
|
+
const partFont = fonts.partsfont
|
|
7
|
+
const annotationFont = fonts.annotationfont
|
|
8
|
+
const endingFont = fonts.repeatfont
|
|
9
|
+
const textFont = fonts.textfont
|
|
10
|
+
const subtitleFont = fonts.subtitlefont
|
|
11
|
+
|
|
12
|
+
const ROW_HEIGHT = 50
|
|
13
|
+
const ENDING_HEIGHT = 10
|
|
14
|
+
const ANNOTATION_HEIGHT = 14
|
|
15
|
+
const PART_MARGIN_TOP = 10
|
|
16
|
+
const PART_MARGIN_BOTTOM = 20
|
|
17
|
+
const TEXT_MARGIN = 16
|
|
18
|
+
|
|
19
|
+
parts.forEach(part => {
|
|
20
|
+
switch (part.type) {
|
|
21
|
+
case "text": {
|
|
22
|
+
text(renderer, part.text, leftMargin, renderer.y, 16, textFont, null, null, false )
|
|
23
|
+
renderer.moveY(TEXT_MARGIN)
|
|
24
|
+
}
|
|
25
|
+
break
|
|
26
|
+
case "subtitle": {
|
|
27
|
+
text(renderer, part.subtitle, leftMargin, renderer.y+PART_MARGIN_TOP, 20, subtitleFont, null, "abcjs-subtitle", false )
|
|
28
|
+
renderer.moveY(PART_MARGIN_BOTTOM)
|
|
29
|
+
}
|
|
30
|
+
break
|
|
31
|
+
case "part":
|
|
32
|
+
if (part.lines.length > 0) {
|
|
33
|
+
text(renderer, part.name, leftMargin, renderer.y+PART_MARGIN_TOP, 20, subtitleFont, part.name, "abcjs-part", false )
|
|
34
|
+
|
|
35
|
+
renderer.moveY(PART_MARGIN_BOTTOM)
|
|
36
|
+
const numCols = part.lines[0].length
|
|
37
|
+
const colWidth = pageWidth / numCols
|
|
38
|
+
part.lines.forEach((line, lineNum) => {
|
|
39
|
+
let hasEnding = false
|
|
40
|
+
let hasAnnotation = false
|
|
41
|
+
line.forEach(measure => {
|
|
42
|
+
if (measure.ending)
|
|
43
|
+
hasEnding = true
|
|
44
|
+
if (measure.annotations && measure.annotations.length > 0)
|
|
45
|
+
hasAnnotation = true
|
|
46
|
+
})
|
|
47
|
+
const extraTop = hasAnnotation ? ANNOTATION_HEIGHT : hasEnding ? ENDING_HEIGHT : 0
|
|
48
|
+
line.forEach((measure, barNum) => {
|
|
49
|
+
const RECT_WIDTH = 1
|
|
50
|
+
if (!measure.noBorder) {
|
|
51
|
+
renderer.paper.rect({x: leftMargin + barNum * colWidth, y: renderer.y, width: colWidth, height: extraTop + ROW_HEIGHT})
|
|
52
|
+
renderer.paper.rect({x: leftMargin + barNum * colWidth + RECT_WIDTH, y: renderer.y + RECT_WIDTH, width: colWidth - RECT_WIDTH * 2, height: extraTop + ROW_HEIGHT - RECT_WIDTH * 2})
|
|
53
|
+
let repeatLeft = 0
|
|
54
|
+
let repeatRight = 0
|
|
55
|
+
const top = renderer.y
|
|
56
|
+
const left = leftMargin + colWidth * barNum
|
|
57
|
+
if (measure.hasStartRepeat) {
|
|
58
|
+
drawRepeat(renderer, left, top, top+ROW_HEIGHT+extraTop, true, extraTop)
|
|
59
|
+
repeatLeft = 12
|
|
60
|
+
}
|
|
61
|
+
if (measure.hasEndRepeat) {
|
|
62
|
+
drawRepeat(renderer, left+colWidth, top, top+ROW_HEIGHT+extraTop, false, extraTop)
|
|
63
|
+
repeatRight = 12
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let endingWidth = 0
|
|
67
|
+
if (measure.ending) {
|
|
68
|
+
const endingEl = text(renderer, measure.ending, leftMargin + barNum * colWidth + 4, top + 10, 12, endingFont, null, null, false )
|
|
69
|
+
endingWidth = endingEl.getBBox().width + 4
|
|
70
|
+
}
|
|
71
|
+
drawMeasure(renderer, top, leftMargin+repeatLeft, colWidth, lineNum, barNum, measure.chord, chordFont, repeatLeft+repeatRight, ROW_HEIGHT, extraTop)
|
|
72
|
+
if (measure.annotations && measure.annotations.length > 0) {
|
|
73
|
+
drawAnnotations(renderer, top, leftMargin + barNum * colWidth +endingWidth, measure.annotations, annotationFont)
|
|
74
|
+
}
|
|
75
|
+
if (extraTop) {
|
|
76
|
+
renderer.paper.rectBeneath({x: leftMargin + barNum * colWidth, y: renderer.y, width: colWidth, height: extraTop, fill: '#e8e8e8', stroke: 'none'})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
renderer.moveY(extraTop + ROW_HEIGHT)
|
|
81
|
+
})
|
|
82
|
+
renderer.moveY(PART_MARGIN_BOTTOM)
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function drawPercent(renderer, x, y, offset) {
|
|
90
|
+
var lineX1 = x - 10
|
|
91
|
+
var lineX2 = x + 10
|
|
92
|
+
var lineY1 = y + 10
|
|
93
|
+
var lineY2 = y - 10
|
|
94
|
+
var leftDotX = x - 10
|
|
95
|
+
var leftDotY = -renderer.yToPitch(offset) + 2
|
|
96
|
+
var rightDotX = x + 6.5
|
|
97
|
+
var rightDotY = -renderer.yToPitch(offset) -2.3
|
|
98
|
+
|
|
99
|
+
renderer.paper.lineToBack({x1: lineX1, x2: lineX2, y1: lineY1, y2: lineY2, 'stroke-width': '3px', 'stroke-linecap':"round" })
|
|
100
|
+
|
|
101
|
+
printSymbol(renderer, leftDotX, leftDotY, "dots.dot", {
|
|
102
|
+
scalex: 1,
|
|
103
|
+
scaley: 1,
|
|
104
|
+
klass: "",
|
|
105
|
+
name: "dot"
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
printSymbol(renderer, rightDotX, rightDotY, "dots.dot", {
|
|
109
|
+
scalex: 1,
|
|
110
|
+
scaley: 1,
|
|
111
|
+
klass: "",
|
|
112
|
+
name: "dot"
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function drawRepeat(renderer, x, y1, y2, isStart, offset) {
|
|
118
|
+
const lineX = isStart ? x+2 : x-4
|
|
119
|
+
const circleX = isStart ? x+9 : x-11
|
|
120
|
+
|
|
121
|
+
renderer.paper.openGroup({klass:'abcjs-repeat'})
|
|
122
|
+
printStem(renderer, lineX, 3 + renderer.lineThickness, y1, y2, null, "bar")
|
|
123
|
+
|
|
124
|
+
printSymbol(renderer, circleX, -renderer.yToPitch(offset)-4, "dots.dot", {
|
|
125
|
+
scalex: 1,
|
|
126
|
+
scaley: 1,
|
|
127
|
+
klass: "",
|
|
128
|
+
name: "dot"
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
printSymbol(renderer, circleX, -renderer.yToPitch(offset)-8, "dots.dot", {
|
|
132
|
+
scalex: 1,
|
|
133
|
+
scaley: 1,
|
|
134
|
+
klass: "",
|
|
135
|
+
name: "dot"
|
|
136
|
+
});
|
|
137
|
+
renderer.paper.closeGroup()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const symbols = {
|
|
141
|
+
'segno': "scripts.segno",
|
|
142
|
+
'coda': "scripts.coda",
|
|
143
|
+
"fermata": "scripts.ufermata",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function drawAnnotations(renderer, offset, left, annotations, annotationFont) {
|
|
147
|
+
left += 3
|
|
148
|
+
let el
|
|
149
|
+
for (let a = 0; a < annotations.length; a++) {
|
|
150
|
+
switch (annotations[a]) {
|
|
151
|
+
case 'segno':
|
|
152
|
+
case 'coda':
|
|
153
|
+
case "fermata": {
|
|
154
|
+
left += 12
|
|
155
|
+
el = printSymbol(renderer, left, -3, symbols[annotations[a]], {
|
|
156
|
+
scalex: 1,
|
|
157
|
+
scaley: 1,
|
|
158
|
+
//klass: renderer.controller.classes.generate(klass),
|
|
159
|
+
name: symbols[annotations[a]]
|
|
160
|
+
});
|
|
161
|
+
const box = el.getBBox()
|
|
162
|
+
left += box.width
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
default:
|
|
166
|
+
text(renderer, annotations[a], left, offset + 12, 12, annotationFont, null, null, false )
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function drawMeasure(renderer, offset, leftMargin, colWidth, lineNum, barNum, chords, chordFont, margin, height, extraTop) {
|
|
173
|
+
const left = leftMargin + colWidth * barNum
|
|
174
|
+
if (!chords[1] && !chords[2] && !chords[3])
|
|
175
|
+
drawSingleChord(renderer, left, offset+extraTop, colWidth-margin, height, chords[0], chordFont, extraTop)
|
|
176
|
+
else if (!chords[1] && !chords[3])
|
|
177
|
+
drawTwoChords(renderer, left, offset, colWidth-margin, height, chords[0], chords[2], chordFont, extraTop)
|
|
178
|
+
else
|
|
179
|
+
drawFourChords(renderer, left, offset, colWidth-margin, height, chords, chordFont, extraTop)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function renderChord(renderer, x, y, size, chord, font, maxWidth) {
|
|
183
|
+
const el = text(renderer, chord, x, y, size, font, null, "abcjs-chord", true)
|
|
184
|
+
let bb = el.getBBox()
|
|
185
|
+
let fontSize = size
|
|
186
|
+
while (bb.width > maxWidth && fontSize >= 14) {
|
|
187
|
+
fontSize -= 2
|
|
188
|
+
el.setAttribute('font-size', fontSize)
|
|
189
|
+
bb = el.getBBox()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const MAX_ONE_CHORD = 34
|
|
194
|
+
const MAX_TWO_CHORDS = 26
|
|
195
|
+
const MAX_FOUR_CHORDS = 20
|
|
196
|
+
const TOP_MARGIN = -3
|
|
197
|
+
|
|
198
|
+
function drawSingleChord(renderer, left, top, width, height, chord, font, extraTop) {
|
|
199
|
+
if (chord === '%')
|
|
200
|
+
drawPercent(renderer, left+width/2, top+height/2, extraTop+height/2)
|
|
201
|
+
else
|
|
202
|
+
renderChord(renderer, left+width/2, top+height/2+TOP_MARGIN, MAX_ONE_CHORD, chord, font, width)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function drawTwoChords(renderer, left, top, width, height, chord1, chord2, font, extraTop) {
|
|
206
|
+
renderer.paper.lineToBack({x1: left, x2: left+width, y1: top+height+extraTop, y2: top+2 })
|
|
207
|
+
renderChord(renderer, left+width/4, top+height/4+5+extraTop+TOP_MARGIN, MAX_TWO_CHORDS, chord1, font, width/2)
|
|
208
|
+
renderChord(renderer, left+3*width/4, top+3*height/4+extraTop+TOP_MARGIN, MAX_TWO_CHORDS, chord2, font, width/2)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function drawFourChords(renderer, left, top, width, height, chords, font, extraTop) {
|
|
212
|
+
const MARGIN = 3
|
|
213
|
+
renderer.paper.lineToBack({x1: left+MARGIN, x2: left+width-MARGIN, y1: top+height/2+extraTop, y2: top+height/2+extraTop })
|
|
214
|
+
renderer.paper.lineToBack({x1: left+width/2, x2: left+width/2, y1: top+MARGIN+extraTop, y2: top+height-MARGIN+extraTop })
|
|
215
|
+
|
|
216
|
+
if (chords[0])
|
|
217
|
+
renderChord(renderer, left+width/4, top+height/4+2+extraTop+TOP_MARGIN, MAX_FOUR_CHORDS, shortenChord(chords[0]), font, width / 2)
|
|
218
|
+
if (chords[1])
|
|
219
|
+
renderChord(renderer, left+3*width/4, top+height/4+2+extraTop+TOP_MARGIN, MAX_FOUR_CHORDS, shortenChord(chords[1]), font, width / 2)
|
|
220
|
+
if (chords[2])
|
|
221
|
+
renderChord(renderer, left+width/4, top+3*height/4+extraTop+TOP_MARGIN, MAX_FOUR_CHORDS, shortenChord(chords[2]), font, width / 2)
|
|
222
|
+
if (chords[3])
|
|
223
|
+
renderChord(renderer, left+3*width/4, top+3*height/4+extraTop+TOP_MARGIN, MAX_FOUR_CHORDS, shortenChord(chords[3]), font, width / 2)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function shortenChord(chord) {
|
|
227
|
+
if (chord === "No Chord")
|
|
228
|
+
return "N.C."
|
|
229
|
+
return chord
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function text(renderer, str, x, y, size, font, dataName, klass, alignCenter) {
|
|
233
|
+
const attr = {
|
|
234
|
+
x: x,
|
|
235
|
+
y: y,
|
|
236
|
+
stroke:"none",
|
|
237
|
+
'font-size':size,
|
|
238
|
+
'font-style':font.style,
|
|
239
|
+
'font-family':font.face,
|
|
240
|
+
'font-weight':font.weight,
|
|
241
|
+
'text-decoration':font.decoration,
|
|
242
|
+
}
|
|
243
|
+
if (dataName)
|
|
244
|
+
attr['data-name'] = dataName
|
|
245
|
+
if (klass)
|
|
246
|
+
attr['class'] = klass
|
|
247
|
+
attr["text-anchor"] = alignCenter ? "middle" : "start"
|
|
248
|
+
|
|
249
|
+
return renderer.paper.text(str, attr, null, {"alignment-baseline": "middle"})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = drawChordGrid
|
package/src/write/draw/draw.js
CHANGED
|
@@ -3,8 +3,9 @@ var setPaperSize = require('./set-paper-size');
|
|
|
3
3
|
var nonMusic = require('./non-music');
|
|
4
4
|
var spacing = require('../helpers/spacing');
|
|
5
5
|
var Selectables = require('./selectables');
|
|
6
|
+
var drawChordGrid = require('./chord-grid');
|
|
6
7
|
|
|
7
|
-
function draw(renderer, classes, abcTune, width, maxWidth, responsive, scale, selectTypes, tuneNumber, lineOffset) {
|
|
8
|
+
function draw(renderer, classes, abcTune, width, maxWidth, responsive, scale, selectTypes, tuneNumber, lineOffset, chordGrid) {
|
|
8
9
|
var selectables = new Selectables(renderer.paper, selectTypes, tuneNumber);
|
|
9
10
|
var groupClasses = {}
|
|
10
11
|
if (classes.shouldAddClasses)
|
|
@@ -14,48 +15,60 @@ function draw(renderer, classes, abcTune, width, maxWidth, responsive, scale, se
|
|
|
14
15
|
nonMusic(renderer, abcTune.topText, selectables);
|
|
15
16
|
renderer.paper.closeGroup()
|
|
16
17
|
renderer.moveY(renderer.spacing.music);
|
|
18
|
+
|
|
19
|
+
let suppressMusic = false
|
|
20
|
+
if (chordGrid && abcTune.chordGrid) {
|
|
21
|
+
drawChordGrid(renderer, abcTune.chordGrid, renderer.padding.left, width, abcTune.formatting)
|
|
22
|
+
if (chordGrid === 'noMusic')
|
|
23
|
+
suppressMusic = true
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
var staffgroups = [];
|
|
18
27
|
var nStaves = 0;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
+
if (!suppressMusic) {
|
|
29
|
+
for (var line = 0; line < abcTune.lines.length; line++) {
|
|
30
|
+
classes.incrLine();
|
|
31
|
+
var abcLine = abcTune.lines[line];
|
|
32
|
+
if (abcLine.staff) {
|
|
33
|
+
// MAE 26 May 2025 - for incipits staff count limiting
|
|
34
|
+
nStaves++;
|
|
35
|
+
if (abcTune.formatting.maxStaves) {
|
|
36
|
+
if (nStaves > abcTune.formatting.maxStaves) {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
28
39
|
}
|
|
40
|
+
if (classes.shouldAddClasses)
|
|
41
|
+
groupClasses.klass = "abcjs-staff-wrapper abcjs-l" + classes.lineNumber
|
|
42
|
+
renderer.paper.openGroup(groupClasses)
|
|
43
|
+
if (abcLine.vskip) {
|
|
44
|
+
renderer.moveY(abcLine.vskip);
|
|
45
|
+
}
|
|
46
|
+
if (staffgroups.length >= 1)
|
|
47
|
+
addStaffPadding(renderer, renderer.spacing.staffSeparation, staffgroups[staffgroups.length - 1], abcLine.staffGroup);
|
|
48
|
+
var staffgroup = engraveStaffLine(renderer, abcLine.staffGroup, selectables, line);
|
|
49
|
+
staffgroup.line = lineOffset + line; // If there are non-music lines then the staffgroup array won't line up with the line array, so this keeps track.
|
|
50
|
+
staffgroups.push(staffgroup);
|
|
51
|
+
renderer.paper.closeGroup()
|
|
52
|
+
} else if (abcLine.nonMusic) {
|
|
53
|
+
if (classes.shouldAddClasses)
|
|
54
|
+
groupClasses.klass = "abcjs-non-music"
|
|
55
|
+
renderer.paper.openGroup(groupClasses)
|
|
56
|
+
nonMusic(renderer, abcLine.nonMusic, selectables);
|
|
57
|
+
renderer.paper.closeGroup()
|
|
29
58
|
}
|
|
30
|
-
if (classes.shouldAddClasses)
|
|
31
|
-
groupClasses.klass = "abcjs-staff-wrapper abcjs-l" + classes.lineNumber
|
|
32
|
-
renderer.paper.openGroup(groupClasses)
|
|
33
|
-
if (abcLine.vskip) {
|
|
34
|
-
renderer.moveY(abcLine.vskip);
|
|
35
|
-
}
|
|
36
|
-
if (staffgroups.length >= 1)
|
|
37
|
-
addStaffPadding(renderer, renderer.spacing.staffSeparation, staffgroups[staffgroups.length - 1], abcLine.staffGroup);
|
|
38
|
-
var staffgroup = engraveStaffLine(renderer, abcLine.staffGroup, selectables, line);
|
|
39
|
-
staffgroup.line = lineOffset + line; // If there are non-music lines then the staffgroup array won't line up with the line array, so this keeps track.
|
|
40
|
-
staffgroups.push(staffgroup);
|
|
41
|
-
renderer.paper.closeGroup()
|
|
42
|
-
} else if (abcLine.nonMusic) {
|
|
43
|
-
if (classes.shouldAddClasses)
|
|
44
|
-
groupClasses.klass = "abcjs-non-music"
|
|
45
|
-
renderer.paper.openGroup(groupClasses)
|
|
46
|
-
nonMusic(renderer, abcLine.nonMusic, selectables);
|
|
47
|
-
renderer.paper.closeGroup()
|
|
48
59
|
}
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
classes.reset();
|
|
52
|
-
if (
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
if (!suppressMusic) {
|
|
64
|
+
if (abcTune.bottomText && abcTune.bottomText.rows && abcTune.bottomText.rows.length > 0) {
|
|
65
|
+
if (classes.shouldAddClasses)
|
|
66
|
+
groupClasses.klass = "abcjs-meta-bottom"
|
|
67
|
+
renderer.paper.openGroup(groupClasses)
|
|
68
|
+
renderer.moveY(24); // TODO-PER: Empirically discovered. What variable should this be?
|
|
69
|
+
nonMusic(renderer, abcTune.bottomText, selectables);
|
|
70
|
+
renderer.paper.closeGroup()
|
|
71
|
+
}
|
|
59
72
|
}
|
|
60
73
|
setPaperSize(renderer, maxWidth, scale, responsive);
|
|
61
74
|
return { staffgroups: staffgroups, selectables: selectables.getElements() };
|