|
@@ -0,0 +1,692 @@
|
|
|
+// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
|
|
|
+// @author Gregory Ristow (2015)
|
|
|
+
|
|
|
+import { Vex } from './vex';
|
|
|
+
|
|
|
+const attrNamesToIgnoreMap = {
|
|
|
+ path: {
|
|
|
+ x: true,
|
|
|
+ y: true,
|
|
|
+ width: true,
|
|
|
+ height: true,
|
|
|
+ },
|
|
|
+ rect: {
|
|
|
+ },
|
|
|
+ text: {
|
|
|
+ width: true,
|
|
|
+ height: true,
|
|
|
+ },
|
|
|
+};
|
|
|
+
|
|
|
+{
|
|
|
+ const fontAttrNamesToIgnore = {
|
|
|
+ 'font-family': true,
|
|
|
+ 'font-weight': true,
|
|
|
+ 'font-style': true,
|
|
|
+ 'font-size': true,
|
|
|
+ };
|
|
|
+
|
|
|
+ Vex.Merge(attrNamesToIgnoreMap.rect, fontAttrNamesToIgnore);
|
|
|
+ Vex.Merge(attrNamesToIgnoreMap.path, fontAttrNamesToIgnore);
|
|
|
+}
|
|
|
+
|
|
|
+export class SVGContext {
|
|
|
+ constructor(element) {
|
|
|
+ // element is the parent DOM object
|
|
|
+ this.element = element;
|
|
|
+ // Create the SVG in the SVG namespace:
|
|
|
+ this.svgNS = 'http://www.w3.org/2000/svg';
|
|
|
+ const svg = this.create('svg');
|
|
|
+ // Add it to the canvas:
|
|
|
+ this.element.appendChild(svg);
|
|
|
+
|
|
|
+ // Point to it:
|
|
|
+ this.svg = svg;
|
|
|
+ this.groups = [this.svg]; // Create the group stack
|
|
|
+ this.parent = this.svg;
|
|
|
+
|
|
|
+ this.path = '';
|
|
|
+ this.pen = { x: NaN, y: NaN };
|
|
|
+ this.lineWidth = 1.0;
|
|
|
+ this.state = {
|
|
|
+ scale: { x: 1, y: 1 },
|
|
|
+ 'font-family': 'Arial',
|
|
|
+ 'font-size': '8pt',
|
|
|
+ 'font-weight': 'normal',
|
|
|
+ };
|
|
|
+
|
|
|
+ this.attributes = {
|
|
|
+ 'stroke-width': 0.3,
|
|
|
+ 'fill': 'black',
|
|
|
+ 'stroke': 'black',
|
|
|
+ 'stroke-dasharray': 'none',
|
|
|
+ 'font-family': 'Arial',
|
|
|
+ 'font-size': '10pt',
|
|
|
+ 'font-weight': 'normal',
|
|
|
+ 'font-style': 'normal',
|
|
|
+ };
|
|
|
+
|
|
|
+ this.background_attributes = {
|
|
|
+ 'stroke-width': 0,
|
|
|
+ 'fill': 'white',
|
|
|
+ 'stroke': 'white',
|
|
|
+ 'stroke-dasharray': 'none',
|
|
|
+ 'font-family': 'Arial',
|
|
|
+ 'font-size': '10pt',
|
|
|
+ 'font-weight': 'normal',
|
|
|
+ 'font-style': 'normal',
|
|
|
+ };
|
|
|
+
|
|
|
+ this.shadow_attributes = {
|
|
|
+ width: 0,
|
|
|
+ color: 'black',
|
|
|
+ };
|
|
|
+
|
|
|
+ this.state_stack = [];
|
|
|
+
|
|
|
+ // Test for Internet Explorer
|
|
|
+ this.iePolyfill();
|
|
|
+ }
|
|
|
+
|
|
|
+ create(svgElementType) {
|
|
|
+ return document.createElementNS(this.svgNS, svgElementType);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Allow grouping elements in containers for interactivity.
|
|
|
+ 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', Vex.Prefix(cls));
|
|
|
+ if (id) group.setAttribute('id', Vex.Prefix(id));
|
|
|
+
|
|
|
+ if (attrs && attrs.pointerBBox) {
|
|
|
+ group.setAttribute('pointer-events', 'bounding-box');
|
|
|
+ }
|
|
|
+ return group;
|
|
|
+ }
|
|
|
+
|
|
|
+ closeGroup() {
|
|
|
+ this.groups.pop();
|
|
|
+ this.parent = this.groups[this.groups.length - 1];
|
|
|
+ }
|
|
|
+
|
|
|
+ add(elem) {
|
|
|
+ this.parent.appendChild(elem);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Tests if the browser is Internet Explorer; if it is,
|
|
|
+ // we do some tricks to improve text layout. See the
|
|
|
+ // note at ieMeasureTextFix() for details.
|
|
|
+ iePolyfill() {
|
|
|
+ if (typeof (navigator) !== 'undefined') {
|
|
|
+ this.ie = (
|
|
|
+ /MSIE 9/i.test(navigator.userAgent) ||
|
|
|
+ /MSIE 10/i.test(navigator.userAgent) ||
|
|
|
+ /rv:11\.0/i.test(navigator.userAgent) ||
|
|
|
+ /Trident/i.test(navigator.userAgent)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ### Styling & State Methods:
|
|
|
+
|
|
|
+ setFont(family, size, weight) {
|
|
|
+ // Unlike canvas, in SVG italic is handled by font-style,
|
|
|
+ // not weight. So: we search the weight argument and
|
|
|
+ // apply bold and italic to weight and style respectively.
|
|
|
+ let bold = false;
|
|
|
+ let italic = false;
|
|
|
+ let style = 'normal';
|
|
|
+ // Weight might also be a number (200, 400, etc...) so we
|
|
|
+ // test its type to be sure we have access to String methods.
|
|
|
+ if (typeof weight === 'string') {
|
|
|
+ // look for "italic" in the weight:
|
|
|
+ if (weight.indexOf('italic') !== -1) {
|
|
|
+ weight = weight.replace(/italic/g, '');
|
|
|
+ italic = true;
|
|
|
+ }
|
|
|
+ // look for "bold" in weight
|
|
|
+ if (weight.indexOf('bold') !== -1) {
|
|
|
+ weight = weight.replace(/bold/g, '');
|
|
|
+ bold = true;
|
|
|
+ }
|
|
|
+ // remove any remaining spaces
|
|
|
+ weight = weight.replace(/ /g, '');
|
|
|
+ }
|
|
|
+ weight = bold ? 'bold' : weight;
|
|
|
+ weight = (typeof weight === 'undefined' || weight === '') ? 'normal' : weight;
|
|
|
+
|
|
|
+ style = italic ? 'italic' : style;
|
|
|
+
|
|
|
+ const fontAttributes = {
|
|
|
+ 'font-family': family,
|
|
|
+ 'font-size': size + 'pt',
|
|
|
+ 'font-weight': weight,
|
|
|
+ 'font-style': style,
|
|
|
+ };
|
|
|
+
|
|
|
+ // Store the font size so that if the browser is Internet
|
|
|
+ // Explorer we can fix its calculations of text width.
|
|
|
+ this.fontSize = Number(size);
|
|
|
+
|
|
|
+ Vex.Merge(this.attributes, fontAttributes);
|
|
|
+ Vex.Merge(this.state, fontAttributes);
|
|
|
+
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ setRawFont(font) {
|
|
|
+ font = font.trim();
|
|
|
+ // Assumes size first, splits on space -- which is presently
|
|
|
+ // how all existing modules are calling this.
|
|
|
+ const fontArray = font.split(' ');
|
|
|
+
|
|
|
+ this.attributes['font-family'] = fontArray[1];
|
|
|
+ this.state['font-family'] = fontArray[1];
|
|
|
+
|
|
|
+ this.attributes['font-size'] = fontArray[0];
|
|
|
+ this.state['font-size'] = fontArray[0];
|
|
|
+
|
|
|
+ // Saves fontSize for IE polyfill
|
|
|
+ this.fontSize = Number(fontArray[0].match(/\d+/));
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ // @param array {lineDash} as [dashInt, spaceInt, dashInt, spaceInt, etc...]
|
|
|
+ setLineDash(lineDash) {
|
|
|
+ if (Object.prototype.toString.call(lineDash) === '[object Array]') {
|
|
|
+ lineDash = lineDash.join(', ');
|
|
|
+ this.attributes['stroke-dasharray'] = lineDash;
|
|
|
+ return this;
|
|
|
+ } else {
|
|
|
+ throw new Vex.RERR('ArgumentError', 'lineDash must be an array of integers.');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setLineCap(lineCap) {
|
|
|
+ this.attributes['stroke-linecap'] = lineCap;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ### Sizing & Scaling Methods:
|
|
|
+
|
|
|
+ // TODO (GCR): See note at scale() -- seperate our internal
|
|
|
+ // conception of pixel-based width/height from the style.width
|
|
|
+ // and style.height properties eventually to allow users to
|
|
|
+ // apply responsive sizing attributes to the SVG.
|
|
|
+ resize(width, height) {
|
|
|
+ this.width = width;
|
|
|
+ this.height = height;
|
|
|
+ this.element.style.width = width;
|
|
|
+ const attributes = {
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ };
|
|
|
+ this.applyAttributes(this.svg, attributes);
|
|
|
+ this.scale(this.state.scale.x, this.state.scale.y);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ scale(x, y) {
|
|
|
+ // uses viewBox to scale
|
|
|
+ // TODO (GCR): we may at some point want to distinguish the
|
|
|
+ // style.width / style.height properties that are applied to
|
|
|
+ // the SVG object from our internal conception of the SVG
|
|
|
+ // width/height. This would allow us to create automatically
|
|
|
+ // scaling SVG's that filled their containers, for instance.
|
|
|
+ //
|
|
|
+ // As this isn't implemented in Canvas or Raphael contexts,
|
|
|
+ // I've left as is for now, but in using the viewBox to
|
|
|
+ // handle internal scaling, am trying to make it possible
|
|
|
+ // for us to eventually move in that direction.
|
|
|
+
|
|
|
+ this.state.scale = { x, y };
|
|
|
+ const visibleWidth = this.width / x;
|
|
|
+ const visibleHeight = this.height / y;
|
|
|
+ this.setViewBox(0, 0, visibleWidth, visibleHeight);
|
|
|
+
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ setViewBox(...args) {
|
|
|
+ // Override for "x y w h" style:
|
|
|
+ if (args.length === 1) {
|
|
|
+ const [viewBox] = args;
|
|
|
+ this.svg.setAttribute('viewBox', viewBox);
|
|
|
+ } else {
|
|
|
+ const [xMin, yMin, width, height] = args;
|
|
|
+ const viewBoxString = xMin + ' ' + yMin + ' ' + width + ' ' + height;
|
|
|
+ this.svg.setAttribute('viewBox', viewBoxString);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ### Drawing helper methods:
|
|
|
+
|
|
|
+ applyAttributes(element, attributes) {
|
|
|
+ const attrNamesToIgnore = attrNamesToIgnoreMap[element.nodeName];
|
|
|
+ Object
|
|
|
+ .keys(attributes)
|
|
|
+ .forEach(propertyName => {
|
|
|
+ if (attrNamesToIgnore && attrNamesToIgnore[propertyName]) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ element.setAttributeNS(null, propertyName, attributes[propertyName]);
|
|
|
+ });
|
|
|
+
|
|
|
+ return element;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ### Shape & Path Methods:
|
|
|
+
|
|
|
+ clear() {
|
|
|
+ // Clear the SVG by removing all inner children.
|
|
|
+
|
|
|
+ // (This approach is usually slightly more efficient
|
|
|
+ // than removing the old SVG & adding a new one to
|
|
|
+ // the container element, since it does not cause the
|
|
|
+ // container to resize twice. Also, the resize
|
|
|
+ // triggered by removing the entire SVG can trigger
|
|
|
+ // a touchcancel event when the element resizes away
|
|
|
+ // from a touch point.)
|
|
|
+
|
|
|
+ while (this.svg.lastChild) {
|
|
|
+ this.svg.removeChild(this.svg.lastChild);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Replace the viewbox attribute we just removed:
|
|
|
+ this.scale(this.state.scale.x, this.state.scale.y);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ## Rectangles:
|
|
|
+
|
|
|
+ rect(x, y, width, height, attributes) {
|
|
|
+ // Avoid invalid negative height attribs by
|
|
|
+ // flipping the rectangle on its head:
|
|
|
+ if (height < 0) {
|
|
|
+ y += height;
|
|
|
+ height *= -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Create the rect & style it:
|
|
|
+ const rectangle = this.create('rect');
|
|
|
+ if (typeof attributes === 'undefined') {
|
|
|
+ attributes = {
|
|
|
+ fill: 'none',
|
|
|
+ 'stroke-width': this.lineWidth,
|
|
|
+ stroke: 'black',
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ Vex.Merge(attributes, {
|
|
|
+ x,
|
|
|
+ y,
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ });
|
|
|
+
|
|
|
+ this.applyAttributes(rectangle, attributes);
|
|
|
+
|
|
|
+ this.add(rectangle);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ fillRect(x, y, width, height) {
|
|
|
+ if (height < 0) {
|
|
|
+ y += height;
|
|
|
+ height *= -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.rect(x, y, width, height, this.attributes);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ clearRect(x, y, width, height) {
|
|
|
+ // TODO(GCR): Improve implementation of this...
|
|
|
+ // Currently it draws a box of the background color, rather
|
|
|
+ // than creating alpha through lower z-levels.
|
|
|
+ //
|
|
|
+ // See the implementation of this in SVGKit:
|
|
|
+ // http://sourceforge.net/projects/svgkit/
|
|
|
+ // as a starting point.
|
|
|
+ //
|
|
|
+ // Adding a large number of transform paths (as we would
|
|
|
+ // have to do) could be a real performance hit. Since
|
|
|
+ // tabNote seems to be the only module that makes use of this
|
|
|
+ // it may be worth creating a seperate tabStave that would
|
|
|
+ // draw lines around locations of tablature fingering.
|
|
|
+ //
|
|
|
+
|
|
|
+ this.rect(x, y, width, height, this.background_attributes);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ## Paths:
|
|
|
+
|
|
|
+ beginPath() {
|
|
|
+ this.path = '';
|
|
|
+ this.pen.x = NaN;
|
|
|
+ this.pen.y = NaN;
|
|
|
+
|
|
|
+ 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;
|
|
|
+ if (!isNaN(this.pen.x) && !isNaN(this.pen.y)) {
|
|
|
+ this.peth += 'M' + this.pen.x + ' ' + this.pen.y;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ closePath() {
|
|
|
+ this.path += 'Z';
|
|
|
+
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Adapted from the source for Raphael's Element.glow
|
|
|
+ glow() {
|
|
|
+ // Calculate the width & paths of the glow:
|
|
|
+ if (this.shadow_attributes.width > 0) {
|
|
|
+ const sa = this.shadow_attributes;
|
|
|
+ const num_paths = sa.width / 2;
|
|
|
+ // Stroke at varying widths to create effect of gaussian blur:
|
|
|
+ for (let i = 1; i <= num_paths; i++) {
|
|
|
+ const attributes = {
|
|
|
+ stroke: sa.color,
|
|
|
+ 'stroke-linejoin': 'round',
|
|
|
+ 'stroke-linecap': 'round',
|
|
|
+ 'stroke-width': +((sa.width * 0.4) / num_paths * i).toFixed(3),
|
|
|
+ opacity: +((sa.opacity || 0.3) / num_paths).toFixed(3),
|
|
|
+ };
|
|
|
+
|
|
|
+ const path = this.create('path');
|
|
|
+ attributes.d = this.path;
|
|
|
+ this.applyAttributes(path, attributes);
|
|
|
+ this.add(path);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ fill(attributes) {
|
|
|
+ // If our current path is set to glow, make it glow
|
|
|
+ this.glow();
|
|
|
+
|
|
|
+ const path = this.create('path');
|
|
|
+ if (typeof attributes === 'undefined') {
|
|
|
+ attributes = {};
|
|
|
+ Vex.Merge(attributes, this.attributes);
|
|
|
+ attributes.stroke = 'none';
|
|
|
+ }
|
|
|
+
|
|
|
+ attributes.d = this.path;
|
|
|
+ //attributes.class = "testbeam";
|
|
|
+
|
|
|
+ this.applyAttributes(path, attributes);
|
|
|
+ this.add(path);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ stroke(extraAttributes = undefined) {
|
|
|
+ // If our current path is set to glow, make it glow.
|
|
|
+ this.glow();
|
|
|
+
|
|
|
+ const path = this.create('path');
|
|
|
+ const attributes = {};
|
|
|
+ Vex.Merge(attributes, this.attributes);
|
|
|
+ if (extraAttributes) {
|
|
|
+ Vex.Merge(attributes, extraAttributes);
|
|
|
+ }
|
|
|
+ attributes.fill = 'none';
|
|
|
+ attributes['stroke-width'] = this.lineWidth;
|
|
|
+ attributes.d = this.path;
|
|
|
+
|
|
|
+ this.applyAttributes(path, attributes);
|
|
|
+ this.add(path);
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ## Text Methods:
|
|
|
+ measureText(text) {
|
|
|
+ const txt = this.create('text');
|
|
|
+ if (typeof (txt.getBBox) !== 'function') {
|
|
|
+ return { x: 0, y: 0, width: 0, height: 0 };
|
|
|
+ }
|
|
|
+
|
|
|
+ txt.textContent = text;
|
|
|
+ this.applyAttributes(txt, this.attributes);
|
|
|
+
|
|
|
+ // Temporarily add it to the document for measurement.
|
|
|
+ this.svg.appendChild(txt);
|
|
|
+
|
|
|
+ let bbox = txt.getBBox();
|
|
|
+ if (this.ie && text !== '' && this.attributes['font-style'] === 'italic') {
|
|
|
+ bbox = this.ieMeasureTextFix(bbox, text);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.svg.removeChild(txt);
|
|
|
+ return bbox;
|
|
|
+ }
|
|
|
+
|
|
|
+ ieMeasureTextFix(bbox) {
|
|
|
+ // Internet Explorer over-pads text in italics,
|
|
|
+ // resulting in giant width estimates for measureText.
|
|
|
+ // To fix this, we use this formula, tested against
|
|
|
+ // ie 11:
|
|
|
+ // overestimate (in pixels) = FontSize(in pt) * 1.196 + 1.96
|
|
|
+ // And then subtract the overestimate from calculated width.
|
|
|
+
|
|
|
+ const fontSize = Number(this.fontSize);
|
|
|
+ const m = 1.196;
|
|
|
+ const b = 1.9598;
|
|
|
+ const widthCorrection = (m * fontSize) + b;
|
|
|
+ const width = bbox.width - widthCorrection;
|
|
|
+ const height = bbox.height - 1.5;
|
|
|
+
|
|
|
+ // Get non-protected copy:
|
|
|
+ const box = {
|
|
|
+ x: bbox.x,
|
|
|
+ y: bbox.y,
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ };
|
|
|
+
|
|
|
+ return box;
|
|
|
+ }
|
|
|
+
|
|
|
+ fillText(text, x, y) {
|
|
|
+ if (!text || text.length <= 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const attributes = {};
|
|
|
+ Vex.Merge(attributes, this.attributes);
|
|
|
+ attributes.stroke = 'none';
|
|
|
+ attributes.x = x;
|
|
|
+ attributes.y = y;
|
|
|
+
|
|
|
+ const txt = this.create('text');
|
|
|
+ txt.textContent = text;
|
|
|
+ this.applyAttributes(txt, attributes);
|
|
|
+ this.add(txt);
|
|
|
+ }
|
|
|
+
|
|
|
+ save() {
|
|
|
+ // TODO(mmuthanna): State needs to be deep-copied.
|
|
|
+ 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() {
|
|
|
+ // TODO(0xfe): State needs to be deep-restored.
|
|
|
+ const state = this.state_stack.pop();
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+}
|