opensheetmusicdisplay/opensheetmusicdisplay

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

Summary

Maintainability
B
6 hrs
Test Coverage
import Vex from "vexflow";
import VF = Vex.Flow;

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

export class CanvasVexFlowBackend extends VexFlowBackend {
    private zoom: number;

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

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

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

    public getCanvasSize(): number {
        return document.getElementById("osmdCanvasPage" + this.graphicalMusicPage.PageNumber)?.offsetHeight;
        // smaller inner canvas:
        // return Number.parseInt(
        //     document.getElementById("osmdCanvasVexFlowBackendCanvas" + this.graphicalMusicPage.PageNumber)?.style.height, 10);
    }

    public initialize(container: HTMLElement, zoom: number): void {
        this.zoom = zoom;
        this.canvas = document.createElement("canvas");
        if (!this.graphicalMusicPage) {
            this.graphicalMusicPage = new GraphicalMusicPage(undefined);
            this.graphicalMusicPage.PageNumber = 1;
        }
        this.canvas.id = "osmdCanvasVexFlowBackendCanvas" + this.graphicalMusicPage.PageNumber; // needed to extract image buffer from js
        this.inner = document.createElement("div");
        this.inner.id = "osmdCanvasPage" + this.graphicalMusicPage.PageNumber;
        this.inner.style.position = "relative";
        this.canvas.style.zIndex = "0";
        this.inner.appendChild(this.canvas);
        container.appendChild(this.inner);
        this.renderer = new VF.Renderer(this.canvas, this.getVexflowBackendType());
        this.ctx = <VF.CanvasContext>this.renderer.getContext();
    }

    /**
     * Initialize a canvas without attaching it to a DOM node. Can be used to draw in background
     * @param width Width of the canvas
     * @param height Height of the canvas
     */
    public initializeHeadless(width: number = 300, height: number = 300): void {
        if (!this.graphicalMusicPage) {
            // not needed here yet, but just for future safety, make sure the page isn't undefined
            this.graphicalMusicPage = new GraphicalMusicPage(undefined);
            this.graphicalMusicPage.PageNumber = 1;
        }
        this.canvas = document.createElement("canvas");
        (this.canvas as any).width = width;
        (this.canvas as any).height = height;
        this.renderer = new VF.Renderer(this.canvas, this.getVexflowBackendType());
        this.ctx = <VF.CanvasContext>this.renderer.getContext();
    }

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

    public free(): void {
        if (this.canvas) {
            // the following seems to only be necessary on iOS, preventing a memory leak (#1411)
            (this.canvas as any).width = 0;
            (this.canvas as any).height = 0;

            this.canvas.remove();
            delete this.canvas;
            this.canvas = null;
        }
    }

    public clear(): void {
        (<any>this.ctx).clearRect(0, 0, (<any>this.canvas).width, (<any>this.canvas).height);

        // set background color if not transparent
        if (this.rules.PageBackgroundColor) {
            this.ctx.save();
            // note that this will hide the cursor
            this.ctx.setFillStyle(this.rules.PageBackgroundColor);
            this.zoom = 1; // remove
            this.ctx.fillRect(0, 0, (this.canvas as any).width / this.zoom, (this.canvas as any).height / this.zoom);
            this.ctx.restore();
        }
    }

    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,
                      color: string = undefined, fontFamily: string = undefined): Node  {
        const old: string = this.CanvasRenderingCtx.font;
        this.CanvasRenderingCtx.save();
        this.CanvasRenderingCtx.font = VexFlowConverter.font(
            fontHeight,
            fontStyle,
            font,
            this.rules,
            fontFamily
        );
        this.CanvasRenderingCtx.fillStyle = color;
        this.CanvasRenderingCtx.strokeStyle = color;
        this.CanvasRenderingCtx.fillText(text, screenPosition.x, screenPosition.y + heightInPixel);
        this.CanvasRenderingCtx.restore();
        this.CanvasRenderingCtx.font = old;
        return undefined; // can't return svg dom node
    }
    public renderRectangle(rectangle: RectangleF2D, styleId: number, colorHex: string, alpha: number = 1): Node {
        const old: string | CanvasGradient | CanvasPattern = this.CanvasRenderingCtx.fillStyle;
        if (colorHex) {
            this.CanvasRenderingCtx.fillStyle = colorHex;
        } else {
            this.CanvasRenderingCtx.fillStyle = VexFlowConverter.style(styleId);
        }
        this.CanvasRenderingCtx.globalAlpha = alpha;
        this.ctx.fillRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
        this.CanvasRenderingCtx.fillStyle = old;
        this.CanvasRenderingCtx.globalAlpha = 1;
        return undefined; // can't return dom node like with SVG
    }

    public renderLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF", lineWidth: number = 2, id?: string): Node {
        const oldStyle: string | CanvasGradient | CanvasPattern = this.CanvasRenderingCtx.strokeStyle;
        this.CanvasRenderingCtx.strokeStyle = color;
        this.CanvasRenderingCtx.beginPath();
        this.CanvasRenderingCtx.moveTo(start.x, start.y);
        this.CanvasRenderingCtx.lineTo(stop.x, stop.y);
        this.CanvasRenderingCtx.stroke();
        this.CanvasRenderingCtx.strokeStyle = oldStyle;
        return undefined; // can't return svg dom node
    }

    public renderCurve(points: PointF2D[]): Node {
        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();
        return undefined;
    }

    public renderPath(points: PointF2D[], fill: boolean = true, id?: string): Node {
        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
        }
        return undefined;
    }

    private ctx: VF.CanvasContext;

    public get CanvasRenderingCtx(): CanvasRenderingContext2D {
        // This clusterfuck is only there to counter act my favorite vexflow line:
        // ctx.vexFlowCanvasContext = ctx;
        // No idea why they are saving the context but we wrap the types here
        return <CanvasRenderingContext2D>(this.ctx as any).vexFlowCanvasContext;
    }
}