VexFlow - Copyright (c) Mohit Muthanna 2010.
import { Vex } from './vex';
import { Flow } from './tables';
import { Element } from './element';
import { BoundingBoxComputation } from './boundingboxcomputation';
import { BoundingBox } from './boundingbox';
function processOutline(outline, originX, originY, scaleX, scaleY, outlineFns) {
let command;
let x;
let y;
let i = 0;
function nextX() { return originX + outline[i++] * scaleX; }
function nextY() { return originY + outline[i++] * scaleY; }
function doOutline(command, ...args) {
outlineFns[command](...args);
}
while (i < outline.length) {
command = outline[i++];
switch (command) {
case 'm':
case 'l':
doOutline(command, nextX(), nextY());
break;
case 'q':
x = nextX();
y = nextY();
doOutline(command, nextX(), nextY(), x, y);
break;
case 'b':
x = nextX();
y = nextY();
doOutline(command, nextX(), nextY(), nextX(), nextY(), x, y);
break;
case 'z':
break;
default:
break;
}
}
}
export class Glyph extends Element {
/*
Static methods used to implement loading and rendering glyphs.
Below categoryPath can be any metric path under 'glyphs', so stem.up would respolve
to glyphs.stem.up.shifX, glyphs.stem.up.shiftY, etc.
*/
static lookupFontMetric({ font, category, code, key, defaultValue }) {
let value = font.lookupMetric(`glyphs.${category}.${code}.${key}`, null);
if (value === null) {
value = font.lookupMetric(`glyphs.${category}.${key}`, defaultValue);
}
return value;
}
static lookupGlyph(fontStack, code) {
if (!fontStack) {
throw Vex.RERR('BAD_FONTSTACK', 'Font stack is misconfigured');
}
let glyph;
let font;
for (let i = 0; i < fontStack.length; i++) {
font = fontStack[i];
glyph = font.getGlyphs()[code];
if (glyph) break;
}
if (!glyph) {
throw new Vex.RERR('BadGlyph', `Glyph ${code} does not exist in font.`);
}
return { glyph, font };
}
static loadMetrics(fontStack, code, category = null) {
const { glyph, font } = Glyph.lookupGlyph(fontStack, code);
const x_shift = category ? Glyph.lookupFontMetric({
font, category, code,
key: 'shiftX', defaultValue: 0
}) : 0;
const y_shift = category ? Glyph.lookupFontMetric({
font, category, code,
key: 'shiftY', defaultValue: 0
}) : 0;
const scale = category ? Glyph.lookupFontMetric({
font, category, code,
key: 'scale', defaultValue: 1
}) : 1;
const x_min = glyph.x_min;
const x_max = glyph.x_max;
const ha = glyph.ha;
let outline;
const CACHE = true;
if (glyph.o) {
if (CACHE) {
if (glyph.cached_outline) {
outline = glyph.cached_outline;
} else {
outline = glyph.o.split(' ');
glyph.cached_outline = outline;
}
} else {
if (glyph.cached_outline) delete glyph.cached_outline;
outline = glyph.o.split(' ');
}
return {
x_min,
x_max,
x_shift,
y_shift,
scale,
ha,
outline,
font,
};
} else {
throw new Vex.RERR('BadGlyph', `Glyph ${code} has no outline defined.`);
}
}
/**
* A quick and dirty static glyph renderer. Renders glyphs from the default
* font defined in Vex.Flow.Font.
*
* @param {!Object} ctx The canvas context.
* @param {number} x_pos X coordinate.
* @param {number} y_pos Y coordinate.
* @param {number} point The point size to use.
* @param {string} val The glyph code in font.getGlyphs()
*/
static renderGlyph(ctx, x_pos, y_pos, point, val, options) {
const params = {
fontStack: Flow.DEFAULT_FONT_STACK,
category: null,
...options
};
const metrics = Glyph.loadMetrics(params.fontStack, val, params.category);
point = params.category ? Glyph.lookupFontMetric({
font: metrics.font,
category: params.category,
code: val,
key: 'point',
defaultValue: point
}) : point;
const scale = point * 72.0 / (metrics.font.getResolution() * 100.0);
Glyph.renderOutline(ctx, metrics.outline, scale * metrics.scale, x_pos + metrics.x_shift, y_pos + metrics.y_shift, options);
return metrics;
}
static renderOutline(ctx, outline, scale, x_pos, y_pos, options) {
ctx.beginPath();
ctx.moveTo(x_pos, y_pos);
processOutline(outline, x_pos, y_pos, scale, -scale, {
m: ctx.moveTo.bind(ctx),
l: ctx.lineTo.bind(ctx),
q: ctx.quadraticCurveTo.bind(ctx),
b: ctx.bezierCurveTo.bind(ctx),
z: ctx.fill.bind(ctx), // ignored
}, options);
ctx.fill();
}
static getOutlineBoundingBox(outline, scale, x_pos, y_pos) {
const bboxComp = new BoundingBoxComputation();
processOutline(outline, x_pos, y_pos, scale, -scale, {
m: bboxComp.addPoint.bind(bboxComp),
l: bboxComp.addPoint.bind(bboxComp),
q: bboxComp.addQuadraticCurve.bind(bboxComp),
b: bboxComp.addBezierCurve.bind(bboxComp),
z: bboxComp.noOp.bind(bboxComp),
});
return new BoundingBox(
bboxComp.x1,
bboxComp.y1,
bboxComp.width(),
bboxComp.height()
);
}
/**
* @constructor
*/
constructor(code, point, options) {
super();
this.setAttribute('type', 'Glyph');
this.code = code;
this.point = point;
this.options = {
fontStack: this.getFontStack(),
category: null,
};
this.metrics = null;
this.x_shift = 0;
this.y_shift = 0;
this.originShift = {
x: 0,
y: 0,
};
if (options) {
this.setOptions(options);
} else {
this.reset();
}
}
getCode() {
return this.code;
}
setOptions(options) {
this.options = { ...this.options, ...options };
this.reset();
}
setPoint(point) { this.point = point; return this; }
setStave(stave) { this.stave = stave; return this; }
setXShift(x_shift) { this.x_shift = x_shift; return this; }
setYShift(y_shift) { this.y_shift = y_shift; return this; }
reset() {
this.metrics = Glyph.loadMetrics(this.options.fontStack, this.code, this.options.category);
Override point from metrics file
this.point = this.options.category ? Glyph.lookupFontMetric({
category: this.options.category,
font: this.metrics.font,
code: this.code,
key: 'point',
defaultValue: this.point,
}) : this.point;
this.scale = this.point * 72 / (this.metrics.font.getResolution() * 100);
this.bbox = Glyph.getOutlineBoundingBox(
this.metrics.outline,
this.scale * this.metrics.scale,
this.metrics.x_shift,
this.metrics.y_shift,
);
}
getMetrics() {
if (!this.metrics) {
throw new Vex.RuntimeError('BadGlyph', `Glyph ${this.code} is not initialized.`);
}
return {
x_min: this.metrics.x_min * this.scale * this.metrics.scale,
x_max: this.metrics.x_max * this.scale * this.metrics.scale,
width: this.bbox.getW(),
height: this.bbox.getH(),
};
}
setOriginX(x) {
const { bbox } = this;
const originX = Math.abs(bbox.getX() / bbox.getW());
const xShift = (x - originX) * bbox.getW();
this.originShift.x = -xShift;
}
setOriginY(y) {
const { bbox } = this;
const originY = Math.abs(bbox.getY() / bbox.getH());
const yShift = (y - originY) * bbox.getH();
this.originShift.y = -yShift;
}
setOrigin(x, y) {
this.setOriginX(x);
this.setOriginY(y);
}
render(ctx, x, y) {
if (!this.metrics) {
throw new Vex.RuntimeError('BadGlyph', `Glyph ${this.code} is not initialized.`);
}
const outline = this.metrics.outline;
const scale = this.scale * this.metrics.scale;
this.setRendered();
this.applyStyle(ctx);
Glyph.renderOutline(ctx, outline, scale,
x + this.originShift.x + this.metrics.x_shift,
y + this.originShift.y + this.metrics.y_shift);
this.restoreStyle(ctx);
}
renderToStave(x) {
this.checkContext();
if (!this.metrics) {
throw new Vex.RuntimeError('BadGlyph', `Glyph ${this.code} is not initialized.`);
}
if (!this.stave) {
throw new Vex.RuntimeError('GlyphError', 'No valid stave');
}
const outline = this.metrics.outline;
const scale = this.scale * this.metrics.scale;
this.setRendered();
this.applyStyle();
Glyph.renderOutline(this.context, outline, scale,
x + this.x_shift + this.metrics.x_shift, this.stave.getYForGlyphs() + this.y_shift + this.metrics.y_shift);
this.restoreStyle();
}
}