From 758a039d94a0e0c7d2a6b5b3f2ce78c0748bfdda Mon Sep 17 00:00:00 2001 From: Tom Madams Date: Mon, 27 Sep 2021 10:19:29 -0700 Subject: [PATCH 1/4] allow render contexts to be passed directly to the Renderer constructor --- src/canvascontext.ts | 23 ++++++-- src/factory.ts | 27 +++++---- src/renderer.ts | 127 +++++++++++++++++++++---------------------- 3 files changed, 93 insertions(+), 84 deletions(-) diff --git a/src/canvascontext.ts b/src/canvascontext.ts index 5b17332f55..7d24f9e3aa 100644 --- a/src/canvascontext.ts +++ b/src/canvascontext.ts @@ -146,18 +146,29 @@ export class CanvasContext implements RenderContext { return this; } - // Only called if Renderer.USE_CANVAS_PROXY is true. scale(x: number, y: number): this { this.vexFlowCanvasContext.scale(x, y); return this; } - // CanvasRenderingContext2D does not have a resize function. - // renderer.ts calls ctx.scale() instead, so this method is never used. - // eslint-disable-next-line resize(width: number, height: number): this { - // DO NOTHING. - return this; + const canvasElement = this.vexFlowCanvasContext.canvas; + const devicePixelRatio = window.devicePixelRatio || 1; + + // Scale the canvas size by the device pixel ratio clamping to the maximum + // supported size. + [width, height] = CanvasContext.SanitizeCanvasDims(width * devicePixelRatio, height * devicePixelRatio); + + // Divide back down by the pixel ratio and convert to integers. + width = (width / devicePixelRatio) | 0; + height = (height / devicePixelRatio) | 0; + + canvasElement.width = width * devicePixelRatio; + canvasElement.height = height * devicePixelRatio; + canvasElement.style.width = width + 'px'; + canvasElement.style.height = height + 'px'; + + return this.scale(devicePixelRatio, devicePixelRatio); } rect(x: number, y: number, width: number, height: number): this { diff --git a/src/factory.ts b/src/factory.ts index e4cbf5b082..fa98bdb656 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -109,7 +109,6 @@ export class Factory { }, renderer: { elementId: '', - backend: Renderer.Backends.SVG, width: 500, height: 200, background: '#FFF', @@ -126,7 +125,11 @@ export class Factory { } /** - * Static simplified function to access constructor without providing FactoryOptions + * Static simplified function to access constructor without providing FactoryOptions. + * + * The type of renderer created depends on the type of element specified. A CANVAS renderer + * will be created for an HTMLCanvasElement and an SVG renderer will be created for an + * HTMLDivElement. * * Example: * @@ -135,7 +138,7 @@ export class Factory { * `const vf: Factory = Vex.Flow.Factory.newFromElementId('boo', 1200, 600 );` */ static newFromElementId(elementId: string | null, width = 500, height = 200): Factory { - return new Factory({ renderer: { elementId, width, height, backend: Renderer.Backends.SVG } }); + return new Factory({ renderer: { elementId, width, height } }); } reset(): void { @@ -162,19 +165,19 @@ export class Factory { initRenderer(): void { if (!this.options.renderer) throw new RuntimeError('NoRenderer'); - const { elementId, backend, width, height, background } = this.options.renderer; - if (elementId === '') { + const { elementId, width, height, background } = this.options.renderer; + if (elementId == null || elementId === '') { L(this); throw new RuntimeError('HTML DOM element not set in Factory'); } - this.context = Renderer.buildContext( - elementId as string, - backend ?? Renderer.Backends.SVG, - width, - height, - background - ); + let backend = this.options.renderer.backend; + if (backend === undefined) { + const elem = document.getElementById(elementId); + backend = elem instanceof HTMLCanvasElement ? Renderer.Backends.CANVAS : Renderer.Backends.SVG; + } + + this.context = Renderer.buildContext(elementId as string, backend, width, height, background); } getContext(): RenderContext { diff --git a/src/renderer.ts b/src/renderer.ts index f81f93e597..e337301ef2 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -9,29 +9,16 @@ import { RuntimeError } from './util'; // A ContextBuilder is either Renderer.getSVGContext or Renderer.getCanvasContext. export type ContextBuilder = typeof Renderer.getSVGContext | typeof Renderer.getCanvasContext; +// eslint-disable-next-line +function isRenderContext(obj: any): obj is RenderContext { + return obj.setShadowBlur !== undefined; +} + /** * Support Canvas & SVG rendering contexts. */ export class Renderer { - protected elementId?: string; - protected element: HTMLCanvasElement | HTMLDivElement; - protected backend: number; - protected ctx: RenderContext; - // eslint-disable-next-line - protected paper: any; - - static readonly Backends = { - CANVAS: 1, - SVG: 2, - }; - - // End of line types - static readonly LineEndType = { - NONE: 1, // No leg - UP: 2, // Upward leg - DOWN: 3, // Downward leg - }; static lastContext?: RenderContext = undefined; @@ -108,63 +95,56 @@ export class Renderer { * - a div element, which will contain the SVG output * @param backend Renderer.Backends.CANVAS or Renderer.Backends.SVG */ - constructor(canvasId: string | HTMLCanvasElement | HTMLDivElement, backend: number) { - if (!canvasId) { - throw new RuntimeError('BadArgument', 'Invalid id for renderer.'); - } else if (typeof canvasId === 'string') { - this.elementId = canvasId; - this.element = document.getElementById(canvasId as string) as HTMLCanvasElement | HTMLDivElement; - } else if ('getContext' in canvasId /* HTMLCanvasElement */) { - this.element = canvasId as HTMLCanvasElement; + constructor(context: RenderContext); + constructor(canvas: string | HTMLCanvasElement, backend: Renderer.Backends.CANVAS); + constructor(canvas: string | HTMLDivElement, backend: Renderer.Backends.SVG); + constructor(arg0: string | HTMLCanvasElement | HTMLDivElement | RenderContext, arg1?: number) { + if (isRenderContext(arg0)) { + // The user has provided what looks like a RenderContext, let's just use it. + // TODO(tommadams): RenderContext is an interface, can we introduce a context base class + // to make this check more robust? + this.ctx = arg0; } else { - // Assume it's a HTMLDivElement. - this.element = canvasId as HTMLDivElement; - } + if (arg1 === undefined) { + // The backend must be specified if the render context isn't directly provided. + throw new RuntimeError('InvalidArgument', 'Missing backend argument'); + } + const backend: number = arg1; - // Verify backend and create context - this.backend = backend; - if (this.backend === Renderer.Backends.CANVAS) { - const canvasElement = this.element as HTMLCanvasElement; - if (!canvasElement.getContext) { - throw new RuntimeError('BadElement', `Can't get canvas context from element: ${canvasId}`); + let element: HTMLElement; + if (typeof arg0 == 'string') { + const maybeElement = document.getElementById(arg0); + if (maybeElement == null) { + throw new RuntimeError('BadElementId', `Can't find element with ID "${maybeElement}"`); + } + element = maybeElement; } else { - const context = canvasElement.getContext('2d'); - if (context) { - this.ctx = new CanvasContext(context); - } else { - throw new RuntimeError('BadElement', `Can't get canvas context from element: ${canvasId}`); + element = arg0 as HTMLElement; + } + + // Verify backend and create context + if (backend === Renderer.Backends.CANVAS) { + if (!(element instanceof HTMLCanvasElement)) { + throw new RuntimeError('BadElement', 'CANVAS context requires an HTMLCanvasElement'); } + const context = element.getContext('2d'); + if (!context) { + throw new RuntimeError('BadElement', "Can't get canvas context"); + } + this.ctx = new CanvasContext(context); + } else if (backend === Renderer.Backends.SVG) { + if (!(element instanceof HTMLDivElement)) { + throw new RuntimeError('BadElement', 'SVG context requires an HTMLDivElement.'); + } + this.ctx = new SVGContext(element); + } else { + throw new RuntimeError('InvalidBackend', `No support for backend: ${backend}`); } - } else if (this.backend === Renderer.Backends.SVG) { - this.ctx = new SVGContext(this.element); - } else { - throw new RuntimeError('InvalidBackend', `No support for backend: ${this.backend}`); } } resize(width: number, height: number): this { - if (this.backend === Renderer.Backends.CANVAS) { - const canvasElement = this.element as HTMLCanvasElement; - const devicePixelRatio = window.devicePixelRatio || 1; - - // Scale the canvas size by the device pixel ratio clamping to the maximum - // supported size. - [width, height] = CanvasContext.SanitizeCanvasDims(width * devicePixelRatio, height * devicePixelRatio); - - // Divide back down by the pixel ratio and convert to integers. - width = (width / devicePixelRatio) | 0; - height = (height / devicePixelRatio) | 0; - - canvasElement.width = width * devicePixelRatio; - canvasElement.height = height * devicePixelRatio; - canvasElement.style.width = width + 'px'; - canvasElement.style.height = height + 'px'; - - this.ctx.scale(devicePixelRatio, devicePixelRatio); - } else { - this.ctx.resize(width, height); - } - + this.ctx.resize(width, height); return this; } @@ -172,3 +152,18 @@ export class Renderer { return this.ctx; } } + +// eslint-disable-next-line +export namespace Renderer { + export enum Backends { + CANVAS = 1, + SVG = 2, + } + + // End of line types + export enum LineEndType { + NONE = 1, // No leg + UP = 2, // Upward leg + DOWN = 3, // Downward leg + } +} From 3ae2c72a258d6b971f4f65f578b079c4961568c6 Mon Sep 17 00:00:00 2001 From: Tom Madams Date: Tue, 28 Sep 2021 08:17:53 -0700 Subject: [PATCH 2/4] add renderer test --- tests/renderer_tests.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/renderer_tests.ts b/tests/renderer_tests.ts index 798bd68e75..f94fe17d2f 100644 --- a/tests/renderer_tests.ts +++ b/tests/renderer_tests.ts @@ -7,12 +7,15 @@ /* eslint-disable */ // @ts-nocheck +import { CanvasContext } from 'canvascontext'; import { Factory, FactoryOptions } from 'factory'; import { Formatter } from 'formatter'; import { Renderer } from 'renderer'; import { Stave } from 'stave'; import { StaveNote } from 'stavenote'; +import { SVGContext } from 'svgcontext'; import { RenderContext } from 'types/common'; +import { RuntimeError } from 'util'; import { TestOptions, VexFlowTests } from './vexflow_test_helpers'; // TODO: Should FactoryOptions.renderer.elementId also accept a canvas | div? @@ -42,6 +45,7 @@ const RendererTests = { // Pass in: element ID string OR canvas/div element. run('Renderer API with element ID string', stringElementId, USE_RENDERER); run('Renderer API with canvas or div', canvasOrDivElement, USE_RENDERER); + run('Renderer API with context', passRenderContext); run('Factory API with element ID string', stringElementId, USE_FACTORY); run('Factory API with canvas or div', canvasOrDivElement, USE_FACTORY); }, @@ -155,4 +159,26 @@ function canvasOrDivElement(options: TestOptions): void { ok(true); } +/** + * Pass the render context directly to the Renderer constructor. + */ +function passRenderContext(options: TestOptions): void { + let context: RenderContext; + const element = document.getElementById(options.elementId) as HTMLCanvasElement | HTMLDivElement; + if (element instanceof HTMLCanvasElement) { + const ctx = element.getContext('2d'); + if (!ctx) { + throw new RuntimeError(`Couldn't get context from element "${options.elemendId}"`); + } + context = new CanvasContext(ctx); + } else { + context = new SVGContext(element); + } + + const renderer = new Renderer(context); + renderer.resize(STAVE_WIDTH, STAVE_HEIGHT); + drawStave(new Stave(0, 0, STAVE_WIDTH - STAVE_RIGHT_MARGIN).setContext(context), context); + ok(true); +} + export { RendererTests }; From 9043e135944bdb78d83223a150603d14d91cd02d Mon Sep 17 00:00:00 2001 From: Tom Madams Date: Tue, 28 Sep 2021 09:07:24 -0700 Subject: [PATCH 3/4] use window.HTMLCanvasElement so code works under node.js --- src/factory.ts | 6 +++++- src/renderer.ts | 2 +- tests/renderer_tests.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index fa98bdb656..9c16031011 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -174,7 +174,11 @@ export class Factory { let backend = this.options.renderer.backend; if (backend === undefined) { const elem = document.getElementById(elementId); - backend = elem instanceof HTMLCanvasElement ? Renderer.Backends.CANVAS : Renderer.Backends.SVG; + if (elem instanceof window.HTMLCanvasElement) { + backend = Renderer.Backends.CANVAS; + } else { + backend = Renderer.Backends.SVG; + } } this.context = Renderer.buildContext(elementId as string, backend, width, height, background); diff --git a/src/renderer.ts b/src/renderer.ts index e337301ef2..c3733d9b61 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -124,7 +124,7 @@ export class Renderer { // Verify backend and create context if (backend === Renderer.Backends.CANVAS) { - if (!(element instanceof HTMLCanvasElement)) { + if (!(element instanceof window.HTMLCanvasElement)) { throw new RuntimeError('BadElement', 'CANVAS context requires an HTMLCanvasElement'); } const context = element.getContext('2d'); diff --git a/tests/renderer_tests.ts b/tests/renderer_tests.ts index f94fe17d2f..52d0938e3f 100644 --- a/tests/renderer_tests.ts +++ b/tests/renderer_tests.ts @@ -165,7 +165,7 @@ function canvasOrDivElement(options: TestOptions): void { function passRenderContext(options: TestOptions): void { let context: RenderContext; const element = document.getElementById(options.elementId) as HTMLCanvasElement | HTMLDivElement; - if (element instanceof HTMLCanvasElement) { + if (element instanceof window.HTMLCanvasElement) { const ctx = element.getContext('2d'); if (!ctx) { throw new RuntimeError(`Couldn't get context from element "${options.elemendId}"`); From 2b473c8221ed8bf7dc873d9a739664a937e66b1f Mon Sep 17 00:00:00 2001 From: Tom Madams Date: Wed, 6 Oct 2021 22:13:29 -0700 Subject: [PATCH 4/4] address review comments and add custom RenderContext demo --- demos/node/README.md | 2 + demos/node/customcontext.js | 192 ++++++++++++++++++++++++++++++++++++ src/factory.ts | 4 +- src/renderer.ts | 2 +- 4 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 demos/node/customcontext.js diff --git a/demos/node/README.md b/demos/node/README.md index 81fc2122a8..b3eea39697 100644 --- a/demos/node/README.md +++ b/demos/node/README.md @@ -5,3 +5,5 @@ You can use VexFlow in Node JS by calling `const Vex = require(...)` on the JS l `node canvas.js > output.html` creates an HTML page containing the VexFlow output. `node svg.js > output.svg` creates a SVG image file containing the VexFlow output. + +`node customcontext.js` demonstrates how to use VexFlow with a custom RenderContext. diff --git a/demos/node/customcontext.js b/demos/node/customcontext.js new file mode 100644 index 0000000000..2b86da0f82 --- /dev/null +++ b/demos/node/customcontext.js @@ -0,0 +1,192 @@ +// node customcontext.js + +/* eslint-disable no-console */ + +const { createCanvas } = require('canvas'); +const Vex = require('../../build/vexflow-debug'); +const VF = Vex.Flow; + +// A custom Vex.Flow.RenderContext implementation. +// This is just a stub for demonstration purposes that console.logs all method +// calls and arguments. +class CustomContext { + constructor() { + this.font = ''; + this.fillStyle = ''; + this.strokeStyle = ''; + } + + log(func, ...args) { + for (let i = 0; i < args.length; ++i) { + if (typeof args[i] == 'string') { + args[i] = `"${args[i]}"`; + } + } + console.log(`${func}(${args.join(', ')})`); + } + + clear() { + this.log('clear'); + } + + setFont(family, size, weight = '') { + this.log('setFont', family, size, weight); + return this; + } + + setRawFont(font) { + this.log('setRawFont', font); + return this; + } + + setFillStyle(style) { + this.log('setFillStyle', style); + return this; + } + + setBackgroundFillStyle(style) { + this.log('setBackgroundFillStyle', style); + return this; + } + + setStrokeStyle(style) { + this.log('setStrokeStyle', style); + return this; + } + + setShadowColor(color) { + this.log('setShadowColor', color); + return this; + } + + setShadowBlur(blur) { + this.log('setShadowBlur', blur); + return this; + } + + setLineWidth(width) { + this.log('setLineWidth', width); + return this; + } + + setLineCap(capType) { + this.log('setLineCap', capType); + return this; + } + + setLineDash(dashPattern) { + this.log('setLineDash', `[${dashPattern.join(', ')}]`); + return this; + } + + scale(x, y) { + this.log('scale', x, y); + return this; + } + + rect(x, y, width, height) { + this.log('rect', x, y, width, height); + return this; + } + + resize(width, height) { + this.log('resize', width, height); + return this; + } + + fillRect(x, y, width, height) { + this.log('fillRect', x, y, width, height); + return this; + } + + clearRect(x, y, width, height) { + this.log('clearRect', x, y, width, height); + return this; + } + + beginPath() { + this.log('beginPath'); + return this; + } + + moveTo(x, y) { + this.log('moveTo', x, y); + return this; + } + + lineTo(x, y) { + this.log('lineTo', x, y); + return this; + } + + bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) { + this.log('bezierCurveTo', cp1x, cp1y, cp2x, cp2y, x, y); + return this; + } + + quadraticCurveTo(cpx, cpy, x, y) { + this.log('quadraticCurveTo', cpx, cpy, x, y); + return this; + } + + arc(x, y, radius, startAngle, endAngle, antiClockwise) { + this.log('arc', x, y, radius, startAngle, endAngle, antiClockwise); + return this; + } + + fill(attributes) { + this.log('fill'); + return this; + } + + stroke() { + this.log('stroke'); + return this; + } + + closePath() { + this.log('closePath'); + return this; + } + + fillText(text, x, y) { + this.log('fillText', text, x, y); + return this; + } + + save() { + this.log('save'); + return this; + } + + restore() { + this.log('restore'); + return this; + } + + openGroup(cls, id, attrs) { + this.log('openGroup', cls, id); + } + + closeGroup() { + this.log('closeGroup'); + } + + add(child) { + this.log('add'); + } + + measureText(text) { + this.log('measureText', text); + return { width: 0, height: 10 }; + } +} + +const renderer = new VF.Renderer(new CustomContext()); +const context = renderer.getContext(); +context.setFont('Arial', 10, '').setBackgroundFillStyle('#eed'); + +const stave = new VF.Stave(10, 40, 400); +stave.addClef('treble'); +stave.addTimeSignature('4/4'); +stave.setContext(context).draw(); diff --git a/src/factory.ts b/src/factory.ts index 5389c93426..792c6a0057 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -150,11 +150,11 @@ export class Factory { initRenderer(): void { const { elementId, width, height, background } = this.options.renderer; - if (elementId === null) { + if (elementId == null) { return; } - if (elementId === '') { + if (elementId == '') { L(this); throw new RuntimeError('renderer.elementId not set in FactoryOptions'); } diff --git a/src/renderer.ts b/src/renderer.ts index b841bd7e27..972be94e21 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -131,7 +131,7 @@ export class Renderer { } this.ctx = new CanvasContext(context); } else if (backend === Renderer.Backends.SVG) { - if (!(element instanceof HTMLDivElement)) { + if (!(element instanceof window.HTMLDivElement)) { throw new RuntimeError('BadElement', 'SVG context requires an HTMLDivElement.'); } this.ctx = new SVGContext(element);