Cursor.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  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. import { CursorOptions } from "./OSMDOptions";
  17. import { BoundingBox } from "../MusicalScore/Graphical/BoundingBox";
  18. import { GraphicalNote } from "../MusicalScore/Graphical/GraphicalNote";
  19. import { GraphicalStaffEntry } from "../MusicalScore/Graphical/GraphicalStaffEntry";
  20. import { IPlaybackListener } from "../Common/Interfaces/IPlaybackListener";
  21. import { CursorPosChangedData } from "../Common/DataObjects/CursorPosChangedData";
  22. import { PointF2D } from "../Common/DataObjects";
  23. /**
  24. * A cursor which can iterate through the music sheet.
  25. */
  26. export class Cursor implements IPlaybackListener {
  27. constructor(container: HTMLElement, openSheetMusicDisplay: OpenSheetMusicDisplay, cursorOptions: CursorOptions) {
  28. this.container = container;
  29. this.openSheetMusicDisplay = openSheetMusicDisplay;
  30. this.rules = this.openSheetMusicDisplay.EngravingRules;
  31. this.cursorOptions = cursorOptions;
  32. // set cursor id
  33. // TODO add this for the OSMD object as well and refactor this into a util method?
  34. let id: number = 0;
  35. this.cursorElementId = "cursorImg-0";
  36. // find unique cursor id in document
  37. while (document.getElementById(this.cursorElementId)) {
  38. id++;
  39. this.cursorElementId = `cursorImg-${id}`;
  40. }
  41. const curs: HTMLElement = document.createElement("img");
  42. curs.id = this.cursorElementId;
  43. curs.style.position = "absolute";
  44. if (this.cursorOptions.follow === true) {
  45. this.wantedZIndex = "-1";
  46. curs.style.zIndex = this.wantedZIndex;
  47. } else {
  48. this.wantedZIndex = "-2";
  49. curs.style.zIndex = this.wantedZIndex;
  50. }
  51. this.cursorElement = <HTMLImageElement>curs;
  52. this.container.appendChild(curs);
  53. }
  54. public cursorPositionChanged(timestamp: Fraction, data: CursorPosChangedData): void {
  55. // update iterator so cursor.NotesUnderCursor() etc works
  56. while (this.iterator.CurrentEnrolledTimestamp.lt(timestamp) && !this.iterator.EndReached) {
  57. // if iterator.EndReached, this would loop endlessly, because then moveToNext() just returns, without changes
  58. this.iterator.moveToNext();
  59. }
  60. if (this.iterator.CurrentEnrolledTimestamp.gt(timestamp)) {
  61. this.iterator = new MusicPartManagerIterator(this.manager.MusicSheet, timestamp);
  62. }
  63. this.updateWithTimestamp(data.PredictedPosition);
  64. }
  65. public pauseOccurred(o: object): void {
  66. // throw new Error("Method not implemented.");
  67. }
  68. public selectionEndReached(o: object): void {
  69. // throw new Error("Method not implemented.");
  70. }
  71. public resetOccurred(o: object): void {
  72. this.reset();
  73. }
  74. public notesPlaybackEventOccurred(o: object): void {
  75. // throw new Error("Method not implemented.");
  76. }
  77. public adjustToBackgroundColor(): void {
  78. let zIndex: string;
  79. if (!this.rules.PageBackgroundColor) {
  80. zIndex = this.wantedZIndex;
  81. } else {
  82. zIndex = "1";
  83. }
  84. this.cursorElement.style.zIndex = zIndex;
  85. }
  86. private container: HTMLElement;
  87. public cursorElement: HTMLImageElement;
  88. /** a unique id of the cursor's HTMLElement in the document.
  89. * Should be constant between re-renders and backend changes,
  90. * but different between different OSMD objects on the same page.
  91. */
  92. public cursorElementId: string;
  93. /** The desired zIndex (layer) of the cursor when no background color is set.
  94. * When a background color is set, using a negative zIndex would make the cursor invisible.
  95. */
  96. public wantedZIndex: string;
  97. private openSheetMusicDisplay: OpenSheetMusicDisplay;
  98. private rules: EngravingRules;
  99. private manager: MusicPartManager;
  100. public iterator: MusicPartManagerIterator;
  101. private graphic: GraphicalMusicSheet;
  102. public hidden: boolean = false;
  103. public currentPageNumber: number = 1;
  104. private cursorOptions: CursorOptions;
  105. private skipInvisibleNotes: boolean = true;
  106. /** Where to scroll to when FollowCursor is enabled.
  107. * Default center (will scroll so that the current cursor position is in the center of the screen),
  108. * alternatively set to "start" to scroll so that the current position is at the top of the screen.
  109. */
  110. public ScrollPosition: ScrollLogicalPosition = "center";
  111. /** Initialize the cursor. Necessary before using functions like show() and next(). */
  112. public init(manager: MusicPartManager, graphic: GraphicalMusicSheet): void {
  113. this.manager = manager;
  114. this.graphic = graphic;
  115. this.reset();
  116. this.hidden = false;
  117. }
  118. /**
  119. * Make the cursor visible
  120. */
  121. public show(): void {
  122. this.hidden = false;
  123. //this.resetIterator(); // TODO maybe not here? though setting measure range to draw, rerendering, then handling cursor show is difficult
  124. this.update();
  125. this.adjustToBackgroundColor();
  126. }
  127. public resetIterator(): void {
  128. if (!this.openSheetMusicDisplay.Sheet || !this.openSheetMusicDisplay.Sheet.SourceMeasures) { // just a safety measure
  129. console.log("OSMD.Cursor.resetIterator(): sheet or measures were null/undefined.");
  130. return;
  131. }
  132. // set selection start, so that when there's MinMeasureToDraw set, the cursor starts there right away instead of at measure 1
  133. const lastSheetMeasureIndex: number = this.openSheetMusicDisplay.Sheet.SourceMeasures.length - 1; // last measure in data model
  134. let startMeasureIndex: number = this.rules.MinMeasureToDrawIndex;
  135. startMeasureIndex = Math.min(startMeasureIndex, lastSheetMeasureIndex);
  136. let endMeasureIndex: number = this.rules.MaxMeasureToDrawIndex;
  137. endMeasureIndex = Math.min(endMeasureIndex, lastSheetMeasureIndex);
  138. const updateSelectionStart: boolean = this.openSheetMusicDisplay.Sheet && (
  139. !this.openSheetMusicDisplay.Sheet.SelectionStart ||
  140. this.openSheetMusicDisplay.Sheet.SelectionStart.WholeValue < startMeasureIndex) &&
  141. this.openSheetMusicDisplay.Sheet.SourceMeasures.length > startMeasureIndex;
  142. if (updateSelectionStart) {
  143. this.openSheetMusicDisplay.Sheet.SelectionStart = this.openSheetMusicDisplay.Sheet.SourceMeasures[startMeasureIndex].AbsoluteTimestamp;
  144. }
  145. if (this.openSheetMusicDisplay.Sheet && this.openSheetMusicDisplay.Sheet.SourceMeasures.length > endMeasureIndex) {
  146. const lastMeasure: SourceMeasure = this.openSheetMusicDisplay.Sheet.SourceMeasures[endMeasureIndex];
  147. this.openSheetMusicDisplay.Sheet.SelectionEnd = Fraction.plus(lastMeasure.AbsoluteTimestamp, lastMeasure.Duration);
  148. }
  149. this.iterator = this.manager.getIterator();
  150. // remember SkipInvisibleNotes setting, which otherwise gets reset to default value
  151. this.iterator.SkipInvisibleNotes = this.skipInvisibleNotes;
  152. }
  153. private getStaffEntryFromVoiceEntry(voiceEntry: VoiceEntry): VexFlowStaffEntry {
  154. const measureIndex: number = voiceEntry.ParentSourceStaffEntry.VerticalContainerParent.ParentMeasure.measureListIndex;
  155. const staffIndex: number = voiceEntry.ParentSourceStaffEntry.ParentStaff.idInMusicSheet;
  156. return <VexFlowStaffEntry>this.graphic.findGraphicalStaffEntryFromMeasureList(staffIndex, measureIndex, voiceEntry.ParentSourceStaffEntry);
  157. }
  158. public updateWithTimestamp(timestamp: Fraction): void {
  159. const sheetTimestamp: Fraction = this.manager.absoluteEnrolledToSheetTimestamp(timestamp);
  160. const values: [number, MusicSystem, GraphicalStaffEntry] = this.graphic.calculateXPositionFromTimestamp(sheetTimestamp);
  161. const x: number = values[0];
  162. const currentSystem: MusicSystem = values[1];
  163. this.updateCurrentPageFromSystem(currentSystem);
  164. const previousStaffEntry: GraphicalStaffEntry = values[2];
  165. if (!previousStaffEntry) {
  166. return; // TODO maybe fix calculateXPositionFromTimestamp() instead
  167. }
  168. // for samples starting with a precount measure (e.g. Mozart - An Chloe), the measure number can be 0,
  169. // so without max(n, 1), [topMeasureNumber - 1] would be [-1], causing an error
  170. const topMeasureNumber: number = Math.max(previousStaffEntry.parentMeasure.MeasureNumber, 1);
  171. // we have to find the top measure, otherwise the cursor with type 3 "jumps around" between vertical measures
  172. let topMeasure: GraphicalMeasure;
  173. for (const measure of this.graphic.MeasureList[topMeasureNumber - 1]) {
  174. if (measure) {
  175. topMeasure = measure;
  176. break;
  177. }
  178. }
  179. const points: [PointF2D, PointF2D] = this.graphic.calculateCursorPoints(x, currentSystem);
  180. const y: number = points[0].y;
  181. const height: number = points[1].y - y;
  182. this.updateWidthAndStyle(topMeasure.PositionAndShape, x, y, height);
  183. if (this.openSheetMusicDisplay.FollowCursor) {
  184. const diff: number = this.cursorElement.getBoundingClientRect().top;
  185. this.cursorElement.scrollIntoView(
  186. {behavior: diff < 1000 ? "smooth" : "auto", block: this.ScrollPosition}
  187. );
  188. }
  189. // Show cursor
  190. // // Old cursor: this.graphic.Cursors.push(cursor);
  191. this.cursorElement.style.display = "";
  192. }
  193. public update(): void {
  194. if (this.hidden || this.hidden === undefined || this.hidden === null) {
  195. return;
  196. }
  197. this.updateCurrentPage(); // attach cursor to new page DOM if necessary
  198. // this.graphic?.Cursors?.length = 0;
  199. const iterator: MusicPartManagerIterator = this.Iterator;
  200. // TODO when measure draw range (drawUpToMeasureNumber) was changed, next/update can fail to move cursor. but of course it can be reset before.
  201. let voiceEntries: VoiceEntry[] = iterator.CurrentVisibleVoiceEntries();
  202. let currentMeasureIndex: number = iterator.CurrentMeasureIndex;
  203. let x: number = 0, y: number = 0, height: number = 0;
  204. let musicSystem: MusicSystem;
  205. if (voiceEntries.length === 0 && !iterator.FrontReached && !iterator.EndReached) {
  206. // e.g. when the note at the current position is in an instrument that's now invisible, and there's no other note at this position, vertically
  207. iterator.moveToPrevious();
  208. voiceEntries = iterator.CurrentVisibleVoiceEntries();
  209. iterator.moveToNext();
  210. // after this, the else condition below should trigger, positioning the cursor at the left-most note. See #1312
  211. }
  212. if (iterator.FrontReached && voiceEntries.length === 0) {
  213. // show beginning of first measure (of stafflines, to create a visual difference to the first note position)
  214. // this position is technically before the sheet/first note - e.g. cursor.Iterator.CurrentTimestamp.RealValue = -1
  215. iterator.moveToNext();
  216. voiceEntries = iterator.CurrentVisibleVoiceEntries();
  217. const firstVisibleMeasure: GraphicalMeasure = this.findVisibleGraphicalMeasure(iterator.CurrentMeasureIndex);
  218. x = firstVisibleMeasure.PositionAndShape.AbsolutePosition.x;
  219. musicSystem = firstVisibleMeasure.ParentMusicSystem;
  220. iterator.moveToPrevious();
  221. } else if (iterator.EndReached || !iterator.CurrentVoiceEntries || voiceEntries.length === 0) {
  222. // show end of last measure (of stafflines, to create a visual difference to the first note position)
  223. // this position is technically after the sheet/last note - e.g. cursor.Iterator.CurrentTimestamp.RealValue = 99999
  224. iterator.moveToPrevious();
  225. voiceEntries = iterator.CurrentVisibleVoiceEntries();
  226. currentMeasureIndex = iterator.CurrentMeasureIndex;
  227. const lastVisibleMeasure: GraphicalMeasure = this.findVisibleGraphicalMeasure(iterator.CurrentMeasureIndex);
  228. x = lastVisibleMeasure.PositionAndShape.AbsolutePosition.x + lastVisibleMeasure.PositionAndShape.Size.width;
  229. musicSystem = lastVisibleMeasure.ParentMusicSystem;
  230. iterator.moveToNext();
  231. } else if (iterator.CurrentMeasure.isReducedToMultiRest) {
  232. // multiple measure rests aren't used when one
  233. const multiRestGMeasure: GraphicalMeasure = this.findVisibleGraphicalMeasure(iterator.CurrentMeasureIndex);
  234. const totalRestMeasures: number = multiRestGMeasure.parentSourceMeasure.multipleRestMeasures;
  235. const currentRestMeasureNumber: number = iterator.CurrentMeasure.multipleRestMeasureNumber;
  236. const progressRatio: number = currentRestMeasureNumber / (totalRestMeasures + 1);
  237. const effectiveWidth: number = multiRestGMeasure.PositionAndShape.Size.width - (multiRestGMeasure as VexFlowMeasure).beginInstructionsWidth;
  238. x = multiRestGMeasure.PositionAndShape.AbsolutePosition.x + (multiRestGMeasure as VexFlowMeasure).beginInstructionsWidth + progressRatio * effectiveWidth;
  239. musicSystem = multiRestGMeasure.ParentMusicSystem;
  240. } else {
  241. // get all staff entries inside the current voice entry
  242. const gseArr: VexFlowStaffEntry[] = voiceEntries.map(ve => this.getStaffEntryFromVoiceEntry(ve));
  243. // sort them by x position and take the leftmost entry
  244. const gse: VexFlowStaffEntry =
  245. gseArr.sort((a, b) => a?.PositionAndShape?.AbsolutePosition?.x <= b?.PositionAndShape?.AbsolutePosition?.x ? -1 : 1 )[0];
  246. if (gse) {
  247. x = gse.PositionAndShape.AbsolutePosition.x;
  248. musicSystem = gse.parentMeasure.ParentMusicSystem;
  249. }
  250. // debug: change color of notes under cursor (needs re-render)
  251. // for (const gve of gse.graphicalVoiceEntries) {
  252. // for (const note of gve.notes) {
  253. // note.sourceNote.NoteheadColor = "#0000FF";
  254. // }
  255. // }
  256. }
  257. if (!musicSystem) {
  258. return;
  259. }
  260. // y is common for both multirest and non-multirest, given the MusicSystem
  261. // note: StaffLines[0] is guaranteed to exist in this.findVisibleGraphicalMeasure
  262. y = musicSystem.PositionAndShape.AbsolutePosition.y + musicSystem.StaffLines[0].PositionAndShape.RelativePosition.y;
  263. let endY: number = musicSystem.PositionAndShape.AbsolutePosition.y;
  264. const bottomStaffline: StaffLine = musicSystem.StaffLines[musicSystem.StaffLines.length - 1];
  265. if (bottomStaffline) { // can be undefined if drawFromMeasureNumber changed after cursor was shown (extended issue 68)
  266. endY += bottomStaffline.PositionAndShape.RelativePosition.y + bottomStaffline.StaffHeight;
  267. }
  268. height = endY - y;
  269. // Update the graphical cursor
  270. const measurePositionAndShape: BoundingBox = this.graphic.findGraphicalMeasure(currentMeasureIndex, 0).PositionAndShape;
  271. this.updateWidthAndStyle(measurePositionAndShape, x, y, height);
  272. if (this.openSheetMusicDisplay.FollowCursor && this.cursorOptions.follow) {
  273. if (!this.openSheetMusicDisplay.EngravingRules.RenderSingleHorizontalStaffline) {
  274. const diff: number = this.cursorElement.getBoundingClientRect().top;
  275. this.cursorElement.scrollIntoView({behavior: diff < 1000 ? "smooth" : "auto", block: "center"});
  276. } else {
  277. this.cursorElement.scrollIntoView({behavior: "smooth", inline: "center"});
  278. }
  279. }
  280. // Show cursor
  281. // // Old cursor: this.graphic.Cursors.push(cursor);
  282. this.cursorElement.style.display = "";
  283. }
  284. private findVisibleGraphicalMeasure(measureIndex: number): GraphicalMeasure {
  285. for (let i: number = 0; i < this.graphic.NumberOfStaves; i++) {
  286. const measure: GraphicalMeasure = this.graphic.findGraphicalMeasure(this.iterator.CurrentMeasureIndex, i);
  287. if (measure?.ParentStaff.ParentInstrument.Visible) {
  288. return measure;
  289. }
  290. }
  291. }
  292. public updateWidthAndStyle(measurePositionAndShape: BoundingBox, x: number, y: number, height: number): void {
  293. const cursorElement: HTMLImageElement = this.cursorElement;
  294. let newWidth: number = 0;
  295. let heightCalc: number = height;
  296. switch (this.cursorOptions.type) {
  297. case 1:
  298. cursorElement.style.top = (y * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
  299. cursorElement.style.left = ((x - 1.5) * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
  300. heightCalc = (height * 10.0 * this.openSheetMusicDisplay.zoom);
  301. cursorElement.height = heightCalc;
  302. cursorElement.style.height = heightCalc + "px";
  303. newWidth = 5 * this.openSheetMusicDisplay.zoom;
  304. break;
  305. case 2:
  306. cursorElement.style.top = ((y-2.5) * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
  307. cursorElement.style.left = (x * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
  308. heightCalc = (1.5 * 10.0 * this.openSheetMusicDisplay.zoom);
  309. cursorElement.height = heightCalc;
  310. cursorElement.style.height = heightCalc + "px";
  311. newWidth = 5 * this.openSheetMusicDisplay.zoom;
  312. break;
  313. case 3:
  314. cursorElement.style.top = measurePositionAndShape.AbsolutePosition.y * 10.0 * this.openSheetMusicDisplay.zoom +"px";
  315. cursorElement.style.left = measurePositionAndShape.AbsolutePosition.x * 10.0 * this.openSheetMusicDisplay.zoom +"px";
  316. heightCalc = (height * 10.0 * this.openSheetMusicDisplay.zoom);
  317. cursorElement.height = heightCalc;
  318. cursorElement.style.height = heightCalc + "px";
  319. newWidth = measurePositionAndShape.Size.width * 10 * this.openSheetMusicDisplay.zoom;
  320. break;
  321. case 4:
  322. cursorElement.style.top = measurePositionAndShape.AbsolutePosition.y * 10.0 * this.openSheetMusicDisplay.zoom +"px";
  323. cursorElement.style.left = measurePositionAndShape.AbsolutePosition.x * 10.0 * this.openSheetMusicDisplay.zoom +"px";
  324. heightCalc = (height * 10.0 * this.openSheetMusicDisplay.zoom);
  325. cursorElement.height = heightCalc;
  326. cursorElement.style.height = heightCalc + "px";
  327. newWidth = (x-measurePositionAndShape.AbsolutePosition.x) * 10 * this.openSheetMusicDisplay.zoom;
  328. break;
  329. default:
  330. cursorElement.style.top = (y * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
  331. cursorElement.style.left = ((x - 1.5) * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
  332. heightCalc = (height * 10.0 * this.openSheetMusicDisplay.zoom);
  333. cursorElement.height = heightCalc;
  334. cursorElement.style.height = heightCalc + "px";
  335. newWidth = 3 * 10.0 * this.openSheetMusicDisplay.zoom;
  336. break;
  337. }
  338. if (newWidth !== cursorElement.width) {
  339. cursorElement.width = newWidth;
  340. this.updateStyle(newWidth, this.cursorOptions);
  341. }
  342. }
  343. /**
  344. * Hide the cursor
  345. */
  346. public hide(): void {
  347. // Hide the actual cursor element
  348. this.cursorElement.style.display = "none";
  349. //this.graphic.Cursors.length = 0;
  350. // Forcing the sheet to re-render is not necessary anymore
  351. //if (!this.hidden) {
  352. // this.openSheetMusicDisplay.render();
  353. //}
  354. this.hidden = true;
  355. }
  356. /**
  357. * Go to previous entry
  358. */
  359. public previous(): void {
  360. this.iterator.moveToPreviousVisibleVoiceEntry(false);
  361. this.update();
  362. }
  363. /**
  364. * Go to next entry
  365. */
  366. public next(): void {
  367. this.Iterator.moveToNextVisibleVoiceEntry(false); // moveToNext() would not skip notes in hidden (visible = false) parts
  368. this.update();
  369. }
  370. /**
  371. * reset cursor to start
  372. */
  373. public reset(): void {
  374. this.resetIterator();
  375. //this.iterator.moveToNext();
  376. const iterTmp: MusicPartManagerIterator = this.manager.getIterator(this.graphic.ParentMusicSheet.SelectionStart);
  377. this.updateWithTimestamp(iterTmp.CurrentEnrolledTimestamp);
  378. }
  379. private updateStyle(width: number, cursorOptions: CursorOptions = undefined): void {
  380. if (cursorOptions !== undefined) {
  381. this.cursorOptions = cursorOptions;
  382. }
  383. // Create a dummy canvas to generate the gradient for the cursor
  384. // FIXME This approach needs to be improved
  385. const c: HTMLCanvasElement = document.createElement("canvas");
  386. c.width = this.cursorElement.width;
  387. c.height = 1;
  388. const ctx: CanvasRenderingContext2D = c.getContext("2d");
  389. ctx.globalAlpha = this.cursorOptions.alpha;
  390. // Generate the gradient
  391. const gradient: CanvasGradient = ctx.createLinearGradient(0, 0, this.cursorElement.width, 0);
  392. switch (this.cursorOptions.type) {
  393. case 1:
  394. case 2:
  395. case 3:
  396. case 4:
  397. gradient.addColorStop(1, this.cursorOptions.color);
  398. break;
  399. default:
  400. gradient.addColorStop(0, "white"); // it was: "transparent"
  401. gradient.addColorStop(0.2, this.cursorOptions.color);
  402. gradient.addColorStop(0.8, this.cursorOptions.color);
  403. gradient.addColorStop(1, "white"); // it was: "transparent"
  404. break;
  405. }
  406. ctx.fillStyle = gradient;
  407. ctx.fillRect(0, 0, width, 1);
  408. // Set the actual image
  409. this.cursorElement.src = c.toDataURL("image/png");
  410. }
  411. public get Iterator(): MusicPartManagerIterator {
  412. // if (this.openSheetMusicDisplay.PlaybackManager) {
  413. // return this.openSheetMusicDisplay.PlaybackManager.CursorIterator;
  414. // }
  415. // PlaybackManager.CursorIterator is often not at the visible cursor position.
  416. return this.iterator;
  417. }
  418. public get Hidden(): boolean {
  419. return this.hidden;
  420. }
  421. /** returns voices under the current Cursor position. Without instrument argument, all voices are returned. */
  422. public VoicesUnderCursor(instrument?: Instrument): VoiceEntry[] {
  423. return this.Iterator.CurrentVisibleVoiceEntries(instrument);
  424. }
  425. public NotesUnderCursor(instrument?: Instrument): Note[] {
  426. const voiceEntries: VoiceEntry[] = this.VoicesUnderCursor(instrument);
  427. const notes: Note[] = [];
  428. voiceEntries.forEach(voiceEntry => {
  429. notes.push.apply(notes, voiceEntry.Notes);
  430. });
  431. return notes;
  432. }
  433. public GNotesUnderCursor(instrument?: Instrument): GraphicalNote[] {
  434. const voiceEntries: VoiceEntry[] = this.VoicesUnderCursor(instrument);
  435. const notes: GraphicalNote[] = [];
  436. voiceEntries.forEach(voiceEntry => {
  437. notes.push(...voiceEntry.Notes.map(note => this.rules.GNote(note)));
  438. });
  439. return notes;
  440. }
  441. /** Check if there was a change in current page, and attach cursor element to the corresponding HTMLElement (div).
  442. * This is only necessary if using PageFormat (multiple pages).
  443. */
  444. public updateCurrentPage(): number {
  445. let timestamp: Fraction = this.iterator.currentTimeStamp;
  446. if (timestamp.RealValue < 0) {
  447. timestamp = new Fraction(0, 0);
  448. }
  449. for (const page of this.graphic.MusicPages) {
  450. const lastSystemTimestamp: Fraction = page.MusicSystems.last().GetSystemsLastTimeStamp();
  451. if (lastSystemTimestamp.gt(timestamp)) {
  452. // gt: the last timestamp of the last system is equal to the first of the next page,
  453. // so we do need to use gt, not gte here.
  454. const newPageNumber: number = page.PageNumber;
  455. if (newPageNumber !== this.currentPageNumber) {
  456. this.container.removeChild(this.cursorElement);
  457. this.container = document.getElementById("osmdCanvasPage" + newPageNumber);
  458. this.container.appendChild(this.cursorElement);
  459. // TODO maybe store this.pageCurrentlyAttachedTo, though right now it isn't necessary
  460. // alternative to remove/append:
  461. // this.openSheetMusicDisplay.enableOrDisableCursor(true);
  462. }
  463. return this.currentPageNumber = newPageNumber;
  464. }
  465. }
  466. return 1;
  467. }
  468. public get SkipInvisibleNotes(): boolean {
  469. return this.skipInvisibleNotes;
  470. }
  471. public set SkipInvisibleNotes(value: boolean) {
  472. this.skipInvisibleNotes = value;
  473. this.iterator.SkipInvisibleNotes = value;
  474. }
  475. public get CursorOptions(): CursorOptions {
  476. return this.cursorOptions;
  477. }
  478. public set CursorOptions(value: CursorOptions) {
  479. this.cursorOptions = value;
  480. }
  481. public updateCurrentPageFromSystem(system: MusicSystem): number {
  482. if (system?.Parent) {
  483. const newPageNumber: number = system.Parent.PageNumber;
  484. if (newPageNumber !== this.currentPageNumber) {
  485. this.container.removeChild(this.cursorElement);
  486. this.container = document.getElementById("osmdCanvasPage" + newPageNumber);
  487. this.container.appendChild(this.cursorElement);
  488. // TODO maybe store this.pageCurrentlyAttachedTo, though right now it isn't necessary
  489. // alternative to remove/append:
  490. // this.openSheetMusicDisplay.enableOrDisableCursor(true);
  491. }
  492. return this.currentPageNumber = newPageNumber;
  493. }
  494. return 1;
  495. }
  496. public Dispose(): void {
  497. this.rules = undefined;
  498. this.openSheetMusicDisplay = undefined;
  499. this.cursorOptions = undefined;
  500. }
  501. }