VexFlowMusicSheetDrawer.ts 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. import Vex, { IRenderContext } from "vexflow";
  2. import VF = Vex.Flow;
  3. import { LabelRenderSpecs, MusicSheetDrawer } from "../MusicSheetDrawer";
  4. import { RectangleF2D } from "../../../Common/DataObjects/RectangleF2D";
  5. import { VexFlowMeasure } from "./VexFlowMeasure";
  6. import { PointF2D } from "../../../Common/DataObjects/PointF2D";
  7. import { GraphicalLabel } from "../GraphicalLabel";
  8. import { VexFlowTextMeasurer } from "./VexFlowTextMeasurer";
  9. import { MusicSystem } from "../MusicSystem";
  10. import { GraphicalObject } from "../GraphicalObject";
  11. import { GraphicalLayers } from "../DrawingEnums";
  12. import { GraphicalStaffEntry } from "../GraphicalStaffEntry";
  13. import { VexFlowBackend } from "./VexFlowBackend";
  14. import { VexFlowOctaveShift } from "./VexFlowOctaveShift";
  15. import { VexFlowInstantaneousDynamicExpression } from "./VexFlowInstantaneousDynamicExpression";
  16. import { VexFlowInstrumentBracket } from "./VexFlowInstrumentBracket";
  17. import { VexFlowInstrumentBrace } from "./VexFlowInstrumentBrace";
  18. import { GraphicalLyricEntry } from "../GraphicalLyricEntry";
  19. import { VexFlowStaffLine } from "./VexFlowStaffLine";
  20. import { StaffLine } from "../StaffLine";
  21. import { GraphicalSlur } from "../GraphicalSlur";
  22. import { PlacementEnum } from "../../VoiceData/Expressions/AbstractExpression";
  23. import { GraphicalInstantaneousTempoExpression } from "../GraphicalInstantaneousTempoExpression";
  24. import { GraphicalInstantaneousDynamicExpression } from "../GraphicalInstantaneousDynamicExpression";
  25. import log from "loglevel";
  26. import { GraphicalContinuousDynamicExpression } from "../GraphicalContinuousDynamicExpression";
  27. import { VexFlowContinuousDynamicExpression } from "./VexFlowContinuousDynamicExpression";
  28. import { DrawingParameters } from "../DrawingParameters";
  29. import { GraphicalMusicPage } from "../GraphicalMusicPage";
  30. import { GraphicalMusicSheet } from "../GraphicalMusicSheet";
  31. import { GraphicalUnknownExpression } from "../GraphicalUnknownExpression";
  32. import { VexFlowPedal } from "./VexFlowPedal";
  33. import { GraphicalGlissando } from "../GraphicalGlissando";
  34. import { VexFlowGlissando } from "./VexFlowGlissando";
  35. import { VexFlowGraphicalNote } from "./VexFlowGraphicalNote";
  36. import { SvgVexFlowBackend } from "./SvgVexFlowBackend";
  37. import { VexflowVibratoBracket } from "./VexflowVibratoBracket";
  38. /**
  39. * This is a global constant which denotes the height in pixels of the space between two lines of the stave
  40. * (when zoom = 1.0)
  41. * @type number
  42. */
  43. export const unitInPixels: number = 10;
  44. export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
  45. private backend: VexFlowBackend;
  46. private backends: VexFlowBackend[] = [];
  47. private zoom: number = 1.0;
  48. public get Zoom(): number {
  49. return this.zoom;
  50. }
  51. private pageIdx: number = 0; // this is a bad solution, should use MusicPage.PageNumber instead.
  52. constructor(drawingParameters: DrawingParameters = new DrawingParameters()) {
  53. super(new VexFlowTextMeasurer(drawingParameters.Rules), drawingParameters);
  54. }
  55. public get Backends(): VexFlowBackend[] {
  56. return this.backends;
  57. }
  58. protected initializeBackendForPage(page: GraphicalMusicPage): void {
  59. this.backend = this.backends[page.PageNumber - 1];
  60. }
  61. public drawSheet(graphicalMusicSheet: GraphicalMusicSheet): void {
  62. // vexflow 3.x: change default font
  63. if (this.rules.DefaultVexFlowNoteFont === "gonville") {
  64. (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];
  65. } // else keep new vexflow default Bravura (more cursive, bold).
  66. // sizing defaults in Vexflow
  67. (Vex.Flow as any).STAVE_LINE_THICKNESS = this.rules.StaffLineWidth * unitInPixels;
  68. (Vex.Flow as any).STEM_WIDTH = this.rules.StemWidth * unitInPixels;
  69. // sets scale/size of notes/rest notes:
  70. (Vex.Flow as any).DEFAULT_NOTATION_FONT_SCALE = this.rules.VexFlowDefaultNotationFontScale; // default 39
  71. (Vex.Flow as any).DEFAULT_TAB_FONT_SCALE = this.rules.VexFlowDefaultTabFontScale; // default 39 // TODO doesn't seem to do anything
  72. this.pageIdx = 0;
  73. for (const graphicalMusicPage of graphicalMusicSheet.MusicPages) {
  74. if (graphicalMusicPage.PageNumber > this.rules.MaxPageToDrawNumber) {
  75. break;
  76. }
  77. const backend: VexFlowBackend = this.backends[this.pageIdx];
  78. backend.graphicalMusicPage = graphicalMusicPage;
  79. backend.scale(this.zoom);
  80. //backend.resize(graphicalMusicSheet.ParentMusicSheet.pageWidth * unitInPixels * this.zoom,
  81. // EngravingRules.Rules.PageHeight * unitInPixels * this.zoom);
  82. this.pageIdx += 1;
  83. }
  84. this.pageIdx = 0;
  85. this.backend = this.backends[0];
  86. super.drawSheet(graphicalMusicSheet);
  87. }
  88. protected drawPage(page: GraphicalMusicPage): void {
  89. if (!page) {
  90. return;
  91. }
  92. 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
  93. super.drawPage(page);
  94. this.pageIdx += 1;
  95. }
  96. public clear(): void {
  97. for (const backend of this.backends) {
  98. backend.clear();
  99. }
  100. }
  101. public setZoom(zoom: number): void {
  102. this.zoom = zoom;
  103. }
  104. /**
  105. * Converts a distance from unit to pixel space.
  106. * @param unitDistance the distance in units
  107. * @returns {number} the distance in pixels
  108. */
  109. public calculatePixelDistance(unitDistance: number): number {
  110. return unitDistance * unitInPixels;
  111. }
  112. protected drawStaffLine(staffLine: StaffLine): void {
  113. const stafflineNode: Node = this.backend.getContext().openGroup();
  114. if (stafflineNode) {
  115. (stafflineNode as SVGGElement).classList.add("staffline");
  116. }
  117. super.drawStaffLine(staffLine);
  118. const absolutePos: PointF2D = staffLine.PositionAndShape.AbsolutePosition;
  119. if (this.rules.RenderSlurs) {
  120. this.drawSlurs(staffLine as VexFlowStaffLine, absolutePos);
  121. }
  122. this.backend.getContext().closeGroup();
  123. if (this.rules.RenderGlissandi) {
  124. this.drawGlissandi(staffLine as VexFlowStaffLine, absolutePos);
  125. }
  126. }
  127. private drawSlurs(vfstaffLine: VexFlowStaffLine, absolutePos: PointF2D): void {
  128. for (const graphicalSlur of vfstaffLine.GraphicalSlurs) {
  129. // don't draw crossed slurs, as their curve calculation is not implemented yet:
  130. if (graphicalSlur.slur.isCrossed()) {
  131. continue;
  132. }
  133. this.drawSlur(graphicalSlur, absolutePos);
  134. }
  135. }
  136. private drawGlissandi(vfStaffLine: VexFlowStaffLine, absolutePos: PointF2D): void {
  137. for (const gGliss of vfStaffLine.GraphicalGlissandi) {
  138. this.drawGlissando(gGliss, absolutePos);
  139. }
  140. }
  141. private drawGlissando(gGliss: GraphicalGlissando, abs: PointF2D): void {
  142. if (!gGliss.StaffLine.ParentStaff.isTab) {
  143. gGliss.calculateLine(this.rules);
  144. }
  145. if (gGliss.Line) {
  146. const newStart: PointF2D = new PointF2D(gGliss.Line.Start.x + abs.x, gGliss.Line.Start.y);
  147. const newEnd: PointF2D = new PointF2D(gGliss.Line.End.x + abs.x, gGliss.Line.End.y);
  148. // note that we do not add abs.y, because GraphicalGlissando.calculateLine() uses AbsolutePosition for y,
  149. // because unfortunately RelativePosition seems imprecise.
  150. this.drawLine(newStart, newEnd, gGliss.Color, gGliss.Width);
  151. } else {
  152. const vfTie: VF.StaveTie = (gGliss as VexFlowGlissando).vfTie;
  153. if (vfTie) {
  154. const context: IRenderContext = this.backend.getContext();
  155. vfTie.setContext(context);
  156. vfTie.draw();
  157. }
  158. }
  159. }
  160. private drawSlur(graphicalSlur: GraphicalSlur, abs: PointF2D): void {
  161. const curvePointsInPixels: PointF2D[] = [];
  162. // 1) create inner or original curve:
  163. const p1: PointF2D = new PointF2D(graphicalSlur.bezierStartPt.x + abs.x, graphicalSlur.bezierStartPt.y + abs.y);
  164. const p2: PointF2D = new PointF2D(graphicalSlur.bezierStartControlPt.x + abs.x, graphicalSlur.bezierStartControlPt.y + abs.y);
  165. const p3: PointF2D = new PointF2D(graphicalSlur.bezierEndControlPt.x + abs.x, graphicalSlur.bezierEndControlPt.y + abs.y);
  166. const p4: PointF2D = new PointF2D(graphicalSlur.bezierEndPt.x + abs.x, graphicalSlur.bezierEndPt.y + abs.y);
  167. // put screen transformed points into array
  168. curvePointsInPixels.push(this.applyScreenTransformation(p1));
  169. curvePointsInPixels.push(this.applyScreenTransformation(p2));
  170. curvePointsInPixels.push(this.applyScreenTransformation(p3));
  171. curvePointsInPixels.push(this.applyScreenTransformation(p4));
  172. //DEBUG: Render control points
  173. /*
  174. for (const point of curvePointsInPixels) {
  175. const pointRect: RectangleF2D = new RectangleF2D(point.x - 2, point.y - 2, 4, 4);
  176. this.backend.renderRectangle(pointRect, 3, "#000000", 1);
  177. }*/
  178. // 2) create second outer curve to create a thickness for the curve:
  179. if (graphicalSlur.placement === PlacementEnum.Above) {
  180. p1.y -= 0.05;
  181. p2.y -= 0.3;
  182. p3.y -= 0.3;
  183. p4.y -= 0.05;
  184. } else {
  185. p1.y += 0.05;
  186. p2.y += 0.3;
  187. p3.y += 0.3;
  188. p4.y += 0.05;
  189. }
  190. // put screen transformed points into array
  191. curvePointsInPixels.push(this.applyScreenTransformation(p1));
  192. curvePointsInPixels.push(this.applyScreenTransformation(p2));
  193. curvePointsInPixels.push(this.applyScreenTransformation(p3));
  194. curvePointsInPixels.push(this.applyScreenTransformation(p4));
  195. graphicalSlur.SVGElement = this.backend.renderCurve(curvePointsInPixels);
  196. }
  197. protected drawMeasure(measure: VexFlowMeasure): void {
  198. measure.setAbsoluteCoordinates(
  199. measure.PositionAndShape.AbsolutePosition.x * unitInPixels,
  200. measure.PositionAndShape.AbsolutePosition.y * unitInPixels
  201. );
  202. const context: Vex.IRenderContext = this.backend.getContext();
  203. try {
  204. measure.draw(context);
  205. // Vexflow errors can happen here. If we don't catch errors, rendering will stop after this measure.
  206. } catch (ex) {
  207. log.warn("VexFlowMusicSheetDrawer.drawMeasure", ex);
  208. }
  209. let newBuzzRollId: number = 0;
  210. // Draw the StaffEntries
  211. for (const staffEntry of measure.staffEntries) {
  212. this.drawStaffEntry(staffEntry);
  213. newBuzzRollId = this.drawBuzzRolls(staffEntry, newBuzzRollId);
  214. }
  215. }
  216. protected drawBuzzRolls(staffEntry: GraphicalStaffEntry, newBuzzRollId): number {
  217. for (const gve of staffEntry.graphicalVoiceEntries) {
  218. for (const note of gve.notes) {
  219. if (note.sourceNote.TremoloInfo?.tremoloUnmeasured) {
  220. const thickness: number = this.rules.TremoloBuzzRollThickness;
  221. const baseLength: number = 0.9;
  222. const baseHeight: number = 0.5;
  223. const vfNote: VexFlowGraphicalNote = note as VexFlowGraphicalNote;
  224. let stemTip: PointF2D;
  225. let stemHeight: number;
  226. const directionSign: number = vfNote.vfnote[0].getStemDirection(); // 1 or -1
  227. let stemElement: HTMLElement;
  228. if (this.backend instanceof SvgVexFlowBackend) {
  229. stemElement = vfNote.getStemSVG();
  230. }
  231. const hasBbox: boolean = (stemElement as any)?.getBbox !== undefined;
  232. if (hasBbox) {
  233. // apparently sometimes the stemElement is null, in that case we need to use the canvas method.
  234. const rect: SVGRect = (stemElement as any).getBBox();
  235. stemTip = new PointF2D(rect.x / 10, rect.y / 10);
  236. stemHeight = rect.height / 10;
  237. } else { // if this.backend instanceof CanvasVexFlowBackend // also seems to work for SVG
  238. stemHeight = vfNote.vfnote[0].getStemLength() / 10;
  239. stemTip = new PointF2D(
  240. (vfNote.vfnote[0].getStem() as any).x_begin / 10,
  241. (vfNote.vfnote[0].getStem() as any).y_top / 10,
  242. );
  243. if (directionSign === 1) {
  244. stemTip.y -= stemHeight;
  245. }
  246. }
  247. // this.DrawOverlayLine(stemTip, new PointF2D(stemTip.x + 5, stemTip.y), vfNote.ParentMusicPage); // debug
  248. let startHeight: number = stemTip.y + stemHeight / 3;
  249. if (vfNote.vfnote[0].getBeamCount() > 1) {
  250. startHeight = stemTip.y + (stemHeight / 2);
  251. if (directionSign === -1) {
  252. // downwards stem, z paints in downwards direction, so we need to start further up
  253. startHeight -= (baseHeight + 0.2);
  254. }
  255. // note that buzz rolls usually don't appear on notes smaller than 16ths, rather on longer ones
  256. }
  257. const buzzStartX: number = stemTip.x - 0.5; // top left start point
  258. const buzzStartY: number = startHeight;
  259. const pathPoints: PointF2D[] = [];
  260. // movements to draw the "z" point by point: (drawing by numbers)
  261. const movements: PointF2D[] = [
  262. new PointF2D(0, -thickness), // down a bit
  263. new PointF2D(baseLength-thickness, 0), // to the right
  264. new PointF2D(-baseLength+thickness,-baseHeight), // down left (etc)
  265. new PointF2D(0, -thickness),
  266. new PointF2D(baseLength, 0),
  267. new PointF2D(0, thickness),
  268. new PointF2D(-baseLength+thickness, 0),
  269. new PointF2D(baseLength-thickness, baseHeight),
  270. new PointF2D(0, thickness),
  271. new PointF2D(-baseLength, 0)
  272. ];
  273. let currentPoint: PointF2D = new PointF2D(buzzStartX, buzzStartY);
  274. pathPoints.push(currentPoint);
  275. for (const movement of movements) {
  276. currentPoint = pathPoints.last();
  277. pathPoints.push(new PointF2D(currentPoint.x + movement.x, currentPoint.y - movement.y));
  278. }
  279. this.DrawPath(pathPoints, vfNote.ParentMusicPage, true, `buzzRoll${newBuzzRollId}`);
  280. newBuzzRollId++;
  281. }
  282. }
  283. }
  284. return newBuzzRollId;
  285. }
  286. // private drawPixel(coord: PointF2D): void {
  287. // coord = this.applyScreenTransformation(coord);
  288. // const ctx: any = this.backend.getContext();
  289. // const oldStyle: string = ctx.fillStyle;
  290. // ctx.fillStyle = "#00FF00FF";
  291. // ctx.fillRect( coord.x, coord.y, 2, 2 );
  292. // ctx.fillStyle = oldStyle;
  293. // }
  294. /** Draws a line in the current backend. Only usable while pages are drawn sequentially, because backend reference is updated in that process.
  295. * To add your own lines after rendering, use DrawOverlayLine.
  296. */
  297. protected drawLine(start: PointF2D, stop: PointF2D, color: string = "#000000FF", lineWidth: number = 0.2): Node {
  298. // 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.
  299. start = this.applyScreenTransformation(start);
  300. stop = this.applyScreenTransformation(stop);
  301. /*if (!this.backend) {
  302. this.backend = this.backends[0];
  303. }*/
  304. return this.backend.renderLine(start, stop, color, lineWidth * unitInPixels);
  305. }
  306. /** Lets a user/developer draw an overlay line on the score. Use this instead of drawLine, which is for OSMD internally only.
  307. * The MusicPage has to be specified, because each page and Vexflow backend has its own relative coordinates.
  308. * (the AbsolutePosition of a GraphicalNote is relative to its backend)
  309. * To get a MusicPage, use GraphicalNote.ParentMusicPage.
  310. */
  311. public DrawOverlayLine(start: PointF2D, stop: PointF2D, musicPage: GraphicalMusicPage,
  312. color: string = "#FF0000FF", lineWidth: number = 0.2,
  313. id?: string): Node {
  314. if (!musicPage.PageNumber || musicPage.PageNumber > this.backends.length || musicPage.PageNumber < 1) {
  315. console.log("VexFlowMusicSheetDrawer.drawOverlayLine: invalid page number / music page number doesn't correspond to an existing backend.");
  316. return;
  317. }
  318. const musicPageIndex: number = musicPage.PageNumber - 1;
  319. const backendToUse: VexFlowBackend = this.backends[musicPageIndex];
  320. start = this.applyScreenTransformation(start);
  321. stop = this.applyScreenTransformation(stop);
  322. if (!id) {
  323. id = `overlayLine ${start.x}/${start.y}`;
  324. }
  325. return backendToUse.renderLine(start, stop, color, lineWidth * unitInPixels, id);
  326. }
  327. public DrawPath(inputPoints: PointF2D[], musicPage: GraphicalMusicPage,
  328. fill: boolean = true, id?: string): Node {
  329. const musicPageIndex: number = musicPage.PageNumber - 1;
  330. const backendToUse: VexFlowBackend = this.backends[musicPageIndex];
  331. const transformedPoints: PointF2D[] = [];
  332. for (const inputPoint of inputPoints) {
  333. transformedPoints.push(this.applyScreenTransformation(inputPoint));
  334. }
  335. return backendToUse.renderPath(transformedPoints, fill, id);
  336. }
  337. protected drawSkyLine(staffline: StaffLine): void {
  338. const startPosition: PointF2D = staffline.PositionAndShape.AbsolutePosition;
  339. const width: number = staffline.PositionAndShape.Size.width;
  340. this.drawSampledLine(staffline.SkyLine, startPosition, width);
  341. }
  342. protected drawBottomLine(staffline: StaffLine): void {
  343. const startPosition: PointF2D = new PointF2D(staffline.PositionAndShape.AbsolutePosition.x,
  344. staffline.PositionAndShape.AbsolutePosition.y);
  345. const width: number = staffline.PositionAndShape.Size.width;
  346. this.drawSampledLine(staffline.BottomLine, startPosition, width, "#0000FFFF");
  347. }
  348. /**
  349. * Draw a line with a width and start point in a chosen color (used for skyline/bottom line debugging) from
  350. * a simple array
  351. * @param line numeric array. 0 marks the base line. Direction given by sign. Dimensions in units
  352. * @param startPosition Start position in units
  353. * @param width Max line width in units
  354. * @param color Color to paint in. Default is red
  355. */
  356. private drawSampledLine(line: number[], startPosition: PointF2D, width: number, color: string = "#FF0000FF"): void {
  357. const indices: number[] = [];
  358. let currentValue: number = 0;
  359. //Loops through bottom line, grabs all indices that don't equal the previously grabbed index
  360. //Starting with 0 (gets index of all line changes)
  361. for (let i: number = 0; i < line.length; i++) {
  362. if (line[i] !== currentValue) {
  363. indices.push(i);
  364. currentValue = line[i];
  365. }
  366. }
  367. const absolute: PointF2D = startPosition;
  368. if (indices.length > 0) {
  369. const samplingUnit: number = this.rules.SamplingUnit;
  370. let horizontalStart: PointF2D = new PointF2D(absolute.x, absolute.y);
  371. let horizontalEnd: PointF2D = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y);
  372. this.drawLine(horizontalStart, horizontalEnd, color);
  373. let verticalStart: PointF2D;
  374. let verticalEnd: PointF2D;
  375. if (line[0] >= 0) {
  376. verticalStart = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y);
  377. verticalEnd = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y + line[indices[0]]);
  378. this.drawLine(verticalStart, verticalEnd, color);
  379. }
  380. for (let i: number = 1; i < indices.length; i++) {
  381. horizontalStart = new PointF2D(indices[i - 1] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]);
  382. horizontalEnd = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]);
  383. this.drawLine(horizontalStart, horizontalEnd, color);
  384. verticalStart = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]);
  385. verticalEnd = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i]]);
  386. this.drawLine(verticalStart, verticalEnd, color);
  387. }
  388. if (indices[indices.length - 1] < line.length) {
  389. horizontalStart = new PointF2D(indices[indices.length - 1] / samplingUnit + absolute.x, absolute.y + line[indices[indices.length - 1]]);
  390. horizontalEnd = new PointF2D(absolute.x + width, absolute.y + line[indices[indices.length - 1]]);
  391. this.drawLine(horizontalStart, horizontalEnd, color);
  392. } else {
  393. horizontalStart = new PointF2D(indices[indices.length - 1] / samplingUnit + absolute.x, absolute.y);
  394. horizontalEnd = new PointF2D(absolute.x + width, absolute.y);
  395. this.drawLine(horizontalStart, horizontalEnd, color);
  396. }
  397. } else {
  398. // Flat line
  399. const start: PointF2D = new PointF2D(absolute.x, absolute.y);
  400. const end: PointF2D = new PointF2D(absolute.x + width, absolute.y);
  401. this.drawLine(start, end, color);
  402. }
  403. }
  404. private drawStaffEntry(staffEntry: GraphicalStaffEntry): void {
  405. if (staffEntry.FingeringEntries.length > 0) {
  406. for (const fingeringEntry of staffEntry.FingeringEntries) {
  407. fingeringEntry.SVGNode = this.drawLabel(fingeringEntry, GraphicalLayers.Notes);
  408. }
  409. }
  410. // Draw ChordSymbols
  411. if (staffEntry.graphicalChordContainers !== undefined && staffEntry.graphicalChordContainers.length > 0) {
  412. for (const graphicalChordContainer of staffEntry.graphicalChordContainers) {
  413. const label: GraphicalLabel = graphicalChordContainer.GraphicalLabel;
  414. label.SVGNode = this.drawLabel(label, <number>GraphicalLayers.Notes);
  415. }
  416. }
  417. if (this.rules.RenderLyrics) {
  418. if (staffEntry.LyricsEntries.length > 0) {
  419. this.drawLyrics(staffEntry.LyricsEntries, <number>GraphicalLayers.Notes);
  420. }
  421. }
  422. }
  423. /**
  424. * Draw all lyrics to the canvas
  425. * @param lyricEntries Array of lyric entries to be drawn
  426. * @param layer Number of the layer that the lyrics should be drawn in
  427. */
  428. private drawLyrics(lyricEntries: GraphicalLyricEntry[], layer: number): void {
  429. lyricEntries.forEach(lyricsEntry => {
  430. const label: GraphicalLabel = lyricsEntry.GraphicalLabel;
  431. label.Label.colorDefault = this.rules.DefaultColorLyrics;
  432. label.SVGNode = this.drawLabel(label, layer);
  433. });
  434. }
  435. protected drawInstrumentBrace(brace: GraphicalObject, system: MusicSystem): void {
  436. // Draw InstrumentBrackets at beginning of line
  437. const vexBrace: VexFlowInstrumentBrace = (brace as VexFlowInstrumentBrace);
  438. vexBrace.draw(this.backend.getContext());
  439. }
  440. protected drawGroupBracket(bracket: GraphicalObject, system: MusicSystem): void {
  441. // Draw InstrumentBrackets at beginning of line
  442. const vexBrace: VexFlowInstrumentBracket = (bracket as VexFlowInstrumentBracket);
  443. vexBrace.draw(this.backend.getContext());
  444. }
  445. protected drawOctaveShifts(staffLine: StaffLine): void {
  446. for (const graphicalOctaveShift of staffLine.OctaveShifts) {
  447. if (graphicalOctaveShift) {
  448. const vexFlowOctaveShift: VexFlowOctaveShift = graphicalOctaveShift as VexFlowOctaveShift;
  449. const ctx: Vex.IRenderContext = this.backend.getContext();
  450. const textBracket: VF.TextBracket = vexFlowOctaveShift.getTextBracket();
  451. if (this.rules.DefaultColorMusic) {
  452. (textBracket as any).render_options.color = this.rules.DefaultColorMusic;
  453. }
  454. textBracket.setContext(ctx);
  455. try {
  456. textBracket.draw();
  457. } catch (ex) {
  458. log.warn(ex);
  459. }
  460. }
  461. }
  462. }
  463. protected drawPedals(staffLine: StaffLine): void {
  464. for (const graphicalPedal of staffLine.Pedals) {
  465. if (graphicalPedal) {
  466. const vexFlowPedal: VexFlowPedal = graphicalPedal as VexFlowPedal;
  467. const ctx: Vex.IRenderContext = this.backend.getContext();
  468. const pedalMarking: Vex.Flow.PedalMarking = vexFlowPedal.getPedalMarking();
  469. (pedalMarking as any).render_options.color = this.rules.DefaultColorMusic;
  470. pedalMarking.setContext(ctx);
  471. pedalMarking.draw();
  472. }
  473. }
  474. }
  475. protected drawWavyLines(staffLine: StaffLine): void {
  476. for (const graphicalWavyLine of staffLine.WavyLines) {
  477. if (graphicalWavyLine) {
  478. const vexFlowVibratoBracket: VexflowVibratoBracket = graphicalWavyLine as VexflowVibratoBracket;
  479. const ctx: Vex.IRenderContext = this.backend.getContext();
  480. const vfVibratoBracket: Vex.Flow.VibratoBracket = vexFlowVibratoBracket.getVibratoBracket();
  481. (vfVibratoBracket as any).setContext(ctx);
  482. vfVibratoBracket.draw();
  483. }
  484. }
  485. }
  486. protected drawExpressions(staffline: StaffLine): void {
  487. // Draw all Expressions
  488. for (const abstractGraphicalExpression of staffline.AbstractExpressions) {
  489. // Draw InstantaniousDynamics
  490. if (abstractGraphicalExpression instanceof GraphicalInstantaneousDynamicExpression) {
  491. this.drawInstantaneousDynamic((abstractGraphicalExpression as VexFlowInstantaneousDynamicExpression));
  492. // Draw InstantaniousTempo
  493. } else if (abstractGraphicalExpression instanceof GraphicalInstantaneousTempoExpression) {
  494. const label: GraphicalLabel = (abstractGraphicalExpression as GraphicalInstantaneousTempoExpression).GraphicalLabel;
  495. label.SVGNode = this.drawLabel(label, GraphicalLayers.Notes);
  496. // Draw ContinuousDynamics
  497. } else if (abstractGraphicalExpression instanceof GraphicalContinuousDynamicExpression) {
  498. this.drawContinuousDynamic((abstractGraphicalExpression as VexFlowContinuousDynamicExpression));
  499. // Draw ContinuousTempo
  500. // } else if (abstractGraphicalExpression instanceof GraphicalContinuousTempoExpression) {
  501. // this.drawLabel((abstractGraphicalExpression as GraphicalContinuousTempoExpression).GraphicalLabel, GraphicalLayers.Notes);
  502. // // Draw Mood
  503. // } else if (abstractGraphicalExpression instanceof GraphicalMoodExpression) {
  504. // GraphicalMoodExpression; graphicalMood = (GraphicalMoodExpression); abstractGraphicalExpression;
  505. // drawLabel(graphicalMood.GetGraphicalLabel, <number>GraphicalLayers.Notes);
  506. // Draw Unknown
  507. } else if (abstractGraphicalExpression instanceof GraphicalUnknownExpression) {
  508. const label: GraphicalLabel = abstractGraphicalExpression.Label;
  509. label.SVGNode = this.drawLabel(label, <number>GraphicalLayers.Notes);
  510. } else {
  511. log.warn("Unkown type of expression!");
  512. }
  513. }
  514. }
  515. protected drawInstantaneousDynamic(instantaneousDynamic: GraphicalInstantaneousDynamicExpression): void {
  516. const label: GraphicalLabel = (instantaneousDynamic as VexFlowInstantaneousDynamicExpression).Label;
  517. label.SVGNode = this.drawLabel(label, <number>GraphicalLayers.Notes);
  518. }
  519. protected drawContinuousDynamic(graphicalExpression: VexFlowContinuousDynamicExpression): void {
  520. if (graphicalExpression.IsVerbal) {
  521. const label: GraphicalLabel = graphicalExpression.Label;
  522. label.SVGNode = this.drawLabel(label, <number>GraphicalLayers.Notes);
  523. } else {
  524. for (const line of graphicalExpression.Lines) {
  525. const start: PointF2D = new PointF2D(graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.x + line.Start.x,
  526. graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.y + line.Start.y);
  527. const end: PointF2D = new PointF2D(graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.x + line.End.x,
  528. graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.y + line.End.y);
  529. this.drawLine(start, end, line.colorHex ?? "#000000", line.Width);
  530. // the null check for colorHex is not strictly necessary anymore, but the previous default color was red.
  531. }
  532. }
  533. }
  534. /**
  535. * Renders a Label to the screen (e.g. Title, composer..)
  536. * @param graphicalLabel holds the label string, the text height in units and the font parameters
  537. * @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.
  538. * @param bitmapWidth Not needed for now.
  539. * @param bitmapHeight Not needed for now.
  540. * @param heightInPixel the height of the text in screen coordinates
  541. * @param screenPosition the position of the lower left corner of the text in screen coordinates
  542. */
  543. protected renderLabel(graphicalLabel: GraphicalLabel, layer: GraphicalLayers, specs: LabelRenderSpecs): Node {
  544. return this._renderLabel(graphicalLabel, specs);
  545. }
  546. private _renderLabel(graphicalLabel: GraphicalLabel, specs: LabelRenderSpecs): Node {
  547. if (!graphicalLabel.Label.print) {
  548. return undefined;
  549. }
  550. const height: number = graphicalLabel.Label.fontHeight * unitInPixels;
  551. const { font } = graphicalLabel.Label;
  552. let color: string;
  553. if (this.rules.ColoringEnabled) {
  554. color = graphicalLabel.Label.colorDefault;
  555. if (graphicalLabel.ColorXML) {
  556. color = graphicalLabel.ColorXML;
  557. }
  558. if (graphicalLabel.Label.color) {
  559. color = graphicalLabel.Label.color.toString();
  560. }
  561. if (!color) {
  562. color = this.rules.DefaultColorLabel;
  563. }
  564. }
  565. let { fontStyle, fontFamily } = graphicalLabel.Label;
  566. if (!fontStyle) {
  567. fontStyle = this.rules.DefaultFontStyle;
  568. }
  569. if (!fontFamily) {
  570. fontFamily = this.rules.DefaultFontFamily;
  571. }
  572. let node: Node;
  573. for (let i: number = 0; i < graphicalLabel.TextLines?.length; i++) {
  574. const currLine: {text: string, xOffset: number, width: number} = graphicalLabel.TextLines[i];
  575. const xOffsetInPixel: number = this.calculatePixelDistance(currLine.xOffset);
  576. const linePosition: PointF2D = new PointF2D(specs.ScreenPosition.x + xOffsetInPixel, specs.ScreenPosition.y);
  577. const newNode: Node =
  578. this.backend.renderText(height, fontStyle, font, currLine.text, specs.FontHeightInPixel, linePosition, color, graphicalLabel.Label.fontFamily);
  579. if (!node) {
  580. node = newNode;
  581. } else {
  582. node.appendChild(newNode);
  583. }
  584. specs.ScreenPosition.y = specs.ScreenPosition.y + specs.FontHeightInPixel;
  585. if (graphicalLabel.TextLines.length > 1) {
  586. specs.ScreenPosition.y += this.rules.SpacingBetweenTextLines;
  587. }
  588. }
  589. // font currently unused, replaced by fontFamily
  590. return node; // this will be a merge conflict with annotations, refactor there to handle node array instead of single node
  591. }
  592. /**
  593. * Renders a rectangle with the given style to the screen.
  594. * It is given in screen coordinates.
  595. * @param rectangle the rect in screen coordinates
  596. * @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.
  597. * @param styleId the style id
  598. * @param alpha alpha value between 0 and 1
  599. */
  600. protected renderRectangle(rectangle: RectangleF2D, layer: number, styleId: number, colorHex: string, alpha: number): Node {
  601. return this.backend.renderRectangle(rectangle, styleId, colorHex, alpha);
  602. }
  603. /**
  604. * Converts a point from unit to pixel space.
  605. * @param point
  606. * @returns {PointF2D}
  607. */
  608. protected applyScreenTransformation(point: PointF2D): PointF2D {
  609. return new PointF2D(point.x * unitInPixels, point.y * unitInPixels);
  610. }
  611. /**
  612. * Converts a rectangle from unit to pixel space.
  613. * @param rectangle
  614. * @returns {RectangleF2D}
  615. */
  616. protected applyScreenTransformationForRect(rectangle: RectangleF2D): RectangleF2D {
  617. return new RectangleF2D(rectangle.x * unitInPixels, rectangle.y * unitInPixels, rectangle.width * unitInPixels, rectangle.height * unitInPixels);
  618. }
  619. }