SvgVexFlowBackend.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import Vex from "vexflow";
  2. import {VexFlowBackend} from "./VexFlowBackend";
  3. import {VexFlowConverter} from "./VexFlowConverter";
  4. import {FontStyles} from "../../../Common/Enums/FontStyles";
  5. import {Fonts} from "../../../Common/Enums/Fonts";
  6. import {RectangleF2D} from "../../../Common/DataObjects/RectangleF2D";
  7. import {PointF2D} from "../../../Common/DataObjects/PointF2D";
  8. import {BackendType} from "../../../OpenSheetMusicDisplay/OSMDOptions";
  9. import {EngravingRules} from "../EngravingRules";
  10. import log from "loglevel";
  11. export class SvgVexFlowBackend extends VexFlowBackend {
  12. private ctx: Vex.Flow.SVGContext;
  13. private zoom: number;
  14. constructor(rules: EngravingRules) {
  15. super();
  16. this.rules = rules;
  17. }
  18. public getVexflowBackendType(): Vex.Flow.Renderer.Backends {
  19. return Vex.Flow.Renderer.Backends.SVG;
  20. }
  21. public getOSMDBackendType(): BackendType {
  22. return BackendType.SVG;
  23. }
  24. public getCanvasSize(): number {
  25. return document.getElementById("osmdCanvasPage" + this.graphicalMusicPage.PageNumber)?.offsetHeight;
  26. }
  27. public initialize(container: HTMLElement, zoom: number): void {
  28. this.zoom = zoom;
  29. this.canvas = document.createElement("div");
  30. this.canvas.id = "osmdCanvasPage" + this.graphicalMusicPage.PageNumber;
  31. // this.canvas.id = uniqueID // TODO create unique tagName like with cursor now?
  32. this.inner = this.canvas;
  33. this.inner.style.position = "relative";
  34. this.canvas.style.zIndex = "0";
  35. container.appendChild(this.inner);
  36. this.renderer = new Vex.Flow.Renderer(this.canvas, this.getVexflowBackendType());
  37. this.ctx = <Vex.Flow.SVGContext>this.renderer.getContext();
  38. this.ctx.svg.id = "osmdSvgPage" + this.graphicalMusicPage.PageNumber;
  39. }
  40. public getContext(): Vex.Flow.SVGContext {
  41. return this.ctx;
  42. }
  43. public getSvgElement(): SVGElement {
  44. return this.ctx.svg;
  45. }
  46. removeNode(node: Node): boolean {
  47. const svg: SVGElement = this.ctx?.svg;
  48. if (!svg) {
  49. return false;
  50. }
  51. // unfortunately there's no method svg.hasChild(node). traversing all nodes seems inefficient.
  52. try {
  53. svg.removeChild(node);
  54. } catch (ex) {
  55. // log.error("SvgVexFlowBackend.removeNode: error:"); // unnecessary, stacktrace is in exception
  56. log.error(ex);
  57. return false;
  58. }
  59. return true;
  60. }
  61. public clear(): void {
  62. if (!this.ctx) {
  63. return;
  64. }
  65. //const { svg } = this.ctx; // seems to make svg static between osmd instances.
  66. const svg: SVGElement = this.ctx.svg;
  67. // removes all children from the SVG element,
  68. // effectively clearing the SVG viewport
  69. while (svg.lastChild) {
  70. svg.removeChild(svg.lastChild);
  71. }
  72. // set background color if not transparent
  73. if (this.rules.PageBackgroundColor) {
  74. this.ctx.save();
  75. // note that this will hide the cursor
  76. this.ctx.setFillStyle(this.rules.PageBackgroundColor);
  77. this.ctx.setStrokeStyle("#12345600"); // transparent
  78. this.ctx.fillRect(0, 0, this.canvas.offsetWidth / this.zoom, this.canvas.offsetHeight / this.zoom);
  79. this.ctx.restore();
  80. }
  81. }
  82. public scale(k: number): void {
  83. this.ctx.scale(k, k);
  84. }
  85. public translate(x: number, y: number): void {
  86. // TODO: implement this
  87. }
  88. public renderText(fontHeight: number, fontStyle: FontStyles, font: Fonts, text: string,
  89. heightInPixel: number, screenPosition: PointF2D,
  90. color: string = undefined, fontFamily: string = undefined): Node {
  91. this.ctx.save();
  92. const node: Node = this.ctx.openGroup();
  93. if (color) {
  94. this.ctx.attributes.fill = color;
  95. this.ctx.attributes.stroke = color;
  96. }
  97. let fontFamilyVexFlow: string = fontFamily;
  98. if (!fontFamily || fontFamily === "default") {
  99. fontFamilyVexFlow = this.rules.DefaultFontFamily;
  100. }
  101. this.ctx.setFont(fontFamilyVexFlow, fontHeight, VexFlowConverter.fontStyle(fontStyle));
  102. // font size is set by VexFlow in `pt`. This overwrites the font so it's set to px instead
  103. this.ctx.attributes["font-size"] = `${fontHeight}px`;
  104. this.ctx.state["font-size"] = `${fontHeight}px`;
  105. let fontWeightVexflow: string = "normal";
  106. let fontStyleVexflow: string = "normal";
  107. switch (fontStyle) {
  108. case FontStyles.Bold:
  109. fontWeightVexflow = "bold";
  110. break;
  111. case FontStyles.Italic:
  112. fontStyleVexflow = "italic";
  113. break;
  114. case FontStyles.BoldItalic:
  115. fontWeightVexflow = "bold";
  116. fontStyleVexflow = "italic";
  117. break;
  118. default:
  119. fontWeightVexflow = "normal";
  120. }
  121. this.ctx.attributes["font-weight"] = fontWeightVexflow;
  122. this.ctx.state["font-weight"] = fontWeightVexflow;
  123. this.ctx.attributes["font-style"] = fontStyleVexflow;
  124. this.ctx.state["font-style"] = fontStyleVexflow;
  125. this.ctx.fillText(text, screenPosition.x, screenPosition.y + heightInPixel);
  126. this.ctx.closeGroup();
  127. this.ctx.restore();
  128. return node;
  129. }
  130. public renderRectangle(rectangle: RectangleF2D, styleId: number, colorHex: string, alpha: number = 1): Node {
  131. this.ctx.save();
  132. const node: Node = this.ctx.openGroup();
  133. if (colorHex) {
  134. this.ctx.attributes.fill = colorHex;
  135. } else {
  136. this.ctx.attributes.fill = VexFlowConverter.style(styleId);
  137. }
  138. this.ctx.attributes["fill-opacity"] = alpha;
  139. this.ctx.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
  140. this.ctx.restore();
  141. this.ctx.attributes["fill-opacity"] = 1;
  142. this.ctx.closeGroup();
  143. return node;
  144. }
  145. public renderLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF", lineWidth: number = 2): Node {
  146. this.ctx.save();
  147. const node: Node = this.ctx.openGroup();
  148. this.ctx.beginPath();
  149. this.ctx.moveTo(start.x, start.y);
  150. this.ctx.lineTo(stop.x, stop.y);
  151. this.ctx.attributes.stroke = color;
  152. //this.ctx.attributes.strokeStyle = color;
  153. //this.ctx.attributes["font-weight"] = "bold";
  154. //this.ctx.attributes["stroke-linecap"] = "round";
  155. this.ctx.lineWidth = lineWidth;
  156. this.ctx.stroke();
  157. this.ctx.closeGroup();
  158. this.ctx.restore();
  159. return node;
  160. }
  161. public renderCurve(points: PointF2D[]): void {
  162. this.ctx.beginPath();
  163. this.ctx.moveTo(points[0].x, points[0].y);
  164. this.ctx.bezierCurveTo(
  165. points[1].x,
  166. points[1].y,
  167. points[2].x,
  168. points[2].y,
  169. points[3].x,
  170. points[3].y
  171. );
  172. this.ctx.lineTo(points[7].x, points[7].y);
  173. this.ctx.bezierCurveTo(
  174. points[6].x,
  175. points[6].y,
  176. points[5].x,
  177. points[5].y,
  178. points[4].x,
  179. points[4].y
  180. );
  181. this.ctx.lineTo(points[0].x, points[0].y);
  182. //this.ctx.stroke();
  183. this.ctx.closePath();
  184. this.ctx.fill();
  185. }
  186. public export(): void {
  187. // See: https://stackoverflow.com/questions/38477972/javascript-save-svg-element-to-file-on-disk
  188. // first create a clone of our svg node so we don't mess the original one
  189. const clone: SVGElement = (this.ctx.svg.cloneNode(true) as SVGElement);
  190. // create a doctype that is SVG
  191. const svgDocType: DocumentType = document.implementation.createDocumentType(
  192. "svg",
  193. "-//W3C//DTD SVG 1.1//EN",
  194. "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"
  195. );
  196. // Create a new svg document
  197. const svgDoc: Document = document.implementation.createDocument("http://www.w3.org/2000/svg", "svg", svgDocType);
  198. // replace the documentElement with our clone
  199. svgDoc.replaceChild(clone, svgDoc.documentElement);
  200. // get the data
  201. const svgData: string = (new XMLSerializer()).serializeToString(svgDoc);
  202. // now you've got your svg data, the following will depend on how you want to download it
  203. // e.g yo could make a Blob of it for FileSaver.js
  204. /*
  205. var blob = new Blob([svgData.replace(/></g, '>\n\r<')]);
  206. saveAs(blob, 'myAwesomeSVG.svg');
  207. */
  208. // here I'll just make a simple a with download attribute
  209. const a: HTMLAnchorElement = document.createElement("a");
  210. a.href = "data:image/svg+xml; charset=utf8, " + encodeURIComponent(svgData.replace(/></g, ">\n\r<"));
  211. a.download = "opensheetmusicdisplay_download.svg";
  212. a.innerHTML = window.location.href + "/download";
  213. document.body.appendChild(a);
  214. }
  215. }