abcjs 6.5.2 → 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.
@@ -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 re arrange the elements 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 startRepeatPlaceholder = []; // There is a place holder for each voice.
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
- // figure out repeats and endings --
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
- meter = { el_type: 'meter', num: element.value[0].num, den: element.value[0].den };
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 = options.visualObj.getMeterFraction().num / options.visualObj.getMeterFraction().den;
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
- abselem3.addFixedX(new TempoElement(elem, this.tuneNumber, createNoteHead));
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
@@ -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
- for (var line = 0; line < abcTune.lines.length; line++) {
20
- classes.incrLine();
21
- var abcLine = abcTune.lines[line];
22
- if (abcLine.staff) {
23
- // MAE 26 May 2025 - for incipits staff count limiting
24
- nStaves++;
25
- if (abcTune.formatting.maxStaves){
26
- if (nStaves > abcTune.formatting.maxStaves){
27
- break;
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 (abcTune.bottomText && abcTune.bottomText.rows && abcTune.bottomText.rows.length > 0) {
53
- if (classes.shouldAddClasses)
54
- groupClasses.klass = "abcjs-meta-bottom"
55
- renderer.paper.openGroup(groupClasses)
56
- renderer.moveY(24); // TODO-PER: Empirically discovered. What variable should this be?
57
- nonMusic(renderer, abcTune.bottomText, selectables);
58
- renderer.paper.closeGroup()
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() };