123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451 |
- import {MusicPartManagerIterator} from "../MusicalScore/MusicParts/MusicPartManagerIterator";
- import {MusicPartManager} from "../MusicalScore/MusicParts/MusicPartManager";
- import {VoiceEntry} from "../MusicalScore/VoiceData/VoiceEntry";
- import {VexFlowStaffEntry} from "../MusicalScore/Graphical/VexFlow/VexFlowStaffEntry";
- import {MusicSystem} from "../MusicalScore/Graphical/MusicSystem";
- import {OpenSheetMusicDisplay} from "./OpenSheetMusicDisplay";
- import {GraphicalMusicSheet} from "../MusicalScore/Graphical/GraphicalMusicSheet";
- import {Instrument} from "../MusicalScore/Instrument";
- import {Note} from "../MusicalScore/VoiceData/Note";
- import {Fraction} from "../Common/DataObjects/Fraction";
- import { EngravingRules } from "../MusicalScore/Graphical/EngravingRules";
- import { SourceMeasure } from "../MusicalScore/VoiceData/SourceMeasure";
- import { StaffLine } from "../MusicalScore/Graphical/StaffLine";
- import { GraphicalMeasure } from "../MusicalScore/Graphical/GraphicalMeasure";
- import { VexFlowMeasure } from "../MusicalScore/Graphical/VexFlow/VexFlowMeasure";
- import { CursorOptions } from "./OSMDOptions";
- import { BoundingBox } from "../MusicalScore/Graphical/BoundingBox";
- import { GraphicalNote } from "../MusicalScore/Graphical/GraphicalNote";
- import { GraphicalStaffEntry } from "../MusicalScore/Graphical/GraphicalStaffEntry";
- import { IPlaybackListener } from "../Common/Interfaces/IPlaybackListener";
- import { CursorPosChangedData } from "../Common/DataObjects/CursorPosChangedData";
- import { PointF2D } from "../Common/DataObjects";
- /**
- * A cursor which can iterate through the music sheet.
- */
- export class Cursor implements IPlaybackListener {
- constructor(container: HTMLElement, openSheetMusicDisplay: OpenSheetMusicDisplay, cursorOptions: CursorOptions) {
- this.container = container;
- this.openSheetMusicDisplay = openSheetMusicDisplay;
- this.rules = this.openSheetMusicDisplay.EngravingRules;
- this.cursorOptions = cursorOptions;
- // set cursor id
- // TODO add this for the OSMD object as well and refactor this into a util method?
- let id: number = 0;
- this.cursorElementId = "cursorImg-0";
- // find unique cursor id in document
- while (document.getElementById(this.cursorElementId)) {
- id++;
- this.cursorElementId = `cursorImg-${id}`;
- }
- const curs: HTMLElement = document.createElement("img");
- curs.id = this.cursorElementId;
- curs.style.position = "absolute";
- if (this.cursorOptions.follow === true) {
- curs.style.zIndex = "-1";
- } else {
- curs.style.zIndex = "-2";
- }
- this.cursorElement = <HTMLImageElement>curs;
- this.container.appendChild(curs);
- }
- public cursorPositionChanged(timestamp: Fraction, data: CursorPosChangedData): void {
- // if (this.iterator.CurrentEnrolledTimestamp.lt(timestamp)) {
- // this.iterator.moveToNext();
- // while (this.iterator.CurrentEnrolledTimestamp.lt(timestamp)) {
- // this.iterator.moveToNext();
- // }
- // } else if (this.iterator.CurrentEnrolledTimestamp.gt(timestamp)) {
- // this.iterator = new MusicPartManagerIterator(this.manager.MusicSheet, timestamp);
- // }
- this.updateWithTimestamp(data.PredictedPosition);
- }
- public pauseOccurred(o: object): void {
- // throw new Error("Method not implemented.");
- }
- public selectionEndReached(o: object): void {
- // throw new Error("Method not implemented.");
- }
- public resetOccurred(o: object): void {
- this.reset();
- }
- public notesPlaybackEventOccurred(o: object): void {
- // throw new Error("Method not implemented.");
- }
- private container: HTMLElement;
- public cursorElement: HTMLImageElement;
- /** a unique id of the cursor's HTMLElement in the document.
- * Should be constant between re-renders and backend changes,
- * but different between different OSMD objects on the same page.
- */
- public cursorElementId: string;
- private openSheetMusicDisplay: OpenSheetMusicDisplay;
- private rules: EngravingRules;
- private manager: MusicPartManager;
- public iterator: MusicPartManagerIterator;
- private graphic: GraphicalMusicSheet;
- public hidden: boolean = false;
- public currentPageNumber: number = 1;
- private cursorOptions: CursorOptions;
- /** Initialize the cursor. Necessary before using functions like show() and next(). */
- public init(manager: MusicPartManager, graphic: GraphicalMusicSheet): void {
- this.manager = manager;
- this.graphic = graphic;
- this.reset();
- this.hidden = false;
- }
- /**
- * Make the cursor visible
- */
- public show(): void {
- this.hidden = false;
- //this.resetIterator(); // TODO maybe not here? though setting measure range to draw, rerendering, then handling cursor show is difficult
- this.update();
- }
- public resetIterator(): void {
- if (!this.openSheetMusicDisplay.Sheet || !this.openSheetMusicDisplay.Sheet.SourceMeasures) { // just a safety measure
- console.log("OSMD.Cursor.resetIterator(): sheet or measures were null/undefined.");
- return;
- }
- // set selection start, so that when there's MinMeasureToDraw set, the cursor starts there right away instead of at measure 1
- const lastSheetMeasureIndex: number = this.openSheetMusicDisplay.Sheet.SourceMeasures.length - 1; // last measure in data model
- let startMeasureIndex: number = this.rules.MinMeasureToDrawIndex;
- startMeasureIndex = Math.min(startMeasureIndex, lastSheetMeasureIndex);
- let endMeasureIndex: number = this.rules.MaxMeasureToDrawIndex;
- endMeasureIndex = Math.min(endMeasureIndex, lastSheetMeasureIndex);
- const updateSelectionStart: boolean = this.openSheetMusicDisplay.Sheet && (
- !this.openSheetMusicDisplay.Sheet.SelectionStart ||
- this.openSheetMusicDisplay.Sheet.SelectionStart.WholeValue < startMeasureIndex) &&
- this.openSheetMusicDisplay.Sheet.SourceMeasures.length > startMeasureIndex;
- if (updateSelectionStart) {
- this.openSheetMusicDisplay.Sheet.SelectionStart = this.openSheetMusicDisplay.Sheet.SourceMeasures[startMeasureIndex].AbsoluteTimestamp;
- }
- if (this.openSheetMusicDisplay.Sheet && this.openSheetMusicDisplay.Sheet.SourceMeasures.length > endMeasureIndex) {
- const lastMeasure: SourceMeasure = this.openSheetMusicDisplay.Sheet.SourceMeasures[endMeasureIndex];
- this.openSheetMusicDisplay.Sheet.SelectionEnd = Fraction.plus(lastMeasure.AbsoluteTimestamp, lastMeasure.Duration);
- }
- this.iterator = this.manager.getIterator();
- }
- private getStaffEntryFromVoiceEntry(voiceEntry: VoiceEntry): VexFlowStaffEntry {
- const measureIndex: number = voiceEntry.ParentSourceStaffEntry.VerticalContainerParent.ParentMeasure.measureListIndex;
- const staffIndex: number = voiceEntry.ParentSourceStaffEntry.ParentStaff.idInMusicSheet;
- return <VexFlowStaffEntry>this.graphic.findGraphicalStaffEntryFromMeasureList(staffIndex, measureIndex, voiceEntry.ParentSourceStaffEntry);
- }
- public updateWithTimestamp(timestamp: Fraction): void {
- const sheetTimestamp: Fraction = this.manager.absoluteEnrolledToSheetTimestamp(timestamp);
- const values: [number, MusicSystem, GraphicalStaffEntry] = this.graphic.calculateXPositionFromTimestamp(sheetTimestamp);
- const x: number = values[0];
- const currentSystem: MusicSystem = values[1];
- this.updateCurrentPageFromSystem(currentSystem);
- const previousStaffEntry: GraphicalStaffEntry = values[2];
- if (!previousStaffEntry) {
- return; // TODO maybe fix calculateXPositionFromTimestamp() instead
- }
- // for samples starting with a precount measure (e.g. Mozart - An Chloe), the measure number can be 0,
- // so without max(n, 1), [topMeasureNumber - 1] would be [-1], causing an error
- const topMeasureNumber: number = Math.max(previousStaffEntry.parentMeasure.MeasureNumber, 1);
- // we have to find the top measure, otherwise the cursor with type 3 "jumps around" between vertical measures
- let topMeasure: GraphicalMeasure;
- for (const measure of this.graphic.MeasureList[topMeasureNumber - 1]) {
- if (measure) {
- topMeasure = measure;
- break;
- }
- }
- const points: [PointF2D, PointF2D] = this.graphic.calculateCursorPoints(x, currentSystem);
- const y: number = points[0].y;
- const height: number = points[1].y - y;
- this.updateWidthAndStyle(topMeasure.PositionAndShape, x, y, height);
- if (this.openSheetMusicDisplay.FollowCursor) {
- const diff: number = this.cursorElement.getBoundingClientRect().top;
- this.cursorElement.scrollIntoView({behavior: diff < 1000 ? "smooth" : "auto", block: "center"});
- }
- // Show cursor
- // // Old cursor: this.graphic.Cursors.push(cursor);
- this.cursorElement.style.display = "";
- }
- public update(): void {
- if (this.hidden || this.hidden === undefined || this.hidden === null) {
- return;
- }
- this.updateCurrentPage(); // attach cursor to new page DOM if necessary
- // this.graphic?.Cursors?.length = 0;
- let iterator: MusicPartManagerIterator;
- if (this.openSheetMusicDisplay.PlaybackManager) {
- iterator = this.openSheetMusicDisplay.PlaybackManager.CursorIterator;
- } else {
- iterator = this.iterator;
- }
- // TODO when measure draw range (drawUpToMeasureNumber) was changed, next/update can fail to move cursor. but of course it can be reset before.
- const voiceEntries: VoiceEntry[] = iterator.CurrentVisibleVoiceEntries();
- if (iterator.EndReached || !iterator.CurrentVoiceEntries || voiceEntries.length === 0) {
- return;
- }
- let x: number = 0, y: number = 0, height: number = 0;
- let musicSystem: MusicSystem;
- if (iterator.CurrentMeasure.isReducedToMultiRest) {
- const multiRestGMeasure: GraphicalMeasure = this.graphic.findGraphicalMeasure(iterator.CurrentMeasureIndex, 0);
- const totalRestMeasures: number = multiRestGMeasure.parentSourceMeasure.multipleRestMeasures;
- const currentRestMeasureNumber: number = iterator.CurrentMeasure.multipleRestMeasureNumber;
- const progressRatio: number = currentRestMeasureNumber / (totalRestMeasures + 1);
- const effectiveWidth: number = multiRestGMeasure.PositionAndShape.Size.width - (multiRestGMeasure as VexFlowMeasure).beginInstructionsWidth;
- x = multiRestGMeasure.PositionAndShape.AbsolutePosition.x + (multiRestGMeasure as VexFlowMeasure).beginInstructionsWidth + progressRatio * effectiveWidth;
- musicSystem = multiRestGMeasure.ParentMusicSystem;
- } else {
- // get all staff entries inside the current voice entry
- const gseArr: VexFlowStaffEntry[] = voiceEntries.map(ve => this.getStaffEntryFromVoiceEntry(ve));
- // sort them by x position and take the leftmost entry
- const gse: VexFlowStaffEntry =
- gseArr.sort((a, b) => a?.PositionAndShape?.AbsolutePosition?.x <= b?.PositionAndShape?.AbsolutePosition?.x ? -1 : 1 )[0];
- if(gse){
- x = gse.PositionAndShape.AbsolutePosition.x;
- musicSystem = gse.parentMeasure.ParentMusicSystem;
- }
- // debug: change color of notes under cursor (needs re-render)
- // for (const gve of gse.graphicalVoiceEntries) {
- // for (const note of gve.notes) {
- // note.sourceNote.NoteheadColor = "#0000FF";
- // }
- // }
- }
- if (!musicSystem) {
- return;
- }
- // y is common for both multirest and non-multirest, given the MusicSystem
- y = musicSystem.PositionAndShape.AbsolutePosition.y + musicSystem.StaffLines[0].PositionAndShape.RelativePosition.y ?? 0;
- const bottomStaffline: StaffLine = musicSystem.StaffLines[musicSystem.StaffLines.length - 1];
- const endY: number = musicSystem.PositionAndShape.AbsolutePosition.y +
- bottomStaffline.PositionAndShape.RelativePosition.y + bottomStaffline.StaffHeight;
- height = endY - y;
- // Update the graphical cursor
- const measurePositionAndShape: BoundingBox = this.graphic.findGraphicalMeasure(iterator.CurrentMeasureIndex, 0).PositionAndShape;
- this.updateWidthAndStyle(measurePositionAndShape, x, y, height);
- if (this.openSheetMusicDisplay.FollowCursor) {
- if (!this.openSheetMusicDisplay.EngravingRules.RenderSingleHorizontalStaffline) {
- const diff: number = this.cursorElement.getBoundingClientRect().top;
- this.cursorElement.scrollIntoView({behavior: diff < 1000 ? "smooth" : "auto", block: "center"});
- } else {
- this.cursorElement.scrollIntoView({behavior: "smooth", inline: "center"});
- }
- }
- // Show cursor
- // // Old cursor: this.graphic.Cursors.push(cursor);
- this.cursorElement.style.display = "";
- }
- public updateWidthAndStyle(measurePositionAndShape: BoundingBox, x: number, y: number, height: number): void {
- const cursorElement: HTMLImageElement = this.cursorElement;
- let newWidth: number = 0;
- let heightCalc: number = height;
- switch (this.cursorOptions.type) {
- case 1:
- cursorElement.style.top = (y * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
- cursorElement.style.left = ((x - 1.5) * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
- heightCalc = (height * 10.0 * this.openSheetMusicDisplay.zoom);
- cursorElement.height = heightCalc;
- cursorElement.style.height = heightCalc + "px";
- newWidth = 5 * this.openSheetMusicDisplay.zoom;
- break;
- case 2:
- cursorElement.style.top = ((y-2.5) * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
- cursorElement.style.left = (x * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
- heightCalc = (1.5 * 10.0 * this.openSheetMusicDisplay.zoom);
- cursorElement.height = heightCalc;
- cursorElement.style.height = heightCalc + "px";
- newWidth = 5 * this.openSheetMusicDisplay.zoom;
- break;
- case 3:
- cursorElement.style.top = measurePositionAndShape.AbsolutePosition.y * 10.0 * this.openSheetMusicDisplay.zoom +"px";
- cursorElement.style.left = measurePositionAndShape.AbsolutePosition.x * 10.0 * this.openSheetMusicDisplay.zoom +"px";
- heightCalc = (height * 10.0 * this.openSheetMusicDisplay.zoom);
- cursorElement.height = heightCalc;
- cursorElement.style.height = heightCalc + "px";
- newWidth = measurePositionAndShape.Size.width * 10 * this.openSheetMusicDisplay.zoom;
- break;
- case 4:
- cursorElement.style.top = measurePositionAndShape.AbsolutePosition.y * 10.0 * this.openSheetMusicDisplay.zoom +"px";
- cursorElement.style.left = measurePositionAndShape.AbsolutePosition.x * 10.0 * this.openSheetMusicDisplay.zoom +"px";
- heightCalc = (height * 10.0 * this.openSheetMusicDisplay.zoom);
- cursorElement.height = heightCalc;
- cursorElement.style.height = heightCalc + "px";
- newWidth = (x-measurePositionAndShape.AbsolutePosition.x) * 10 * this.openSheetMusicDisplay.zoom;
- break;
- default:
- cursorElement.style.top = (y * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
- cursorElement.style.left = ((x - 1.5) * 10.0 * this.openSheetMusicDisplay.zoom) + "px";
- heightCalc = (height * 10.0 * this.openSheetMusicDisplay.zoom);
- cursorElement.height = heightCalc;
- cursorElement.style.height = heightCalc + "px";
- newWidth = 3 * 10.0 * this.openSheetMusicDisplay.zoom;
- break;
- }
- if (newWidth !== cursorElement.width) {
- cursorElement.width = newWidth;
- this.updateStyle(newWidth, this.cursorOptions);
- }
- }
- /**
- * Hide the cursor
- */
- public hide(): void {
- // Hide the actual cursor element
- this.cursorElement.style.display = "none";
- //this.graphic.Cursors.length = 0;
- // Forcing the sheet to re-render is not necessary anymore
- //if (!this.hidden) {
- // this.openSheetMusicDisplay.render();
- //}
- this.hidden = true;
- }
- /**
- * Go to next entry
- */
- public next(): void {
- this.iterator.moveToNextVisibleVoiceEntry(false); // moveToNext() would not skip notes in hidden (visible = false) parts
- this.update();
- }
- /**
- * reset cursor to start
- */
- public reset(): void {
- this.resetIterator();
- //this.iterator.moveToNext();
- const iterTmp: MusicPartManagerIterator = this.manager.getIterator(this.graphic.ParentMusicSheet.SelectionStart);
- this.updateWithTimestamp(iterTmp.CurrentEnrolledTimestamp);
- }
- private updateStyle(width: number, cursorOptions: CursorOptions = undefined): void {
- if (cursorOptions !== undefined) {
- this.cursorOptions = cursorOptions;
- }
- // Create a dummy canvas to generate the gradient for the cursor
- // FIXME This approach needs to be improved
- const c: HTMLCanvasElement = document.createElement("canvas");
- c.width = this.cursorElement.width;
- c.height = 1;
- const ctx: CanvasRenderingContext2D = c.getContext("2d");
- ctx.globalAlpha = this.cursorOptions.alpha;
- // Generate the gradient
- const gradient: CanvasGradient = ctx.createLinearGradient(0, 0, this.cursorElement.width, 0);
- switch (this.cursorOptions.type) {
- case 1:
- case 2:
- case 3:
- case 4:
- gradient.addColorStop(1, this.cursorOptions.color);
- break;
- default:
- gradient.addColorStop(0, "white"); // it was: "transparent"
- gradient.addColorStop(0.2, this.cursorOptions.color);
- gradient.addColorStop(0.8, this.cursorOptions.color);
- gradient.addColorStop(1, "white"); // it was: "transparent"
- break;
- }
- ctx.fillStyle = gradient;
- ctx.fillRect(0, 0, width, 1);
- // Set the actual image
- this.cursorElement.src = c.toDataURL("image/png");
- }
- public get Iterator(): MusicPartManagerIterator {
- return this.iterator;
- }
- public get Hidden(): boolean {
- return this.hidden;
- }
- /** returns voices under the current Cursor position. Without instrument argument, all voices are returned. */
- public VoicesUnderCursor(instrument?: Instrument): VoiceEntry[] {
- return this.iterator.CurrentVisibleVoiceEntries(instrument);
- }
- public NotesUnderCursor(instrument?: Instrument): Note[] {
- const voiceEntries: VoiceEntry[] = this.VoicesUnderCursor(instrument);
- const notes: Note[] = [];
- voiceEntries.forEach(voiceEntry => {
- notes.push.apply(notes, voiceEntry.Notes);
- });
- return notes;
- }
- public GNotesUnderCursor(instrument?: Instrument): GraphicalNote[] {
- const voiceEntries: VoiceEntry[] = this.VoicesUnderCursor(instrument);
- const notes: GraphicalNote[] = [];
- voiceEntries.forEach(voiceEntry => {
- notes.push(...voiceEntry.Notes.map(note => this.rules.GNote(note)));
- });
- return notes;
- }
- /** Check if there was a change in current page, and attach cursor element to the corresponding HTMLElement (div).
- * This is only necessary if using PageFormat (multiple pages).
- */
- public updateCurrentPage(): number {
- const timestamp: Fraction = this.iterator.currentTimeStamp;
- for (const page of this.graphic.MusicPages) {
- const lastSystemTimestamp: Fraction = page.MusicSystems.last().GetSystemsLastTimeStamp();
- if (lastSystemTimestamp.gt(timestamp)) {
- // gt: the last timestamp of the last system is equal to the first of the next page,
- // so we do need to use gt, not gte here.
- const newPageNumber: number = page.PageNumber;
- if (newPageNumber !== this.currentPageNumber) {
- this.container.removeChild(this.cursorElement);
- this.container = document.getElementById("osmdCanvasPage" + newPageNumber);
- this.container.appendChild(this.cursorElement);
- // TODO maybe store this.pageCurrentlyAttachedTo, though right now it isn't necessary
- // alternative to remove/append:
- // this.openSheetMusicDisplay.enableOrDisableCursor(true);
- }
- return this.currentPageNumber = newPageNumber;
- }
- }
- return 1;
- }
- public updateCurrentPageFromSystem(system: MusicSystem): number {
- if (system !== undefined) {
- const newPageNumber: number = system.Parent.PageNumber;
- if (newPageNumber !== this.currentPageNumber) {
- this.container.removeChild(this.cursorElement);
- this.container = document.getElementById("osmdCanvasPage" + newPageNumber);
- this.container.appendChild(this.cursorElement);
- // TODO maybe store this.pageCurrentlyAttachedTo, though right now it isn't necessary
- // alternative to remove/append:
- // this.openSheetMusicDisplay.enableOrDisableCursor(true);
- }
- return this.currentPageNumber = newPageNumber;
- }
- return 1;
- }
- }
|