SvgVexFlowBackend.ts 9.5 KB

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