opensheetmusicdisplay/opensheetmusicdisplay

View on GitHub
src/MusicalScore/Graphical/VexFlow/SvgVexFlowBackend.ts

Summary

Maintainability
C
1 day
Test Coverage
import Vex from "vexflow";
import VF = Vex.Flow;

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";
import {BackendType} from "../../../OpenSheetMusicDisplay/OSMDOptions";
import {EngravingRules} from "../EngravingRules";
import log from "loglevel";

export class SvgVexFlowBackend extends VexFlowBackend {

    private ctx: VF.SVGContext;
    public zoom: number; // currently unused

    constructor(rules: EngravingRules) {
        super();
        this.rules = rules;
    }

    public getVexflowBackendType(): VF.Renderer.Backends {
        return VF.Renderer.Backends.SVG;
    }

    public getOSMDBackendType(): BackendType {
        return BackendType.SVG;
    }

    public getCanvasSize(): number {
        return document.getElementById("osmdCanvasPage" + this.graphicalMusicPage.PageNumber)?.offsetHeight;
    }

    public initialize(container: HTMLElement, zoom: number): void {
        this.zoom = zoom;
        this.canvas = document.createElement("div");
        this.canvas.id = "osmdCanvasPage" + this.graphicalMusicPage.PageNumber;
        // this.canvas.id = uniqueID // TODO create unique tagName like with cursor now?
        this.inner = this.canvas;
        this.inner.style.position = "relative";
        this.canvas.style.zIndex = "0";
        container.appendChild(this.inner);
        this.renderer = new VF.Renderer(this.canvas, this.getVexflowBackendType());
        this.ctx = <VF.SVGContext>this.renderer.getContext();
        this.ctx.svg.id = "osmdSvgPage" + this.graphicalMusicPage.PageNumber;
    }

    public getContext(): VF.SVGContext {
        return this.ctx;
    }

    public getSvgElement(): SVGElement {
        return this.ctx.svg;
    }

    removeNode(node: Node): boolean {
        const svg: SVGElement = this.ctx?.svg;
        if (!svg) {
            return false;
        }
        // unfortunately there's no method svg.hasChild(node). traversing all nodes seems inefficient.
        try {
            svg.removeChild(node);
        } catch (ex) {
            // log.error("SvgVexFlowBackend.removeNode: error:"); // unnecessary, stacktrace is in exception
            log.error(ex);
            return false;
        }
        return true;
    }

    public free(): void {
        //const { svg } = this.ctx; // seems to make svg static between osmd instances.
        const svg: SVGElement = this.ctx.svg;
        // removes all children from the SVG element,
        // effectively clearing the SVG viewport
        while (svg.lastChild) {
            svg.removeChild(svg.lastChild);
        }
    }

    public clear(): void {
        if (!this.ctx) {
            return;
        }
        this.free();

        // set background color if not transparent
        if (this.rules.PageBackgroundColor) {
        //     this.ctx.save();
        //     // note that this will hide the cursor if its zIndex is negative.
        //     this.ctx.setFillStyle(this.rules.PageBackgroundColor);
        //     this.ctx.setStrokeStyle("#12345600"); // transparent

        //     this.ctx.fillRect(0, 0, this.canvas.offsetWidth / this.zoom, this.canvas.offsetHeight / this.zoom);
        //     this.ctx.restore();
            this.ctx.svg.style["background-color"] = this.rules.PageBackgroundColor;
            // note that the cursor would be invisible if its zIndex remained negative here,
            //   so we have to push it to a higher layer and make it more transparent.
            // effectively, setting a background color will make the cursor more transparent.
        }
    }

    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,
                      color: string = undefined, fontFamily: string = undefined): Node {
        this.ctx.save();
        const node: Node = this.ctx.openGroup("text");

        if (color) {
            this.ctx.attributes.fill = color;
            this.ctx.attributes.stroke = color;
        }
        let fontFamilyVexFlow: string = fontFamily;
        if (!fontFamily || fontFamily === "default") {
            fontFamilyVexFlow = this.rules.DefaultFontFamily;
        }
        this.ctx.setFont(fontFamilyVexFlow, 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`;
        let fontWeightVexflow: string = "normal";
        let fontStyleVexflow: string = "normal";
        switch (fontStyle) {
            case FontStyles.Bold:
                fontWeightVexflow = "bold";
                break;
            case FontStyles.Italic:
                fontStyleVexflow = "italic";
                break;
            case FontStyles.BoldItalic:
                fontWeightVexflow = "bold";
                fontStyleVexflow = "italic";
                break;
            default:
                fontWeightVexflow = "normal";
        }
        this.ctx.attributes["font-weight"] = fontWeightVexflow;
        this.ctx.state["font-weight"] = fontWeightVexflow;
        this.ctx.attributes["font-style"] = fontStyleVexflow;
        this.ctx.state["font-style"] = fontStyleVexflow;
        this.ctx.fillText(text, screenPosition.x, screenPosition.y + heightInPixel);
        this.ctx.closeGroup();
        this.ctx.restore();
        return node;
    }
    public renderRectangle(rectangle: RectangleF2D, styleId: number, colorHex: string, alpha: number = 1): Node {
        this.ctx.save();
        const node: Node = this.ctx.openGroup("rect");
        if (colorHex) {
            this.ctx.attributes.fill = colorHex;
        } else {
            this.ctx.attributes.fill = VexFlowConverter.style(styleId);
        }
        this.ctx.attributes["fill-opacity"] = alpha;
        this.ctx.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
        this.ctx.restore();
        this.ctx.attributes["fill-opacity"] = 1;
        this.ctx.closeGroup();
        return node;
    }

    public renderLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF", lineWidth: number = 2, id?: string): Node {
        this.ctx.save();
        const node: Node = this.ctx.openGroup("line", id);
        this.ctx.beginPath();
        this.ctx.moveTo(start.x, start.y);
        this.ctx.lineTo(stop.x, stop.y);

        this.ctx.attributes.stroke = color;
        //this.ctx.attributes.strokeStyle = color;
        //this.ctx.attributes["font-weight"] = "bold";
        //this.ctx.attributes["stroke-linecap"] = "round";

        this.ctx.lineWidth = lineWidth;

        this.ctx.stroke();
        this.ctx.closeGroup();
        this.ctx.restore();
        return node;
    }

    public renderCurve(points: PointF2D[]): Node {
        const node: Node = this.ctx.openGroup("curve");
        this.ctx.beginPath();
        this.ctx.moveTo(points[0].x, points[0].y);
        this.ctx.bezierCurveTo(
            points[1].x,
            points[1].y,
            points[2].x,
            points[2].y,
            points[3].x,
            points[3].y
            );
        this.ctx.lineTo(points[7].x, points[7].y);
        this.ctx.bezierCurveTo(
            points[6].x,
            points[6].y,
            points[5].x,
            points[5].y,
            points[4].x,
            points[4].y
            );
        this.ctx.lineTo(points[0].x, points[0].y);
        //this.ctx.stroke();
        this.ctx.closePath();
        this.ctx.fill();
        this.ctx.closeGroup();
        return node;
    }

    public renderPath(points: PointF2D[], fill: boolean = true, id?: string): Node {
        const node: Node = this.ctx.openGroup("path", id);
        this.ctx.beginPath();
        let currentPoint: PointF2D;
        for (const point of points) {
            if (!currentPoint) {
                this.ctx.moveTo(point.x, point.y);
                currentPoint = point;
                continue;
            }
            this.ctx.lineTo(point.x, point.y);
            // this.ctx.stroke();
        }
        this.ctx.closePath();
        if (fill) {
            this.ctx.fill();
        } else {
            this.ctx.stroke(); // just trace outline, don't fill inner area
        }
        this.ctx.stroke();
        this.ctx.closeGroup();
        return node;
    }

    public export(): void {
        // See: https://stackoverflow.com/questions/38477972/javascript-save-svg-element-to-file-on-disk

        // first create a clone of our svg node so we don't mess the original one
        const clone: SVGElement = (this.ctx.svg.cloneNode(true) as SVGElement);

        // create a doctype that is SVG
        const svgDocType: DocumentType = document.implementation.createDocumentType(
            "svg",
            "-//W3C//DTD SVG 1.1//EN",
            "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"
        );
        // Create a new svg document
        const svgDoc: Document = document.implementation.createDocument("http://www.w3.org/2000/svg", "svg", svgDocType);
        // replace the documentElement with our clone
        svgDoc.replaceChild(clone, svgDoc.documentElement);

        // get the data
        const svgData: string = (new XMLSerializer()).serializeToString(svgDoc);

        // now you've got your svg data, the following will depend on how you want to download it
        // e.g yo could make a Blob of it for FileSaver.js
        /*
        var blob = new Blob([svgData.replace(/></g, '>\n\r<')]);
        saveAs(blob, 'myAwesomeSVG.svg');
        */
        // here I'll just make a simple a with download attribute

        const a: HTMLAnchorElement = document.createElement("a");
        a.href = "data:image/svg+xml; charset=utf8, " + encodeURIComponent(svgData.replace(/></g, ">\n\r<"));
        a.download = "opensheetmusicdisplay_download.svg";
        a.innerHTML = window.location.href + "/download";
        document.body.appendChild(a);
      }
}