UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature.

342 lines (341 loc) 12.1 kB
import { BoundingBox } from './boundingbox.js'; import { BoundingBoxComputation } from './boundingboxcomputation.js'; import { Element } from './element.js'; import { Tables } from './tables.js'; import { defined, RuntimeError } from './util.js'; class GlyphCacheEntry { constructor(fontStack, code, category) { this.point = -1; 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 { constructor() { this.cache = new Map(); } lookup(code, category) { 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 { constructor(outline, originX, originY, scale) { this.outline = outline; this.originX = originX; this.originY = originY; this.scale = scale; this.i = 0; this.precision = 1; this.precision = Math.pow(10, Tables.RENDER_PRECISION_PLACES); } done() { return this.i >= this.outline.length; } next() { return Math.round((this.outline[this.i++] * this.precision) / this.precision); } nextX() { return Math.round((this.originX + this.outline[this.i++] * this.scale) * this.precision) / this.precision; } nextY() { return Math.round((this.originY - this.outline[this.i++] * this.scale) * this.precision) / this.precision; } static parse(str) { const result = []; const parts = str.split(' '); let i = 0; while (i < parts.length) { switch (parts[i++]) { case 'm': result.push(0, parseInt(parts[i++]), parseInt(parts[i++])); break; case 'l': result.push(1, parseInt(parts[i++]), parseInt(parts[i++])); break; case 'q': result.push(2, parseInt(parts[i++]), parseInt(parts[i++]), parseInt(parts[i++]), parseInt(parts[i++])); break; case 'b': result.push(3, parseInt(parts[i++]), parseInt(parts[i++]), parseInt(parts[i++]), parseInt(parts[i++]), parseInt(parts[i++]), parseInt(parts[i++])); break; } } return result; } } class Glyph extends Element { static get CATEGORY() { return "Glyph"; } static lookupFontMetric(font, category, code, key, defaultValue) { let value = font.lookupMetric(`glyphs.${category}.${code}.${key}`, undefined); if (value === undefined) { value = font.lookupMetric(`glyphs.${category}.${key}`, defaultValue); } return value; } static lookupGlyph(fontStack, code) { defined(fontStack, 'BadFontStack', '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) return { glyph, font }; } throw new RuntimeError('BadGlyph', `Glyph ${code} does not exist in font.`); } static loadMetrics(fontStack, code, category) { 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, }; } static renderGlyph(ctx, x_pos, y_pos, point, code, options) { var _a; const data = Glyph.cache.lookup(code, options === null || options === void 0 ? void 0 : options.category); const metrics = data.metrics; if (data.point != -1) { point = data.point; } const customScale = (_a = options === null || options === void 0 ? void 0 : options.scale) !== null && _a !== void 0 ? _a : 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, outline, scale, x_pos, y_pos) { const go = new GlyphOutline(outline, x_pos, y_pos, scale); ctx.beginPath(); let x, y; while (!go.done()) { switch (go.next()) { case 0: ctx.moveTo(go.nextX(), go.nextY()); break; case 1: ctx.lineTo(go.nextX(), go.nextY()); break; case 2: x = go.nextX(); y = go.nextY(); ctx.quadraticCurveTo(go.nextX(), go.nextY(), x, y); break; case 3: x = go.nextX(); y = go.nextY(); ctx.bezierCurveTo(go.nextX(), go.nextY(), go.nextX(), go.nextY(), x, y); break; } } ctx.fill(); } static getOutlineBoundingBox(outline, scale, x_pos, y_pos) { const go = new GlyphOutline(outline, x_pos, y_pos, scale); const bboxComp = new BoundingBoxComputation(); let penX = x_pos; let penY = y_pos; let x, y; while (!go.done()) { switch (go.next()) { case 0: penX = go.nextX(); penY = go.nextY(); break; case 1: bboxComp.addPoint(penX, penY); penX = go.nextX(); penY = go.nextY(); bboxComp.addPoint(penX, penY); break; case 2: x = go.nextX(); y = go.nextY(); bboxComp.addQuadraticCurve(penX, penY, go.nextX(), go.nextY(), x, y); penX = x; penY = y; break; case 3: 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, point, category) { 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; } constructor(code, point, options) { super(); this.bbox = new BoundingBox(0, 0, 0, 0); this.topGlyphs = []; this.botGlyphs = []; this.options = {}; this.scale = 1; 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(); } } draw(...args) { } getCode() { return this.code; } setOptions(options) { this.options = Object.assign(Object.assign({}, this.options), options); this.reset(); } setPoint(point) { this.point = point; return this; } setStave(stave) { this.stave = stave; return this; } getXShift() { return this.x_shift; } setXShift(x_shift) { this.x_shift = x_shift; return this; } getYshift() { return this.y_shift; } setYShift(y_shift) { this.y_shift = y_shift; return this; } reset() { const data = Glyph.cache.lookup(this.code, this.options.category); this.metrics = data.metrics; 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() { return defined(this.metrics, 'BadGlyph', `Glyph ${this.code} is not initialized.`); } getMetrics() { 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) { 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) { 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() { return defined(this.stave, 'NoStave', 'No stave attached to instance.'); } renderToStave(x) { 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(); } } Glyph.cache = new GlyphCache(); Glyph.CURRENT_CACHE_KEY = ''; Glyph.MUSIC_FONT_STACK = []; export { Glyph };