opensheetmusicdisplay/opensheetmusicdisplay

View on GitHub
src/VexFlowPatch/src/renderer.js

Summary

Maintainability
A
2 hrs
Test Coverage
// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
//
// ## Description
// Support for different rendering contexts: Canvas, Raphael

import { CanvasContext } from './canvascontext';
import { RaphaelContext } from './raphaelcontext';
import { SVGContext } from './svgcontext';
import { Vex } from './vex';

let lastContext = null;

export class Renderer {
  static get Backends() {
    return {
      CANVAS: 1,
      RAPHAEL: 2,
      SVG: 3,
      VML: 4,
    };
  }

  // End of line types
  static get LineEndType() {
    return {
      NONE: 1, // No leg
      UP: 2,   // Upward leg
      DOWN: 3, // Downward leg
    };
  }

  // Set this to true if you're using VexFlow inside a runtime
  // that does not allow modifiying canvas objects. There is a small
  // performance degradation due to the extra indirection.
  static get USE_CANVAS_PROXY() {
    return false;
  }

  static get lastContext() {
    return lastContext;
  }
  static set lastContext(ctx) {
    lastContext = ctx;
  }

  static buildContext(elementId, backend, width, height, background) {
    const renderer = new Renderer(elementId, backend);
    if (width && height) {
      renderer.resize(width, height);
    }

    if (!background) background = '#FFF';
    const ctx = renderer.getContext();
    ctx.setBackgroundFillStyle(background);
    Renderer.lastContext = ctx;
    return ctx;
  }

  static getCanvasContext(elementId, width, height, background) {
    return Renderer.buildContext(elementId, Renderer.Backends.CANVAS, width, height, background);
  }

  static getRaphaelContext(elementId, width, height, background) {
    return Renderer.buildContext(elementId, Renderer.Backends.RAPHAEL, width, height, background);
  }

  static getSVGContext(elementId, width, height, background) {
    return Renderer.buildContext(elementId, Renderer.Backends.SVG, width, height, background);
  }

  static bolsterCanvasContext(ctx) {
    if (Renderer.USE_CANVAS_PROXY) {
      return new CanvasContext(ctx);
    }

    const methodNames = [
      'clear', 'setFont', 'setRawFont', 'setFillStyle', 'setBackgroundFillStyle',
      'setStrokeStyle', 'setShadowColor', 'setShadowBlur', 'setLineWidth',
      'setLineCap', 'setLineDash', 'openGroup', 'closeGroup', 'getGroup',
    ];

    ctx.vexFlowCanvasContext = ctx;

    methodNames.forEach(methodName => {
      ctx[methodName] = ctx[methodName] || CanvasContext.prototype[methodName];
    });

    return ctx;
  }

  // Draw a dashed line (horizontal, vertical or diagonal
  // dashPattern = [3,3] draws a 3 pixel dash followed by a three pixel space.
  // setting the second number to 0 draws a solid line.
  static drawDashedLine(context, fromX, fromY, toX, toY, dashPattern) {
    context.beginPath();

    const dx = toX - fromX;
    const dy = toY - fromY;
    const angle = Math.atan2(dy, dx);
    let x = fromX;
    let y = fromY;
    context.moveTo(fromX, fromY);
    let idx = 0;
    let draw = true;
    while (!((dx < 0 ? x <= toX : x >= toX) && (dy < 0 ? y <= toY : y >= toY))) {
      const dashLength = dashPattern[idx++ % dashPattern.length];
      const nx = x + (Math.cos(angle) * dashLength);
      x = dx < 0 ? Math.max(toX, nx) : Math.min(toX, nx);
      const ny = y + (Math.sin(angle) * dashLength);
      y = dy < 0 ? Math.max(toY, ny) : Math.min(toY, ny);
      if (draw) {
        context.lineTo(x, y);
      } else {
        context.moveTo(x, y);
      }
      draw = !draw;
    }

    context.closePath();
    context.stroke();
  }

  constructor(elementId, backend) {
    this.elementId = elementId;
    if (!this.elementId) {
      throw new Vex.RERR('BadArgument', 'Invalid id for renderer.');
    }

    this.element = document.getElementById(elementId);
    if (!this.element) this.element = elementId;

    // Verify backend and create context
    this.ctx = null;
    this.paper = null;
    this.backend = backend;
    if (this.backend === Renderer.Backends.CANVAS) {
      // Create context.
      if (!this.element.getContext) {
        throw new Vex.RERR('BadElement', `Can't get canvas context from element: ${elementId}`);
      }
      // VexFlowPatch: add willReadFrequently attribute for getImageData() performance
      this.ctx = Renderer.bolsterCanvasContext(this.element.getContext('2d', {willReadFrequently: true}));
    } else if (this.backend === Renderer.Backends.RAPHAEL) {
      this.ctx = new RaphaelContext(this.element);
    } else if (this.backend === Renderer.Backends.SVG) {
      this.ctx = new SVGContext(this.element);
    } else {
      throw new Vex.RERR('InvalidBackend', `No support for backend: ${this.backend}`);
    }
  }

  resize(width, height) {
    if (this.backend === Renderer.Backends.CANVAS) {
      if (!this.element.getContext) {
        throw new Vex.RERR(
          'BadElement', `Can't get canvas context from element: ${this.elementId}`
        );
      }
      [width, height] = CanvasContext.SanitizeCanvasDims(width, height);

      const devicePixelRatio = window.devicePixelRatio || 1;

      this.element.width = width * devicePixelRatio;
      this.element.height = height * devicePixelRatio;
      this.element.style.width = width + 'px';
      this.element.style.height = height + 'px';

      // VexFlowPatch: add willReadFrequently attribute for getImageData() performance
      this.ctx = Renderer.bolsterCanvasContext(this.element.getContext('2d', {willReadFrequently: true}));
      this.ctx.scale(devicePixelRatio, devicePixelRatio);
    } else {
      this.ctx.resize(width, height);
    }

    return this;
  }

  getContext() { return this.ctx; }
}