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 , openSheetMusicDisplay , cursorOptions ) { this . container = container ; this . openSheetMusicDisplay = openSheetMusicDisplay ; this . rules = this . openSheetMusicDisplay . EngravingRules ; this . cursorOptions = cursorOptions ; let id = 0 ; this . cursorElementId = "cursorImg-0" ; while ( document . getElementById ( this . cursorElementId ) ) { id ++ ; this . cursorElementId = `cursorImg-${ id }` ; } const curs = 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 ( ) { let zIndex ; 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 ; /** Initialize the cursor. Necessary before using functions like show() and next(). */ public init ( manager , graphic ) { this . manager = manager ; this . graphic = graphic ; this . reset ( ) ; this . hidden = true ; this . hide ( ) ; } /**
     * Make the cursor visible
     */ public show ( ) { this . hidden = false ; this . update ( ) ; this . adjustToBackgroundColor ( ) ; } public resetIterator ( ) { if ( ! this . openSheetMusicDisplay . Sheet || ! this . openSheetMusicDisplay . Sheet . SourceMeasures ) { console . log ( "OSMD.Cursor.resetIterator(): sheet or measures were null/undefined." ) ; return ; } const lastSheetMeasureIndex = this . openSheetMusicDisplay . Sheet . SourceMeasures . length - 1 ; let startMeasureIndex = this . rules . MinMeasureToDrawIndex ; startMeasureIndex = Math . min ( startMeasureIndex , lastSheetMeasureIndex ) ; let endMeasureIndex = 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 = this . openSheetMusicDisplay . Sheet . SourceMeasures [ endMeasureIndex ] ; this . openSheetMusicDisplay . Sheet . SelectionEnd = Fraction . plus ( lastMeasure . AbsoluteTimestamp , lastMeasure . Duration ) ; } this . iterator = this . manager . getIterator ( ) ; } private getStaffEntryFromVoiceEntry ( voiceEntry ) { const measureIndex = voiceEntry . ParentSourceStaffEntry . VerticalContainerParent . ParentMeasure . measureListIndex ; const staffIndex = voiceEntry . ParentSourceStaffEntry . ParentStaff . idInMusicSheet ; return < VexFlowStaffEntry > this . graphic . findGraphicalStaffEntryFromMeasureList ( staffIndex , measureIndex , voiceEntry . ParentSourceStaffEntry ) ; } public update ( ) { if ( this . hidden || this . hidden === undefined || this . hidden === null ) { return ; } this . updateCurrentPage ( ) ; const iterator = this . iterator ; const voiceEntries = iterator . CurrentVisibleVoiceEntries ( ) ; if ( iterator . EndReached || ! iterator . CurrentVoiceEntries || voiceEntries . length === 0 ) { return ; } let x = 0 , y = 0 , height = 0 ; let musicSystem ; if ( iterator . CurrentMeasure . isReducedToMultiRest ) { const multiRestGMeasure = this . graphic . findGraphicalMeasure ( iterator . CurrentMeasureIndex , 0 ) ; const totalRestMeasures = multiRestGMeasure . parentSourceMeasure . multipleRestMeasures ; const currentRestMeasureNumber = iterator . CurrentMeasure . multipleRestMeasureNumber ; const progressRatio = currentRestMeasureNumber / ( totalRestMeasures + 1 ) ; const effectiveWidth = multiRestGMeasure . PositionAndShape . Size . width - ( multiRestGMeasure as VexFlowMeasure ) . beginInstructionsWidth ; x = multiRestGMeasure . PositionAndShape . AbsolutePosition . x + ( multiRestGMeasure as VexFlowMeasure ) . beginInstructionsWidth + progressRatio * effectiveWidth ; musicSystem = multiRestGMeasure . ParentMusicSystem ; } else { const gseArr = voiceEntries . map ( ve => this . getStaffEntryFromVoiceEntry ( ve ) ) ; const gse = gseArr . sort ( ( a , b ) => a ?. PositionAndShape ?. AbsolutePosition ?. x <= b ?. PositionAndShape ?. AbsolutePosition ?. x ? - 1 : 1 ) [ 0 ] ; x = gse . PositionAndShape . AbsolutePosition . x ; musicSystem = gse . parentMeasure . ParentMusicSystem ; } if ( ! musicSystem ) { return ; } y = musicSystem . PositionAndShape . AbsolutePosition . y + musicSystem . StaffLines [ 0 ] . PositionAndShape . RelativePosition . y ; const bottomStaffline = musicSystem . StaffLines [ musicSystem . StaffLines . length - 1 ] ; const endY = musicSystem . PositionAndShape . AbsolutePosition . y + bottomStaffline . PositionAndShape . RelativePosition . y + bottomStaffline . StaffHeight ; height = endY - y ; const measurePositionAndShape = this . graphic . findGraphicalMeasure ( iterator . CurrentMeasureIndex , 0 ) . PositionAndShape ; this . updateWidthAndStyle ( measurePositionAndShape , x , y , height ) ; if ( this . openSheetMusicDisplay . FollowCursor && this . cursorOptions . follow ) { if ( ! this . openSheetMusicDisplay . EngravingRules . RenderSingleHorizontalStaffline ) { const diff = this . cursorElement . getBoundingClientRect ( ) . top ; this . cursorElement . scrollIntoView ( { behavior : diff < 1000 ? "smooth" : "auto" , block : "center" } ) ; } else { this . cursorElement . scrollIntoView ( { behavior : "smooth" , inline : "center" } ) ; } } this . cursorElement . style . display = "" ; } public updateWidthAndStyle ( measurePositionAndShape , x , y , height ) { const cursorElement = this . cursorElement ; let newWidth = 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 ) { cursorElement . width = newWidth ; this . updateStyle ( newWidth , this . cursorOptions ) ; } } /**
     * Hide the cursor
     */ public hide ( ) { this . cursorElement . style . display = "none" ; this . hidden = true ; } /**
     * Go to next entry
     */ public next ( ) { this . iterator . moveToNextVisibleVoiceEntry ( false ) ; this . update ( ) ; } /**
     * reset cursor to start
     */ public reset ( ) { this . resetIterator ( ) ; this . update ( ) ; } private updateStyle ( width , cursorOptions = undefined ) { if ( cursorOptions !== undefined ) { this . cursorOptions = cursorOptions ; } const c = document . createElement ( "canvas" ) ; c . width = this . cursorElement . width ; c . height = 1 ; const ctx = c . getContext ( "2d" ) ; ctx . globalAlpha = this . cursorOptions . alpha ; const gradient = 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" ) ; gradient . addColorStop ( 0.2 , this . cursorOptions . color ) ; gradient . addColorStop ( 0.8 , this . cursorOptions . color ) ; gradient . addColorStop ( 1 , "white" ) ; break ; } ctx . fillStyle = gradient ; ctx . fillRect ( 0 , 0 , width , 1 ) ; 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 ? ) { return this . iterator . CurrentVisibleVoiceEntries ( instrument ) ; } public NotesUnderCursor ( instrument ? ) { const voiceEntries = this . VoicesUnderCursor ( instrument ) ; const notes = [ ] ; voiceEntries . forEach ( voiceEntry => { notes . push . apply ( notes , voiceEntry . Notes ) ; } ) ; return notes ; } public GNotesUnderCursor ( instrument ? ) { const voiceEntries = this . VoicesUnderCursor ( instrument ) ; const notes = [ ] ; 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 ( ) { const timestamp = this . iterator . currentTimeStamp ; for ( const page of this . graphic . MusicPages ) { const lastSystemTimestamp = page . MusicSystems . last ( ) . GetSystemsLastTimeStamp ( ) ; if ( lastSystemTimestamp . gt ( timestamp ) ) { const newPageNumber = page . PageNumber ; if ( newPageNumber !== this . currentPageNumber ) { this . container . removeChild ( this . cursorElement ) ; this . container = document . getElementById ( "osmdCanvasPage" + newPageNumber ) ; this . container . appendChild ( this . cursorElement ) ; } return this . currentPageNumber = newPageNumber ; } } return 1 ; } }