UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature.

487 lines (429 loc) 15.6 kB
import { ChordSymbolMetrics } from './chordsymbol'; import { ClefMetrics } from './clef'; import { NoteHeadMetrics } from './notehead'; import { OrnamentMetrics } from './ornament'; import { StringNumberMetrics } from './stringnumber'; import { TupletMetrics } from './tuplet'; import { defined } from './util'; export interface FontInfo { /** CSS font-family, e.g., 'Arial', 'Helvetica Neue, Arial, sans-serif', 'Times, serif' */ family?: string; /** * CSS font-size (e.g., '10pt', '12px'). * For backwards compatibility with 3.0.9, plain numbers are assumed to be specified in 'pt'. */ size?: number | string; /** `bold` or a number (e.g., 900) as inspired by CSS font-weight. */ weight?: string | number; /** `italic` as inspired by CSS font-style. */ style?: string; } export type FontModule = { data: FontData; metrics: FontMetrics }; export interface FontData { glyphs: Record<string, FontGlyph>; fontFamily?: string; resolution: number; generatedOn?: string; } /** Specified in the `xxx_metrics.ts` files. */ // eslint-disable-next-line export interface FontMetrics extends Record<string, any> { smufl: boolean; stave?: Record<string, number>; accidental?: Record<string, number>; clef_default?: ClefMetrics; clef_small?: ClefMetrics; pedalMarking?: Record<string, Record<string, number>>; digits?: Record<string, number>; articulation?: Record<string, Record<string, number>>; tremolo?: Record<string, Record<string, number>>; chordSymbol?: ChordSymbolMetrics; ornament?: Record<string, OrnamentMetrics>; noteHead?: NoteHeadMetrics; stringNumber?: StringNumberMetrics; tuplet?: TupletMetrics; glyphs: Record< string, { point?: number; shiftX?: number; shiftY?: number; scale?: number; [key: string]: { point?: number; shiftX?: number; shiftY?: number; scale?: number } | number | undefined; } >; } export interface FontGlyph { x_min: number; x_max: number; y_min?: number; y_max?: number; ha: number; leftSideBearing?: number; advanceWidth?: number; // The o (outline) field is optional, because robotoslab_glyphs.ts & petalumascript_glyphs.ts // do not include glyph outlines. We rely on *.woff files to provide the glyph outlines. o?: string; cached_outline?: number[]; } export enum FontWeight { NORMAL = 'normal', BOLD = 'bold', } export enum FontStyle { NORMAL = 'normal', ITALIC = 'italic', } // Internal <span></span> element for parsing CSS font shorthand strings. let fontParser: HTMLSpanElement; const Fonts: Record<string, Font> = {}; export class Font { ////////////////////////////////////////////////////////////////////////////////////////////////// // STATIC MEMBERS /** Default sans-serif font family. */ static SANS_SERIF: string = 'Arial, sans-serif'; /** Default serif font family. */ static SERIF: string = 'Times New Roman, serif'; /** Default font size in `pt`. */ static SIZE: number = 10; // CSS Font Sizes: 36pt == 48px == 3em == 300% == 0.5in /** Given a length (for units: pt, px, em, %, in, mm, cm) what is the scale factor to convert it to px? */ static scaleToPxFrom: Record<string, number> = { pt: 4 / 3, px: 1, em: 16, '%': 4 / 25, in: 96, mm: 96 / 25.4, cm: 96 / 2.54, }; /** * @param fontSize a font size to convert. Can be specified as a CSS length string (e.g., '16pt', '1em') * or as a number (the unit is assumed to be 'pt'). See `Font.scaleToPxFrom` for the supported * units (e.g., pt, em, %). * @returns the number of pixels that is equivalent to `fontSize` */ static convertSizeToPixelValue(fontSize: string | number = Font.SIZE): number { if (typeof fontSize === 'number') { // Assume the numeric fontSize is specified in pt. return fontSize * Font.scaleToPxFrom.pt; } else { const value = parseFloat(fontSize); if (isNaN(value)) { return 0; } const unit = fontSize.replace(/[\d.\s]/g, '').toLowerCase(); // Extract the unit by removing all numbers, dots, spaces. const conversionFactor = Font.scaleToPxFrom[unit] ?? 1; return value * conversionFactor; } } /** * @param fontSize a font size to convert. Can be specified as a CSS length string (e.g., '16pt', '1em') * or as a number (the unit is assumed to be 'pt'). See `Font.scaleToPxFrom` for the supported * units (e.g., pt, em, %). * @returns the number of points that is equivalent to `fontSize` */ static convertSizeToPointValue(fontSize: string | number = Font.SIZE): number { if (typeof fontSize === 'number') { // Assume the numeric fontSize is specified in pt. return fontSize; } else { const value = parseFloat(fontSize); if (isNaN(value)) { return 0; } const unit = fontSize.replace(/[\d.\s]/g, '').toLowerCase(); // Extract the unit by removing all numbers, dots, spaces. const conversionFactor = (Font.scaleToPxFrom[unit] ?? 1) / Font.scaleToPxFrom.pt; return value * conversionFactor; } } /** * @param f * @param size * @param weight * @param style * @returns the `size` field will include the units (e.g., '12pt', '16px'). */ static validate( f?: string | FontInfo, size?: string | number, weight?: string | number, style?: string ): Required<FontInfo> { // If f is a string but all other arguments are undefined, we assume that // f is CSS font shorthand (e.g., 'italic bold 10pt Arial'). if (typeof f === 'string' && size === undefined && weight === undefined && style === undefined) { return Font.fromCSSString(f); } let family: string | undefined; if (typeof f === 'object') { // f is a FontInfo object, so we extract its fields. family = f.family; size = f.size; weight = f.weight; style = f.style; } else { // f is a string representing the font family name or undefined. family = f; } family = family ?? Font.SANS_SERIF; size = size ?? Font.SIZE + 'pt'; weight = weight ?? FontWeight.NORMAL; style = style ?? FontStyle.NORMAL; if (weight === '') { weight = FontWeight.NORMAL; } if (style === '') { style = FontStyle.NORMAL; } // If size is a number, we assume the unit is `pt`. if (typeof size === 'number') { size = `${size}pt`; } // If weight is a number (e.g., 900), turn it into a string representation of that number. if (typeof weight === 'number') { weight = weight.toString(); } // At this point, `family`, `size`, `weight`, and `style` are all strings. return { family, size, weight, style }; } /** * @param cssFontShorthand a string formatted as CSS font shorthand (e.g., 'italic bold 15pt Arial'). */ static fromCSSString(cssFontShorthand: string): Required<FontInfo> { // Let the browser parse this string for us. // First, create a span element. // Then, set its style.font and extract it back out. if (!fontParser) { fontParser = document.createElement('span'); } fontParser.style.font = cssFontShorthand; const { fontFamily, fontSize, fontWeight, fontStyle } = fontParser.style; return { family: fontFamily, size: fontSize, weight: fontWeight, style: fontStyle }; } /** * @returns a CSS font shorthand string of the form `italic bold 16pt Arial`. */ static toCSSString(fontInfo?: FontInfo): string { if (!fontInfo) { return ''; } let style: string; const st = fontInfo.style; if (st === FontStyle.NORMAL || st === '' || st === undefined) { style = ''; // no space! Omit the style section. } else { style = st.trim() + ' '; } let weight: string; const wt = fontInfo.weight; if (wt === FontWeight.NORMAL || wt === '' || wt === undefined) { weight = ''; // no space! Omit the weight section. } else if (typeof wt === 'number') { weight = wt + ' '; } else { weight = wt.trim() + ' '; } let size: string; const sz = fontInfo.size; if (sz === undefined) { size = Font.SIZE + 'pt '; } else if (typeof sz === 'number') { size = sz + 'pt '; } else { // size is already a string. size = sz.trim() + ' '; } const family: string = fontInfo.family ?? Font.SANS_SERIF; return `${style}${weight}${size}${family}`; } /** * @param fontSize a number representing a font size, or a string font size with units. * @param scaleFactor multiply the size by this factor. * @returns size * scaleFactor (e.g., 16pt * 3 = 48pt, 8px * 0.5 = 4px, 24 * 2 = 48). * If the fontSize argument was a number, the return value will be a number. * If the fontSize argument was a string, the return value will be a string. */ static scaleSize<T extends number | string>(fontSize: T, scaleFactor: number): T { if (typeof fontSize === 'number') { return (fontSize * scaleFactor) as T; } else { const value = parseFloat(fontSize); const unit = fontSize.replace(/[\d.\s]/g, ''); // Remove all numbers, dots, spaces. return `${value * scaleFactor}${unit}` as T; } } /** * @param weight a string (e.g., 'bold') or a number (e.g., 600 / semi-bold in the OpenType spec). * @returns true if the font weight indicates bold. */ static isBold(weight?: string | number): boolean { if (!weight) { return false; } else if (typeof weight === 'number') { return weight >= 600; } else { // a string can be 'bold' or '700' const parsedWeight = parseInt(weight, 10); if (isNaN(parsedWeight)) { return weight.toLowerCase() === 'bold'; } else { return parsedWeight >= 600; } } } /** * @param style * @returns true if the font style indicates 'italic'. */ static isItalic(style?: string): boolean { if (!style) { return false; } else { return style.toLowerCase() === FontStyle.ITALIC; } } /** * Customize this field to specify a different CDN for delivering web fonts. * Alternative: https://cdn.jsdelivr.net/npm/vexflow-fonts@1.0.3/ * Or you can use your own host. */ static WEB_FONT_HOST = 'https://unpkg.com/vexflow-fonts@1.0.3/'; /** * These font files will be loaded from the CDN specified by `Font.WEB_FONT_HOST` when * `await Font.loadWebFonts()` is called. Customize this field to specify a different * set of fonts to load. See: `Font.loadWebFonts()`. */ static WEB_FONT_FILES: Record<string /* fontName */, string /* fontPath */> = { 'Roboto Slab': 'robotoslab/RobotoSlab-Medium_2.001.woff', PetalumaScript: 'petaluma/PetalumaScript_1.10_FS.woff', }; /** * @param fontName * @param woffURL The absolute or relative URL to the woff file. * @param includeWoff2 If true, we assume that a woff2 file is in * the same folder as the woff file, and will append a `2` to the url. */ // Support distributions of the typescript compiler that do not yet include the FontFace API declarations. // eslint-disable-next-line // @ts-ignore static async loadWebFont(fontName: string, woffURL: string, includeWoff2: boolean = true): Promise<FontFace> { const woff2URL = includeWoff2 ? `url(${woffURL}2) format('woff2'), ` : ''; const woff1URL = `url(${woffURL}) format('woff')`; const woffURLs = woff2URL + woff1URL; // eslint-disable-next-line // @ts-ignore const fontFace = new FontFace(fontName, woffURLs); await fontFace.load(); // eslint-disable-next-line // @ts-ignore document.fonts.add(fontFace); return fontFace; } /** * Load the web fonts that are used by ChordSymbol. For example, `flow.html` calls: * `await Vex.Flow.Font.loadWebFonts();` * Alternatively, you may load web fonts with a stylesheet link (e.g., from Google Fonts), * and a @font-face { font-family: ... } rule in your CSS. * If you do not load either of these fonts, ChordSymbol will fall back to Times or Arial, * depending on the current music engraving font. * * You can customize `Font.WEB_FONT_HOST` and `Font.WEB_FONT_FILES` to load different fonts * for your app. */ static async loadWebFonts(): Promise<void> { const host = Font.WEB_FONT_HOST; const files = Font.WEB_FONT_FILES; for (const fontName in files) { const fontPath = files[fontName]; Font.loadWebFont(fontName, host + fontPath); } } /** * @param fontName * @param data optionally set the Font object's `.data` property. * This is usually done when setting up a font for the first time. * @param metrics optionally set the Font object's `.metrics` property. * This is usually done when setting up a font for the first time. * @returns a Font object with the given `fontName`. * Reuse an existing Font object if a matching one is found. */ static load(fontName: string, data?: FontData, metrics?: FontMetrics): Font { let font = Fonts[fontName]; if (!font) { font = new Font(fontName); Fonts[fontName] = font; } if (data) { font.setData(data); } if (metrics) { font.setMetrics(metrics); } return font; } ////////////////////////////////////////////////////////////////////////////////////////////////// // INSTANCE MEMBERS protected name: string; protected data?: FontData; protected metrics?: FontMetrics; /** * Use `Font.load(fontName)` to get a Font object. * Do not call this constructor directly. */ private constructor(fontName: string) { this.name = fontName; } getName(): string { return this.name; } getData(): FontData { return defined(this.data, 'FontError', 'Missing font data'); } getMetrics(): FontMetrics { return defined(this.metrics, 'FontError', 'Missing metrics'); } setData(data: FontData): void { this.data = data; } setMetrics(metrics: FontMetrics): void { this.metrics = metrics; } hasData(): boolean { return this.data !== undefined; } getResolution(): number { return this.getData().resolution; } getGlyphs(): Record<string, FontGlyph> { return this.getData().glyphs; } /** * Use the provided key to look up a value in this font's metrics file (e.g., bravura_metrics.ts, petaluma_metrics.ts). * @param key is a string separated by periods (e.g., stave.endPaddingMax, clef.lineCount.'5'.shiftY). * @param defaultValue is returned if the lookup fails. * @returns the retrieved value (or `defaultValue` if the lookup fails). */ // eslint-disable-next-line lookupMetric(key: string, defaultValue?: Record<string, any> | number): any { const keyParts = key.split('.'); // Start with the top level font metrics object, and keep looking deeper into the object (via each part of the period-delimited key). let currObj = this.getMetrics(); for (let i = 0; i < keyParts.length; i++) { const keyPart = keyParts[i]; const value = currObj[keyPart]; if (value === undefined) { // If the key lookup fails, we fall back to the defaultValue. return defaultValue; } // The most recent lookup succeeded, so we drill deeper into the object. currObj = value; } // After checking every part of the key (i.e., the loop completed), return the most recently retrieved value. return currObj; } /** For debugging. */ toString(): string { return '[' + this.name + ' Font]'; } }