import Vex, { IRenderContext } from "vexflow"; import VF = Vex.Flow; import { LabelRenderSpecs, MusicSheetDrawer } from "../MusicSheetDrawer"; import { RectangleF2D } from "../../../Common/DataObjects/RectangleF2D"; import { VexFlowMeasure } from "./VexFlowMeasure"; import { PointF2D } from "../../../Common/DataObjects/PointF2D"; import { GraphicalLabel } from "../GraphicalLabel"; import { VexFlowTextMeasurer } from "./VexFlowTextMeasurer"; import { MusicSystem } from "../MusicSystem"; import { GraphicalObject } from "../GraphicalObject"; import { GraphicalLayers } from "../DrawingEnums"; import { GraphicalStaffEntry } from "../GraphicalStaffEntry"; import { VexFlowBackend } from "./VexFlowBackend"; import { VexFlowOctaveShift } from "./VexFlowOctaveShift"; import { VexFlowInstantaneousDynamicExpression } from "./VexFlowInstantaneousDynamicExpression"; import { VexFlowInstrumentBracket } from "./VexFlowInstrumentBracket"; import { VexFlowInstrumentBrace } from "./VexFlowInstrumentBrace"; import { GraphicalLyricEntry } from "../GraphicalLyricEntry"; import { VexFlowStaffLine } from "./VexFlowStaffLine"; import { StaffLine } from "../StaffLine"; import { GraphicalSlur } from "../GraphicalSlur"; import { PlacementEnum } from "../../VoiceData/Expressions/AbstractExpression"; import { GraphicalInstantaneousTempoExpression } from "../GraphicalInstantaneousTempoExpression"; import { GraphicalInstantaneousDynamicExpression } from "../GraphicalInstantaneousDynamicExpression"; import log from "loglevel"; import { GraphicalContinuousDynamicExpression } from "../GraphicalContinuousDynamicExpression"; import { VexFlowContinuousDynamicExpression } from "./VexFlowContinuousDynamicExpression"; import { DrawingParameters } from "../DrawingParameters"; import { GraphicalMusicPage } from "../GraphicalMusicPage"; import { GraphicalMusicSheet } from "../GraphicalMusicSheet"; import { GraphicalUnknownExpression } from "../GraphicalUnknownExpression"; import { VexFlowPedal } from "./VexFlowPedal"; import { GraphicalGlissando } from "../GraphicalGlissando"; import { VexFlowGlissando } from "./VexFlowGlissando"; import { VexFlowGraphicalNote } from "./VexFlowGraphicalNote"; import { SvgVexFlowBackend } from "./SvgVexFlowBackend"; import { VexflowVibratoBracket } from "./VexflowVibratoBracket"; /** * This is a global constant which denotes the height in pixels of the space between two lines of the stave * (when zoom = 1.0) * @type number */ export const unitInPixels: number = 10; export class VexFlowMusicSheetDrawer extends MusicSheetDrawer { private backend: VexFlowBackend; private backends: VexFlowBackend[] = []; private zoom: number = 1.0; public get Zoom(): number { return this.zoom; } private pageIdx: number = 0; // this is a bad solution, should use MusicPage.PageNumber instead. constructor(drawingParameters: DrawingParameters = new DrawingParameters()) { super(new VexFlowTextMeasurer(drawingParameters.Rules), drawingParameters); } public get Backends(): VexFlowBackend[] { return this.backends; } protected initializeBackendForPage(page: GraphicalMusicPage): void { this.backend = this.backends[page.PageNumber - 1]; } public drawSheet(graphicalMusicSheet: GraphicalMusicSheet): void { // vexflow 3.x: change default font if (this.rules.DefaultVexFlowNoteFont === "gonville") { (Vex.Flow as any).DEFAULT_FONT_STACK = [(Vex.Flow as any).Fonts?.Gonville, (Vex.Flow as any).Fonts?.Bravura, (Vex.Flow as any).Fonts?.Custom]; } // else keep new vexflow default Bravura (more cursive, bold). // sizing defaults in Vexflow (Vex.Flow as any).STAVE_LINE_THICKNESS = this.rules.StaffLineWidth * unitInPixels; (Vex.Flow as any).STEM_WIDTH = this.rules.StemWidth * unitInPixels; // sets scale/size of notes/rest notes: (Vex.Flow as any).DEFAULT_NOTATION_FONT_SCALE = this.rules.VexFlowDefaultNotationFontScale; // default 39 (Vex.Flow as any).DEFAULT_TAB_FONT_SCALE = this.rules.VexFlowDefaultTabFontScale; // default 39 // TODO doesn't seem to do anything this.pageIdx = 0; for (const graphicalMusicPage of graphicalMusicSheet.MusicPages) { if (graphicalMusicPage.PageNumber > this.rules.MaxPageToDrawNumber) { break; } const backend: VexFlowBackend = this.backends[this.pageIdx]; backend.graphicalMusicPage = graphicalMusicPage; backend.scale(this.zoom); //backend.resize(graphicalMusicSheet.ParentMusicSheet.pageWidth * unitInPixels * this.zoom, // EngravingRules.Rules.PageHeight * unitInPixels * this.zoom); this.pageIdx += 1; } this.pageIdx = 0; this.backend = this.backends[0]; super.drawSheet(graphicalMusicSheet); } protected drawPage(page: GraphicalMusicPage): void { if (!page) { return; } this.backend = this.backends[page.PageNumber - 1]; // TODO we may need to set this in a couple of other places. this.pageIdx is a bad solution super.drawPage(page); this.pageIdx += 1; } public clear(): void { for (const backend of this.backends) { backend.clear(); } } public setZoom(zoom: number): void { this.zoom = zoom; } /** * Converts a distance from unit to pixel space. * @param unitDistance the distance in units * @returns {number} the distance in pixels */ public calculatePixelDistance(unitDistance: number): number { return unitDistance * unitInPixels; } protected drawStaffLine(staffLine: StaffLine): void { const stafflineNode: Node = this.backend.getContext().openGroup(); if (stafflineNode) { (stafflineNode as SVGGElement).classList.add("staffline"); } super.drawStaffLine(staffLine); const absolutePos: PointF2D = staffLine.PositionAndShape.AbsolutePosition; if (this.rules.RenderSlurs) { this.drawSlurs(staffLine as VexFlowStaffLine, absolutePos); } this.backend.getContext().closeGroup(); if (this.rules.RenderGlissandi) { this.drawGlissandi(staffLine as VexFlowStaffLine, absolutePos); } } private drawSlurs(vfstaffLine: VexFlowStaffLine, absolutePos: PointF2D): void { for (const graphicalSlur of vfstaffLine.GraphicalSlurs) { // don't draw crossed slurs, as their curve calculation is not implemented yet: if (graphicalSlur.slur.isCrossed()) { continue; } this.drawSlur(graphicalSlur, absolutePos); } } private drawGlissandi(vfStaffLine: VexFlowStaffLine, absolutePos: PointF2D): void { for (const gGliss of vfStaffLine.GraphicalGlissandi) { this.drawGlissando(gGliss, absolutePos); } } private drawGlissando(gGliss: GraphicalGlissando, abs: PointF2D): void { if (!gGliss.StaffLine.ParentStaff.isTab) { gGliss.calculateLine(this.rules); } if (gGliss.Line) { const newStart: PointF2D = new PointF2D(gGliss.Line.Start.x + abs.x, gGliss.Line.Start.y); const newEnd: PointF2D = new PointF2D(gGliss.Line.End.x + abs.x, gGliss.Line.End.y); // note that we do not add abs.y, because GraphicalGlissando.calculateLine() uses AbsolutePosition for y, // because unfortunately RelativePosition seems imprecise. this.drawLine(newStart, newEnd, gGliss.Color, gGliss.Width); } else { const vfTie: VF.StaveTie = (gGliss as VexFlowGlissando).vfTie; if (vfTie) { const context: IRenderContext = this.backend.getContext(); vfTie.setContext(context); vfTie.draw(); } } } private drawSlur(graphicalSlur: GraphicalSlur, abs: PointF2D): void { const curvePointsInPixels: PointF2D[] = []; // 1) create inner or original curve: const p1: PointF2D = new PointF2D(graphicalSlur.bezierStartPt.x + abs.x, graphicalSlur.bezierStartPt.y + abs.y); const p2: PointF2D = new PointF2D(graphicalSlur.bezierStartControlPt.x + abs.x, graphicalSlur.bezierStartControlPt.y + abs.y); const p3: PointF2D = new PointF2D(graphicalSlur.bezierEndControlPt.x + abs.x, graphicalSlur.bezierEndControlPt.y + abs.y); const p4: PointF2D = new PointF2D(graphicalSlur.bezierEndPt.x + abs.x, graphicalSlur.bezierEndPt.y + abs.y); // put screen transformed points into array curvePointsInPixels.push(this.applyScreenTransformation(p1)); curvePointsInPixels.push(this.applyScreenTransformation(p2)); curvePointsInPixels.push(this.applyScreenTransformation(p3)); curvePointsInPixels.push(this.applyScreenTransformation(p4)); //DEBUG: Render control points /* for (const point of curvePointsInPixels) { const pointRect: RectangleF2D = new RectangleF2D(point.x - 2, point.y - 2, 4, 4); this.backend.renderRectangle(pointRect, 3, "#000000", 1); }*/ // 2) create second outer curve to create a thickness for the curve: if (graphicalSlur.placement === PlacementEnum.Above) { p1.y -= 0.05; p2.y -= 0.3; p3.y -= 0.3; p4.y -= 0.05; } else { p1.y += 0.05; p2.y += 0.3; p3.y += 0.3; p4.y += 0.05; } // put screen transformed points into array curvePointsInPixels.push(this.applyScreenTransformation(p1)); curvePointsInPixels.push(this.applyScreenTransformation(p2)); curvePointsInPixels.push(this.applyScreenTransformation(p3)); curvePointsInPixels.push(this.applyScreenTransformation(p4)); graphicalSlur.SVGElement = this.backend.renderCurve(curvePointsInPixels); } protected drawMeasure(measure: VexFlowMeasure): void { measure.setAbsoluteCoordinates( measure.PositionAndShape.AbsolutePosition.x * unitInPixels, measure.PositionAndShape.AbsolutePosition.y * unitInPixels ); const context: Vex.IRenderContext = this.backend.getContext(); try { measure.draw(context); // Vexflow errors can happen here. If we don't catch errors, rendering will stop after this measure. } catch (ex) { log.warn("VexFlowMusicSheetDrawer.drawMeasure", ex); } let newBuzzRollId: number = 0; // Draw the StaffEntries for (const staffEntry of measure.staffEntries) { this.drawStaffEntry(staffEntry); newBuzzRollId = this.drawBuzzRolls(staffEntry, newBuzzRollId); } } protected drawBuzzRolls(staffEntry: GraphicalStaffEntry, newBuzzRollId): number { for (const gve of staffEntry.graphicalVoiceEntries) { for (const note of gve.notes) { if (note.sourceNote.TremoloInfo?.tremoloUnmeasured) { const thickness: number = this.rules.TremoloBuzzRollThickness; const baseLength: number = 0.9; const baseHeight: number = 0.5; const vfNote: VexFlowGraphicalNote = note as VexFlowGraphicalNote; let stemTip: PointF2D; let stemHeight: number; const directionSign: number = vfNote.vfnote[0].getStemDirection(); // 1 or -1 let stemElement: HTMLElement; if (this.backend instanceof SvgVexFlowBackend) { stemElement = vfNote.getStemSVG(); } const hasBbox: boolean = (stemElement as any)?.getBbox !== undefined; if (hasBbox) { // apparently sometimes the stemElement is null, in that case we need to use the canvas method. const rect: SVGRect = (stemElement as any).getBBox(); stemTip = new PointF2D(rect.x / 10, rect.y / 10); stemHeight = rect.height / 10; } else { // if this.backend instanceof CanvasVexFlowBackend // also seems to work for SVG stemHeight = vfNote.vfnote[0].getStemLength() / 10; stemTip = new PointF2D( (vfNote.vfnote[0].getStem() as any).x_begin / 10, (vfNote.vfnote[0].getStem() as any).y_top / 10, ); if (directionSign === 1) { stemTip.y -= stemHeight; } } // this.DrawOverlayLine(stemTip, new PointF2D(stemTip.x + 5, stemTip.y), vfNote.ParentMusicPage); // debug let startHeight: number = stemTip.y + stemHeight / 3; if (vfNote.vfnote[0].getBeamCount() > 1) { startHeight = stemTip.y + (stemHeight / 2); if (directionSign === -1) { // downwards stem, z paints in downwards direction, so we need to start further up startHeight -= (baseHeight + 0.2); } // note that buzz rolls usually don't appear on notes smaller than 16ths, rather on longer ones } const buzzStartX: number = stemTip.x - 0.5; // top left start point const buzzStartY: number = startHeight; const pathPoints: PointF2D[] = []; // movements to draw the "z" point by point: (drawing by numbers) const movements: PointF2D[] = [ new PointF2D(0, -thickness), // down a bit new PointF2D(baseLength-thickness, 0), // to the right new PointF2D(-baseLength+thickness,-baseHeight), // down left (etc) new PointF2D(0, -thickness), new PointF2D(baseLength, 0), new PointF2D(0, thickness), new PointF2D(-baseLength+thickness, 0), new PointF2D(baseLength-thickness, baseHeight), new PointF2D(0, thickness), new PointF2D(-baseLength, 0) ]; let currentPoint: PointF2D = new PointF2D(buzzStartX, buzzStartY); pathPoints.push(currentPoint); for (const movement of movements) { currentPoint = pathPoints.last(); pathPoints.push(new PointF2D(currentPoint.x + movement.x, currentPoint.y - movement.y)); } this.DrawPath(pathPoints, vfNote.ParentMusicPage, true, `buzzRoll${newBuzzRollId}`); newBuzzRollId++; } } } return newBuzzRollId; } // private drawPixel(coord: PointF2D): void { // coord = this.applyScreenTransformation(coord); // const ctx: any = this.backend.getContext(); // const oldStyle: string = ctx.fillStyle; // ctx.fillStyle = "#00FF00FF"; // ctx.fillRect( coord.x, coord.y, 2, 2 ); // ctx.fillStyle = oldStyle; // } /** Draws a line in the current backend. Only usable while pages are drawn sequentially, because backend reference is updated in that process. * To add your own lines after rendering, use DrawOverlayLine. */ protected drawLine(start: PointF2D, stop: PointF2D, color: string = "#000000FF", lineWidth: number = 0.2): Node { // TODO maybe the backend should be given as an argument here as well, otherwise this can't be used after rendering of multiple pages is done. start = this.applyScreenTransformation(start); stop = this.applyScreenTransformation(stop); /*if (!this.backend) { this.backend = this.backends[0]; }*/ return this.backend.renderLine(start, stop, color, lineWidth * unitInPixels); } /** Lets a user/developer draw an overlay line on the score. Use this instead of drawLine, which is for OSMD internally only. * The MusicPage has to be specified, because each page and Vexflow backend has its own relative coordinates. * (the AbsolutePosition of a GraphicalNote is relative to its backend) * To get a MusicPage, use GraphicalNote.ParentMusicPage. */ public DrawOverlayLine(start: PointF2D, stop: PointF2D, musicPage: GraphicalMusicPage, color: string = "#FF0000FF", lineWidth: number = 0.2, id?: string): Node { if (!musicPage.PageNumber || musicPage.PageNumber > this.backends.length || musicPage.PageNumber < 1) { console.log("VexFlowMusicSheetDrawer.drawOverlayLine: invalid page number / music page number doesn't correspond to an existing backend."); return; } const musicPageIndex: number = musicPage.PageNumber - 1; const backendToUse: VexFlowBackend = this.backends[musicPageIndex]; start = this.applyScreenTransformation(start); stop = this.applyScreenTransformation(stop); if (!id) { id = `overlayLine ${start.x}/${start.y}`; } return backendToUse.renderLine(start, stop, color, lineWidth * unitInPixels, id); } public DrawPath(inputPoints: PointF2D[], musicPage: GraphicalMusicPage, fill: boolean = true, id?: string): Node { const musicPageIndex: number = musicPage.PageNumber - 1; const backendToUse: VexFlowBackend = this.backends[musicPageIndex]; const transformedPoints: PointF2D[] = []; for (const inputPoint of inputPoints) { transformedPoints.push(this.applyScreenTransformation(inputPoint)); } return backendToUse.renderPath(transformedPoints, fill, id); } protected drawSkyLine(staffline: StaffLine): void { const startPosition: PointF2D = staffline.PositionAndShape.AbsolutePosition; const width: number = staffline.PositionAndShape.Size.width; this.drawSampledLine(staffline.SkyLine, startPosition, width); } protected drawBottomLine(staffline: StaffLine): void { const startPosition: PointF2D = new PointF2D(staffline.PositionAndShape.AbsolutePosition.x, staffline.PositionAndShape.AbsolutePosition.y); const width: number = staffline.PositionAndShape.Size.width; this.drawSampledLine(staffline.BottomLine, startPosition, width, "#0000FFFF"); } /** * Draw a line with a width and start point in a chosen color (used for skyline/bottom line debugging) from * a simple array * @param line numeric array. 0 marks the base line. Direction given by sign. Dimensions in units * @param startPosition Start position in units * @param width Max line width in units * @param color Color to paint in. Default is red */ private drawSampledLine(line: number[], startPosition: PointF2D, width: number, color: string = "#FF0000FF"): void { const indices: number[] = []; let currentValue: number = 0; //Loops through bottom line, grabs all indices that don't equal the previously grabbed index //Starting with 0 (gets index of all line changes) for (let i: number = 0; i < line.length; i++) { if (line[i] !== currentValue) { indices.push(i); currentValue = line[i]; } } const absolute: PointF2D = startPosition; if (indices.length > 0) { const samplingUnit: number = this.rules.SamplingUnit; let horizontalStart: PointF2D = new PointF2D(absolute.x, absolute.y); let horizontalEnd: PointF2D = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y); this.drawLine(horizontalStart, horizontalEnd, color); let verticalStart: PointF2D; let verticalEnd: PointF2D; if (line[0] >= 0) { verticalStart = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y); verticalEnd = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y + line[indices[0]]); this.drawLine(verticalStart, verticalEnd, color); } for (let i: number = 1; i < indices.length; i++) { horizontalStart = new PointF2D(indices[i - 1] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]); horizontalEnd = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]); this.drawLine(horizontalStart, horizontalEnd, color); verticalStart = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]); verticalEnd = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i]]); this.drawLine(verticalStart, verticalEnd, color); } if (indices[indices.length - 1] < line.length) { horizontalStart = new PointF2D(indices[indices.length - 1] / samplingUnit + absolute.x, absolute.y + line[indices[indices.length - 1]]); horizontalEnd = new PointF2D(absolute.x + width, absolute.y + line[indices[indices.length - 1]]); this.drawLine(horizontalStart, horizontalEnd, color); } else { horizontalStart = new PointF2D(indices[indices.length - 1] / samplingUnit + absolute.x, absolute.y); horizontalEnd = new PointF2D(absolute.x + width, absolute.y); this.drawLine(horizontalStart, horizontalEnd, color); } } else { // Flat line const start: PointF2D = new PointF2D(absolute.x, absolute.y); const end: PointF2D = new PointF2D(absolute.x + width, absolute.y); this.drawLine(start, end, color); } } private drawStaffEntry(staffEntry: GraphicalStaffEntry): void { if (staffEntry.FingeringEntries.length > 0) { for (const fingeringEntry of staffEntry.FingeringEntries) { fingeringEntry.SVGNode = this.drawLabel(fingeringEntry, GraphicalLayers.Notes); } } // Draw ChordSymbols if (staffEntry.graphicalChordContainers !== undefined && staffEntry.graphicalChordContainers.length > 0) { for (const graphicalChordContainer of staffEntry.graphicalChordContainers) { const label: GraphicalLabel = graphicalChordContainer.GraphicalLabel; label.SVGNode = this.drawLabel(label, GraphicalLayers.Notes); } } if (this.rules.RenderLyrics) { if (staffEntry.LyricsEntries.length > 0) { this.drawLyrics(staffEntry.LyricsEntries, GraphicalLayers.Notes); } } } /** * Draw all lyrics to the canvas * @param lyricEntries Array of lyric entries to be drawn * @param layer Number of the layer that the lyrics should be drawn in */ private drawLyrics(lyricEntries: GraphicalLyricEntry[], layer: number): void { lyricEntries.forEach(lyricsEntry => { const label: GraphicalLabel = lyricsEntry.GraphicalLabel; label.Label.colorDefault = this.rules.DefaultColorLyrics; label.SVGNode = this.drawLabel(label, layer); }); } protected drawInstrumentBrace(brace: GraphicalObject, system: MusicSystem): void { // Draw InstrumentBrackets at beginning of line const vexBrace: VexFlowInstrumentBrace = (brace as VexFlowInstrumentBrace); vexBrace.draw(this.backend.getContext()); } protected drawGroupBracket(bracket: GraphicalObject, system: MusicSystem): void { // Draw InstrumentBrackets at beginning of line const vexBrace: VexFlowInstrumentBracket = (bracket as VexFlowInstrumentBracket); vexBrace.draw(this.backend.getContext()); } protected drawOctaveShifts(staffLine: StaffLine): void { for (const graphicalOctaveShift of staffLine.OctaveShifts) { if (graphicalOctaveShift) { const vexFlowOctaveShift: VexFlowOctaveShift = graphicalOctaveShift as VexFlowOctaveShift; const ctx: Vex.IRenderContext = this.backend.getContext(); const textBracket: VF.TextBracket = vexFlowOctaveShift.getTextBracket(); if (this.rules.DefaultColorMusic) { (textBracket as any).render_options.color = this.rules.DefaultColorMusic; } textBracket.setContext(ctx); try { textBracket.draw(); } catch (ex) { log.warn(ex); } } } } protected drawPedals(staffLine: StaffLine): void { for (const graphicalPedal of staffLine.Pedals) { if (graphicalPedal) { const vexFlowPedal: VexFlowPedal = graphicalPedal as VexFlowPedal; const ctx: Vex.IRenderContext = this.backend.getContext(); const pedalMarking: Vex.Flow.PedalMarking = vexFlowPedal.getPedalMarking(); (pedalMarking as any).render_options.color = this.rules.DefaultColorMusic; pedalMarking.setContext(ctx); pedalMarking.draw(); } } } protected drawWavyLines(staffLine: StaffLine): void { for (const graphicalWavyLine of staffLine.WavyLines) { if (graphicalWavyLine) { const vexFlowVibratoBracket: VexflowVibratoBracket = graphicalWavyLine as VexflowVibratoBracket; const ctx: Vex.IRenderContext = this.backend.getContext(); const vfVibratoBracket: Vex.Flow.VibratoBracket = vexFlowVibratoBracket.getVibratoBracket(); (vfVibratoBracket as any).setContext(ctx); vfVibratoBracket.draw(); } } } protected drawExpressions(staffline: StaffLine): void { // Draw all Expressions for (const abstractGraphicalExpression of staffline.AbstractExpressions) { // Draw InstantaniousDynamics if (abstractGraphicalExpression instanceof GraphicalInstantaneousDynamicExpression) { this.drawInstantaneousDynamic((abstractGraphicalExpression as VexFlowInstantaneousDynamicExpression)); // Draw InstantaniousTempo } else if (abstractGraphicalExpression instanceof GraphicalInstantaneousTempoExpression) { const label: GraphicalLabel = (abstractGraphicalExpression as GraphicalInstantaneousTempoExpression).GraphicalLabel; label.SVGNode = this.drawLabel(label, GraphicalLayers.Notes); // Draw ContinuousDynamics } else if (abstractGraphicalExpression instanceof GraphicalContinuousDynamicExpression) { this.drawContinuousDynamic((abstractGraphicalExpression as VexFlowContinuousDynamicExpression)); // Draw ContinuousTempo // } else if (abstractGraphicalExpression instanceof GraphicalContinuousTempoExpression) { // this.drawLabel((abstractGraphicalExpression as GraphicalContinuousTempoExpression).GraphicalLabel, GraphicalLayers.Notes); // // Draw Mood // } else if (abstractGraphicalExpression instanceof GraphicalMoodExpression) { // GraphicalMoodExpression; graphicalMood = (GraphicalMoodExpression); abstractGraphicalExpression; // drawLabel(graphicalMood.GetGraphicalLabel, GraphicalLayers.Notes); // Draw Unknown } else if (abstractGraphicalExpression instanceof GraphicalUnknownExpression) { const label: GraphicalLabel = abstractGraphicalExpression.Label; label.SVGNode = this.drawLabel(label, GraphicalLayers.Notes); } else { log.warn("Unkown type of expression!"); } } } protected drawInstantaneousDynamic(instantaneousDynamic: GraphicalInstantaneousDynamicExpression): void { const label: GraphicalLabel = (instantaneousDynamic as VexFlowInstantaneousDynamicExpression).Label; label.SVGNode = this.drawLabel(label, GraphicalLayers.Notes); } protected drawContinuousDynamic(graphicalExpression: VexFlowContinuousDynamicExpression): void { if (graphicalExpression.IsVerbal) { const label: GraphicalLabel = graphicalExpression.Label; label.SVGNode = this.drawLabel(label, GraphicalLayers.Notes); } else { for (const line of graphicalExpression.Lines) { const start: PointF2D = new PointF2D(graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.x + line.Start.x, graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.y + line.Start.y); const end: PointF2D = new PointF2D(graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.x + line.End.x, graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.y + line.End.y); this.drawLine(start, end, line.colorHex ?? "#000000", line.Width); // the null check for colorHex is not strictly necessary anymore, but the previous default color was red. } } } /** * Renders a Label to the screen (e.g. Title, composer..) * @param graphicalLabel holds the label string, the text height in units and the font parameters * @param layer is the current rendering layer. There are many layers on top of each other to which can be rendered. Not needed for now. * @param bitmapWidth Not needed for now. * @param bitmapHeight Not needed for now. * @param heightInPixel the height of the text in screen coordinates * @param screenPosition the position of the lower left corner of the text in screen coordinates */ protected renderLabel(graphicalLabel: GraphicalLabel, layer: GraphicalLayers, specs: LabelRenderSpecs): Node { return this._renderLabel(graphicalLabel, specs); } private _renderLabel(graphicalLabel: GraphicalLabel, specs: LabelRenderSpecs): Node { if (!graphicalLabel.Label.print) { return undefined; } const height: number = graphicalLabel.Label.fontHeight * unitInPixels; const { font } = graphicalLabel.Label; let color: string; if (this.rules.ColoringEnabled) { color = graphicalLabel.Label.colorDefault; if (graphicalLabel.ColorXML) { color = graphicalLabel.ColorXML; } if (graphicalLabel.Label.color) { color = graphicalLabel.Label.color.toString(); } if (!color) { color = this.rules.DefaultColorLabel; } } let { fontStyle, fontFamily } = graphicalLabel.Label; if (!fontStyle) { fontStyle = this.rules.DefaultFontStyle; } if (!fontFamily) { fontFamily = this.rules.DefaultFontFamily; } let node: Node; for (let i: number = 0; i < graphicalLabel.TextLines?.length; i++) { const currLine: {text: string, xOffset: number, width: number} = graphicalLabel.TextLines[i]; const xOffsetInPixel: number = this.calculatePixelDistance(currLine.xOffset); const linePosition: PointF2D = new PointF2D(specs.ScreenPosition.x + xOffsetInPixel, specs.ScreenPosition.y); const newNode: Node = this.backend.renderText(height, fontStyle, font, currLine.text, specs.FontHeightInPixel, linePosition, color, graphicalLabel.Label.fontFamily); if (!node) { node = newNode; } else { node.appendChild(newNode); } specs.ScreenPosition.y = specs.ScreenPosition.y + specs.FontHeightInPixel; if (graphicalLabel.TextLines.length > 1) { specs.ScreenPosition.y += this.rules.SpacingBetweenTextLines; } } // font currently unused, replaced by fontFamily return node; // this will be a merge conflict with annotations, refactor there to handle node array instead of single node } /** * Renders a rectangle with the given style to the screen. * It is given in screen coordinates. * @param rectangle the rect in screen coordinates * @param layer is the current rendering layer. There are many layers on top of each other to which can be rendered. Not needed for now. * @param styleId the style id * @param alpha alpha value between 0 and 1 */ protected renderRectangle(rectangle: RectangleF2D, layer: number, styleId: number, colorHex: string, alpha: number): Node { return this.backend.renderRectangle(rectangle, styleId, colorHex, alpha); } /** * Converts a point from unit to pixel space. * @param point * @returns {PointF2D} */ protected applyScreenTransformation(point: PointF2D): PointF2D { return new PointF2D(point.x * unitInPixels, point.y * unitInPixels); } /** * Converts a rectangle from unit to pixel space. * @param rectangle * @returns {RectangleF2D} */ protected applyScreenTransformationForRect(rectangle: RectangleF2D): RectangleF2D { return new RectangleF2D(rectangle.x * unitInPixels, rectangle.y * unitInPixels, rectangle.width * unitInPixels, rectangle.height * unitInPixels); } }