vexflow
Version:
A JavaScript library for rendering music notation and guitar tablature.
542 lines (472 loc) • 15.2 kB
text/typescript
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
// MIT License
import { BoundingBox } from './boundingbox';
import { BoundingBoxComputation } from './boundingboxcomputation';
import { Element } from './element';
import { Font, FontGlyph } from './font';
import { RenderContext } from './rendercontext';
import { Stave } from './stave';
import { Tables } from './tables';
import { Category } from './typeguard';
import { defined, RuntimeError } from './util';
export interface GlyphProps {
code_head: string;
ledger_code_head?: string;
dot_shiftY: number;
position: string;
rest: boolean;
line_below: number;
line_above: number;
stem_beam_extension: number;
stem_up_extension: number;
stem_down_extension: number;
stem: boolean;
code?: string;
code_flag_upstem?: string;
code_flag_downstem?: string;
flag?: boolean;
width?: number;
text?: string;
tabnote_stem_down_extension: number;
tabnote_stem_up_extension: number;
beam_count: number;
shift_y?: number;
getWidth(a?: number): number;
}
export interface GlyphOptions {
category?: string;
}
export interface GlyphMetrics {
width: number;
height: number;
x_min: number;
x_max: number;
x_shift: number;
y_shift: number;
scale: number;
ha: number;
outline: number[];
font: Font;
}
export const enum OutlineCode {
MOVE = 0,
LINE = 1,
QUADRATIC = 2,
BEZIER = 3,
}
class GlyphCacheEntry {
metrics: GlyphMetrics;
bbox: BoundingBox;
point: number = -1;
constructor(fontStack: Font[], code: string, category?: string) {
this.metrics = Glyph.loadMetrics(fontStack, code, category);
this.bbox = Glyph.getOutlineBoundingBox(
this.metrics.outline,
this.metrics.scale,
this.metrics.x_shift,
this.metrics.y_shift
);
if (category) {
this.point = Glyph.lookupFontMetric(this.metrics.font, category, code, 'point', -1);
}
}
}
class GlyphCache {
protected cache: Map<string, Record<string, GlyphCacheEntry>> = new Map();
lookup(code: string, category?: string): GlyphCacheEntry {
let entries = this.cache.get(Glyph.CURRENT_CACHE_KEY);
if (entries === undefined) {
entries = {};
this.cache.set(Glyph.CURRENT_CACHE_KEY, entries);
}
const key = category ? `${code}%${category}` : code;
let entry = entries[key];
if (entry === undefined) {
entry = new GlyphCacheEntry(Glyph.MUSIC_FONT_STACK, code, category);
entries[key] = entry;
}
return entry;
}
}
class GlyphOutline {
private i: number = 0;
private precision = 1;
constructor(private outline: number[], private originX: number, private originY: number, private scale: number) {
// Automatically assign private properties: this.outline, this.originX, this.originY, and this.scale.
this.precision = Math.pow(10, Tables.RENDER_PRECISION_PLACES);
}
done(): boolean {
return this.i >= this.outline.length;
}
next(): number {
return Math.round((this.outline[this.i++] * this.precision) / this.precision);
}
nextX(): number {
return Math.round((this.originX + this.outline[this.i++] * this.scale) * this.precision) / this.precision;
}
nextY(): number {
return Math.round((this.originY - this.outline[this.i++] * this.scale) * this.precision) / this.precision;
}
static parse(str: string): number[] {
const result: number[] = [];
const parts = str.split(' ');
let i = 0;
while (i < parts.length) {
switch (parts[i++]) {
case 'm':
result.push(OutlineCode.MOVE, parseInt(parts[i++]), parseInt(parts[i++]));
break;
case 'l':
result.push(OutlineCode.LINE, parseInt(parts[i++]), parseInt(parts[i++]));
break;
case 'q':
result.push(
OutlineCode.QUADRATIC,
parseInt(parts[i++]),
parseInt(parts[i++]),
parseInt(parts[i++]),
parseInt(parts[i++])
);
break;
case 'b':
result.push(
OutlineCode.BEZIER,
parseInt(parts[i++]),
parseInt(parts[i++]),
parseInt(parts[i++]),
parseInt(parts[i++]),
parseInt(parts[i++]),
parseInt(parts[i++])
);
break;
}
}
return result;
}
}
export class Glyph extends Element {
//////////////////////////////////////////////////////////////////////////////////////////////////
// STATIC MEMBERS
static get CATEGORY(): string {
return Category.Glyph;
}
protected static cache = new GlyphCache();
// The current cache key for GlyphCache above.
// Computed when Flow.setMusicFont(...) is called.
// It is set to a comma separated list of font names.
public static CURRENT_CACHE_KEY: string = '';
// Used by the GlyphCache above.
// Set when Flow.setMusicFont(...) is called.
public static MUSIC_FONT_STACK: Font[] = [];
/**
* Pass a key of the form `glyphs.{category}.{code}.{key}` to Font.lookupMetric(). If the initial lookup fails,
* try again with the path `glyphs.{category}.{key}`. If the second lookup fails, return the defaultValue.
*
* @param font
* @param category any metric path under 'glyphs', so 'stem.up' could resolve to glyphs.stem.up.shiftX, glyphs.stem.up.shiftY, etc.
* @param code
* @param key
* @param defaultValue
*/
static lookupFontMetric(font: Font, category: string, code: string, key: string, defaultValue: number): number {
let value = font.lookupMetric(`glyphs.${category}.${code}.${key}`, undefined);
if (value === undefined) {
// The first lookup failed, so we omit .${code} and try again (with a defaultValue this time).
value = font.lookupMetric(`glyphs.${category}.${key}`, defaultValue);
}
return value;
}
static lookupGlyph(fontStack: Font[], code: string): { font: Font; glyph: FontGlyph } {
defined(fontStack, 'BadFontStack', 'Font stack is misconfigured');
let glyph: FontGlyph;
let font: Font;
for (let i = 0; i < fontStack.length; i++) {
font = fontStack[i];
glyph = font.getGlyphs()[code];
if (glyph) return { glyph, font };
}
throw new RuntimeError('BadGlyph', `Glyph ${code} does not exist in font.`);
}
static loadMetrics(fontStack: Font[], code: string, category?: string): GlyphMetrics {
const { glyph, font } = Glyph.lookupGlyph(fontStack, code);
if (!glyph.o) throw new RuntimeError('BadGlyph', `Glyph ${code} has no outline defined.`);
let x_shift = 0;
let y_shift = 0;
let scale = 1;
if (category && font) {
x_shift = Glyph.lookupFontMetric(font, category, code, 'shiftX', 0);
y_shift = Glyph.lookupFontMetric(font, category, code, 'shiftY', 0);
scale = Glyph.lookupFontMetric(font, category, code, 'scale', 1);
}
const x_min = glyph.x_min;
const x_max = glyph.x_max;
const ha = glyph.ha;
if (!glyph.cached_outline) {
glyph.cached_outline = GlyphOutline.parse(glyph.o);
}
return {
x_min,
x_max,
x_shift,
y_shift,
scale,
ha,
outline: glyph.cached_outline,
font,
width: x_max - x_min,
height: ha,
};
}
/**
* Renders glyphs from the default font stack.
*
* @param ctx Canvas or SVG context
* @param x_pos x coordinate
* @param y_pos y coordinate
* @param point the point size of the font
* @param code the glyph code in font.getGlyphs()
* @param options
* @returns
*/
static renderGlyph(
ctx: RenderContext,
x_pos: number,
y_pos: number,
point: number,
code: string,
options?: { category?: string; scale?: number }
): GlyphMetrics {
const data = Glyph.cache.lookup(code, options?.category);
const metrics = data.metrics;
if (data.point != -1) {
point = data.point;
}
const customScale = options?.scale ?? 1;
const scale = ((point * 72.0) / (metrics.font.getResolution() * 100.0)) * metrics.scale * customScale;
Glyph.renderOutline(
ctx,
metrics.outline,
scale,
x_pos + metrics.x_shift * customScale,
y_pos + metrics.y_shift * customScale
);
return metrics;
}
static renderOutline(ctx: RenderContext, outline: number[], scale: number, x_pos: number, y_pos: number): void {
const go = new GlyphOutline(outline, x_pos, y_pos, scale);
ctx.beginPath();
let x, y: number;
while (!go.done()) {
switch (go.next()) {
case OutlineCode.MOVE:
ctx.moveTo(go.nextX(), go.nextY());
break;
case OutlineCode.LINE:
ctx.lineTo(go.nextX(), go.nextY());
break;
case OutlineCode.QUADRATIC:
x = go.nextX();
y = go.nextY();
ctx.quadraticCurveTo(go.nextX(), go.nextY(), x, y);
break;
case OutlineCode.BEZIER:
x = go.nextX();
y = go.nextY();
ctx.bezierCurveTo(go.nextX(), go.nextY(), go.nextX(), go.nextY(), x, y);
break;
}
}
ctx.fill();
}
static getOutlineBoundingBox(outline: number[], scale: number, x_pos: number, y_pos: number): BoundingBox {
const go = new GlyphOutline(outline, x_pos, y_pos, scale);
const bboxComp = new BoundingBoxComputation();
// (penX, penY) hold the pen position: the start of each stroke.
let penX: number = x_pos;
let penY: number = y_pos;
let x, y: number;
while (!go.done()) {
switch (go.next()) {
case OutlineCode.MOVE:
// Note that we don't add any points to the bounding box until a srroke is actually drawn.
penX = go.nextX();
penY = go.nextY();
break;
case OutlineCode.LINE:
bboxComp.addPoint(penX, penY);
penX = go.nextX();
penY = go.nextY();
bboxComp.addPoint(penX, penY);
break;
case OutlineCode.QUADRATIC:
x = go.nextX();
y = go.nextY();
bboxComp.addQuadraticCurve(penX, penY, go.nextX(), go.nextY(), x, y);
penX = x;
penY = y;
break;
case OutlineCode.BEZIER:
x = go.nextX();
y = go.nextY();
bboxComp.addBezierCurve(penX, penY, go.nextX(), go.nextY(), go.nextX(), go.nextY(), x, y);
penX = x;
penY = y;
break;
}
}
return new BoundingBox(bboxComp.getX1(), bboxComp.getY1(), bboxComp.width(), bboxComp.height());
}
static getWidth(code: string, point: number, category?: string): number {
const data = Glyph.cache.lookup(code, category);
if (data.point != -1) {
point = data.point;
}
const scale = (point * 72) / (data.metrics.font.getResolution() * 100);
return data.bbox.getW() * scale;
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// INSTANCE MEMBERS
bbox: BoundingBox = new BoundingBox(0, 0, 0, 0);
code: string;
// metrics is initialized in the constructor by reset() or setOptions() which calls reset().
// eslint-disable-next-line
metrics!: GlyphMetrics;
topGlyphs: Glyph[] = [];
botGlyphs: Glyph[] = [];
protected options: GlyphOptions = {};
protected originShift: { x: number; y: number };
protected x_shift: number;
protected y_shift: number;
scale: number = 1;
protected point: number;
protected stave?: Stave;
/**
* @param code
* @param point
* @param options
*/
constructor(code: string, point: number, options?: GlyphOptions) {
super();
this.code = code;
this.point = point;
this.originShift = { x: 0, y: 0 };
this.x_shift = 0;
this.y_shift = 0;
if (options) {
this.setOptions(options);
} else {
this.reset();
}
}
// eslint-disable-next-line
draw(...args: any[]): void {
// DO NOTHING.
}
getCode(): string {
return this.code;
}
// eslint-disable-next-line
setOptions(options: any): void {
this.options = { ...this.options, ...options };
this.reset();
}
setPoint(point: number): this {
this.point = point;
return this;
}
setStave(stave: Stave): this {
this.stave = stave;
return this;
}
getXShift(): number {
return this.x_shift;
}
setXShift(x_shift: number): this {
this.x_shift = x_shift;
return this;
}
getYshift(): number {
return this.y_shift;
}
setYShift(y_shift: number): this {
this.y_shift = y_shift;
return this;
}
reset(): void {
const data = Glyph.cache.lookup(this.code, this.options.category);
this.metrics = data.metrics;
// Override point from metrics file
if (data.point != -1) {
this.point = data.point;
}
this.scale = (this.point * 72) / (this.metrics.font.getResolution() * 100);
this.bbox = new BoundingBox(
data.bbox.getX() * this.scale,
data.bbox.getY() * this.scale,
data.bbox.getW() * this.scale,
data.bbox.getH() * this.scale
);
}
checkMetrics(): GlyphMetrics {
return defined(this.metrics, 'BadGlyph', `Glyph ${this.code} is not initialized.`);
}
getMetrics(): GlyphMetrics {
const metrics = this.checkMetrics();
const metricsScale = metrics.scale;
return {
x_min: metrics.x_min * this.scale * metricsScale,
x_max: metrics.x_max * this.scale * metricsScale,
width: this.bbox.getW(),
height: this.bbox.getH(),
scale: this.scale * metricsScale,
x_shift: metrics.x_shift,
y_shift: metrics.y_shift,
outline: metrics.outline,
font: metrics.font,
ha: metrics.ha,
};
}
setOriginX(x: number): void {
const { bbox } = this;
const originX = Math.abs(bbox.getX() / bbox.getW());
const xShift = (x - originX) * bbox.getW();
this.originShift.x = -xShift;
}
setOriginY(y: number): void {
const { bbox } = this;
const originY = Math.abs(bbox.getY() / bbox.getH());
const yShift = (y - originY) * bbox.getH();
this.originShift.y = -yShift;
}
setOrigin(x: number, y: number): void {
this.setOriginX(x);
this.setOriginY(y);
}
render(ctx: RenderContext, x: number, y: number): void {
const metrics = this.checkMetrics();
const outline = metrics.outline;
const scale = this.scale * metrics.scale;
this.setRendered();
this.applyStyle(ctx);
const xPos = x + this.originShift.x + metrics.x_shift;
const yPos = y + this.originShift.y + metrics.y_shift;
Glyph.renderOutline(ctx, outline, scale, xPos, yPos);
this.restoreStyle(ctx);
}
checkStave(): Stave {
return defined(this.stave, 'NoStave', 'No stave attached to instance.');
}
renderToStave(x: number): void {
const context = this.checkContext();
const metrics = this.checkMetrics();
const stave = this.checkStave();
const outline = metrics.outline;
const scale = this.scale * metrics.scale;
this.setRendered();
this.applyStyle();
const xPos = x + this.x_shift + metrics.x_shift;
const yPos = stave.getYForGlyphs() + this.y_shift + metrics.y_shift;
Glyph.renderOutline(context, outline, scale, xPos, yPos);
this.restoreStyle();
}
}