VexFlowMusicSheetDrawer.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. import Vex = require("vexflow");
  2. import { MusicSheetDrawer } from "../MusicSheetDrawer";
  3. import { RectangleF2D } from "../../../Common/DataObjects/RectangleF2D";
  4. import { VexFlowMeasure } from "./VexFlowMeasure";
  5. import { PointF2D } from "../../../Common/DataObjects/PointF2D";
  6. import { GraphicalLabel } from "../GraphicalLabel";
  7. import { VexFlowTextMeasurer } from "./VexFlowTextMeasurer";
  8. import { MusicSystem } from "../MusicSystem";
  9. import { GraphicalObject } from "../GraphicalObject";
  10. import { GraphicalLayers } from "../DrawingEnums";
  11. import { GraphicalStaffEntry } from "../GraphicalStaffEntry";
  12. import { VexFlowBackend } from "./VexFlowBackend";
  13. import { VexFlowOctaveShift } from "./VexFlowOctaveShift";
  14. import { VexFlowInstantaneousDynamicExpression } from "./VexFlowInstantaneousDynamicExpression";
  15. import { VexFlowInstrumentBracket } from "./VexFlowInstrumentBracket";
  16. import { VexFlowInstrumentBrace } from "./VexFlowInstrumentBrace";
  17. import { GraphicalLyricEntry } from "../GraphicalLyricEntry";
  18. import { VexFlowStaffLine } from "./VexFlowStaffLine";
  19. import { StaffLine } from "../StaffLine";
  20. import { EngravingRules } from "../EngravingRules";
  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 = require("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. /**
  32. * This is a global constant which denotes the height in pixels of the space between two lines of the stave
  33. * (when zoom = 1.0)
  34. * @type number
  35. */
  36. export const unitInPixels: number = 10;
  37. export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
  38. private backend: VexFlowBackend;
  39. private backends: VexFlowBackend[] = [];
  40. private zoom: number = 1.0;
  41. private pageIdx: number = 0; // this is a bad solution, should use MusicPage.PageNumber instead.
  42. constructor(drawingParameters: DrawingParameters = new DrawingParameters()) {
  43. super(new VexFlowTextMeasurer(), drawingParameters);
  44. }
  45. public get Backends(): VexFlowBackend[] {
  46. return this.backends;
  47. }
  48. public drawSheet(graphicalMusicSheet: GraphicalMusicSheet): void {
  49. this.pageIdx = 0;
  50. for (const graphicalMusicPage of graphicalMusicSheet.MusicPages) {
  51. const backend: VexFlowBackend = this.backends[this.pageIdx];
  52. backend.graphicalMusicPage = graphicalMusicPage;
  53. backend.scale(this.zoom);
  54. //backend.resize(graphicalMusicSheet.ParentMusicSheet.pageWidth * unitInPixels * this.zoom,
  55. // EngravingRules.Rules.PageHeight * unitInPixels * this.zoom);
  56. this.pageIdx += 1;
  57. }
  58. this.pageIdx = 0;
  59. this.backend = this.backends[0];
  60. super.drawSheet(graphicalMusicSheet);
  61. }
  62. protected drawPage(page: GraphicalMusicPage): void {
  63. 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
  64. super.drawPage(page);
  65. this.pageIdx += 1;
  66. this.backend = this.backends[this.pageIdx];
  67. }
  68. public clear(): void {
  69. for (const backend of this.backends) {
  70. backend.clear();
  71. }
  72. }
  73. public setZoom(zoom: number): void {
  74. this.zoom = zoom;
  75. }
  76. /**
  77. * Converts a distance from unit to pixel space.
  78. * @param unitDistance the distance in units
  79. * @returns {number} the distance in pixels
  80. */
  81. public calculatePixelDistance(unitDistance: number): number {
  82. return unitDistance * unitInPixels;
  83. }
  84. protected drawStaffLine(staffLine: StaffLine): void {
  85. super.drawStaffLine(staffLine);
  86. const absolutePos: PointF2D = staffLine.PositionAndShape.AbsolutePosition;
  87. if (EngravingRules.Rules.RenderSlurs) {
  88. this.drawSlurs(staffLine as VexFlowStaffLine, absolutePos);
  89. }
  90. }
  91. private drawSlurs(vfstaffLine: VexFlowStaffLine, absolutePos: PointF2D): void {
  92. for (const graphicalSlur of vfstaffLine.GraphicalSlurs) {
  93. // don't draw crossed slurs, as their curve calculation is not implemented yet:
  94. if (graphicalSlur.slur.isCrossed()) {
  95. continue;
  96. }
  97. this.drawSlur(graphicalSlur, absolutePos);
  98. }
  99. }
  100. private drawSlur(graphicalSlur: GraphicalSlur, abs: PointF2D): void {
  101. const curvePointsInPixels: PointF2D[] = [];
  102. // 1) create inner or original curve:
  103. const p1: PointF2D = new PointF2D(graphicalSlur.bezierStartPt.x + abs.x, graphicalSlur.bezierStartPt.y + abs.y);
  104. const p2: PointF2D = new PointF2D(graphicalSlur.bezierStartControlPt.x + abs.x, graphicalSlur.bezierStartControlPt.y + abs.y);
  105. const p3: PointF2D = new PointF2D(graphicalSlur.bezierEndControlPt.x + abs.x, graphicalSlur.bezierEndControlPt.y + abs.y);
  106. const p4: PointF2D = new PointF2D(graphicalSlur.bezierEndPt.x + abs.x, graphicalSlur.bezierEndPt.y + abs.y);
  107. // put screen transformed points into array
  108. curvePointsInPixels.push(this.applyScreenTransformation(p1));
  109. curvePointsInPixels.push(this.applyScreenTransformation(p2));
  110. curvePointsInPixels.push(this.applyScreenTransformation(p3));
  111. curvePointsInPixels.push(this.applyScreenTransformation(p4));
  112. // 2) create second outer curve to create a thickness for the curve:
  113. if (graphicalSlur.placement === PlacementEnum.Above) {
  114. p1.y -= 0.05;
  115. p2.y -= 0.3;
  116. p3.y -= 0.3;
  117. p4.y -= 0.05;
  118. } else {
  119. p1.y += 0.05;
  120. p2.y += 0.3;
  121. p3.y += 0.3;
  122. p4.y += 0.05;
  123. }
  124. // put screen transformed points into array
  125. curvePointsInPixels.push(this.applyScreenTransformation(p1));
  126. curvePointsInPixels.push(this.applyScreenTransformation(p2));
  127. curvePointsInPixels.push(this.applyScreenTransformation(p3));
  128. curvePointsInPixels.push(this.applyScreenTransformation(p4));
  129. this.backend.renderCurve(curvePointsInPixels);
  130. }
  131. protected drawMeasure(measure: VexFlowMeasure): void {
  132. measure.setAbsoluteCoordinates(
  133. measure.PositionAndShape.AbsolutePosition.x * unitInPixels,
  134. measure.PositionAndShape.AbsolutePosition.y * unitInPixels
  135. );
  136. measure.draw(this.backend.getContext());
  137. // Draw the StaffEntries
  138. for (const staffEntry of measure.staffEntries) {
  139. this.drawStaffEntry(staffEntry);
  140. }
  141. }
  142. // private drawPixel(coord: PointF2D): void {
  143. // coord = this.applyScreenTransformation(coord);
  144. // const ctx: any = this.backend.getContext();
  145. // const oldStyle: string = ctx.fillStyle;
  146. // ctx.fillStyle = "#00FF00FF";
  147. // ctx.fillRect( coord.x, coord.y, 2, 2 );
  148. // ctx.fillStyle = oldStyle;
  149. // }
  150. /** Draws a line in the current backend. Only usable while pages are drawn sequentially, because backend reference is updated in that process.
  151. * To add your own lines after rendering, use DrawOverlayLine.
  152. */
  153. protected drawLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF", lineWidth: number = 0.2): void {
  154. // 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.
  155. start = this.applyScreenTransformation(start);
  156. stop = this.applyScreenTransformation(stop);
  157. /*if (!this.backend) {
  158. this.backend = this.backends[0];
  159. }*/
  160. this.backend.renderLine(start, stop, color, lineWidth * unitInPixels);
  161. }
  162. /** Lets a user/developer draw an overlay line on the score. Use this instead of drawLine, which is for OSMD internally only.
  163. * The MusicPage has to be specified, because each page and Vexflow backend has its own relative coordinates.
  164. * (the AbsolutePosition of a GraphicalNote is relative to its backend)
  165. * To get a MusicPage, use GraphicalNote.ParentMusicPage.
  166. */
  167. public DrawOverlayLine(start: PointF2D, stop: PointF2D, musicPage: GraphicalMusicPage,
  168. color: string = "#FF0000FF", lineWidth: number = 0.2): void {
  169. if (!musicPage.PageNumber || musicPage.PageNumber > this.backends.length || musicPage.PageNumber < 1) {
  170. console.log("VexFlowMusicSheetDrawer.drawOverlayLine: invalid page number / music page number doesn't correspond to an existing backend.");
  171. return;
  172. }
  173. const musicPageIndex: number = musicPage.PageNumber - 1;
  174. const backendToUse: VexFlowBackend = this.backends[musicPageIndex];
  175. start = this.applyScreenTransformation(start);
  176. stop = this.applyScreenTransformation(stop);
  177. backendToUse.renderLine(start, stop, color, lineWidth * unitInPixels);
  178. }
  179. protected drawSkyLine(staffline: StaffLine): void {
  180. const startPosition: PointF2D = staffline.PositionAndShape.AbsolutePosition;
  181. const width: number = staffline.PositionAndShape.Size.width;
  182. this.drawSampledLine(staffline.SkyLine, startPosition, width);
  183. }
  184. protected drawBottomLine(staffline: StaffLine): void {
  185. const startPosition: PointF2D = new PointF2D(staffline.PositionAndShape.AbsolutePosition.x,
  186. staffline.PositionAndShape.AbsolutePosition.y);
  187. const width: number = staffline.PositionAndShape.Size.width;
  188. this.drawSampledLine(staffline.BottomLine, startPosition, width, "#0000FFFF");
  189. }
  190. /**
  191. * Draw a line with a width and start point in a chosen color (used for skyline/bottom line debugging) from
  192. * a simple array
  193. * @param line numeric array. 0 marks the base line. Direction given by sign. Dimensions in units
  194. * @param startPosition Start position in units
  195. * @param width Max line width in units
  196. * @param color Color to paint in. Default is red
  197. */
  198. private drawSampledLine(line: number[], startPosition: PointF2D, width: number, color: string = "#FF0000FF"): void {
  199. const indices: number[] = [];
  200. let currentValue: number = 0;
  201. for (let i: number = 0; i < line.length; i++) {
  202. if (line[i] !== currentValue) {
  203. indices.push(i);
  204. currentValue = line[i];
  205. }
  206. }
  207. const absolute: PointF2D = startPosition;
  208. if (indices.length > 0) {
  209. const samplingUnit: number = EngravingRules.Rules.SamplingUnit;
  210. let horizontalStart: PointF2D = new PointF2D(absolute.x, absolute.y);
  211. let horizontalEnd: PointF2D = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y);
  212. this.drawLine(horizontalStart, horizontalEnd, color);
  213. let verticalStart: PointF2D;
  214. let verticalEnd: PointF2D;
  215. if (line[0] >= 0) {
  216. verticalStart = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y);
  217. verticalEnd = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y + line[indices[0]]);
  218. this.drawLine(verticalStart, verticalEnd, color);
  219. }
  220. for (let i: number = 1; i < indices.length; i++) {
  221. horizontalStart = new PointF2D(indices[i - 1] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]);
  222. horizontalEnd = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]);
  223. this.drawLine(horizontalStart, horizontalEnd, color);
  224. verticalStart = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]);
  225. verticalEnd = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i]]);
  226. this.drawLine(verticalStart, verticalEnd, color);
  227. }
  228. if (indices[indices.length - 1] < line.length) {
  229. horizontalStart = new PointF2D(indices[indices.length - 1] / samplingUnit + absolute.x, absolute.y + line[indices[indices.length - 1]]);
  230. horizontalEnd = new PointF2D(absolute.x + width, absolute.y + line[indices[indices.length - 1]]);
  231. this.drawLine(horizontalStart, horizontalEnd, color);
  232. } else {
  233. horizontalStart = new PointF2D(indices[indices.length - 1] / samplingUnit + absolute.x, absolute.y);
  234. horizontalEnd = new PointF2D(absolute.x + width, absolute.y);
  235. this.drawLine(horizontalStart, horizontalEnd, color);
  236. }
  237. } else {
  238. // Flat line
  239. const start: PointF2D = new PointF2D(absolute.x, absolute.y);
  240. const end: PointF2D = new PointF2D(absolute.x + width, absolute.y);
  241. this.drawLine(start, end, color);
  242. }
  243. }
  244. private drawStaffEntry(staffEntry: GraphicalStaffEntry): void {
  245. // Draw ChordSymbols
  246. if (staffEntry.graphicalChordContainers !== undefined && staffEntry.graphicalChordContainers.length > 0) {
  247. for (const graphicalChordContainer of staffEntry.graphicalChordContainers) {
  248. this.drawLabel(graphicalChordContainer.GetGraphicalLabel, <number>GraphicalLayers.Notes);
  249. }
  250. }
  251. if (EngravingRules.Rules.RenderLyrics) {
  252. if (staffEntry.LyricsEntries.length > 0) {
  253. this.drawLyrics(staffEntry.LyricsEntries, <number>GraphicalLayers.Notes);
  254. }
  255. }
  256. }
  257. /**
  258. * Draw all lyrics to the canvas
  259. * @param lyricEntries Array of lyric entries to be drawn
  260. * @param layer Number of the layer that the lyrics should be drawn in
  261. */
  262. private drawLyrics(lyricEntries: GraphicalLyricEntry[], layer: number): void {
  263. lyricEntries.forEach(lyricsEntry => this.drawLabel(lyricsEntry.GraphicalLabel, layer));
  264. }
  265. protected drawInstrumentBrace(brace: GraphicalObject, system: MusicSystem): void {
  266. // Draw InstrumentBrackets at beginning of line
  267. const vexBrace: VexFlowInstrumentBrace = (brace as VexFlowInstrumentBrace);
  268. vexBrace.draw(this.backend.getContext());
  269. }
  270. protected drawGroupBracket(bracket: GraphicalObject, system: MusicSystem): void {
  271. // Draw InstrumentBrackets at beginning of line
  272. const vexBrace: VexFlowInstrumentBracket = (bracket as VexFlowInstrumentBracket);
  273. vexBrace.draw(this.backend.getContext());
  274. }
  275. protected drawOctaveShifts(staffLine: StaffLine): void {
  276. for (const graphicalOctaveShift of staffLine.OctaveShifts) {
  277. if (graphicalOctaveShift) {
  278. const ctx: Vex.IRenderContext = this.backend.getContext();
  279. const textBracket: Vex.Flow.TextBracket = (graphicalOctaveShift as VexFlowOctaveShift).getTextBracket();
  280. textBracket.setContext(ctx);
  281. const startX: number = staffLine.PositionAndShape.AbsolutePosition.x + textBracket.start.getX() / 10;
  282. const stopX: number = staffLine.PositionAndShape.AbsolutePosition.x + textBracket.stop.getX() / 10;
  283. if ((<any>textBracket).position === Vex.Flow.TextBracket.Positions.TOP) {
  284. const headroom: number = staffLine.SkyBottomLineCalculator.getSkyLineMinInRange(startX, stopX);
  285. if (headroom === Infinity) { // will cause Vexflow error
  286. return;
  287. }
  288. textBracket.start.getStave().options.space_above_staff_ln = headroom;
  289. } else {
  290. const footroom: number = staffLine.SkyBottomLineCalculator.getBottomLineMaxInRange(startX, stopX);
  291. if (footroom === Infinity) { // will cause Vexflow error
  292. return;
  293. }
  294. textBracket.start.getStave().options.space_below_staff_ln = footroom;
  295. }
  296. textBracket.draw();
  297. }
  298. }
  299. }
  300. protected drawExpressions(staffline: StaffLine): void {
  301. // Draw all Expressions
  302. for (const abstractGraphicalExpression of staffline.AbstractExpressions) {
  303. // Draw InstantaniousDynamics
  304. if (abstractGraphicalExpression instanceof GraphicalInstantaneousDynamicExpression) {
  305. this.drawInstantaneousDynamic((abstractGraphicalExpression as VexFlowInstantaneousDynamicExpression));
  306. // Draw InstantaniousTempo
  307. } else if (abstractGraphicalExpression instanceof GraphicalInstantaneousTempoExpression) {
  308. this.drawLabel((abstractGraphicalExpression as GraphicalInstantaneousTempoExpression).GraphicalLabel, GraphicalLayers.Notes);
  309. // Draw ContinuousDynamics
  310. } else if (abstractGraphicalExpression instanceof GraphicalContinuousDynamicExpression) {
  311. this.drawContinuousDynamic((abstractGraphicalExpression as VexFlowContinuousDynamicExpression));
  312. // Draw ContinuousTempo
  313. // } else if (abstractGraphicalExpression instanceof GraphicalContinuousTempoExpression) {
  314. // this.drawLabel((abstractGraphicalExpression as GraphicalContinuousTempoExpression).GraphicalLabel, GraphicalLayers.Notes);
  315. // // Draw Mood
  316. // } else if (abstractGraphicalExpression instanceof GraphicalMoodExpression) {
  317. // GraphicalMoodExpression; graphicalMood = (GraphicalMoodExpression); abstractGraphicalExpression;
  318. // drawLabel(graphicalMood.GetGraphicalLabel, (int)GraphicalLayers.Notes);
  319. // // Draw Unknown
  320. // } else if (abstractGraphicalExpression instanceof GraphicalUnknownExpression) {
  321. // GraphicalUnknownExpression; graphicalUnknown =
  322. // (GraphicalUnknownExpression); abstractGraphicalExpression;
  323. // drawLabel(graphicalUnknown.GetGraphicalLabel, (int)GraphicalLayers.Notes);
  324. // }
  325. } else {
  326. log.warn("Unkown type of expression!");
  327. }
  328. }
  329. }
  330. protected drawInstantaneousDynamic(instantaneousDynamic: GraphicalInstantaneousDynamicExpression): void {
  331. this.drawLabel((instantaneousDynamic as VexFlowInstantaneousDynamicExpression).Label, <number>GraphicalLayers.Notes);
  332. }
  333. protected drawContinuousDynamic(graphicalExpression: VexFlowContinuousDynamicExpression): void {
  334. if (graphicalExpression.IsVerbal) {
  335. this.drawLabel(graphicalExpression.Label, <number>GraphicalLayers.Notes);
  336. } else {
  337. for (const line of graphicalExpression.Lines) {
  338. const start: PointF2D = new PointF2D(graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.x + line.Start.x,
  339. graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.y + line.Start.y);
  340. const end: PointF2D = new PointF2D(graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.x + line.End.x,
  341. graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.y + line.End.y);
  342. this.drawLine(start, end, "black", line.Width);
  343. }
  344. }
  345. }
  346. /**
  347. * Renders a Label to the screen (e.g. Title, composer..)
  348. * @param graphicalLabel holds the label string, the text height in units and the font parameters
  349. * @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.
  350. * @param bitmapWidth Not needed for now.
  351. * @param bitmapHeight Not needed for now.
  352. * @param heightInPixel the height of the text in screen coordinates
  353. * @param screenPosition the position of the lower left corner of the text in screen coordinates
  354. */
  355. protected renderLabel(graphicalLabel: GraphicalLabel, layer: number, bitmapWidth: number,
  356. bitmapHeight: number, heightInPixel: number, screenPosition: PointF2D): void {
  357. const height: number = graphicalLabel.Label.fontHeight * unitInPixels;
  358. const { fontStyle, font, text } = graphicalLabel.Label;
  359. let color: string;
  360. if (EngravingRules.Rules.ColoringEnabled) {
  361. color = graphicalLabel.Label.colorDefault;
  362. if (!color) {
  363. color = EngravingRules.Rules.DefaultColorLabel;
  364. }
  365. }
  366. this.backend.renderText(height, fontStyle, font, text, heightInPixel, screenPosition, color);
  367. }
  368. /**
  369. * Renders a rectangle with the given style to the screen.
  370. * It is given in screen coordinates.
  371. * @param rectangle the rect in screen coordinates
  372. * @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.
  373. * @param styleId the style id
  374. * @param alpha alpha value between 0 and 1
  375. */
  376. protected renderRectangle(rectangle: RectangleF2D, layer: number, styleId: number, alpha: number): void {
  377. this.backend.renderRectangle(rectangle, styleId, alpha);
  378. }
  379. /**
  380. * Converts a point from unit to pixel space.
  381. * @param point
  382. * @returns {PointF2D}
  383. */
  384. protected applyScreenTransformation(point: PointF2D): PointF2D {
  385. return new PointF2D(point.x * unitInPixels, point.y * unitInPixels);
  386. }
  387. /**
  388. * Converts a rectangle from unit to pixel space.
  389. * @param rectangle
  390. * @returns {RectangleF2D}
  391. */
  392. protected applyScreenTransformationForRect(rectangle: RectangleF2D): RectangleF2D {
  393. return new RectangleF2D(rectangle.x * unitInPixels, rectangle.y * unitInPixels, rectangle.width * unitInPixels, rectangle.height * unitInPixels);
  394. }
  395. }