UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature.

331 lines (276 loc) 9.49 kB
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010. // MIT License import { Font, FontInfo } from './font'; import { GroupAttributes, RenderContext, TextMeasure } from './rendercontext'; import { globalObject, warn } from './util'; import { isHTMLCanvas } from './web'; // https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/offscreencanvas // https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/master/types/offscreencanvas/index.d.ts interface OffscreenCanvas extends EventTarget { width: number; height: number; // ...more stuff that we removed. } // https://html.spec.whatwg.org/multipage/canvas.html#offscreencanvasrenderingcontext2d interface OffscreenCanvasRenderingContext2D extends CanvasState, CanvasTransform, CanvasCompositing, CanvasImageSmoothing, CanvasFillStrokeStyles, CanvasShadowStyles, CanvasFilters, CanvasRect, CanvasDrawPath, CanvasText, CanvasDrawImage, CanvasImageData, CanvasPathDrawingStyles, CanvasTextDrawingStyles, CanvasPath { readonly canvas: OffscreenCanvas; } /** * A rendering context for the Canvas backend. This class serves as a proxy for the * underlying CanvasRenderingContext2D object, part of the browser's API. */ export class CanvasContext extends RenderContext { /** The 2D rendering context from the Canvas API. Forward method calls to this object. */ context2D: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; /** * The HTMLCanvasElement or OffscreenCanvas that is associated with the above context. * If there was no associated `<canvas>` element, just store the default WIDTH / HEIGHT. */ canvas: HTMLCanvasElement | OffscreenCanvas | { width: number; height: number }; /** Height of one line of text (in pixels). */ textHeight: number = 0; static get WIDTH(): number { return 600; } static get HEIGHT(): number { return 400; } static get CANVAS_BROWSER_SIZE_LIMIT(): number { return 32767; // Chrome/Firefox. Could be determined more precisely by npm module canvas-size. } /** * Ensure that width and height do not exceed the browser limit. * @returns array of [width, height] clamped to the browser limit. */ static sanitizeCanvasDims(width: number, height: number): [number, number] { const limit = this.CANVAS_BROWSER_SIZE_LIMIT; if (Math.max(width, height) > limit) { warn('Canvas dimensions exceed browser limit. Cropping to ' + limit); if (width > limit) { width = limit; } if (height > limit) { height = limit; } } return [width, height]; } constructor(context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D) { super(); this.context2D = context; if (!context.canvas) { this.canvas = { width: CanvasContext.WIDTH, height: CanvasContext.HEIGHT, }; } else { this.canvas = context.canvas; } } /** * Set all pixels to transparent black rgba(0,0,0,0). */ clear(): void { this.context2D.clearRect(0, 0, this.canvas.width, this.canvas.height); } // eslint-disable-next-line openGroup(cls?: string, id?: string, attrs?: GroupAttributes): any { // Containers not implemented. } closeGroup(): void { // Containers not implemented. } // eslint-disable-next-line add(child: any): void { // Containers not implemented. } setFillStyle(style: string): this { this.context2D.fillStyle = style; return this; } /** CanvasContext ignores `setBackgroundFillStyle()`. */ // eslint-disable-next-line setBackgroundFillStyle(style: string): this { // DO NOTHING return this; } setStrokeStyle(style: string): this { this.context2D.strokeStyle = style; return this; } setShadowColor(color: string): this { this.context2D.shadowColor = color; return this; } setShadowBlur(blur: number): this { // CanvasRenderingContext2D does not scale the shadow blur by the current // transform, so we have to do it manually. We assume uniform scaling // (though allow for rotation) because the blur can only be scaled // uniformly anyway. const t = this.context2D.getTransform(); const scale = Math.sqrt(t.a * t.a + t.b * t.b + t.c * t.c + t.d * t.d); this.context2D.shadowBlur = scale * blur; return this; } setLineWidth(width: number): this { this.context2D.lineWidth = width; return this; } setLineCap(capType: CanvasLineCap): this { this.context2D.lineCap = capType; return this; } setLineDash(dash: number[]): this { this.context2D.setLineDash(dash); return this; } scale(x: number, y: number): this { this.context2D.scale(x, y); return this; } resize(width: number, height: number, devicePixelRatio?: number): this { const canvas = this.context2D.canvas; const dpr: number = devicePixelRatio ?? globalObject().devicePixelRatio ?? 1; // Scale the canvas size by the device pixel ratio clamping to the maximum supported size. [width, height] = CanvasContext.sanitizeCanvasDims(width * dpr, height * dpr); // Divide back down by the pixel ratio and convert to integers. width = (width / dpr) | 0; height = (height / dpr) | 0; canvas.width = width * dpr; canvas.height = height * dpr; // The canvas could be an instance of either HTMLCanvasElement or an OffscreenCanvas. // Only HTMLCanvasElement has a style attribute. if (isHTMLCanvas(canvas)) { canvas.style.width = width + 'px'; canvas.style.height = height + 'px'; } return this.scale(dpr, dpr); } rect(x: number, y: number, width: number, height: number): this { this.context2D.rect(x, y, width, height); return this; } fillRect(x: number, y: number, width: number, height: number): this { this.context2D.fillRect(x, y, width, height); return this; } /** * Set the pixels in a rectangular area to transparent black rgba(0,0,0,0). */ clearRect(x: number, y: number, width: number, height: number): this { this.context2D.clearRect(x, y, width, height); return this; } beginPath(): this { this.context2D.beginPath(); return this; } moveTo(x: number, y: number): this { this.context2D.moveTo(x, y); return this; } lineTo(x: number, y: number): this { this.context2D.lineTo(x, y); return this; } bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): this { this.context2D.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); return this; } quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): this { this.context2D.quadraticCurveTo(cpx, cpy, x, y); return this; } arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise: boolean): this { this.context2D.arc(x, y, radius, startAngle, endAngle, counterclockwise); return this; } fill(): this { this.context2D.fill(); return this; } stroke(): this { this.context2D.stroke(); return this; } closePath(): this { this.context2D.closePath(); return this; } measureText(text: string): TextMeasure { const metrics = this.context2D.measureText(text); let y = 0; let height = 0; if (metrics.fontBoundingBoxAscent) { y = -metrics.fontBoundingBoxAscent; height = metrics.fontBoundingBoxDescent + metrics.fontBoundingBoxAscent; } else { y = -metrics.actualBoundingBoxAscent; height = metrics.actualBoundingBoxDescent + metrics.actualBoundingBoxAscent; } // Return x, y, width & height in the same manner as svg getBBox return { x: 0, y: y, width: metrics.width, height: height, }; } fillText(text: string, x: number, y: number): this { this.context2D.fillText(text, x, y); return this; } save(): this { this.context2D.save(); return this; } restore(): this { this.context2D.restore(); return this; } set fillStyle(style: string | CanvasGradient | CanvasPattern) { this.context2D.fillStyle = style; } get fillStyle(): string | CanvasGradient | CanvasPattern { return this.context2D.fillStyle; } set strokeStyle(style: string | CanvasGradient | CanvasPattern) { this.context2D.strokeStyle = style; } get strokeStyle(): string | CanvasGradient | CanvasPattern { return this.context2D.strokeStyle; } /** * @param f is 1) a `FontInfo` object or * 2) a string formatted as CSS font shorthand (e.g., 'bold 10pt Arial') or * 3) a string representing the font family (one of `size`, `weight`, or `style` must also be provided). * @param size a string specifying the font size and unit (e.g., '16pt'), or a number (the unit is assumed to be 'pt'). * @param weight is a string (e.g., 'bold', 'normal') or a number (100, 200, ... 900). * @param style is a string (e.g., 'italic', 'normal'). */ setFont(f?: string | FontInfo, size?: string | number, weight?: string | number, style?: string): this { const fontInfo = Font.validate(f, size, weight, style); this.context2D.font = Font.toCSSString(fontInfo); this.textHeight = Font.convertSizeToPixelValue(fontInfo.size); return this; } /** Return a string of the form `'italic bold 15pt Arial'` */ getFont(): string { return this.context2D.font; } }