Skip to content

Commit

Permalink
Consolidate the setFont(...) API and handle edge cases.
Browse files Browse the repository at this point in the history
Add .font setter/getter to Element and rename internal instance member to `textFont`.
Add test code for setFont() in tests/font_tests.ts.
Other smaller fixes.
  • Loading branch information
ronyeh committed Nov 4, 2021
1 parent f87c95a commit a08b73b
Show file tree
Hide file tree
Showing 28 changed files with 293 additions and 250 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ Please help test this beta release and [report issues on GitHub](https://github.
- Migrate VexFlow to **TypeScript** with a ES6 target.
- Improve handling of music fonts and text fonts.
- Optional lazy loading of music fonts.
- `setFont(...)` method can be called in these ways:
- `setFont(family, size, weight, style)`
- `setFont(cssShorthand)`
- e.g., `setFont('bold 10pt Arial')`
- `setFont(fontInfoObject)`
- e.g., `setFont({ family: 'Times', size: 12 })`

## Breaking

Expand All @@ -23,8 +29,10 @@ Please help test this beta release and [report issues on GitHub](https://github.
- `ChordSymbol.metrics` was previously named `ChordSymbol.chordSymbolMetrics`.
- `StaveNote.LEDGER_LINE_OFFSET` was previously named `StaveNote.DEFAULT_LEDGER_LINE_OFFSET`.
- **Fonts**

- `TextFontMetrics` has been merged into `FontGlyph` due to substantial overlap.
- `Flow.NOTATION_FONT_SCALE` was previously named `Flow.DEFAULT_NOTATION_FONT_SCALE`.
- `setFont(...)` in `CanvasContext` and `SVGContext` previously took arguments: `family`, `size`, `weight`. The `weight` argument allowed strings like `'italic bold'`. This no longer works, and `'italic'` must now be passed into the `style` argument.

# 3.0.9 / 2020-04-21

Expand Down
16 changes: 6 additions & 10 deletions demos/node/customcontext.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
// 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.
// This is just a stub for demonstration purposes that console.logs method calls and arguments.
class CustomContext extends VF.RenderContext {
constructor() {
super();
this.font = '';
this.fillStyle = '';
this.strokeStyle = '';
}
Expand All @@ -30,14 +26,14 @@ class CustomContext extends VF.RenderContext {
this.log('clear');
}

setFont(family, size, weight = '') {
this.log('setFont', family, size, weight);
setFont(f, sz, wt, st) {
this.log('setFont', f, sz, wt, st);
return this;
}

setRawFont(font) {
this.log('setRawFont', font);
return this;
getFont() {
this.log(`getFont() => '10pt Arial'`);
return '10pt Arial';
}

setFillStyle(style) {
Expand Down
6 changes: 3 additions & 3 deletions src/annotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class Annotation extends Modifier {
for (let i = 0; i < annotations.length; ++i) {
let textWidth = 0;
const annotation = annotations[i];
const textFormatter = TextFormatter.create(annotation.font);
const textFormatter = TextFormatter.create(annotation.textFont);

// Calculate if the vertical extent will exceed a single line and adjust accordingly.
const numLines = Math.floor(textFormatter.maxHeight / Tables.STAVE_LINE_DISTANCE) + 1;
Expand Down Expand Up @@ -110,7 +110,7 @@ export class Annotation extends Modifier {
this.text = text;
this.justification = Justify.CENTER;
this.vert_justification = Annotation.VerticalJustify.TOP;
this.setFont(this.getDefaultFont());
this.resetFont();

// The default width is calculated from the text.
this.setWidth(Tables.textWidth(text));
Expand Down Expand Up @@ -153,7 +153,7 @@ export class Annotation extends Modifier {
ctx.save();
const classString = Object.keys(this.getAttribute('classes')).join(' ');
ctx.openGroup(classString, this.getAttribute('id'));
ctx.setFont(this.font);
ctx.setFont(this.textFont);

const text_width = ctx.measureText(this.text).width;

Expand Down
4 changes: 2 additions & 2 deletions src/bend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class Bend extends Modifier {
this.text = text;
this.x_shift = 0;
this.release = release;
this.setFont(this.getDefaultFont());
this.resetFont();
this.render_options = {
line_width: 1.5,
line_style: '#777777',
Expand Down Expand Up @@ -223,7 +223,7 @@ export class Bend extends Modifier {

const renderText = (x: number, text: string) => {
ctx.save();
ctx.setFont(this.font);
ctx.setFont(this.textFont);
const render_x = x - ctx.measureText(text).width / 2;
ctx.fillText(text, render_x, annotation_y);
ctx.restore();
Expand Down
35 changes: 12 additions & 23 deletions src/canvascontext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,34 +253,23 @@ export class CanvasContext extends RenderContext {
return this.context2D.strokeStyle;
}

setFont(family: string, size: number, weight?: string): this {
this.context2D.font = (weight || '') + ' ' + size + 'pt ' + family;
this.textHeight = (size * 4) / 3;
/**
* @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.toPixels(fontInfo.size);
return this;
}

/** Return a string of the form `'italic bold 15pt Arial'` */
getFont(): string {
return this.context2D.font;
}

setRawFont(font: string): this {
this.context2D.font = font;

const fontArray = font.split(' ');
const size = Number(fontArray[0].match(/\d+/));
// The font size is specified in points, scale it to canvas units.
// CSS specifies dpi to be 96 and there are 72 points to an inch: 96/72 == 4/3.
this.textHeight = (size * 4) / 3;

return this;
}

set font(value: string) {
this.setRawFont(value);
}

get font(): string {
return this.context2D.font;
}
}
41 changes: 21 additions & 20 deletions src/chordsymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,9 @@ export class ChordSymbol extends Modifier {
const reportedWidths = [];

for (const symbol of symbols) {
// symbol.font was initialized by the constructor via this.setFont().
// symbol.textFont was initialized by the constructor via this.resetFont().
// eslint-disable-next-line
const fontSize = Font.convertSizeToNumber(symbol.font!.size);
const fontSize = Font.convertSizeToNumber(symbol.textFont!.size);
const fontAdj = Font.scaleSize(fontSize, 0.05);
const glyphAdj = fontAdj * 2;
let lineSpaces = 1;
Expand Down Expand Up @@ -365,14 +365,15 @@ export class ChordSymbol extends Modifier {

constructor() {
super();
this.setFont(this.getDefaultFont());
this.resetFont();
}

/**
* Default text font.
* Choose a font family that works well with the current music engraving font.
* Overrides `Element.getDefaultFont()`.
* @override `Element.TEXT_FONT`.
*/
getDefaultFont(): Required<FontInfo> {
static get TEXT_FONT(): Required<FontInfo> {
let family = 'Roboto Slab, Times, serif';
if (Tables.currentMusicFont().getName() === 'Petaluma') {
// Fixes Issue #1180
Expand Down Expand Up @@ -613,26 +614,26 @@ export class ChordSymbol extends Modifier {
}

/**
* @param f a string that specifies the font family, or a `FontInfo` options object.
* If the first argument is a `FontInfo`, the other arguments below are ignored.
* Set the chord symbol's font family, size, weight, style (e.g., `Arial`, `10pt`, `bold`, `italic`).
*
* @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 inserted into the font-weight attribute (e.g., font-weight="bold")
* @param style is inserted into the font-style attribute (e.g., font-style="italic")
* @param weight is a string (e.g., 'bold', 'normal') or a number (100, 200, ... 900).
* @param style is a string (e.g., 'italic', 'normal').
*
* @override See: Element.
*/
setFont(
f: string | FontInfo = Font.SANS_SERIF,
size: string | number = Font.SIZE,
weight: string | number = FontWeight.NORMAL,
style: string = FontStyle.NORMAL
): this {
setFont(f?: string | FontInfo, size?: string | number, weight?: string | number, style?: string): this {
super.setFont(f, size, weight, style);
this.textFormatter = TextFormatter.create(this.font);
this.textFormatter = TextFormatter.create(this.textFont);
return this;
}

/** Just change the font size, while keeping everything else the same. */
setFontSize(size: number): this {
this.setFont({ ...this.font, size });
this.setFont({ ...this.textFont, size });
return this;
}

Expand Down Expand Up @@ -696,7 +697,7 @@ export class ChordSymbol extends Modifier {
ctx.openGroup(classString, this.getAttribute('id'));

const start = note.getModifierStartXY(Modifier.Position.ABOVE, this.index);
ctx.setFont(this.font);
ctx.setFont(this.textFont);

let y: number;

Expand Down Expand Up @@ -754,8 +755,8 @@ export class ChordSymbol extends Modifier {
if (symbol.symbolType === SymbolTypes.TEXT) {
if (isSuper || isSub) {
ctx.save();
if (this.font) {
const { family, size, weight, style } = this.font;
if (this.textFont) {
const { family, size, weight, style } = this.textFont;
const smallerFontSize = Font.scaleSize(size, ChordSymbol.superSubRatio);
ctx.setFont(family, smallerFontSize, weight, style);
}
Expand Down
83 changes: 56 additions & 27 deletions src/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@ export abstract class Element {
protected boundingBox?: BoundingBox;
protected registry?: Registry;

/** Some elements include text. You can customize the font family, size, weight, and style. */
protected font?: Required<FontInfo>;
/**
* Some elements include text.
* The `textFont` property contains information required to style the text (i.e., font family, size, weight, and style).
* It starts as `undefined`, and must be set using `setFont()` or `resetFont()`.
*/
protected textFont?: Required<FontInfo> = undefined;

constructor() {
this.attrs = {
Expand All @@ -83,41 +87,66 @@ export abstract class Element {
return (<typeof Element>this.constructor).CATEGORY;
}

/**
* Return the default text font. To render text, an element should
* call `this.setFont(this.getDefaultFont())` in its constructor.
*/
getDefaultFont(): Required<FontInfo> {
return (<typeof Element>this.constructor).TEXT_FONT;
}

/**
* Set the element's font family, size, weight, style (e.g., `Arial`, `10pt`, `bold`, `italic`).
*
* @param f a string that specifies the font family, or a `FontInfo` options object.
* If the first argument is a `FontInfo`, the other arguments below are ignored.
* @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 inserted into the font-weight attribute (e.g., font-weight="bold")
* @param style is inserted into the font-style attribute (e.g., font-style="italic")
* @param weight is a string (e.g., 'bold', 'normal') or a number (100, 200, ... 900).
* @param style is a string (e.g., 'italic', 'normal').
* If no arguments are provided, then the font is set to the default font.
* Each Element subclass may specify its own default by overriding the static `TEXT_FONT` property.
*/
setFont(
f: string | FontInfo = Font.SANS_SERIF,
size: string | number = Font.SIZE,
weight: string | number = FontWeight.NORMAL,
style: string = FontStyle.NORMAL
): this {
if (typeof f === 'string') {
this.font = { family: f, size, weight, style };
setFont(f?: string | FontInfo, size?: string | number, weight?: string | number, style?: string): this {
// Allow subclasses to override `TEXT_FONT`.
const defaultTextFont = (<typeof Element>this.constructor).TEXT_FONT;
const sizeWeightStyleUndefined = size === undefined && weight === undefined && style === undefined;
if (typeof f === 'object' || (f === undefined && sizeWeightStyleUndefined)) {
// `f` is case 1) a FontInfo object, or all arguments are undefined.
// Do not check `arguments.length === 0`, to allow the extreme edge case:
// setFont(undefined, undefined, undefined, undefined).
this.textFont = { ...defaultTextFont, ...f };
} else if (typeof f === 'string' && sizeWeightStyleUndefined) {
// `f` is case 2) CSS font shorthand.
this.textFont = Font.fromCSSString(f);
} else {
// Follow CSS conventions. Unspecified params are reset to the default.
this.font = { ...this.getDefaultFont(), ...f };
// `f` is case 3) a font family string (e.g., 'Times New Roman')
// or it is undefined while one or more of the other arguments is provided.
// Following CSS conventions, any unspecified params are reset to the default.
this.textFont = Font.validate(
f ?? defaultTextFont.family,
size ?? defaultTextFont.size,
weight ?? defaultTextFont.weight,
style ?? defaultTextFont.style
);
}
return this;
}

/**
* Reset the text font to the style indicated by the static `TEXT_FONT` property.
* Subclasses can call this to initialize `textFont` for the first time.
*/
resetFont(): void {
this.setFont();
}

getFont(): string {
return Font.toCSSString(this.textFont);
}

/** Return a copy of the FontInfo object, or undefined if `setFont()` has never been called. */
getFont(): Required<FontInfo> | undefined {
return this.font ? { ...this.font } : undefined;
getFontInfo(): Required<FontInfo> | undefined {
return this.textFont ? { ...this.textFont } : undefined;
}

set font(f: string) {
this.setFont(f);
}

get font(): string {
return Font.toCSSString(this.textFont);
}

/** Set the draw style of a stemmable note. */
Expand Down
Loading

0 comments on commit a08b73b

Please sign in to comment.