abcjs 6.3.0 → 6.4.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.
Files changed (46) hide show
  1. package/README.md +4 -0
  2. package/RELEASE.md +44 -0
  3. package/dist/abcjs-basic-min.js +2 -2
  4. package/dist/abcjs-basic.js +1028 -622
  5. package/dist/abcjs-basic.js.map +1 -1
  6. package/dist/abcjs-plugin-min.js +2 -2
  7. package/index.js +1 -0
  8. package/package.json +1 -1
  9. package/src/api/tune-metrics.js +18 -0
  10. package/src/data/abc_tune.js +13 -2
  11. package/src/edit/abc_editarea.js +4 -1
  12. package/src/parse/abc_parse.js +2 -0
  13. package/src/parse/abc_parse_directive.js +6 -0
  14. package/src/synth/abc_midi_flattener.js +40 -462
  15. package/src/synth/abc_midi_sequencer.js +25 -10
  16. package/src/synth/chord-track.js +562 -0
  17. package/src/synth/create-note-map.js +2 -1
  18. package/src/synth/create-synth.js +91 -42
  19. package/src/test/abc_parser_lint.js +1 -0
  20. package/src/write/creation/abstract-engraver.js +4 -1
  21. package/src/write/creation/decoration.js +3 -2
  22. package/src/write/creation/elements/tie-element.js +23 -0
  23. package/src/write/draw/draw.js +1 -1
  24. package/src/write/engraver-controller.js +9 -5
  25. package/src/write/interactive/create-analysis.js +50 -0
  26. package/src/write/interactive/find-selectable-element.js +24 -0
  27. package/src/write/interactive/selection.js +5 -45
  28. package/src/write/layout/layout-in-grid.js +83 -0
  29. package/src/write/layout/layout.js +29 -24
  30. package/src/write/layout/set-upper-and-lower-elements.js +2 -0
  31. package/src/write/layout/staff-group.js +2 -2
  32. package/src/write/layout/voice-elements.js +1 -1
  33. package/src/write/layout/voice.js +1 -1
  34. package/src/write/renderer.js +3 -0
  35. package/temp.txt +1 -48
  36. package/types/index.d.ts +96 -32
  37. package/version.js +1 -1
  38. package/abc2xml_239/abc2xml.html +0 -769
  39. package/abc2xml_239/abc2xml.py +0 -2248
  40. package/abc2xml_239/abc2xml_changelog.html +0 -124
  41. package/abc2xml_239/lazy-river.abc +0 -26
  42. package/abc2xml_239/lazy-river.xml +0 -3698
  43. package/abc2xml_239/mean-to-me.abc +0 -22
  44. package/abc2xml_239/mean-to-me.xml +0 -2954
  45. package/abc2xml_239/pyparsing.py +0 -3672
  46. package/abc2xml_239/pyparsing.pyc +0 -0
@@ -26,13 +26,15 @@ function CreateSynth() {
26
26
  self.audioBuffers = []; // cache of the buffers so starting play can be fast.
27
27
  self.duration = undefined; // the duration of the tune in seconds.
28
28
  self.isRunning = false; // whether there is currently a sound buffer running.
29
- // self.options = undefined
29
+ self.options = undefined
30
+ self.pickupLength = 0
30
31
 
31
32
  // Load and cache all needed sounds
32
33
  self.init = function(options) {
33
34
  if (!options)
34
35
  options = {};
35
- // self.options = options
36
+ if (options.options)
37
+ self.options = options.options
36
38
  registerAudioContext(options.audioContext); // This works no matter what - if there is already an ac it is a nop; if the context is not passed in, then it creates one.
37
39
  var startTime = activeAudioContext().currentTime;
38
40
  self.debugCallback = options.debugCallback;
@@ -115,6 +117,7 @@ function CreateSynth() {
115
117
  var meter = options.visualObj.getMeterFraction();
116
118
  if (meter.den)
117
119
  self.meterSize = options.visualObj.getMeterFraction().num / options.visualObj.getMeterFraction().den;
120
+ self.pickupLength = options.visualObj.getPickupLength()
118
121
  } else if (options.sequence)
119
122
  self.flattened = options.sequence;
120
123
  else
@@ -124,6 +127,7 @@ function CreateSynth() {
124
127
  self.sequenceCallback = params.sequenceCallback;
125
128
  self.callbackContext = params.callbackContext;
126
129
  self.onEnded = params.onEnded;
130
+ self.meterFraction = options.visualObj.getMeterFraction();
127
131
 
128
132
  var allNotes = {};
129
133
  var cached = [];
@@ -136,18 +140,19 @@ function CreateSynth() {
136
140
  if (event.pitch !== undefined) {
137
141
  var pitchNumber = event.pitch;
138
142
  var noteName = pitchToNoteName[pitchNumber];
143
+ var inst = event.instrument !== undefined ? instrumentIndexToName[event.instrument] : currentInstrument
139
144
  if (noteName) {
140
- if (!allNotes[currentInstrument])
141
- allNotes[currentInstrument] = {};
142
- if (!soundsCache[currentInstrument] || !soundsCache[currentInstrument][noteName])
143
- allNotes[currentInstrument][noteName] = true;
145
+ if (!allNotes[inst])
146
+ allNotes[inst] = {};
147
+ if (!soundsCache[inst] || !soundsCache[inst][noteName])
148
+ allNotes[inst][noteName] = true;
144
149
  else {
145
- var label2 = currentInstrument+":"+noteName
150
+ var label2 = inst+":"+noteName
146
151
  if (cached.indexOf(label2) < 0)
147
152
  cached.push(label2);
148
153
  }
149
154
  } else {
150
- var label = currentInstrument+":"+noteName
155
+ var label = inst+":"+noteName
151
156
  console.log("Can't find note: ", pitchNumber, label);
152
157
  if (errorNotes.indexOf(label) < 0)
153
158
  errorNotes.push(label)
@@ -309,8 +314,10 @@ function CreateSynth() {
309
314
  self.stop();
310
315
 
311
316
  var noteMapTracks = createNoteMap(self.flattened);
312
- // if (self.options.swing)
313
- // addSwing(noteMapTracks, self.options.swing, self.beatsPerMeasure)
317
+
318
+ if (self.options.swing)
319
+ addSwing(noteMapTracks, self.options.swing, self.meterFraction, self.pickupLength)
320
+
314
321
  if (self.sequenceCallback)
315
322
  self.sequenceCallback(noteMapTracks, self.callbackContext);
316
323
 
@@ -545,38 +552,80 @@ function CreateSynth() {
545
552
  }
546
553
  };
547
554
 
548
- // // this is a first attempt at adding a little bit of swing to the output, but the algorithm isn't correct.
549
- // function addSwing(noteMapTracks, swing, beatsPerMeasure) {
550
- // console.log("addSwing", noteMapTracks, swing, beatsPerMeasure)
551
- // // Swing should be between -0.9 and 0.9. Make sure the input is between them.
552
- // // Then that is the percentage to add to the first beat, so a negative number moves the second beat earlier.
553
- // // A value of zero is the same as no swing at all.
554
- // // This only works when there are an even number of beats in a measure.
555
- // if (beatsPerMeasure % 2 !== 0)
556
- // return;
557
- // swing = parseFloat(swing)
558
- // if (isNaN(swing))
559
- // return
560
- // if (swing < -0.9)
561
- // swing = -0.9
562
- // if (swing > 0.9)
563
- // swing = 0.9
564
- // var beatLength = (1 / beatsPerMeasure)*2
565
- // swing = beatLength * swing
566
- // for (var t = 0; t < noteMapTracks.length; t++) {
567
- // var track = noteMapTracks[t];
568
- // for (var i = 0; i < track.length; i++) {
569
- // var event = track[i];
570
- // if (event.start % beatLength) {
571
- // // This is the off beat
572
- // event.start += swing;
573
- // } else {
574
- // // This is the beat
575
- // event.end += swing;
576
- // }
577
- // }
578
- // }
579
- // }
555
+ function addSwing(noteMapTracks, swing, meterFraction, pickupLength) {
556
+
557
+ // we can only swing in X/4 and X/8 meters.
558
+ if (meterFraction.den != 4 && meterFraction.den != 8)
559
+ return;
560
+
561
+ swing = parseFloat(swing);
562
+
563
+ // 50 (or less) is no swing,
564
+ if (isNaN(swing) || swing <= 50)
565
+ return;
566
+
567
+ // 66 is triplet swing 2:1, and
568
+ // 60 is swing with a ratio of 3:2.
569
+ // 75 is the maximum swing where the first eight is played as a dotted eight and the second as a sixteenth.
570
+ if (swing > 75)
571
+ swing = 75;
572
+
573
+ // convert the swing percentage to a percentage of increase for the first half of the beat
574
+ swing = swing/50 - 1;
575
+
576
+ // The volume of the swung notes is increased by this factor
577
+ // could be also in the settings. Try out values such 0.1, 0.2
578
+ var volumeIncrease = 0.0;
579
+
580
+ // the beatLength in X/8 meters
581
+ var beatLength = 0.25;
582
+
583
+ // in X/8 meters the 16s swing so the beatLength is halved
584
+ if (meterFraction.den === 8)
585
+ beatLength = beatLength/2;
586
+
587
+ // duration of a half beat
588
+ var halfbeatLength = beatLength/2;
589
+
590
+ // the extra duration of the first swung notes and the delay of the second notes
591
+ var swingDuration = halfbeatLength * swing;
592
+
593
+ for (var t = 0; t < noteMapTracks.length; t++) {
594
+ var track = noteMapTracks[t];
595
+ for (var i = 0; i < track.length; i++) {
596
+ var event = track[i];
597
+ if (
598
+ // is halfbeat
599
+ (event.start-pickupLength) % halfbeatLength == 0 && (event.start-pickupLength) % beatLength != 0
600
+ && (
601
+ // the previous note is on the beat or before OR there is no previous note
602
+ i == 0
603
+ || track[i-1].start <= track[i].start - halfbeatLength
604
+ )
605
+ && (
606
+ // the next note is on the beat or after OR there is no next note
607
+ i == track.length - 1
608
+ || track[i+1].start >= track[i].start + halfbeatLength
609
+ )
610
+ ) {
611
+ var oldEventStart = event.start;
612
+
613
+ event.start += swingDuration;
614
+
615
+ // Increase volume of swung notes
616
+ event.volume *= 1 + volumeIncrease;
617
+
618
+ // if there is a previous note ending at the start of this note, extend its end
619
+ // and decrease its volume
620
+ if (i > 0 && track[i-1].end == oldEventStart) {
621
+ track[i-1].end = event.start;
622
+ track[i-1].volume *= 1 - volumeIncrease;
623
+ }
624
+ }
625
+ }
626
+ }
627
+ }
628
+
580
629
  }
581
630
 
582
631
  module.exports = CreateSynth;
@@ -556,6 +556,7 @@ var ParserLint = function() {
556
556
  subtitlespace: { type: "number", optional: true },
557
557
  sysstaffsep: { type: "number", optional: true },
558
558
  systemsep: { type: "number", optional: true },
559
+ stafftopmargin: { type: "number", optional: true },
559
560
  tabgracefont: fontType,
560
561
  tablabelfont: fontType,
561
562
  tabnumberfont: fontType,
@@ -831,7 +831,10 @@ AbstractEngraver.prototype.createNote = function (elem, nostem, isSingleLineStaf
831
831
  }
832
832
 
833
833
  if (elem.decoration) {
834
- this.decoration.createDecoration(voice, elem.decoration, abselem.top, (notehead) ? notehead.w : 0, abselem, roomtaken, dir, abselem.bottom, elem.positioning, this.hasVocals, this.accentAbove);
834
+ // TODO-PER: nostem is true if this is beamed. In that case we don't know where to place the decoration yet so just make a guess. This should be refactored to not place decorations until after the beams are determined.
835
+ // This should probably be combined with moveDecorations()
836
+ var bottom = nostem ? Math.min(-3, abselem.bottom - 6) : abselem.bottom
837
+ this.decoration.createDecoration(voice, elem.decoration, abselem.top, (notehead) ? notehead.w : 0, abselem, roomtaken, dir, bottom, elem.positioning, this.hasVocals, this.accentAbove);
835
838
  }
836
839
 
837
840
  if (elem.barNumber) {
@@ -160,7 +160,7 @@ var stackedDecoration = function (decoration, width, abselem, yPos, positioning,
160
160
  var height = glyphs.symbolHeightInPitches(symbol) + 1; // adding a little padding so nothing touches.
161
161
  var y = getPlacement(placement);
162
162
  y = (placement === 'above') ? y + height / 2 : y - height / 2;// Center the element vertically.
163
- abselem.addFixedX(new RelativeElement(symbol, deltaX, glyphs.getSymbolWidth(symbol), y, { klass: 'ornament', thickness: glyphs.symbolHeightInPitches(symbol) }));
163
+ abselem.addFixedX(new RelativeElement(symbol, deltaX, glyphs.getSymbolWidth(symbol), y, { klass: 'ornament', thickness: glyphs.symbolHeightInPitches(symbol), position: placement }));
164
164
 
165
165
  incrementPlacement(placement, height);
166
166
  }
@@ -355,7 +355,8 @@ Decoration.prototype.createDecoration = function (voice, decoration, pitch, widt
355
355
  // yPos is an object containing 'above' and 'below'. That is the placement of the next symbol on either side.
356
356
 
357
357
  yPos.above = Math.max(yPos.above, this.minTop);
358
- var hasOne = stackedDecoration(decoration, width, abselem, yPos, positioning.ornamentPosition, this.minTop, this.minBottom, accentAbove);
358
+ yPos.below = Math.min(yPos.below, minPitch);
359
+ var hasOne = stackedDecoration(decoration, width, abselem, yPos, positioning.ornamentPosition, this.minTop, minPitch, accentAbove);
359
360
  //if (hasOne) {
360
361
  // abselem.top = Math.max(yPos.above + 3, abselem.top); // TODO-PER: Not sure why we need this fudge factor.
361
362
  //}
@@ -177,6 +177,29 @@ TieElem.prototype.calcSlurY = function () {
177
177
  } else
178
178
  this.endY = this.above && beamInterferes ? this.anchor2.highestVert : this.anchor2.pitch;
179
179
 
180
+ 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.
181
+ var hasBeam1 = !!this.anchor1.parent.beam
182
+ var hasBeam2 = !!this.anchor2.parent.beam
183
+ if (hasBeam1) {
184
+ var isLastInBeam = this.anchor1.parent === this.anchor1.parent.beam.elems[this.anchor1.parent.beam.elems.length-1]
185
+ if (!isLastInBeam) {
186
+ if (this.above)
187
+ this.startY = this.anchor1.parent.fixed.t
188
+ else
189
+ this.startY = this.anchor1.parent.fixed.b
190
+ }
191
+ }
192
+
193
+ if (hasBeam2) {
194
+ var isFirstInBeam = this.anchor2.parent === this.anchor2.parent.beam.elems[0]
195
+ if (!isFirstInBeam) {
196
+ if (this.above)
197
+ this.endY = this.anchor2.parent.fixed.t
198
+ else
199
+ this.endY = this.anchor2.parent.fixed.b
200
+ }
201
+ }
202
+ }
180
203
  } else if (this.anchor1) {
181
204
  this.startY = this.endY = this.anchor1.pitch;
182
205
  } else if (this.anchor2) {
@@ -20,7 +20,7 @@ function draw(renderer, classes, abcTune, width, maxWidth, responsive, scale, se
20
20
  var abcLine = abcTune.lines[line];
21
21
  if (abcLine.staff) {
22
22
  if (classes.shouldAddClasses)
23
- groupClasses.klass = "abcjs-staff l" + classes.lineNumber
23
+ groupClasses.klass = "abcjs-staff-wrapper abcjs-l" + classes.lineNumber
24
24
  renderer.paper.openGroup(groupClasses)
25
25
  if (abcLine.vskip) {
26
26
  renderer.moveY(abcLine.vskip);
@@ -17,6 +17,7 @@ var GetFontAndAttr = require('./helpers/get-font-and-attr');
17
17
  var GetTextSize = require('./helpers/get-text-size');
18
18
  var draw = require('./draw/draw');
19
19
  var tablatures = require('../api/abc_tablatures');
20
+ var findSelectableElement = require('./interactive/find-selectable-element');
20
21
 
21
22
  /**
22
23
  * @class
@@ -32,6 +33,7 @@ var tablatures = require('../api/abc_tablatures');
32
33
  */
33
34
  var EngraverController = function (paper, params) {
34
35
  params = params || {};
36
+ this.findSelectableElement = findSelectableElement;
35
37
  this.oneSvgPerLine = params.oneSvgPerLine;
36
38
  this.selectionColor = params.selectionColor;
37
39
  this.dragColor = params.dragColor ? params.dragColor : params.selectionColor;
@@ -40,6 +42,7 @@ var EngraverController = function (paper, params) {
40
42
  this.responsive = params.responsive;
41
43
  this.space = 3 * spacing.SPACE;
42
44
  this.initialClef = params.initialClef;
45
+ this.timeBasedLayout = params.timeBasedLayout;
43
46
  this.expandToWidest = !!params.expandToWidest;
44
47
  this.scale = params.scale ? parseFloat(params.scale) : 0;
45
48
  this.classes = new Classes({ shouldAddClasses: params.add_classes });
@@ -131,7 +134,7 @@ EngraverController.prototype.getMeasureWidths = function (abcTune) {
131
134
  this.constructTuneElements(abcTune);
132
135
  // layout() sets the x-coordinate of the abcTune element here:
133
136
  // abcTune.lines[0].staffGroup.voices[0].children[0].x
134
- layout(this.renderer, abcTune, 0, this.space);
137
+ layout(this.renderer, abcTune, 0, this.space, this.timeBasedLayout);
135
138
 
136
139
  var ret = [];
137
140
  var section;
@@ -193,6 +196,7 @@ EngraverController.prototype.setupTune = function (abcTune, tuneNumber) {
193
196
  percmap: abcTune.formatting.percmap,
194
197
  initialClef: this.initialClef,
195
198
  jazzchords: this.jazzchords,
199
+ timeBasedLayout: this.timeBasedLayout,
196
200
  accentAbove: this.accentAbove,
197
201
  germanAlphabet: this.germanAlphabet
198
202
  });
@@ -254,7 +258,7 @@ EngraverController.prototype.engraveTune = function (abcTune, tuneNumber, lineOf
254
258
  //Set the top text now that we know the width
255
259
 
256
260
  // Do all the positioning, both horizontally and vertically
257
- var maxWidth = layout(this.renderer, abcTune, this.width, this.space, this.expandToWidest);
261
+ var maxWidth = layout(this.renderer, abcTune, this.width, this.space, this.expandToWidest, this.timeBasedLayout);
258
262
 
259
263
  //Set the top text now that we know the width
260
264
  if (this.expandToWidest && maxWidth > this.width + 1) {
@@ -301,7 +305,7 @@ EngraverController.prototype.engraveTune = function (abcTune, tuneNumber, lineOf
301
305
  this.selectables = ret.selectables;
302
306
  if (this.oneSvgPerLine) {
303
307
  var div = this.renderer.paper.svg.parentNode;
304
- this.svgs = splitSvgIntoLines(this.renderer, div, abcTune.metaText.title, this.responsive);
308
+ this.svgs = splitSvgIntoLines(this.renderer, div, abcTune.metaText.title, this.responsive, scale);
305
309
  } else {
306
310
  this.svgs = [this.renderer.paper.svg];
307
311
  }
@@ -310,7 +314,7 @@ EngraverController.prototype.engraveTune = function (abcTune, tuneNumber, lineOf
310
314
  this.jazzchords = origJazzChords
311
315
  };
312
316
 
313
- function splitSvgIntoLines(renderer, output, title, responsive) {
317
+ function splitSvgIntoLines(renderer, output, title, responsive, scale) {
314
318
  // Each line is a top level <g> in the svg. To split it into separate
315
319
  // svgs iterate through each of those and put them in a new svg. Since
316
320
  // they are placed absolutely, the viewBox needs to be manipulated to
@@ -335,7 +339,7 @@ function splitSvgIntoLines(renderer, output, title, responsive) {
335
339
  var wrapper = document.createElement("div");
336
340
  var divStyles = "overflow: hidden;"
337
341
  if (responsive !== 'resize')
338
- divStyles += "height:" + height + "px;"
342
+ divStyles += "height:" + (height * scale) + "px;"
339
343
  wrapper.setAttribute("style", divStyles)
340
344
  var svg = duplicateSvg(source)
341
345
  var fullTitle = "Sheet Music for \"" + title + "\" section " + (i + 1)
@@ -0,0 +1,50 @@
1
+ function findNumber(klass, match, target, name) {
2
+ if (klass.indexOf(match) === 0) {
3
+ var value = klass.replace(match, '');
4
+ var num = parseInt(value, 10);
5
+ if ('' + num === value)
6
+ target[name] = num;
7
+ }
8
+ }
9
+
10
+ function createAnalysis(target, ev) {
11
+ var classes = [];
12
+ if (target.absEl.elemset) {
13
+ var classObj = {};
14
+ for (var j = 0; j < target.absEl.elemset.length; j++) {
15
+ var es = target.absEl.elemset[j];
16
+ if (es) {
17
+ var klass = es.getAttribute("class").split(' ');
18
+ for (var k = 0; k < klass.length; k++)
19
+ classObj[klass[k]] = true;
20
+ }
21
+ }
22
+ for (var kk = 0; kk < Object.keys(classObj).length; kk++)
23
+ classes.push(Object.keys(classObj)[kk]);
24
+ }
25
+ var analysis = {};
26
+ for (var ii = 0; ii < classes.length; ii++) {
27
+ findNumber(classes[ii], "abcjs-v", analysis, "voice");
28
+ findNumber(classes[ii], "abcjs-l", analysis, "line");
29
+ findNumber(classes[ii], "abcjs-m", analysis, "measure");
30
+ }
31
+ if (target.staffPos)
32
+ analysis.staffPos = target.staffPos;
33
+ var closest = ev.target;
34
+ while (closest && closest.dataset && !closest.dataset.name && closest.tagName.toLowerCase() !== 'svg')
35
+ closest = closest.parentNode;
36
+ var parent = ev.target;
37
+ while (parent && parent.dataset && !parent.dataset.index && parent.tagName.toLowerCase() !== 'svg')
38
+ parent = parent.parentNode;
39
+ if (parent && parent.dataset) {
40
+ analysis.name = parent.dataset.name;
41
+ analysis.clickedName = closest.dataset.name;
42
+ analysis.parentClasses = parent.classList;
43
+ }
44
+ if (closest && closest.classList)
45
+ analysis.clickedClasses = closest.classList;
46
+ analysis.selectableElement = target.svgEl;
47
+ return {classes: classes, analysis: analysis}
48
+ }
49
+
50
+ module.exports = createAnalysis;
@@ -0,0 +1,24 @@
1
+ var createAnalysis = require('./create-analysis');
2
+
3
+ function findSelectableElement(event) {
4
+ var selectable = event
5
+ while (selectable && selectable.attributes && selectable.tagName.toLowerCase() !== 'svg' && !selectable.attributes.selectable) {
6
+ selectable = selectable.parentNode
7
+ }
8
+ if (selectable && selectable.attributes && selectable.attributes.selectable) {
9
+ var index = selectable.attributes['data-index'].nodeValue
10
+ if (index) {
11
+ index = parseInt(index, 10)
12
+ if (index >= 0 && index < this.selectables.length) {
13
+ var element = this.selectables[index]
14
+ var ret = createAnalysis(element, event)
15
+ ret.index = index
16
+ ret.element = element
17
+ return ret
18
+ }
19
+ }
20
+ }
21
+ return null
22
+ }
23
+
24
+ module.exports = findSelectableElement;
@@ -1,4 +1,5 @@
1
1
  var spacing = require('../helpers/spacing');
2
+ var createAnalysis = require('./create-analysis');
2
3
 
3
4
  function setupSelection(engraver, svgs) {
4
5
  engraver.rangeHighlight = rangeHighlight;
@@ -339,58 +340,17 @@ function setSelection(dragIndex) {
339
340
  }
340
341
  }
341
342
 
343
+
342
344
  function notifySelect(target, dragStep, dragMax, dragIndex, ev) {
343
- var classes = [];
344
- if (target.absEl.elemset) {
345
- var classObj = {};
346
- for (var j = 0; j < target.absEl.elemset.length; j++) {
347
- var es = target.absEl.elemset[j];
348
- if (es) {
349
- var klass = es.getAttribute("class").split(' ');
350
- for (var k = 0; k < klass.length; k++)
351
- classObj[klass[k]] = true;
352
- }
353
- }
354
- for (var kk = 0; kk < Object.keys(classObj).length; kk++)
355
- classes.push(Object.keys(classObj)[kk]);
356
- }
357
- var analysis = {};
358
- for (var ii = 0; ii < classes.length; ii++) {
359
- findNumber(classes[ii], "abcjs-v", analysis, "voice");
360
- findNumber(classes[ii], "abcjs-l", analysis, "line");
361
- findNumber(classes[ii], "abcjs-m", analysis, "measure");
362
- }
363
- if (target.staffPos)
364
- analysis.staffPos = target.staffPos;
365
- var closest = ev.target;
366
- while (closest && closest.dataset && !closest.dataset.name && closest.tagName.toLowerCase() !== 'svg')
367
- closest = closest.parentNode;
368
- var parent = ev.target;
369
- while (parent && parent.dataset && !parent.dataset.index && parent.tagName.toLowerCase() !== 'svg')
370
- parent = parent.parentNode;
371
- if (parent && parent.dataset) {
372
- analysis.name = parent.dataset.name;
373
- analysis.clickedName = closest.dataset.name;
374
- analysis.parentClasses = parent.classList;
375
- }
376
- if (closest && closest.classList)
377
- analysis.clickedClasses = closest.classList;
378
- analysis.selectableElement = target.svgEl;
345
+ var ret = createAnalysis(target, ev)
346
+ var classes = ret.classes
347
+ var analysis = ret.analysis
379
348
 
380
349
  for (var i = 0; i < this.listeners.length; i++) {
381
350
  this.listeners[i](target.absEl.abcelem, target.absEl.tuneNumber, classes.join(' '), analysis, { step: dragStep, max: dragMax, index: dragIndex, setSelection: setSelection.bind(this) }, ev);
382
351
  }
383
352
  }
384
353
 
385
- function findNumber(klass, match, target, name) {
386
- if (klass.indexOf(match) === 0) {
387
- var value = klass.replace(match, '');
388
- var num = parseInt(value, 10);
389
- if ('' + num === value)
390
- target[name] = num;
391
- }
392
- }
393
-
394
354
  function clearSelection() {
395
355
  for (var i = 0; i < this.selected.length; i++) {
396
356
  this.selected[i].unhighlight(undefined, this.renderer.foregroundColor);
@@ -0,0 +1,83 @@
1
+ var getLeftEdgeOfStaff = require('./get-left-edge-of-staff');
2
+
3
+ function layoutInGrid(renderer, staffGroup, timeBasedLayout) {
4
+ var leftEdge = getLeftEdgeOfStaff(renderer, staffGroup.getTextSize, staffGroup.voices, staffGroup.brace, staffGroup.bracket);
5
+ var ret = getTotalDuration(staffGroup, timeBasedLayout.minPadding)
6
+ var totalDuration = ret.totalDuration
7
+ var minSpacing = ret.minSpacing
8
+ var totalWidth = minSpacing * totalDuration
9
+ if (timeBasedLayout.minWidth)
10
+ totalWidth = Math.max(totalWidth, timeBasedLayout.minWidth)
11
+ var leftAlignPadding = timeBasedLayout.minPadding ? timeBasedLayout.minPadding/2 : 2 // If the padding isn't specified still give it some
12
+
13
+ staffGroup.startx = leftEdge
14
+ staffGroup.w = totalWidth + leftEdge
15
+ for (var i = 0; i < staffGroup.voices.length; i++) {
16
+ var voice = staffGroup.voices[i]
17
+ voice.startx = leftEdge
18
+ voice.w = totalWidth + leftEdge
19
+
20
+ var x = leftEdge
21
+ var afterFixedLeft = false
22
+ var durationUnit = 0
23
+ for (var j = 0; j < voice.children.length; j++) {
24
+ var child = voice.children[j]
25
+ if (!afterFixedLeft) {
26
+ if (child.duration !== 0) {
27
+ // We got to the first music element on the line
28
+ afterFixedLeft = true
29
+ durationUnit = (totalWidth + leftEdge - x) / totalDuration
30
+ staffGroup.gridStart = x
31
+ } else {
32
+ // We are still doing the preliminary stuff - clef, time sig, etc.
33
+ child.x = x
34
+ x += child.w + child.minspacing
35
+ }
36
+ }
37
+ if (afterFixedLeft) {
38
+ if (timeBasedLayout.align === 'center')
39
+ child.x = x + (child.duration * durationUnit) / 2 - child.w / 2
40
+ else {
41
+ // left align with padding - but no padding for barlines, they should be right aligned.
42
+ // TODO-PER: it looks better to move bar lines one pixel to right. Not sure why.
43
+ if (child.duration === 0) {
44
+ child.x = x + 1 - child.w
45
+ } else {
46
+ // child.extraw has the width of the accidentals - push the note to the right to take that into consideration. It will be 0 if there is nothing to the left.
47
+ child.x = x + leftAlignPadding - child.extraw
48
+ }
49
+ }
50
+ x += child.duration * durationUnit
51
+ }
52
+ for (var k = 0; k < child.children.length; k++) {
53
+ var grandchild = child.children[k]
54
+ // some elements don't have a dx - Tempo, for instance
55
+ var dx = grandchild.dx ? grandchild.dx : 0
56
+ grandchild.x = child.x + dx
57
+ }
58
+ }
59
+ staffGroup.gridEnd = x
60
+ }
61
+ return totalWidth
62
+ }
63
+
64
+ function getTotalDuration(staffGroup, timeBasedLayout) {
65
+ var maxSpacing = 0
66
+ var maxCount = 0
67
+ for (var i = 0; i < staffGroup.voices.length; i++) {
68
+ var count = 0
69
+ var voice = staffGroup.voices[i]
70
+ for (var j = 0; j < voice.children.length; j++) {
71
+ var element = voice.children[j]
72
+ count += element.duration
73
+ if (element.duration) {
74
+ var width = (element.w+timeBasedLayout) / element.duration
75
+ maxSpacing = Math.max(maxSpacing, width)
76
+ }
77
+ }
78
+ maxCount = Math.max(maxCount, count)
79
+ }
80
+ return { totalDuration: maxCount, minSpacing: maxSpacing}
81
+ }
82
+
83
+ module.exports = layoutInGrid;
@@ -2,8 +2,12 @@ var layoutVoice = require('./voice');
2
2
  var setUpperAndLowerElements = require('./set-upper-and-lower-elements');
3
3
  var layoutStaffGroup = require('./staff-group');
4
4
  var getLeftEdgeOfStaff = require('./get-left-edge-of-staff');
5
+ var layoutInGrid = require('./layout-in-grid');
5
6
 
6
- var layout = function (renderer, abctune, width, space, expandToWidest) {
7
+ // This sets the "x" attribute on all the children in abctune.lines
8
+ // It also sets the "w" and "startx" attributes on "voices"
9
+ // It also sets the "w" and "startx" attributes on "voices.children"
10
+ var layout = function (renderer, abctune, width, space, expandToWidest, timeBasedLayout) {
7
11
  var i;
8
12
  var abcLine;
9
13
  // Adjust the x-coordinates to their absolute positions
@@ -12,7 +16,11 @@ var layout = function (renderer, abctune, width, space, expandToWidest) {
12
16
  abcLine = abctune.lines[i];
13
17
  if (abcLine.staff) {
14
18
  // console.log("=== line", i)
15
- var thisWidth = setXSpacing(renderer, maxWidth, space, abcLine.staffGroup, abctune.formatting, i === abctune.lines.length - 1, false);
19
+ var thisWidth;
20
+ if (timeBasedLayout !== undefined)
21
+ thisWidth = layoutInGrid(renderer, abcLine.staffGroup, timeBasedLayout);
22
+ else
23
+ thisWidth = setXSpacing(renderer, maxWidth, space, abcLine.staffGroup, abctune.formatting, i === abctune.lines.length - 1, false);
16
24
  // console.log(thisWidth, maxWidth)
17
25
  if (Math.round(thisWidth) > Math.round(maxWidth)) { // to take care of floating point weirdness
18
26
  maxWidth = thisWidth
@@ -46,39 +54,36 @@ var layout = function (renderer, abctune, width, space, expandToWidest) {
46
54
  var setXSpacing = function (renderer, width, space, staffGroup, formatting, isLastLine, debug) {
47
55
  var leftEdge = getLeftEdgeOfStaff(renderer, staffGroup.getTextSize, staffGroup.voices, staffGroup.brace, staffGroup.bracket);
48
56
  var newspace = space;
57
+ //dumpGroup("before", staffGroup)
49
58
  for (var it = 0; it < 8; it++) { // TODO-PER: shouldn't need multiple passes, but each pass gets it closer to the right spacing. (Only affects long lines: normal lines break out of this loop quickly.)
50
59
  // console.log("iteration", it)
51
- // dumpGroup("before", staffGroup)
52
- var ret = layoutStaffGroup(newspace, renderer, debug, staffGroup, leftEdge);
53
- // dumpGroup("after",staffGroup)
60
+ var ret = layoutStaffGroup(newspace, renderer.minPadding, debug, staffGroup, leftEdge);
54
61
  newspace = calcHorizontalSpacing(isLastLine, formatting.stretchlast, width + renderer.padding.left, staffGroup.w, newspace, ret.spacingUnits, ret.minSpace, renderer.padding.left + renderer.padding.right);
55
62
  if (debug)
56
63
  console.log("setXSpace", it, staffGroup.w, newspace, staffGroup.minspace);
57
64
  if (newspace === null) break;
58
65
  }
66
+ //dumpGroup("after",staffGroup)
59
67
  centerWholeRests(staffGroup.voices);
60
68
  return staffGroup.w - leftEdge
61
69
  };
62
70
 
63
- // function dumpGroup(label, staffGroup) {
64
- // var output = {
65
- // line: staffGroup.line,
66
- // w: staffGroup.w,
67
- // voice: {
68
- // i: staffGroup.voices[0].i,
69
- // minx: staffGroup.voices[0].minx,
70
- // nextx: staffGroup.voices[0].nextx,
71
- // spacingduration: staffGroup.voices[0].spacingduration,
72
- // w: staffGroup.voices[0].w,
73
- // children: [],
74
- // }
75
- // }
76
- // for (var i = 0; i < staffGroup.voices[0].children.length; i++) {
77
- // var child = staffGroup.voices[0].children[i]
78
- // output.voice.children.push({ fixedW: child.fixed.w, w: child.w, x: child.x, type: child.type })
79
- // }
80
- // console.log(label,output)
81
- // }
71
+ function replacer(key, value) {
72
+ // Filtering out properties
73
+ if (key === 'parent') {
74
+ return 'parent';
75
+ }
76
+ if (key === 'beam') {
77
+ return 'beam';
78
+ }
79
+ return value;
80
+ }
81
+
82
+ function dumpGroup(label, staffGroup) {
83
+ console.log("=================== " + label + " =========================")
84
+ console.log(staffGroup)
85
+ console.log(JSON.stringify(staffGroup, replacer, "\t"))
86
+ }
82
87
 
83
88
  function calcHorizontalSpacing(isLastLine, stretchLast, targetWidth, lineWidth, spacing, spacingUnits, minSpace, padding) {
84
89
  if (isLastLine) {