Source

vendors/P5/Drawer.js

  1. // File imports
  2. import { getNumberOfLedgerLines, getOctave } from "../AlphaTab/ledgerLines";
  3. /**
  4. * @class
  5. * @classdesc Keeps track of current note and where to draw it on the screen along with special information such as number of extra ledger lines
  6. * @category P5
  7. * @author Daniel Griessler <dgriessler20@gmail.com>
  8. */
  9. class Drawer {
  10. /**
  11. * Creates a new Drawer setting up storage of the most recent midi note and information about how to draw it on the screen
  12. * @param {Number} topLine Height of the top line of the selected part to sing
  13. * @param {Number} distanceBetweenLines Distance between lines in the staff
  14. */
  15. constructor(topLine, distanceBetweenLines, baseOctave) {
  16. this.topLine = topLine;
  17. this.distanceBetweenLines = distanceBetweenLines;
  18. // stores the height of the lowest line of the staff being sung
  19. this.firstLine = this.topLine + this.distanceBetweenLines * 5;
  20. // Values >= selected lower limit and <= selected upper limit don't need extra ledger lines
  21. this.lowerLimit = 61; // 61 = C4#
  22. this.upperLimit = 81; // 81 = A5
  23. this.lowerLimit2 = 40; // 40 = E2
  24. this.upperLimit2 = 60; // 60 = C4
  25. this.note = new Note(60);
  26. this.belowOrAbove = 0;
  27. this.noteHeight = 0;
  28. this.baseOctave = baseOctave;
  29. this.updateNote(this.note.midiVal);
  30. }
  31. /**
  32. * Sets the stored height of the top line, the distance between ledger lines, and the base octave
  33. * @param {number} topLine The height of the top ledger line
  34. * @param {number} distanceBetweenLines The y distance between ledger lines
  35. * @param {number} baseOctave The base octave of the current clef
  36. */
  37. setTopLineAndDistanceBetween(topLine, distanceBetweenLines, baseOctave) {
  38. this.topLine = topLine + 1;
  39. this.distanceBetweenLines = distanceBetweenLines;
  40. // stores the height of the lowest line of the staff being sung
  41. this.firstLine =
  42. this.topLine +
  43. this.distanceBetweenLines * (baseOctave === 4 ? 5 : 6);
  44. this.baseOctave = baseOctave;
  45. }
  46. /**
  47. * Sets the base octave
  48. * @param {number} baseOctave The base octave of the current clef
  49. */
  50. setBaseOctave(baseOctave) {
  51. this.baseOctave = baseOctave;
  52. }
  53. /**
  54. * Updates the Drawer to the new provided note
  55. * @param {Number} note New midi value to store. Provide a -1 as a sentinel value for silence
  56. */
  57. updateNote(note) {
  58. this.note.updateNote(note);
  59. this.getHeightOfNote();
  60. this.getExtraFeatures();
  61. }
  62. /**
  63. * Updates the height of the note based on its midi value
  64. */
  65. getHeightOfNote() {
  66. // -1 is a sentinel value for silence which is assigned a default height
  67. if (this.note.midiVal === -1) {
  68. this.noteHeight = this.firstLine;
  69. return;
  70. }
  71. // Calculating the height of a note relies on the cycle in musical notes that occurs between octaves
  72. // This calculates what the height of the note should be based on the first line
  73. const heightMod = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6];
  74. // based on the starting note subtract the base octave since the start note is in that given octave
  75. let octaveMod = this.note.octave - this.baseOctave;
  76. let value = heightMod[this.note.midiVal % heightMod.length];
  77. // Includes bump to jump between octaves
  78. let totalMod = value + octaveMod * 7;
  79. // final height includes division by 2 because each value in the totalMod is distanceBetweenLines/2
  80. this.noteHeight =
  81. this.firstLine - (totalMod * this.distanceBetweenLines) / 2;
  82. }
  83. /**
  84. * Gets the extra features of a note including how many ledger lines to add
  85. */
  86. getExtraFeatures() {
  87. // -1 is a sentinel value for silence which has no ledger lines
  88. if (this.note.midiVal === -1) {
  89. this.belowOrAbove = 0;
  90. return;
  91. }
  92. // // similar to note height, there's a cycle between octaves for ledger lines
  93. // const aboveBelowMod = [1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4];
  94. // sets up base if ledger lines are even needed. base == 0 means no ledger lines
  95. // base < 0 means they go below the staff, base > 0 means they go above the staff
  96. let base = 0;
  97. let actualUpperLimit;
  98. let actualLowerLimit;
  99. if (this.baseOctave === 4) {
  100. actualUpperLimit = this.upperLimit;
  101. actualLowerLimit = this.lowerLimit;
  102. } else {
  103. actualUpperLimit = this.upperLimit2;
  104. actualLowerLimit = this.lowerLimit2;
  105. }
  106. if (this.note.midiVal >= actualUpperLimit) {
  107. base = actualUpperLimit;
  108. } else if (this.note.midiVal <= actualLowerLimit) {
  109. base = -1 * actualLowerLimit;
  110. }
  111. // If need ledger lines, then calculate how many are required
  112. if (base !== 0) {
  113. let direction;
  114. let start;
  115. if (base > 0) {
  116. direction = "up";
  117. start = this.baseOctave === 4 ? "a" : "c";
  118. } else {
  119. direction = "down";
  120. start = this.baseOctave === 4 ? "c" : "e";
  121. }
  122. // LedgerLines loop every 24 notes so if more than 24 then add 7 octaves
  123. let difference = Math.abs(Math.abs(base) - this.note.midiVal);
  124. let loopAdd = 7 * Math.floor(difference / 24);
  125. this.belowOrAbove =
  126. getNumberOfLedgerLines(this.note.midiVal, direction, start) +
  127. loopAdd;
  128. // Signals to draw ledger lines below staff
  129. if (base < 0) {
  130. this.belowOrAbove *= -1;
  131. }
  132. } else {
  133. this.belowOrAbove = 0;
  134. }
  135. }
  136. }
  137. /**
  138. * @class
  139. * @category P5
  140. * @classdesc Stores midi value as its character representation including its octave and if it is sharp
  141. */
  142. class Note {
  143. /**
  144. * Constructs a Note from a provided a given midiVal and converts it to a string which can be accessed
  145. * @param {Number} midiVal Midi value of note to store
  146. */
  147. constructor(midiVal) {
  148. this.updateNote(midiVal);
  149. }
  150. /**
  151. * Updates the note stored to the new note
  152. * @param {Number} note New midi value to store
  153. */
  154. updateNote(note) {
  155. // No point in updating if the midi value matches the current one
  156. if (this.midiVal && note === this.midiVal) {
  157. return;
  158. }
  159. this.midiVal = note;
  160. const noteText = this.numToNote();
  161. this.charPart = noteText.charPart;
  162. this.octave = noteText.octave;
  163. // relies on the char part with being a single letter like G or two letters which is the note and # for sharp
  164. this.isSharp = this.charPart.length === 2;
  165. }
  166. /**
  167. * @typedef {object} NotePackage
  168. * @memberof Drawer
  169. * @property {string} charPart The character part of the note
  170. * @property {number} octave The octave of the note
  171. */
  172. /**
  173. * Converts the stored midi value to its character representation
  174. * @returns {NotePackage} A tuple with the character part and the octave
  175. */
  176. numToNote() {
  177. let charPart;
  178. let octave;
  179. // -1 is a sentinel value for silence which has no char part or octave
  180. if (this.midiVal === -1) {
  181. charPart = "-";
  182. octave = "";
  183. } else {
  184. const letters = [
  185. "C",
  186. "C#",
  187. "D",
  188. "D#",
  189. "E",
  190. "F",
  191. "F#",
  192. "G",
  193. "G#",
  194. "A",
  195. "A#",
  196. "B",
  197. ];
  198. charPart = letters[this.midiVal % letters.length];
  199. octave = getOctave(this.midiVal);
  200. }
  201. return { charPart, octave };
  202. }
  203. }
  204. export default Drawer;