Cursor.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import {MusicPartManagerIterator} from "../MusicalScore/MusicParts/MusicPartManagerIterator";
  2. import {MusicPartManager} from "../MusicalScore/MusicParts/MusicPartManager";
  3. import {VoiceEntry} from "../MusicalScore/VoiceData/VoiceEntry";
  4. import {VexFlowStaffEntry} from "../MusicalScore/Graphical/VexFlow/VexFlowStaffEntry";
  5. import {MusicSystem} from "../MusicalScore/Graphical/MusicSystem";
  6. import {OpenSheetMusicDisplay} from "./OpenSheetMusicDisplay";
  7. import {GraphicalMusicSheet} from "../MusicalScore/Graphical/GraphicalMusicSheet";
  8. import {Instrument} from "../MusicalScore/Instrument";
  9. import {Note} from "../MusicalScore/VoiceData/Note";
  10. import {EngravingRules, Fraction} from "..";
  11. import {SourceMeasure, StaffLine} from "../MusicalScore";
  12. /**
  13. * A cursor which can iterate through the music sheet.
  14. */
  15. export class Cursor {
  16. constructor(container: HTMLElement, openSheetMusicDisplay: OpenSheetMusicDisplay) {
  17. this.container = container;
  18. this.openSheetMusicDisplay = openSheetMusicDisplay;
  19. this.rules = this.openSheetMusicDisplay.EngravingRules;
  20. const curs: HTMLElement = document.createElement("img");
  21. curs.style.position = "absolute";
  22. curs.style.zIndex = "-1";
  23. this.cursorElement = <HTMLImageElement>curs;
  24. this.container.appendChild(curs);
  25. }
  26. private container: HTMLElement;
  27. private openSheetMusicDisplay: OpenSheetMusicDisplay;
  28. private rules: EngravingRules;
  29. private manager: MusicPartManager;
  30. public iterator: MusicPartManagerIterator;
  31. private graphic: GraphicalMusicSheet;
  32. public hidden: boolean = true;
  33. private cursorElement: HTMLImageElement;
  34. /** Initialize the cursor. Necessary before using functions like show() and next(). */
  35. public init(manager: MusicPartManager, graphic: GraphicalMusicSheet): void {
  36. this.manager = manager;
  37. this.graphic = graphic;
  38. this.reset();
  39. this.hidden = true;
  40. this.hide();
  41. }
  42. /**
  43. * Make the cursor visible
  44. */
  45. public show(): void {
  46. this.hidden = false;
  47. this.resetIterator(); // TODO maybe not here? though setting measure range to draw, rerendering, then handling cursor show is difficult
  48. this.update();
  49. }
  50. public resetIterator(): void {
  51. if (!this.openSheetMusicDisplay.Sheet || !this.openSheetMusicDisplay.Sheet.SourceMeasures) { // just a safety measure
  52. console.log("OSMD.Cursor.resetIterator(): sheet or measures were null/undefined.");
  53. return;
  54. }
  55. // set selection start, so that when there's MinMeasureToDraw set, the cursor starts there right away instead of at measure 1
  56. const lastSheetMeasureIndex: number = this.openSheetMusicDisplay.Sheet.SourceMeasures.length - 1; // last measure in data model
  57. let startMeasureIndex: number = this.rules.MinMeasureToDrawIndex;
  58. startMeasureIndex = Math.min(startMeasureIndex, lastSheetMeasureIndex);
  59. let endMeasureIndex: number = this.rules.MaxMeasureToDrawIndex;
  60. endMeasureIndex = Math.min(endMeasureIndex, lastSheetMeasureIndex);
  61. if (this.openSheetMusicDisplay.Sheet && this.openSheetMusicDisplay.Sheet.SourceMeasures.length > startMeasureIndex) {
  62. this.openSheetMusicDisplay.Sheet.SelectionStart = this.openSheetMusicDisplay.Sheet.SourceMeasures[startMeasureIndex].AbsoluteTimestamp;
  63. }
  64. if (this.openSheetMusicDisplay.Sheet && this.openSheetMusicDisplay.Sheet.SourceMeasures.length > endMeasureIndex) {
  65. const lastMeasure: SourceMeasure = this.openSheetMusicDisplay.Sheet.SourceMeasures[endMeasureIndex];
  66. this.openSheetMusicDisplay.Sheet.SelectionEnd = Fraction.plus(lastMeasure.AbsoluteTimestamp, lastMeasure.Duration);
  67. }
  68. this.iterator = this.manager.getIterator();
  69. }
  70. private getStaffEntryFromVoiceEntry(voiceEntry: VoiceEntry): VexFlowStaffEntry {
  71. const measureIndex: number = voiceEntry.ParentSourceStaffEntry.VerticalContainerParent.ParentMeasure.measureListIndex;
  72. const staffIndex: number = voiceEntry.ParentSourceStaffEntry.ParentStaff.idInMusicSheet;
  73. return <VexFlowStaffEntry>this.graphic.findGraphicalStaffEntryFromMeasureList(staffIndex, measureIndex, voiceEntry.ParentSourceStaffEntry);
  74. }
  75. public update(): void {
  76. // Warning! This should NEVER call this.openSheetMusicDisplay.render()
  77. if (this.hidden || this.hidden === undefined) {
  78. return;
  79. }
  80. // this.graphic?.Cursors?.length = 0;
  81. const iterator: MusicPartManagerIterator = this.iterator;
  82. // TODO when measure draw range (drawUpToMeasureNumber) was changed, next/update can fail to move cursor. but of course it can be reset before.
  83. const voiceEntries: VoiceEntry[] = iterator.CurrentVisibleVoiceEntries();
  84. if (iterator.EndReached || iterator.CurrentVoiceEntries === undefined || voiceEntries.length === 0) {
  85. return;
  86. }
  87. let x: number = 0, y: number = 0, height: number = 0;
  88. // get all staff entries inside the current voice entry
  89. const gseArr: VexFlowStaffEntry[] = voiceEntries.map(ve => this.getStaffEntryFromVoiceEntry(ve));
  90. // sort them by x position and take the leftmost entry
  91. const gse: VexFlowStaffEntry =
  92. gseArr.sort((a, b) => a.PositionAndShape.AbsolutePosition.x <= b.PositionAndShape.AbsolutePosition.x ? -1 : 1 )[0];
  93. x = gse.PositionAndShape.AbsolutePosition.x;
  94. const musicSystem: MusicSystem = gse.parentMeasure.ParentMusicSystem;
  95. y = musicSystem.PositionAndShape.AbsolutePosition.y + musicSystem.StaffLines[0].PositionAndShape.RelativePosition.y;
  96. const bottomStaffline: StaffLine = musicSystem.StaffLines[musicSystem.StaffLines.length - 1];
  97. const endY: number = musicSystem.PositionAndShape.AbsolutePosition.y +
  98. bottomStaffline.PositionAndShape.RelativePosition.y + bottomStaffline.StaffHeight;
  99. height = endY - y;
  100. // The following code is not necessary (for now, but it could come useful later):
  101. // it highlights the notes under the cursor.
  102. //let vfNotes: { [voiceID: number]: Vex.Flow.StaveNote; } = gse.vfNotes;
  103. //for (let voiceId in vfNotes) {
  104. // if (vfNotes.hasOwnProperty(voiceId)) {
  105. // vfNotes[voiceId].setStyle({
  106. // fillStyle: "red",
  107. // strokeStyle: "red",
  108. // });
  109. // }
  110. //}
  111. // Update the graphical cursor
  112. // The following is the legacy cursor rendered on the canvas:
  113. // // let cursor: GraphicalLine = new GraphicalLine(new PointF2D(x, y), new PointF2D(x, y + height), 3, OutlineAndFillStyleEnum.PlaybackCursor);
  114. // This the current HTML Cursor:
  115. const cursorElement: HTMLImageElement = this.cursorElement;
  116. cursorElement.style.top = (y * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
  117. cursorElement.style.left = ((x - 1.5) * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
  118. cursorElement.height = (height * 10.0 * this.openSheetMusicDisplay.zoom);
  119. const newWidth: number = 3 * 10.0 * this.openSheetMusicDisplay.zoom;
  120. if (newWidth !== cursorElement.width) {
  121. cursorElement.width = newWidth;
  122. this.updateStyle(newWidth);
  123. }
  124. if (this.openSheetMusicDisplay.FollowCursor) {
  125. const diff: number = this.cursorElement.getBoundingClientRect().top;
  126. this.cursorElement.scrollIntoView({behavior: diff < 1000 ? "smooth" : "auto", block: "center"});
  127. }
  128. // Show cursor
  129. // // Old cursor: this.graphic.Cursors.push(cursor);
  130. this.cursorElement.style.display = "";
  131. }
  132. /**
  133. * Hide the cursor
  134. */
  135. public hide(): void {
  136. // Hide the actual cursor element
  137. this.cursorElement.style.display = "none";
  138. //this.graphic.Cursors.length = 0;
  139. // Forcing the sheet to re-render is not necessary anymore
  140. //if (!this.hidden) {
  141. // this.openSheetMusicDisplay.render();
  142. //}
  143. this.hidden = true;
  144. }
  145. /**
  146. * Go to next entry
  147. */
  148. public next(): void {
  149. this.iterator.moveToNext();
  150. this.update();
  151. }
  152. /**
  153. * reset cursor to start
  154. */
  155. public reset(): void {
  156. this.resetIterator();
  157. //this.iterator.moveToNext();
  158. this.update();
  159. }
  160. private updateStyle(width: number, color: string = "#33e02f"): void {
  161. // Create a dummy canvas to generate the gradient for the cursor
  162. // FIXME This approach needs to be improved
  163. const c: HTMLCanvasElement = document.createElement("canvas");
  164. c.width = this.cursorElement.width;
  165. c.height = 1;
  166. const ctx: CanvasRenderingContext2D = c.getContext("2d");
  167. ctx.globalAlpha = 0.5;
  168. // Generate the gradient
  169. const gradient: CanvasGradient = ctx.createLinearGradient(0, 0, this.cursorElement.width, 0);
  170. gradient.addColorStop(0, "white"); // it was: "transparent"
  171. gradient.addColorStop(0.2, color);
  172. gradient.addColorStop(0.8, color);
  173. gradient.addColorStop(1, "white"); // it was: "transparent"
  174. ctx.fillStyle = gradient;
  175. ctx.fillRect(0, 0, width, 1);
  176. // Set the actual image
  177. this.cursorElement.src = c.toDataURL("image/png");
  178. }
  179. public get Iterator(): MusicPartManagerIterator {
  180. return this.iterator;
  181. }
  182. public get Hidden(): boolean {
  183. return this.hidden;
  184. }
  185. /** returns voices under the current Cursor position. Without instrument argument, all voices are returned. */
  186. public VoicesUnderCursor(instrument?: Instrument): VoiceEntry[] {
  187. return this.iterator.CurrentVisibleVoiceEntries(instrument);
  188. }
  189. public NotesUnderCursor(instrument?: Instrument): Note[] {
  190. const voiceEntries: VoiceEntry[] = this.VoicesUnderCursor(instrument);
  191. const notes: Note[] = [];
  192. voiceEntries.forEach(voiceEntry => {
  193. notes.push.apply(notes, voiceEntry.Notes);
  194. });
  195. return notes;
  196. }
  197. }