UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature

397 lines (345 loc) 10.4 kB
// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010. // // ## Description // A rendering context for the Raphael backend. // // ## Warning: Deprecated for SVGContext // Except in instances where SVG support for IE < 9.0 is // needed, SVGContext is recommended. export class RaphaelContext { constructor(element) { this.element = element; this.paper = Raphael(element); // eslint-disable-line this.path = ''; this.pen = { x: 0, y: 0 }; this.lineWidth = 1.0; this.state = { scale: { x: 1, y: 1 }, font_family: 'Arial', font_size: 8, font_weight: 800, }; this.attributes = { 'stroke-width': 0.3, 'fill': 'black', 'stroke': 'black', 'font': '10pt Arial', }; this.background_attributes = { 'stroke-width': 0, 'fill': 'white', 'stroke': 'white', 'font': '10pt Arial', }; this.shadow_attributes = { width: 0, color: 'black', }; this.state_stack = []; } // Containers not implemented openGroup() {} closeGroup() {} add() {} setFont(family, size, weight) { this.state.font_family = family; this.state.font_size = size; this.state.font_weight = weight; this.attributes.font = (this.state.font_weight || '') + ' ' + (this.state.font_size * this.state.scale.x) + 'pt ' + this.state.font_family; return this; } setRawFont(font) { this.attributes.font = font; return this; } setFillStyle(style) { this.attributes.fill = style; return this; } setBackgroundFillStyle(style) { this.background_attributes.fill = style; this.background_attributes.stroke = style; return this; } setStrokeStyle(style) { this.attributes.stroke = style; return this; } setShadowColor(style) { this.shadow_attributes.color = style; return this; } setShadowBlur(blur) { this.shadow_attributes.width = blur; return this; } setLineWidth(width) { this.attributes['stroke-width'] = width; this.lineWidth = width; } // Empty because there is no equivalent in SVG setLineDash() { return this; } setLineCap() { return this; } scale(x, y) { this.state.scale = { x, y }; // The scale() method is deprecated as of Raphael.JS 2.0, and // can no longer be used as an option in an Element.attr() call. // It is preserved here for users running earlier versions of // Raphael.JS, though it has no effect on the SVG output in // Raphael 2 and higher. this.attributes.transform = 'S' + x + ',' + y + ',0,0'; this.attributes.scale = x + ',' + y + ',0,0'; this.attributes.font = this.state.font_size * this.state.scale.x + 'pt ' + this.state.font_family; this.background_attributes.transform = 'S' + x + ',' + y + ',0,0'; this.background_attributes.font = this.state.font_size * this.state.scale.x + 'pt ' + this.state.font_family; return this; } clear() { this.paper.clear(); } resize(width, height) { this.element.style.width = width; this.paper.setSize(width, height); return this; } // Sets the SVG `viewBox` property, which results in auto scaling images when its container // is resized. // // Usage: `ctx.setViewBox("0 0 600 400")` setViewBox(viewBox) { this.paper.canvas.setAttribute('viewBox', viewBox); } rect(x, y, width, height) { if (height < 0) { y += height; height = -height; } this.paper.rect(x, y, width - 0.5, height - 0.5) .attr(this.attributes) .attr('fill', 'none') .attr('stroke-width', this.lineWidth); return this; } fillRect(x, y, width, height) { if (height < 0) { y += height; height = -height; } this.paper.rect(x, y, width - 0.5, height - 0.5).attr(this.attributes); return this; } clearRect(x, y, width, height) { if (height < 0) { y += height; height = -height; } this.paper.rect(x, y, width - 0.5, height - 0.5) .attr(this.background_attributes); return this; } beginPath() { this.path = ''; this.pen.x = 0; this.pen.y = 0; return this; } moveTo(x, y) { this.path += 'M' + x + ',' + y; this.pen.x = x; this.pen.y = y; return this; } lineTo(x, y) { this.path += 'L' + x + ',' + y; this.pen.x = x; this.pen.y = y; return this; } bezierCurveTo(x1, y1, x2, y2, x, y) { this.path += 'C' + x1 + ',' + y1 + ',' + x2 + ',' + y2 + ',' + x + ',' + y; this.pen.x = x; this.pen.y = y; return this; } quadraticCurveTo(x1, y1, x, y) { this.path += 'Q' + x1 + ',' + y1 + ',' + x + ',' + y; this.pen.x = x; this.pen.y = y; return this; } // This is an attempt (hack) to simulate the HTML5 canvas // arc method. arc(x, y, radius, startAngle, endAngle, antiClockwise) { function normalizeAngle(angle) { while (angle < 0) { angle += Math.PI * 2; } while (angle > Math.PI * 2) { angle -= Math.PI * 2; } return angle; } startAngle = normalizeAngle(startAngle); endAngle = normalizeAngle(endAngle); if (startAngle > endAngle) { const tmp = startAngle; startAngle = endAngle; endAngle = tmp; antiClockwise = !antiClockwise; } const delta = endAngle - startAngle; if (delta > Math.PI) { this.arcHelper(x, y, radius, startAngle, startAngle + delta / 2, antiClockwise); this.arcHelper(x, y, radius, startAngle + delta / 2, endAngle, antiClockwise); } else { this.arcHelper(x, y, radius, startAngle, endAngle, antiClockwise); } return this; } arcHelper(x, y, radius, startAngle, endAngle, antiClockwise) { const x1 = x + radius * Math.cos(startAngle); const y1 = y + radius * Math.sin(startAngle); const x2 = x + radius * Math.cos(endAngle); const y2 = y + radius * Math.sin(endAngle); let largeArcFlag = 0; let sweepFlag = 0; if (antiClockwise) { sweepFlag = 1; if (endAngle - startAngle < Math.PI) { largeArcFlag = 1; } } else if (endAngle - startAngle > Math.PI) { largeArcFlag = 1; } this.path += 'M' + x1 + ',' + y1 + ',A' + radius + ',' + radius + ',0,' + largeArcFlag + ',' + sweepFlag + ',' + x2 + ',' + y2 + 'M' + this.pen.x + ',' + this.pen.y; } // Adapted from the source for Raphael's Element.glow glow() { const out = this.paper.set(); if (this.shadow_attributes.width > 0) { const sa = this.shadow_attributes; const num_paths = sa.width / 2; for (let i = 1; i <= num_paths; i++) { out.push(this.paper.path(this.path).attr({ stroke: sa.color, 'stroke-linejoin': 'round', 'stroke-linecap': 'round', 'stroke-width': +(sa.width / num_paths * i).toFixed(3), opacity: +((sa.opacity || 0.3) / num_paths).toFixed(3), // See note in this.scale(): In Raphael the scale() method // is deprecated and removed as of Raphael 2.0 and replaced // by the transform() method. It is preserved here for // users with earlier versions of Raphael, but has no effect // on the output SVG in Raphael 2.0+. transform: this.attributes.transform, scale: this.attributes.scale, })); } } return out; } fill() { const elem = this.paper.path(this.path) .attr(this.attributes) .attr('stroke-width', 0); this.glow(elem); return this; } stroke() { // The first line of code below is, unfortunately, a bit of a hack: // Raphael's transform() scaling does not scale the stroke-width, so // in order to scale a stroke, we have to manually scale the // stroke-width. // // This works well so long as the X & Y states for this.scale() are // relatively similar. However, if they are very different, we // would expect horizontal and vertical lines to have different // stroke-widths. // // In the future, if we want to support very divergent values for // horizontal and vertical scaling, we may want to consider // implementing SVG scaling with properties of the SVG viewBox & // viewPort and removing it entirely from the Element.attr() calls. // This would more closely parallel the approach taken in // canvascontext.js as well. const strokeWidth = this.lineWidth * (this.state.scale.x + this.state.scale.y) / 2; const elem = this.paper.path(this.path) .attr(this.attributes) .attr('fill', 'none') .attr('stroke-width', strokeWidth); this.glow(elem); return this; } closePath() { this.path += 'Z'; return this; } measureText(text) { const txt = this.paper.text(0, 0, text) .attr(this.attributes) .attr('fill', 'none') .attr('stroke', 'none'); const bounds = txt.getBBox(); txt.remove(); return { width: bounds.width, height: bounds.height, }; } fillText(text, x, y) { this.paper .text( x + (this.measureText(text).width / 2), y - (this.state.font_size / (2.25 * this.state.scale.y)), text ) .attr(this.attributes); return this; } save() { // TODO(mmuthanna): State needs to be deep-copied. this.state_stack.push({ state: { font_family: this.state.font_family, }, attributes: { font: this.attributes.font, fill: this.attributes.fill, stroke: this.attributes.stroke, 'stroke-width': this.attributes['stroke-width'], }, shadow_attributes: { width: this.shadow_attributes.width, color: this.shadow_attributes.color, }, }); return this; } restore() { // TODO(0xfe): State needs to be deep-restored. const state = this.state_stack.pop(); this.state.font_family = state.state.font_family; this.attributes.font = state.attributes.font; this.attributes.fill = state.attributes.fill; this.attributes.stroke = state.attributes.stroke; this.attributes['stroke-width'] = state.attributes['stroke-width']; this.shadow_attributes.width = state.shadow_attributes.width; this.shadow_attributes.color = state.shadow_attributes.color; return this; } }