Cursor.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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 {Fraction} from "../Common/DataObjects/Fraction";
  11. import { EngravingRules } from "../MusicalScore/Graphical/EngravingRules";
  12. import { SourceMeasure } from "../MusicalScore/VoiceData/SourceMeasure";
  13. import { StaffLine } from "../MusicalScore/Graphical/StaffLine";
  14. import { GraphicalMeasure } from "../MusicalScore/Graphical/GraphicalMeasure";
  15. import { VexFlowMeasure } from "../MusicalScore/Graphical/VexFlow/VexFlowMeasure";
  16. /**
  17. * A cursor which can iterate through the music sheet.
  18. */
  19. export class Cursor {
  20. constructor(container: HTMLElement, openSheetMusicDisplay: OpenSheetMusicDisplay) {
  21. this.container = container;
  22. this.openSheetMusicDisplay = openSheetMusicDisplay;
  23. this.rules = this.openSheetMusicDisplay.EngravingRules;
  24. // set cursor id
  25. // TODO add this for the OSMD object as well and refactor this into a util method?
  26. let id: number = 0;
  27. this.cursorElementId = "cursorImg-0";
  28. // find unique cursor id in document
  29. while (document.getElementById(this.cursorElementId)) {
  30. id++;
  31. this.cursorElementId = `cursorImg-${id}`;
  32. }
  33. const curs: HTMLElement = document.createElement("img");
  34. curs.id = this.cursorElementId;
  35. curs.style.position = "absolute";
  36. curs.style.zIndex = "-1";
  37. this.cursorElement = <HTMLImageElement>curs;
  38. this.container.appendChild(curs);
  39. }
  40. private container: HTMLElement;
  41. public cursorElement: HTMLImageElement;
  42. /** a unique id of the cursor's HTMLElement in the document.
  43. * Should be constant between re-renders and backend changes,
  44. * but different between different OSMD objects on the same page.
  45. */
  46. public cursorElementId: string;
  47. private openSheetMusicDisplay: OpenSheetMusicDisplay;
  48. private rules: EngravingRules;
  49. private manager: MusicPartManager;
  50. public iterator: MusicPartManagerIterator;
  51. private graphic: GraphicalMusicSheet;
  52. public hidden: boolean = true;
  53. public currentPageNumber: number = 1;
  54. /** Initialize the cursor. Necessary before using functions like show() and next(). */
  55. public init(manager: MusicPartManager, graphic: GraphicalMusicSheet): void {
  56. this.manager = manager;
  57. this.graphic = graphic;
  58. this.reset();
  59. this.hidden = true;
  60. this.hide();
  61. }
  62. /**
  63. * Make the cursor visible
  64. */
  65. public show(): void {
  66. this.hidden = false;
  67. this.resetIterator(); // TODO maybe not here? though setting measure range to draw, rerendering, then handling cursor show is difficult
  68. this.update();
  69. }
  70. public resetIterator(): void {
  71. if (!this.openSheetMusicDisplay.Sheet || !this.openSheetMusicDisplay.Sheet.SourceMeasures) { // just a safety measure
  72. console.log("OSMD.Cursor.resetIterator(): sheet or measures were null/undefined.");
  73. return;
  74. }
  75. // set selection start, so that when there's MinMeasureToDraw set, the cursor starts there right away instead of at measure 1
  76. const lastSheetMeasureIndex: number = this.openSheetMusicDisplay.Sheet.SourceMeasures.length - 1; // last measure in data model
  77. let startMeasureIndex: number = this.rules.MinMeasureToDrawIndex;
  78. startMeasureIndex = Math.min(startMeasureIndex, lastSheetMeasureIndex);
  79. let endMeasureIndex: number = this.rules.MaxMeasureToDrawIndex;
  80. endMeasureIndex = Math.min(endMeasureIndex, lastSheetMeasureIndex);
  81. if (this.openSheetMusicDisplay.Sheet && this.openSheetMusicDisplay.Sheet.SourceMeasures.length > startMeasureIndex) {
  82. this.openSheetMusicDisplay.Sheet.SelectionStart = this.openSheetMusicDisplay.Sheet.SourceMeasures[startMeasureIndex].AbsoluteTimestamp;
  83. }
  84. if (this.openSheetMusicDisplay.Sheet && this.openSheetMusicDisplay.Sheet.SourceMeasures.length > endMeasureIndex) {
  85. const lastMeasure: SourceMeasure = this.openSheetMusicDisplay.Sheet.SourceMeasures[endMeasureIndex];
  86. this.openSheetMusicDisplay.Sheet.SelectionEnd = Fraction.plus(lastMeasure.AbsoluteTimestamp, lastMeasure.Duration);
  87. }
  88. this.iterator = this.manager.getIterator();
  89. }
  90. private getStaffEntryFromVoiceEntry(voiceEntry: VoiceEntry): VexFlowStaffEntry {
  91. const measureIndex: number = voiceEntry.ParentSourceStaffEntry.VerticalContainerParent.ParentMeasure.measureListIndex;
  92. const staffIndex: number = voiceEntry.ParentSourceStaffEntry.ParentStaff.idInMusicSheet;
  93. return <VexFlowStaffEntry>this.graphic.findGraphicalStaffEntryFromMeasureList(staffIndex, measureIndex, voiceEntry.ParentSourceStaffEntry);
  94. }
  95. public update(): void {
  96. if (this.hidden || this.hidden === undefined || this.hidden === null) {
  97. return;
  98. }
  99. this.updateCurrentPage(); // attach cursor to new page DOM if necessary
  100. // this.graphic?.Cursors?.length = 0;
  101. const iterator: MusicPartManagerIterator = this.iterator;
  102. // TODO when measure draw range (drawUpToMeasureNumber) was changed, next/update can fail to move cursor. but of course it can be reset before.
  103. const voiceEntries: VoiceEntry[] = iterator.CurrentVisibleVoiceEntries();
  104. if (iterator.EndReached || !iterator.CurrentVoiceEntries || voiceEntries.length === 0) {
  105. return;
  106. }
  107. let x: number = 0, y: number = 0, height: number = 0;
  108. let musicSystem: MusicSystem;
  109. if (iterator.CurrentMeasure.isReducedToMultiRest) {
  110. const multiRestGMeasure: GraphicalMeasure = this.graphic.findGraphicalMeasure(iterator.CurrentMeasureIndex, 0);
  111. const totalRestMeasures: number = multiRestGMeasure.parentSourceMeasure.multipleRestMeasures;
  112. const currentRestMeasureNumber: number = iterator.CurrentMeasure.multipleRestMeasureNumber;
  113. const progressRatio: number = currentRestMeasureNumber / (totalRestMeasures + 1);
  114. const effectiveWidth: number = multiRestGMeasure.PositionAndShape.Size.width - (multiRestGMeasure as VexFlowMeasure).beginInstructionsWidth;
  115. x = multiRestGMeasure.PositionAndShape.AbsolutePosition.x + (multiRestGMeasure as VexFlowMeasure).beginInstructionsWidth + progressRatio * effectiveWidth;
  116. musicSystem = multiRestGMeasure.ParentMusicSystem;
  117. } else {
  118. // get all staff entries inside the current voice entry
  119. const gseArr: VexFlowStaffEntry[] = voiceEntries.map(ve => this.getStaffEntryFromVoiceEntry(ve));
  120. // sort them by x position and take the leftmost entry
  121. const gse: VexFlowStaffEntry =
  122. gseArr.sort((a, b) => a?.PositionAndShape?.AbsolutePosition?.x <= b?.PositionAndShape?.AbsolutePosition?.x ? -1 : 1 )[0];
  123. x = gse.PositionAndShape.AbsolutePosition.x;
  124. musicSystem = gse.parentMeasure.ParentMusicSystem;
  125. // debug: change color of notes under cursor (needs re-render)
  126. // for (const gve of gse.graphicalVoiceEntries) {
  127. // for (const note of gve.notes) {
  128. // note.sourceNote.NoteheadColor = "#0000FF";
  129. // }
  130. // }
  131. }
  132. if (!musicSystem) {
  133. return;
  134. }
  135. // y is common for both multirest and non-multirest, given the MusicSystem
  136. y = musicSystem.PositionAndShape.AbsolutePosition.y + musicSystem.StaffLines[0].PositionAndShape.RelativePosition.y;
  137. const bottomStaffline: StaffLine = musicSystem.StaffLines[musicSystem.StaffLines.length - 1];
  138. const endY: number = musicSystem.PositionAndShape.AbsolutePosition.y +
  139. bottomStaffline.PositionAndShape.RelativePosition.y + bottomStaffline.StaffHeight;
  140. height = endY - y;
  141. // Update the graphical cursor
  142. // The following is the legacy cursor rendered on the canvas:
  143. // // let cursor: GraphicalLine = new GraphicalLine(new PointF2D(x, y), new PointF2D(x, y + height), 3, OutlineAndFillStyleEnum.PlaybackCursor);
  144. // This the current HTML Cursor:
  145. const cursorElement: HTMLImageElement = this.cursorElement;
  146. cursorElement.style.top = (y * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
  147. cursorElement.style.left = ((x - 1.5) * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
  148. cursorElement.height = (height * 10.0 * this.openSheetMusicDisplay.zoom);
  149. const newWidth: number = 3 * 10.0 * this.openSheetMusicDisplay.zoom;
  150. if (newWidth !== cursorElement.width) {
  151. cursorElement.width = newWidth;
  152. this.updateStyle(newWidth);
  153. }
  154. if (this.openSheetMusicDisplay.FollowCursor) {
  155. const diff: number = this.cursorElement.getBoundingClientRect().top;
  156. this.cursorElement.scrollIntoView({behavior: diff < 1000 ? "smooth" : "auto", block: "center"});
  157. }
  158. // Show cursor
  159. // // Old cursor: this.graphic.Cursors.push(cursor);
  160. this.cursorElement.style.display = "";
  161. }
  162. /**
  163. * Hide the cursor
  164. */
  165. public hide(): void {
  166. // Hide the actual cursor element
  167. this.cursorElement.style.display = "none";
  168. //this.graphic.Cursors.length = 0;
  169. // Forcing the sheet to re-render is not necessary anymore
  170. //if (!this.hidden) {
  171. // this.openSheetMusicDisplay.render();
  172. //}
  173. this.hidden = true;
  174. }
  175. /**
  176. * Go to next entry
  177. */
  178. public next(): void {
  179. this.iterator.moveToNextVisibleVoiceEntry(false);
  180. this.update();
  181. }
  182. /**
  183. * reset cursor to start
  184. */
  185. public reset(): void {
  186. this.resetIterator();
  187. //this.iterator.moveToNext();
  188. this.update();
  189. }
  190. private updateStyle(width: number, color: string = "#33e02f"): void {
  191. // Create a dummy canvas to generate the gradient for the cursor
  192. // FIXME This approach needs to be improved
  193. const c: HTMLCanvasElement = document.createElement("canvas");
  194. c.width = this.cursorElement.width;
  195. c.height = 1;
  196. const ctx: CanvasRenderingContext2D = c.getContext("2d");
  197. ctx.globalAlpha = 0.5;
  198. // Generate the gradient
  199. const gradient: CanvasGradient = ctx.createLinearGradient(0, 0, this.cursorElement.width, 0);
  200. gradient.addColorStop(0, "white"); // it was: "transparent"
  201. gradient.addColorStop(0.2, color);
  202. gradient.addColorStop(0.8, color);
  203. gradient.addColorStop(1, "white"); // it was: "transparent"
  204. ctx.fillStyle = gradient;
  205. ctx.fillRect(0, 0, width, 1);
  206. // Set the actual image
  207. this.cursorElement.src = c.toDataURL("image/png");
  208. }
  209. public get Iterator(): MusicPartManagerIterator {
  210. return this.iterator;
  211. }
  212. public get Hidden(): boolean {
  213. return this.hidden;
  214. }
  215. /** returns voices under the current Cursor position. Without instrument argument, all voices are returned. */
  216. public VoicesUnderCursor(instrument?: Instrument): VoiceEntry[] {
  217. return this.iterator.CurrentVisibleVoiceEntries(instrument);
  218. }
  219. public NotesUnderCursor(instrument?: Instrument): Note[] {
  220. const voiceEntries: VoiceEntry[] = this.VoicesUnderCursor(instrument);
  221. const notes: Note[] = [];
  222. voiceEntries.forEach(voiceEntry => {
  223. notes.push.apply(notes, voiceEntry.Notes);
  224. });
  225. return notes;
  226. }
  227. /** Check if there was a change in current page, and attach cursor element to the corresponding HTMLElement (div).
  228. * This is only necessary if using PageFormat (multiple pages).
  229. */
  230. public updateCurrentPage(): number {
  231. const timestamp: Fraction = this.iterator.currentTimeStamp;
  232. for (const page of this.graphic.MusicPages) {
  233. const lastSystemTimestamp: Fraction = page.MusicSystems.last().GetSystemsLastTimeStamp();
  234. if (lastSystemTimestamp.gt(timestamp)) {
  235. // gt: the last timestamp of the last system is equal to the first of the next page,
  236. // so we do need to use gt, not gte here.
  237. const newPageNumber: number = page.PageNumber;
  238. if (newPageNumber !== this.currentPageNumber) {
  239. this.container.removeChild(this.cursorElement);
  240. this.container = document.getElementById("osmdCanvasPage" + newPageNumber);
  241. this.container.appendChild(this.cursorElement);
  242. // TODO maybe store this.pageCurrentlyAttachedTo, though right now it isn't necessary
  243. // alternative to remove/append:
  244. // this.openSheetMusicDisplay.enableOrDisableCursor(true);
  245. }
  246. return this.currentPageNumber = newPageNumber;
  247. }
  248. }
  249. return 1;
  250. }
  251. }