/
Cursor.ts
442 lines (404 loc) · 19.8 KB
/
Cursor.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
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";
/**
* A cursor which can iterate through the music sheet.
*/
export class Cursor {
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) {
this.wantedZIndex = "-1";
curs.style.zIndex = this.wantedZIndex;
} else {
this.wantedZIndex = "-2";
curs.style.zIndex = this.wantedZIndex;
}
this.cursorElement = <HTMLImageElement>curs;
this.container.appendChild(curs);
}
public adjustToBackgroundColor(): void {
let zIndex: string;
if (!this.rules.PageBackgroundColor) {
zIndex = this.wantedZIndex;
} else {
zIndex = "1";
}
this.cursorElement.style.zIndex = zIndex;
}
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;
/** The desired zIndex (layer) of the cursor when no background color is set.
* When a background color is set, using a negative zIndex would make the cursor invisible.
*/
public wantedZIndex: string;
private openSheetMusicDisplay: OpenSheetMusicDisplay;
private rules: EngravingRules;
private manager: MusicPartManager;
public iterator: MusicPartManagerIterator;
private graphic: GraphicalMusicSheet;
public hidden: boolean = true;
public currentPageNumber: number = 1;
private cursorOptions: CursorOptions;
private cursorOptionsRendered: CursorOptions;
private skipInvisibleNotes: boolean = true;
/** 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 = true;
this.hide();
}
/**
* 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();
this.adjustToBackgroundColor();
}
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);
if (this.openSheetMusicDisplay.Sheet && this.openSheetMusicDisplay.Sheet.SourceMeasures.length > startMeasureIndex) {
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();
// remember SkipInvisibleNotes setting, which otherwise gets reset to default value
this.iterator.SkipInvisibleNotes = this.skipInvisibleNotes;
}
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 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;
const iterator: MusicPartManagerIterator = 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.
let voiceEntries: VoiceEntry[] = iterator.CurrentVisibleVoiceEntries();
let currentMeasureIndex: number = iterator.CurrentMeasureIndex;
let x: number = 0, y: number = 0, height: number = 0;
let musicSystem: MusicSystem;
if (voiceEntries.length === 0 && !iterator.FrontReached && !iterator.EndReached) {
// 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
iterator.moveToPrevious();
voiceEntries = iterator.CurrentVisibleVoiceEntries();
iterator.moveToNext();
// after this, the else condition below should trigger, positioning the cursor at the left-most note. See #1312
}
if (iterator.FrontReached && voiceEntries.length === 0) {
// show beginning of first measure (of stafflines, to create a visual difference to the first note position)
// this position is technically before the sheet/first note - e.g. cursor.Iterator.CurrentTimestamp.RealValue = -1
iterator.moveToNext();
voiceEntries = iterator.CurrentVisibleVoiceEntries();
const firstVisibleMeasure: GraphicalMeasure = this.findVisibleGraphicalMeasure(iterator.CurrentMeasureIndex);
x = firstVisibleMeasure.PositionAndShape.AbsolutePosition.x;
musicSystem = firstVisibleMeasure.ParentMusicSystem;
iterator.moveToPrevious();
} else if (iterator.EndReached || !iterator.CurrentVoiceEntries || voiceEntries.length === 0) {
// show end of last measure (of stafflines, to create a visual difference to the first note position)
// this position is technically after the sheet/last note - e.g. cursor.Iterator.CurrentTimestamp.RealValue = 99999
iterator.moveToPrevious();
voiceEntries = iterator.CurrentVisibleVoiceEntries();
currentMeasureIndex = iterator.CurrentMeasureIndex;
const lastVisibleMeasure: GraphicalMeasure = this.findVisibleGraphicalMeasure(iterator.CurrentMeasureIndex);
x = lastVisibleMeasure.PositionAndShape.AbsolutePosition.x + lastVisibleMeasure.PositionAndShape.Size.width;
musicSystem = lastVisibleMeasure.ParentMusicSystem;
iterator.moveToNext();
} else if (iterator.CurrentMeasure.isReducedToMultiRest) {
// multiple measure rests aren't used when one
const multiRestGMeasure: GraphicalMeasure = this.findVisibleGraphicalMeasure(iterator.CurrentMeasureIndex);
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
// note: StaffLines[0] is guaranteed to exist in this.findVisibleGraphicalMeasure
y = musicSystem.PositionAndShape.AbsolutePosition.y + musicSystem.StaffLines[0].PositionAndShape.RelativePosition.y;
let endY: number = musicSystem.PositionAndShape.AbsolutePosition.y;
const bottomStaffline: StaffLine = musicSystem.StaffLines[musicSystem.StaffLines.length - 1];
if (bottomStaffline) { // can be undefined if drawFromMeasureNumber changed after cursor was shown
endY += bottomStaffline.PositionAndShape.RelativePosition.y + bottomStaffline.StaffHeight;
}
height = endY - y;
// Update the graphical cursor
const visibleMeasure: GraphicalMeasure = this.findVisibleGraphicalMeasure(currentMeasureIndex);
if (!visibleMeasure) {
return;
}
const measurePositionAndShape: BoundingBox = visibleMeasure.PositionAndShape;
this.updateWidthAndStyle(measurePositionAndShape, x, y, height);
if (this.openSheetMusicDisplay.FollowCursor && this.cursorOptions.follow) {
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 = "";
}
private findVisibleGraphicalMeasure(measureIndex: number): GraphicalMeasure {
for (let i: number = 0; i < this.graphic.NumberOfStaves; i++) {
const measure: GraphicalMeasure = this.graphic.findGraphicalMeasure(this.iterator.CurrentMeasureIndex, i);
if (measure?.ParentStaff.ParentInstrument.Visible) {
return measure;
}
}
}
public updateWidthAndStyle(measurePositionAndShape: BoundingBox, x: number, y: number, height: number): void {
const cursorElement: HTMLImageElement = this.cursorElement;
let newWidth: number = 0;
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";
cursorElement.height = (height * 10.0 * this.openSheetMusicDisplay.zoom);
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";
cursorElement.height = (1.5 * 10.0 * this.openSheetMusicDisplay.zoom);
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";
cursorElement.height = (height * 10.0 * this.openSheetMusicDisplay.zoom);
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";
cursorElement.height = (height * 10.0 * this.openSheetMusicDisplay.zoom);
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";
cursorElement.height = (height * 10.0 * this.openSheetMusicDisplay.zoom);
newWidth = 3 * 10.0 * this.openSheetMusicDisplay.zoom;
break;
}
// if (newWidth !== cursorElement.width) { // this `if` is unnecessary and prevents updating color
cursorElement.width = newWidth;
if (this.cursorOptionsRendered !== this.cursorOptions) {
this.updateStyle(newWidth, this.cursorOptions);
// only update style (creating new cursor element) if options changed.
// For width, it seems to be enough to update cursorElement.width, see osmd#1519
}
}
/**
* 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 previous entry
*/
public previous(): void {
this.iterator.moveToPreviousVisibleVoiceEntry(false);
this.update();
}
/**
* 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();
this.update();
}
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);
this.cursorOptionsRendered = {...this.cursorOptions}; // clone, otherwise !== doesn't work
// 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 {
let timestamp: Fraction = this.iterator.currentTimeStamp;
if (timestamp.RealValue < 0) {
timestamp = new Fraction(0, 0);
}
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 get SkipInvisibleNotes(): boolean {
return this.skipInvisibleNotes;
}
public set SkipInvisibleNotes(value: boolean) {
this.skipInvisibleNotes = value;
this.iterator.SkipInvisibleNotes = value;
}
public get CursorOptions(): CursorOptions {
return this.cursorOptions;
}
public set CursorOptions(value: CursorOptions) {
this.cursorOptions = value;
}
}