UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature.

467 lines (466 loc) 16.7 kB
import { Font, FontStyle, FontWeight } from './font.js'; import { RenderContext } from './rendercontext.js'; import { Tables } from './tables.js'; import { normalizeAngle, prefix, RuntimeError } from './util.js'; const ATTRIBUTES_TO_IGNORE = { path: { x: true, y: true, width: true, height: true, 'font-family': true, 'font-weight': true, 'font-style': true, 'font-size': true, }, rect: { 'font-family': true, 'font-weight': true, 'font-style': true, 'font-size': true, }, text: { width: true, height: true, }, }; const SVG_NS = 'http://www.w3.org/2000/svg'; const TWO_PI = 2 * Math.PI; class MeasureTextCache { constructor() { this.cache = {}; } lookup(text, svg, attributes) { let entries = this.cache[text]; if (entries === undefined) { entries = {}; this.cache[text] = entries; } const family = attributes['font-family']; const size = attributes['font-size']; const weight = attributes['font-weight']; const style = attributes['font-style']; const key = `${family}%${size}%${weight}%${style}`; let entry = entries[key]; if (entry === undefined) { entry = this.measureImpl(text, svg, attributes); entries[key] = entry; } return entry; } measureImpl(text, svg, attributes) { let txt = this.txt; if (!txt) { txt = document.createElementNS(SVG_NS, 'text'); this.txt = txt; } txt.textContent = text; if (attributes['font-family']) txt.setAttributeNS(null, 'font-family', attributes['font-family']); if (attributes['font-size']) txt.setAttributeNS(null, 'font-size', `${attributes['font-size']}`); if (attributes['font-style']) txt.setAttributeNS(null, 'font-style', attributes['font-style']); if (attributes['font-weight']) txt.setAttributeNS(null, 'font-weight', `${attributes['font-weight']}`); svg.appendChild(txt); const bbox = txt.getBBox(); svg.removeChild(txt); return { x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height }; } } class SVGContext extends RenderContext { constructor(element) { super(); this.width = 0; this.height = 0; this.precision = 1; this.backgroundFillStyle = 'white'; this.fontCSSString = ''; this.element = element; this.precision = Math.pow(10, Tables.RENDER_PRECISION_PLACES); const svg = this.create('svg'); this.element.appendChild(svg); this.svg = svg; this.parent = this.svg; this.groups = [this.svg]; this.path = ''; this.pen = { x: NaN, y: NaN }; this.lineWidth = 1.0; const defaultFontAttributes = { 'font-family': Font.SANS_SERIF, 'font-size': Font.SIZE + 'pt', 'font-weight': FontWeight.NORMAL, 'font-style': FontStyle.NORMAL, }; this.state = Object.assign({ scaleX: 1, scaleY: 1 }, defaultFontAttributes); this.attributes = Object.assign({ 'stroke-width': 0.3, 'stroke-dasharray': 'none', fill: 'black', stroke: 'black' }, defaultFontAttributes); this.groupAttributes = []; this.applyAttributes(svg, this.attributes); this.groupAttributes.push(Object.assign({}, this.attributes)); this.shadow_attributes = { width: 0, color: 'black', }; this.state_stack = []; } round(n) { return Math.round(n * this.precision) / this.precision; } create(svgElementType) { return document.createElementNS(SVG_NS, svgElementType); } openGroup(cls, id, attrs) { const group = this.create('g'); this.groups.push(group); this.parent.appendChild(group); this.parent = group; if (cls) group.setAttribute('class', prefix(cls)); if (id) group.setAttribute('id', prefix(id)); if (attrs && attrs.pointerBBox) { group.setAttribute('pointer-events', 'bounding-box'); } this.applyAttributes(group, this.attributes); this.groupAttributes.push(Object.assign(Object.assign({}, this.groupAttributes[this.groupAttributes.length - 1]), this.attributes)); return group; } closeGroup() { this.groups.pop(); this.groupAttributes.pop(); this.parent = this.groups[this.groups.length - 1]; } add(elem) { this.parent.appendChild(elem); } setFillStyle(style) { this.attributes.fill = style; return this; } setBackgroundFillStyle(style) { this.backgroundFillStyle = style; return this; } setStrokeStyle(style) { this.attributes.stroke = style; return this; } setShadowColor(color) { this.shadow_attributes.color = color; return this; } setShadowBlur(blur) { this.shadow_attributes.width = blur; return this; } setLineWidth(width) { this.attributes['stroke-width'] = width; this.lineWidth = width; return this; } setLineDash(lineDash) { if (Object.prototype.toString.call(lineDash) === '[object Array]') { this.attributes['stroke-dasharray'] = lineDash.join(','); return this; } else { throw new RuntimeError('ArgumentError', 'lineDash must be an array of integers.'); } } setLineCap(capType) { this.attributes['stroke-linecap'] = capType; return this; } resize(width, height) { this.width = width; this.height = height; this.element.style.width = width.toString(); this.svg.style.width = width.toString(); this.svg.style.height = height.toString(); const attributes = { width, height, }; this.applyAttributes(this.svg, attributes); this.scale(this.state.scaleX, this.state.scaleY); return this; } scale(x, y) { this.state.scaleX = this.state.scaleX ? this.state.scaleX * x : x; this.state.scaleY = this.state.scaleY ? this.state.scaleY * y : y; const visibleWidth = this.width / this.state.scaleX; const visibleHeight = this.height / this.state.scaleY; this.setViewBox(0, 0, visibleWidth, visibleHeight); return this; } setViewBox(viewBox_or_minX, minY, width, height) { if (typeof viewBox_or_minX === 'string') { this.svg.setAttribute('viewBox', viewBox_or_minX); } else { const viewBoxString = viewBox_or_minX + ' ' + minY + ' ' + width + ' ' + height; this.svg.setAttribute('viewBox', viewBoxString); } } applyAttributes(element, attributes) { const attrNamesToIgnore = ATTRIBUTES_TO_IGNORE[element.nodeName]; for (const attrName in attributes) { if (attrNamesToIgnore && attrNamesToIgnore[attrName]) { continue; } if (attributes[attrName] && (this.groupAttributes.length == 0 || attributes[attrName] != this.groupAttributes[this.groupAttributes.length - 1][attrName])) element.setAttributeNS(null, attrName, attributes[attrName]); } return element; } clear() { while (this.svg.lastChild) { this.svg.removeChild(this.svg.lastChild); } this.scale(this.state.scaleX, this.state.scaleY); } rect(x, y, width, height, attributes) { if (height < 0) { y += height; height *= -1; } const rectangle = this.create('rect'); attributes = attributes !== null && attributes !== void 0 ? attributes : { fill: 'none', 'stroke-width': this.lineWidth, stroke: 'black' }; x = this.round(x); y = this.round(y); width = this.round(width); height = this.round(height); this.applyAttributes(rectangle, Object.assign({ x, y, width, height }, attributes)); this.add(rectangle); return this; } fillRect(x, y, width, height) { const attributes = { fill: this.attributes.fill, stroke: 'none' }; this.rect(x, y, width, height, attributes); return this; } clearRect(x, y, width, height) { this.rect(x, y, width, height, { fill: this.backgroundFillStyle, stroke: 'none' }); return this; } beginPath() { this.path = ''; this.pen.x = NaN; this.pen.y = NaN; return this; } moveTo(x, y) { x = this.round(x); y = this.round(y); this.path += 'M' + x + ' ' + y; this.pen.x = x; this.pen.y = y; return this; } lineTo(x, y) { x = this.round(x); y = this.round(y); this.path += 'L' + x + ' ' + y; this.pen.x = x; this.pen.y = y; return this; } bezierCurveTo(x1, y1, x2, y2, x, y) { x = this.round(x); y = this.round(y); x1 = this.round(x1); y1 = this.round(y1); x2 = this.round(x2); y2 = this.round(y2); this.path += 'C' + x1 + ' ' + y1 + ',' + x2 + ' ' + y2 + ',' + x + ' ' + y; this.pen.x = x; this.pen.y = y; return this; } quadraticCurveTo(x1, y1, x, y) { x = this.round(x); y = this.round(y); x1 = this.round(x1); y1 = this.round(y1); this.path += 'Q' + x1 + ' ' + y1 + ',' + x + ' ' + y; this.pen.x = x; this.pen.y = y; return this; } arc(x, y, radius, startAngle, endAngle, counterclockwise) { let x0 = x + radius * Math.cos(startAngle); let y0 = y + radius * Math.sin(startAngle); x0 = this.round(x0); y0 = this.round(y0); const tmpStartTest = normalizeAngle(startAngle); const tmpEndTest = normalizeAngle(endAngle); if ((!counterclockwise && endAngle - startAngle >= TWO_PI) || (counterclockwise && startAngle - endAngle >= TWO_PI) || tmpStartTest === tmpEndTest) { let x1 = x + radius * Math.cos(startAngle + Math.PI); let y1 = y + radius * Math.sin(startAngle + Math.PI); x1 = this.round(x1); y1 = this.round(y1); radius = this.round(radius); this.path += `M${x0} ${y0} A${radius} ${radius} 0 0 0 ${x1} ${y1} `; this.path += `A${radius} ${radius} 0 0 0 ${x0} ${y0}`; this.pen.x = x0; this.pen.y = y0; } else { let x1 = x + radius * Math.cos(endAngle); let y1 = y + radius * Math.sin(endAngle); startAngle = tmpStartTest; endAngle = tmpEndTest; let large; if (Math.abs(endAngle - startAngle) < Math.PI) { large = counterclockwise; } else { large = !counterclockwise; } if (startAngle > endAngle) { large = !large; } const sweep = !counterclockwise; x1 = this.round(x1); y1 = this.round(y1); radius = this.round(radius); this.path += `M${x0} ${y0} A${radius} ${radius} 0 ${+large} ${+sweep} ${x1} ${y1}`; this.pen.x = x1; this.pen.y = y1; } return this; } closePath() { this.path += 'Z'; return this; } getShadowStyle() { const sa = this.shadow_attributes; return `filter: drop-shadow(0 0 ${sa.width / 1.5}px ${sa.color})`; } fill(attributes) { const path = this.create('path'); if (typeof attributes === 'undefined') { attributes = Object.assign(Object.assign({}, this.attributes), { stroke: 'none' }); } attributes.d = this.path; if (this.shadow_attributes.width > 0) { attributes.style = this.getShadowStyle(); } this.applyAttributes(path, attributes); this.add(path); return this; } stroke() { const path = this.create('path'); const attributes = Object.assign(Object.assign({}, this.attributes), { fill: 'none', 'stroke-width': this.lineWidth, d: this.path }); if (this.shadow_attributes.width > 0) { attributes.style = this.getShadowStyle(); } this.applyAttributes(path, attributes); this.add(path); return this; } measureText(text) { return SVGContext.measureTextCache.lookup(text, this.svg, this.attributes); } fillText(text, x, y) { if (!text || text.length <= 0) { return this; } x = this.round(x); y = this.round(y); const attributes = Object.assign(Object.assign({}, this.attributes), { stroke: 'none', x, y }); const txt = this.create('text'); txt.textContent = text; this.applyAttributes(txt, attributes); this.add(txt); return this; } save() { this.state_stack.push({ state: { 'font-family': this.state['font-family'], 'font-weight': this.state['font-weight'], 'font-style': this.state['font-style'], 'font-size': this.state['font-size'], scale: this.state.scale, }, attributes: { 'font-family': this.attributes['font-family'], 'font-weight': this.attributes['font-weight'], 'font-style': this.attributes['font-style'], 'font-size': this.attributes['font-size'], fill: this.attributes.fill, stroke: this.attributes.stroke, 'stroke-width': this.attributes['stroke-width'], 'stroke-dasharray': this.attributes['stroke-dasharray'], }, shadow_attributes: { width: this.shadow_attributes.width, color: this.shadow_attributes.color, }, lineWidth: this.lineWidth, }); return this; } restore() { const savedState = this.state_stack.pop(); if (savedState) { const state = savedState; this.state['font-family'] = state.state['font-family']; this.state['font-weight'] = state.state['font-weight']; this.state['font-style'] = state.state['font-style']; this.state['font-size'] = state.state['font-size']; this.state.scale = state.state.scale; this.attributes['font-family'] = state.attributes['font-family']; this.attributes['font-weight'] = state.attributes['font-weight']; this.attributes['font-style'] = state.attributes['font-style']; this.attributes['font-size'] = state.attributes['font-size']; this.attributes.fill = state.attributes.fill; this.attributes.stroke = state.attributes.stroke; this.attributes['stroke-width'] = state.attributes['stroke-width']; this.attributes['stroke-dasharray'] = state.attributes['stroke-dasharray']; this.shadow_attributes.width = state.shadow_attributes.width; this.shadow_attributes.color = state.shadow_attributes.color; this.lineWidth = state.lineWidth; } return this; } set fillStyle(style) { this.setFillStyle(style); } get fillStyle() { return this.attributes.fill; } set strokeStyle(style) { this.setStrokeStyle(style); } get strokeStyle() { return this.attributes.stroke; } setFont(f, size, weight, style) { const fontInfo = Font.validate(f, size, weight, style); this.fontCSSString = Font.toCSSString(fontInfo); const fontAttributes = { 'font-family': fontInfo.family, 'font-size': fontInfo.size, 'font-weight': fontInfo.weight, 'font-style': fontInfo.style, }; this.attributes = Object.assign(Object.assign({}, this.attributes), fontAttributes); this.state = Object.assign(Object.assign({}, this.state), fontAttributes); return this; } getFont() { return this.fontCSSString; } } SVGContext.measureTextCache = new MeasureTextCache(); export { SVGContext };