SvgVexFlowBackend.ts 9.1 KB

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