Procházet zdrojové kódy

Adds SVG Rendering option (#154)

* Implementing SVG backend

* Adding back previously removed code

* Adding proper indentation

* Fixing lint issue

* Fixes resize artefacts on SVG mode and renders text with the appropriate font styles/size.
João Portela před 7 roky
rodič
revize
ea3bd5e31e

+ 14 - 2
demo/demo.js

@@ -40,7 +40,8 @@
         nextCursorBtn,
         resetCursorBtn,
         showCursorBtn,
-        hideCursorBtn;
+        hideCursorBtn,
+        backendSelect;
 
     // Initialization code
     function init() {
@@ -59,6 +60,7 @@
         resetCursorBtn = document.getElementById("reset-cursor-btn");
         showCursorBtn = document.getElementById("show-cursor-btn");
         hideCursorBtn = document.getElementById("hide-cursor-btn");
+        backendSelect = document.getElementById("backend-select");
 
         // Hide error
         error();
@@ -87,7 +89,7 @@
         };
 
         // Create OSMD object and canvas
-        OSMD = new opensheetmusicdisplay.OSMD(canvas);
+        OSMD = new opensheetmusicdisplay.OSMD(canvas, false);
         OSMD.setLogLevel('info');
         document.body.appendChild(canvas);
 
@@ -124,6 +126,16 @@
         showCursorBtn.addEventListener("click", function() {
             OSMD.cursor.show();
         });
+
+        backendSelect.addEventListener("change", function(e) {
+            var value = e.target.value;
+            // clears the canvas element
+            canvas.innerHTML = "";
+            OSMD = new opensheetmusicdisplay.OSMD(canvas, false, value);
+            OSMD.setLogLevel('info');
+            selectOnChange();
+
+        });
     }
 
     function Resize(startCallback, endCallback) {

+ 5 - 0
demo/index.html

@@ -34,6 +34,11 @@
             <p>... or just drop your MusicXML file on this page.</p>
         </td>
         <td valign="top" halign="right">
+            <p>Renderer Backend
+                <select id="backend-select" value="canvas">
+                    <option value="canvas">Canvas</option>>
+                    <option value="svg">SVG</option>
+                </select>
             <p>Cursor controls:
                 <input type="button" value="show" id="show-cursor-btn"/>
                 <input type="button" value="hide" id="hide-cursor-btn"/>

+ 30 - 11
external/vexflow/vexflow.d.ts

@@ -68,7 +68,7 @@ declare namespace Vex {
         export class StaveTie {
             constructor(notes_struct: any);
 
-            public setContext(ctx: CanvasContext): StaveTie;
+            public setContext(ctx: RenderContext): StaveTie;
 
             public draw(): void;
         }
@@ -111,7 +111,7 @@ declare namespace Vex {
             public getLineForY(y: number): number;
 
             public getModifiers(pos: any, cat: any): Clef[]; // FIXME
-            public setContext(ctx: CanvasContext): Stave;
+            public setContext(ctx: RenderContext): Stave;
 
             public addModifier(mod: any, pos: any): void;
 
@@ -148,13 +148,18 @@ declare namespace Vex {
         }
 
         export class Renderer {
-            constructor(canvas: HTMLCanvasElement, backend: any);
+            constructor(canvas: HTMLElement, backend: number);
 
-            public static Backends: any;
+            public static Backends: {
+                CANVAS: number,
+                RAPHAEL: number,
+                SVG: number,
+                VML: number
+            };
 
             public resize(a: number, b: number): void;
 
-            public getContext(): CanvasContext;
+            public getContext(): CanvasContext|SVGContext;
         }
 
         export class TimeSignature {
@@ -175,7 +180,7 @@ declare namespace Vex {
         export class Beam {
             constructor(notes: StaveNote[], auto_stem: boolean);
 
-            public setContext(ctx: CanvasContext): Beam;
+            public setContext(ctx: RenderContext): Beam;
 
             public draw(): void;
         }
@@ -183,13 +188,28 @@ declare namespace Vex {
         export class Tuplet {
             constructor(notes: StaveNote[]);
 
-            public setContext(ctx: CanvasContext): Tuplet;
+            public setContext(ctx: RenderContext): Tuplet;
 
             public draw(): void;
         }
 
-        export class CanvasContext {
-            public scale(x: number, y: number): CanvasContext;
+        export class RenderContext {
+            public scale(x: number, y: number): RenderContext;
+            public fillRect(x: number, y: number, width: number, height: number): RenderContext
+            public fillText(text: string, x: number, y: number): RenderContext;
+            public setFont(family: string, size: number, weight: string): RenderContext;
+            public save(): RenderContext;
+            public restore(): RenderContext;
+        }
+
+        export class CanvasContext extends RenderContext {
+            public vexFlowCanvasContext: CanvasRenderingContext2D;
+        }
+
+        export class SVGContext extends RenderContext {
+            public svg: SVGElement;
+            public attributes: any;
+            public state: any;
         }
 
         export class StaveConnector {
@@ -199,11 +219,10 @@ declare namespace Vex {
 
             public setType(type: any): StaveConnector;
 
-            public setContext(ctx: CanvasContext): StaveConnector;
+            public setContext(ctx: RenderContext): StaveConnector;
 
             public draw(): void;
         }
-
     }
 }
 

+ 64 - 0
src/MusicalScore/Graphical/VexFlow/CanvasVexFlowBackend.ts

@@ -0,0 +1,64 @@
+import Vex = require("vexflow");
+
+import {VexFlowBackend} from "./VexFlowBackend";
+import {FontStyles} from "../../../Common/Enums/FontStyles";
+import {Fonts} from "../../../Common/Enums/Fonts";
+import {RectangleF2D} from "../../../Common/DataObjects/RectangleF2D";
+import {PointF2D} from "../../../Common/DataObjects/PointF2D";
+import {VexFlowConverter} from "./VexFlowConverter";
+
+export class CanvasVexFlowBackend extends VexFlowBackend {
+
+    public getBackendType(): number {
+        return Vex.Flow.Renderer.Backends.CANVAS;
+    }
+
+    public initialize(container: HTMLElement): void {
+        this.canvas = document.createElement("canvas");
+        this.inner = document.createElement("div");
+        this.inner.style.position = "relative";
+        this.canvas.style.zIndex = "0";
+        this.inner.appendChild(this.canvas);
+        container.appendChild(this.inner);
+        this.renderer = new Vex.Flow.Renderer(this.canvas, this.getBackendType());
+        this.ctx = <Vex.Flow.CanvasContext>this.renderer.getContext();
+        this.canvasRenderingCtx = this.ctx.vexFlowCanvasContext;
+
+    }
+
+    public getContext(): Vex.Flow.CanvasContext {
+        return this.ctx;
+    }
+
+    public clear(): void {
+        // Doesn't need to do anything
+    }
+
+    public scale(k: number): void {
+        this.ctx.scale(k, k);
+    }
+
+    public translate(x: number, y: number): void {
+        this.canvasRenderingCtx.translate(x, y);
+    }
+    public renderText(fontHeight: number, fontStyle: FontStyles, font: Fonts, text: string,
+                      heightInPixel: number, screenPosition: PointF2D): void  {
+        let old: string = this.canvasRenderingCtx.font;
+        this.canvasRenderingCtx.font = VexFlowConverter.font(
+            fontHeight,
+            fontStyle,
+            font
+        );
+        this.canvasRenderingCtx.fillText(text, screenPosition.x, screenPosition.y + heightInPixel);
+        this.canvasRenderingCtx.font = old;
+    }
+    public renderRectangle(rectangle: RectangleF2D, styleId: number): void {
+        let old: string | CanvasGradient | CanvasPattern = this.canvasRenderingCtx.fillStyle;
+        this.canvasRenderingCtx.fillStyle = VexFlowConverter.style(styleId);
+        this.ctx.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
+        this.canvasRenderingCtx.fillStyle = old;
+    }
+
+    private ctx: Vex.Flow.CanvasContext;
+    private canvasRenderingCtx: CanvasRenderingContext2D;
+}

+ 66 - 0
src/MusicalScore/Graphical/VexFlow/SvgVexFlowBackend.ts

@@ -0,0 +1,66 @@
+import Vex = require("vexflow");
+
+import {VexFlowBackend} from "./VexFlowBackend";
+import {VexFlowConverter} from "./VexFlowConverter";
+import {FontStyles} from "../../../Common/Enums/FontStyles";
+import {Fonts} from "../../../Common/Enums/Fonts";
+import {RectangleF2D} from "../../../Common/DataObjects/RectangleF2D";
+import {PointF2D} from "../../../Common/DataObjects/PointF2D";
+
+export class SvgVexFlowBackend extends VexFlowBackend {
+
+    public getBackendType(): number {
+        return Vex.Flow.Renderer.Backends.SVG;
+    }
+
+    public initialize(container: HTMLElement): void {
+        this.canvas = document.createElement("div");
+        this.inner = this.canvas;
+        this.inner.style.position = "relative";
+        this.canvas.style.zIndex = "0";
+        container.appendChild(this.inner);
+        this.renderer = new Vex.Flow.Renderer(this.canvas, this.getBackendType());
+        this.ctx = <Vex.Flow.SVGContext>this.renderer.getContext();
+
+    }
+
+    public getContext(): Vex.Flow.SVGContext {
+        return this.ctx;
+    }
+
+    public clear(): void {
+        const { svg } = this.ctx;
+        if (!svg) {
+            return;
+        }
+        // removes all children from the SVG element,
+        // effectively clearing the SVG viewport
+        while (svg.lastChild) {
+            svg.removeChild(svg.lastChild);
+        }
+    }
+
+    public scale(k: number): void {
+        this.ctx.scale(k, k);
+    }
+
+    public translate(x: number, y: number): void {
+        // TODO: implement this
+    }
+    public renderText(fontHeight: number, fontStyle: FontStyles, font: Fonts, text: string,
+                      heightInPixel: number, screenPosition: PointF2D): void {
+        this.ctx.save();
+
+        this.ctx.setFont("Times New Roman", fontHeight, VexFlowConverter.fontStyle(fontStyle));
+        // font size is set by VexFlow in `pt`. This overwrites the font so it's set to px instead
+        this.ctx.attributes["font-size"] = `${fontHeight}px`;
+        this.ctx.state["font-size"] = `${fontHeight}px`;
+        this.ctx.fillText(text, screenPosition.x, screenPosition.y + heightInPixel);
+        this.ctx.restore();
+    }
+    public renderRectangle(rectangle: RectangleF2D, styleId: number): void {
+        this.ctx.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
+    }
+
+    private ctx: Vex.Flow.SVGContext;
+}

+ 54 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowBackend.ts

@@ -0,0 +1,54 @@
+import * as Vex from "vexflow";
+import {FontStyles} from "../../../Common/Enums/FontStyles";
+import {Fonts} from "../../../Common/Enums/Fonts";
+import {RectangleF2D} from "../../../Common/DataObjects/RectangleF2D";
+import {PointF2D} from "../../../Common/DataObjects/PointF2D";
+
+export class VexFlowBackends {
+  public static CANVAS: 0;
+  public static RAPHAEL: 1;
+  public static SVG: 2;
+  public static VML: 3;
+
+}
+
+export abstract class VexFlowBackend {
+
+  public abstract initialize(container: HTMLElement): void;
+
+  public getInnerElement(): HTMLElement {
+    return this.inner;
+  }
+
+  public getCanvas(): HTMLElement {
+    return this.canvas;
+  }
+
+  public getRenderer(): Vex.Flow.Renderer {
+    return this.renderer;
+  }
+
+  public abstract getContext(): Vex.Flow.RenderContext;
+
+  // public abstract setWidth(width: number): void;
+  // public abstract setHeight(height: number): void;
+
+  public abstract scale(k: number): void;
+
+  public resize(x: number, y: number): void {
+    this.renderer.resize(x, y);
+  }
+
+  public abstract clear(): void;
+
+  public abstract translate(x: number, y: number): void;
+  public abstract renderText(fontHeight: number, fontStyle: FontStyles, font: Fonts, text: string,
+                             heightInPixel: number, screenPosition: PointF2D): void;
+  public abstract renderRectangle(rectangle: RectangleF2D, styleId: number): void;
+
+  public abstract getBackendType(): number;
+
+  protected renderer: Vex.Flow.Renderer;
+  protected inner: HTMLElement;
+  protected canvas: HTMLElement;
+}

+ 17 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowConverter.ts

@@ -380,6 +380,23 @@ export class VexFlowConverter {
     }
 
     /**
+     * Converts the style into a string that VexFlow RenderContext can understand
+     * as the weight of the font
+     */
+    public static fontStyle(style: FontStyles): string {
+        switch (style) {
+            case FontStyles.Bold:
+                return "bold";
+            case FontStyles.Italic:
+                return "italic";
+            case FontStyles.BoldItalic:
+                return "italic bold";
+            default:
+                return "normal";
+        }
+    }
+
+    /**
      * Convert OutlineAndFillStyle to CSS properties
      * @param styleId
      * @returns {string}

+ 1 - 1
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -181,7 +181,7 @@ export class VexFlowMeasure extends StaffMeasure {
      * Draw this measure on a VexFlow CanvasContext
      * @param ctx
      */
-    public draw(ctx: Vex.Flow.CanvasContext): void {
+    public draw(ctx: Vex.Flow.RenderContext): void {
         // If this is the first stave in the vertical measure, call the format
         // method to set the width of all the voices
         if (this.formatVoices) {

+ 20 - 27
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer.ts

@@ -4,12 +4,12 @@ import {RectangleF2D} from "../../../Common/DataObjects/RectangleF2D";
 import {VexFlowMeasure} from "./VexFlowMeasure";
 import {PointF2D} from "../../../Common/DataObjects/PointF2D";
 import {GraphicalLabel} from "../GraphicalLabel";
-import {VexFlowConverter} from "./VexFlowConverter";
 import {VexFlowTextMeasurer} from "./VexFlowTextMeasurer";
 import {MusicSystem} from "../MusicSystem";
 import {GraphicalObject} from "../GraphicalObject";
 import {GraphicalLayers} from "../DrawingEnums";
 import {GraphicalStaffEntry} from "../GraphicalStaffEntry";
+import {VexFlowBackend} from "./VexFlowBackend";
 
 /**
  * This is a global contant which denotes the height in pixels of the space between two lines of the stave
@@ -20,17 +20,19 @@ export const unitInPixels: number = 10;
 
 export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
     private renderer: Vex.Flow.Renderer;
-    private vfctx: Vex.Flow.CanvasContext;
-    private ctx: CanvasRenderingContext2D;
+    private backend: VexFlowBackend;
     private zoom: number = 1.0;
 
-    constructor(canvas: HTMLCanvasElement, isPreviewImageDrawer: boolean = false) {
+    constructor(element: HTMLElement,
+                backend: VexFlowBackend,
+                isPreviewImageDrawer: boolean = false) {
         super(new VexFlowTextMeasurer(), isPreviewImageDrawer);
-        this.renderer = new Vex.Flow.Renderer(canvas, Vex.Flow.Renderer.Backends.CANVAS);
-        this.vfctx = this.renderer.getContext();
-        // The following is a hack to retrieve the actual canvas' drawing context
-        // Not supposed to work forever....
-        this.ctx = (this.vfctx as any).vexFlowCanvasContext;
+        this.backend = backend;
+        this.renderer = this.backend.getRenderer();
+    }
+
+    public clear(): void {
+        this.backend.clear();
     }
 
     /**
@@ -39,7 +41,7 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
      */
     public scale(k: number): void {
         this.zoom = k;
-        this.vfctx.scale(k, k);
+        this.backend.scale(k);
     }
 
     /**
@@ -48,12 +50,11 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
      * @param y
      */
     public resize(x: number, y: number): void {
-        this.renderer.resize(x, y);
+        this.backend.resize(x, y);
     }
 
     public translate(x: number, y: number): void {
-        // Translation seems not supported by VexFlow
-        this.ctx.translate(x, y);
+        this.backend.translate(x, y);
     }
 
     /**
@@ -70,7 +71,7 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
             measure.PositionAndShape.AbsolutePosition.x * unitInPixels,
             measure.PositionAndShape.AbsolutePosition.y * unitInPixels
         );
-        measure.draw(this.vfctx);
+        measure.draw(this.backend.getContext());
 
         // Draw the StaffEntries
         for (let staffEntry of measure.staffEntries){
@@ -104,15 +105,10 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
      */
     protected renderLabel(graphicalLabel: GraphicalLabel, layer: number, bitmapWidth: number,
                           bitmapHeight: number, heightInPixel: number, screenPosition: PointF2D): void {
-        let ctx: CanvasRenderingContext2D = (this.vfctx as any).vexFlowCanvasContext;
-        let old: string = ctx.font;
-        ctx.font = VexFlowConverter.font(
-            graphicalLabel.Label.fontHeight * unitInPixels,
-            graphicalLabel.Label.fontStyle,
-            graphicalLabel.Label.font
-        );
-        ctx.fillText(graphicalLabel.Label.text, screenPosition.x, screenPosition.y + heightInPixel);
-        ctx.font = old;
+        const height: number = graphicalLabel.Label.fontHeight * unitInPixels;
+        const { fontStyle, font, text } = graphicalLabel.Label;
+
+        this.backend.renderText(height, fontStyle, font, text, heightInPixel, screenPosition);
     }
 
     /**
@@ -123,10 +119,7 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
      * @param styleId the style id
      */
     protected renderRectangle(rectangle: RectangleF2D, layer: number, styleId: number): void {
-        let old: string|CanvasGradient|CanvasPattern = this.ctx.fillStyle;
-        this.ctx.fillStyle = VexFlowConverter.style(styleId);
-        this.ctx.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
-        this.ctx.fillStyle = old;
+       this.backend.renderRectangle(rectangle, styleId);
     }
 
     /**

+ 22 - 12
src/OSMD/OSMD.ts

@@ -1,9 +1,12 @@
 import {IXmlElement} from "./../Common/FileIO/Xml";
 import {VexFlowMusicSheetCalculator} from "./../MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator";
+import {VexFlowBackend} from "./../MusicalScore/Graphical/VexFlow/VexFlowBackend";
 import {MusicSheetReader} from "./../MusicalScore/ScoreIO/MusicSheetReader";
 import {GraphicalMusicSheet} from "./../MusicalScore/Graphical/GraphicalMusicSheet";
 import {MusicSheetCalculator} from "./../MusicalScore/Graphical/MusicSheetCalculator";
 import {VexFlowMusicSheetDrawer} from "./../MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer";
+import {SvgVexFlowBackend} from "./../MusicalScore/Graphical/VexFlow/SvgVexFlowBackend";
+import {CanvasVexFlowBackend} from "./../MusicalScore/Graphical/VexFlow/CanvasVexFlowBackend";
 import {MusicSheet} from "./../MusicalScore/MusicSheet";
 import {Cursor} from "./Cursor";
 import {MXLHelper} from "../Common/FileIO/Mxl";
@@ -18,7 +21,7 @@ export class OSMD {
      * @param container is either the ID, or the actual "div" element which will host the music sheet
      * @autoResize automatically resize the sheet to full page width on window resize
      */
-    constructor(container: string|HTMLElement, autoResize: boolean = false) {
+    constructor(container: string|HTMLElement, autoResize: boolean = false, backend: string = "canvas") {
         // Store container element
         if (typeof container === "string") {
             // ID passed
@@ -30,17 +33,22 @@ export class OSMD {
         if (!this.container) {
             throw new Error("Please pass a valid div container to OSMD");
         }
-        // Create the elements inside the container
-        this.canvas = document.createElement("canvas");
-        this.canvas.style.zIndex = "0";
-        let inner: HTMLElement = document.createElement("div");
-        inner.style.position = "relative";
-        inner.appendChild(this.canvas);
-        this.container.appendChild(inner);
+
+        if (backend === "svg") {
+            this.backend = new SvgVexFlowBackend();
+        } else {
+            this.backend = new CanvasVexFlowBackend();
+        }
+
+        this.backend.initialize(this.container);
+        this.canvas = this.backend.getCanvas();
+        const inner: HTMLElement = this.backend.getInnerElement();
+
         // Create the drawer
-        this.drawer = new VexFlowMusicSheetDrawer(this.canvas);
+        this.drawer = new VexFlowMusicSheetDrawer(this.canvas, this.backend, false);
         // Create the cursor
         this.cursor = new Cursor(inner, this);
+
         if (autoResize) {
             this.autoResize();
         }
@@ -50,7 +58,8 @@ export class OSMD {
     public zoom: number = 1.0;
 
     private container: HTMLElement;
-    private canvas: HTMLCanvasElement;
+    private canvas: HTMLElement;
+    private backend: VexFlowBackend;
     private sheet: MusicSheet;
     private drawer: VexFlowMusicSheetDrawer;
     private graphic: GraphicalMusicSheet;
@@ -145,6 +154,7 @@ export class OSMD {
         this.graphic.Cursors.push(this.graphic.calculateCursorLineAtTimestamp(new Fraction(7, 4), OutlineAndFillStyleEnum.PlaybackCursor));*/
         // Update Sheet Page
         let height: number = this.graphic.MusicPages[0].PositionAndShape.BorderBottom * 10.0 * this.zoom;
+        this.drawer.clear();
         this.drawer.resize(width, height);
         this.drawer.scale(this.zoom);
         // Finally, draw
@@ -191,8 +201,8 @@ export class OSMD {
         this.sheet = undefined;
         this.graphic = undefined;
         this.zoom = 1.0;
-        this.canvas.width = 0;
-        this.canvas.height = 0;
+        // this.canvas.width = 0;
+        // this.canvas.height = 0;
     }
 
     /**

+ 8 - 2
test/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer_Test.ts

@@ -7,6 +7,8 @@ import {VexFlowMusicSheetCalculator} from "../../../../src/MusicalScore/Graphica
 import {TestUtils} from "../../../Util/TestUtils";
 import {IXmlElement} from "../../../../src/Common/FileIO/Xml";
 import {Fraction} from "../../../../src/Common/DataObjects/Fraction";
+import {VexFlowBackend} from "../../../../src/MusicalScore/Graphical/VexFlow/VexFlowBackend";
+import {CanvasVexFlowBackend} from "../../../../src/MusicalScore/Graphical/VexFlow/CanvasVexFlowBackend";
 
 /* tslint:disable:no-unused-expression */
 describe("VexFlow Music Sheet Drawer", () => {
@@ -23,7 +25,9 @@ describe("VexFlow Music Sheet Drawer", () => {
 
         // Create the canvas in the document:
         let canvas: HTMLCanvasElement = document.createElement("canvas");
-        let drawer: VexFlowMusicSheetDrawer = new VexFlowMusicSheetDrawer(canvas);
+        let backend: VexFlowBackend = new CanvasVexFlowBackend();
+        backend.initialize(canvas);
+        let drawer: VexFlowMusicSheetDrawer = new VexFlowMusicSheetDrawer(canvas, backend);
         drawer.drawSheet(gms);
         done();
     });
@@ -41,7 +45,9 @@ describe("VexFlow Music Sheet Drawer", () => {
 
         // Create the canvas in the document:
         let canvas: HTMLCanvasElement = document.createElement("canvas");
-        let drawer: VexFlowMusicSheetDrawer = new VexFlowMusicSheetDrawer(canvas);
+        let backend: VexFlowBackend = new CanvasVexFlowBackend();
+        backend.initialize(canvas);
+        let drawer: VexFlowMusicSheetDrawer = new VexFlowMusicSheetDrawer(canvas, backend);
         drawer.drawSheet(gms);
         done();
     });