text: /**
   * @class Ext.draw.engine.SvgContext
   *
   * A class that imitates a canvas context but generates svg elements instead.
   */
  Ext.define('Ext.draw.engine.SvgContext', {
      requires: ['Ext.draw.Color'],
  
      /**
       * @private
       * Properties to be saved/restored in the `save` and `restore` methods.
       */
      toSave: ['strokeOpacity', 'strokeStyle', 'fillOpacity', 'fillStyle', 'globalAlpha',
               'lineWidth', 'lineCap', 'lineJoin', 'lineDash', 'lineDashOffset', 'miterLimit',
               'shadowOffsetX', 'shadowOffsetY', 'shadowBlur', 'shadowColor',
               'globalCompositeOperation', 'position', 'fillGradient', 'strokeGradient'],
  
      strokeOpacity: 1,
      strokeStyle: 'none',
      fillOpacity: 1,
      fillStyle: 'none',
      lineDas: [],
      lineDashOffset: 0,
      globalAlpha: 1,
      lineWidth: 1,
      lineCap: 'butt',
      lineJoin: 'miter',
      miterLimit: 10,
      shadowOffsetX: 0,
      shadowOffsetY: 0,
      shadowBlur: 0,
      shadowColor: 'none',
      globalCompositeOperation: 'src',
  
      urlStringRe: /^url\(#([\w-]+)\)$/,
  
      constructor: function(SvgSurface) {
          var me = this;
  
          me.surface = SvgSurface;
          // Stack of contexts.
          me.state = [];
          me.matrix = new Ext.draw.Matrix();
          // Currently manipulated path.
          me.path = null;
          me.clear();
      },
  
      /**
       * Clears the context.
       */
      clear: function() {
          // Current group to put paths into.
          this.group = this.surface.mainGroup;
          // Position within the current group.
          this.position = 0;
          this.path = null;
      },
  
      /**
       * @private
       * @param {String} tag
       * @return {*}
       */
      getElement: function(tag) {
          return this.surface.getSvgElement(this.group, tag, this.position++);
      },
  
      /**
       * Pushes the context state to the state stack.
       */
      save: function() {
          var toSave = this.toSave,
              obj = {},
              group = this.getElement('g'),
              key, i;
  
          for (i = 0; i < toSave.length; i++) {
              key = toSave[i];
  
              if (key in this) {
                  obj[key] = this[key];
              }
          }
  
          this.position = 0;
          obj.matrix = this.matrix.clone();
          this.state.push(obj);
          this.group = group;
  
          return group;
      },
  
      /**
       * Pops the state stack and restores the state.
       */
      restore: function() {
          var toSave = this.toSave,
              obj = this.state.pop(),
              group = this.group,
              children = group.dom.childNodes,
              key, i;
  
          // Removing extra DOM elements that were not reused.
          while (children.length > this.position) {
              group.last().destroy();
          }
  
          for (i = 0; i < toSave.length; i++) {
              key = toSave[i];
  
              if (key in obj) {
                  this[key] = obj[key];
              }
              else {
                  delete this[key];
              }
          }
  
          this.setTransform.apply(this, obj.matrix.elements);
          this.group = group.getParent();
      },
  
      /**
       * Changes the transformation matrix to apply the matrix given by the arguments
       * as described below.
       * @param {Number} xx
       * @param {Number} yx
       * @param {Number} xy
       * @param {Number} yy
       * @param {Number} dx
       * @param {Number} dy
       */
      transform: function(xx, yx, xy, yy, dx, dy) {
          var inv;
  
          if (this.path) {
              inv = Ext.draw.Matrix.fly([xx, yx, xy, yy, dx, dy]).inverse();
              this.path.transform(inv);
          }
  
          this.matrix.append(xx, yx, xy, yy, dx, dy);
      },
  
      /**
       * Changes the transformation matrix to the matrix given by the arguments as described below.
       * @param {Number} xx
       * @param {Number} yx
       * @param {Number} xy
       * @param {Number} yy
       * @param {Number} dx
       * @param {Number} dy
       */
      setTransform: function(xx, yx, xy, yy, dx, dy) {
          if (this.path) {
              this.path.transform(this.matrix);
          }
  
          this.matrix.reset();
          this.transform(xx, yx, xy, yy, dx, dy);
      },
  
      /**
       * Scales the current context by the specified horizontal (x) and vertical (y) factors.
       * @param {Number} x The horizontal scaling factor, where 1 equals unity or 100% scale.
       * @param {Number} y The vertical scaling factor.
       */
      scale: function(x, y) {
          this.transform(x, 0, 0, y, 0, 0);
      },
  
      /**
       * Rotates the current context coordinates (that is, a transformation matrix).
       * @param {Number} angle The rotation angle, in radians.
       */
      rotate: function(angle) {
          var xx = Math.cos(angle),
              yx = Math.sin(angle),
              xy = -Math.sin(angle),
              yy = Math.cos(angle);
  
          this.transform(xx, yx, xy, yy, 0, 0);
      },
  
      /**
       * Specifies values to move the origin point in a canvas.
       * @param {Number} x The value to add to horizontal (or x) coordinates.
       * @param {Number} y The value to add to vertical (or y) coordinates.
       */
      translate: function(x, y) {
          this.transform(1, 0, 0, 1, x, y);
      },
  
      setGradientBBox: function(bbox) {
          this.bbox = bbox;
      },
  
      /**
       * Resets the current default path.
       */
      beginPath: function() {
          this.path = new Ext.draw.Path();
      },
  
      /**
       * Creates a new subpath with the given point.
       * @param {Number} x
       * @param {Number} y
       */
      moveTo: function(x, y) {
          if (!this.path) {
              this.beginPath();
          }
  
          this.path.moveTo(x, y);
          this.path.element = null;
      },
  
      /**
       * Adds the given point to the current subpath, connected to the previous one by a straight
       * line.
       * @param {Number} x
       * @param {Number} y
       */
      lineTo: function(x, y) {
          if (!this.path) {
              this.beginPath();
          }
  
          this.path.lineTo(x, y);
          this.path.element = null;
      },
  
      /**
       * Adds a new closed subpath to the path, representing the given rectangle.
       * @param {Number} x
       * @param {Number} y
       * @param {Number} width
       * @param {Number} height
       */
      rect: function(x, y, width, height) {
          this.moveTo(x, y);
          this.lineTo(x + width, y);
          this.lineTo(x + width, y + height);
          this.lineTo(x, y + height);
          this.closePath();
      },
  
      /**
       * Paints the box that outlines the given rectangle onto the canvas, using the current
       * stroke style.
       * @param {Number} x
       * @param {Number} y
       * @param {Number} width
       * @param {Number} height
       */
      strokeRect: function(x, y, width, height) {
          this.beginPath();
          this.rect(x, y, width, height);
          this.stroke();
      },
  
      /**
       * Paints the given rectangle onto the canvas, using the current fill style.
       * @param {Number} x
       * @param {Number} y
       * @param {Number} width
       * @param {Number} height
       */
      fillRect: function(x, y, width, height) {
          this.beginPath();
          this.rect(x, y, width, height);
          this.fill();
      },
  
      /**
       * Marks the current subpath as closed, and starts a new subpath with a point the same
       * as the start and end of the newly closed subpath.
       */
      closePath: function() {
          if (!this.path) {
              this.beginPath();
          }
  
          this.path.closePath();
          this.path.element = null;
      },
  
      /**
       * Arc command using svg parameters.
       * @param {Number} r1
       * @param {Number} r2
       * @param {Number} rotation
       * @param {Number} large
       * @param {Number} swipe
       * @param {Number} x2
       * @param {Number} y2
       */
      arcSvg: function(r1, r2, rotation, large, swipe, x2, y2) {
          if (!this.path) {
              this.beginPath();
          }
  
          this.path.arcSvg(r1, r2, rotation, large, swipe, x2, y2);
          this.path.element = null;
      },
  
      /**
       * Adds points to the subpath such that the arc described by the circumference of the circle
       * described by the arguments, starting at the given start angle and ending at the given
       * end angle, going in the given direction (defaulting to clockwise), is added to the path,
       * connected to the previous point by a straight line.
       * @param {Number} x
       * @param {Number} y
       * @param {Number} radius
       * @param {Number} startAngle
       * @param {Number} endAngle
       * @param {Number} anticlockwise
       */
      arc: function(x, y, radius, startAngle, endAngle, anticlockwise) {
          if (!this.path) {
              this.beginPath();
          }
  
          this.path.arc(x, y, radius, startAngle, endAngle, anticlockwise);
          this.path.element = null;
      },
  
      /**
       * Adds points to the subpath such that the arc described by the circumference of the ellipse
       * described by the arguments, starting at the given start angle and ending at the given
       * end angle, going in the given direction (defaulting to clockwise), is added to the path,
       * connected to the previous point by a straight line.
       * @param {Number} x
       * @param {Number} y
       * @param {Number} radiusX
       * @param {Number} radiusY
       * @param {Number} rotation
       * @param {Number} startAngle
       * @param {Number} endAngle
       * @param {Number} anticlockwise
       */
      ellipse: function(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise) {
          if (!this.path) {
              this.beginPath();
          }
  
          this.path.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise);
          this.path.element = null;
      },
  
      /**
       * Adds an arc with the given control points and radius to the current subpath, connected
       * to the previous point by a straight line. If two radii are provided, the first controls
       * the width of the arc's ellipse, and the second controls the height. If only one is provided,
       * or if they are the same, the arc is from a circle. In the case of an ellipse, the rotation
       * argument controls the clockwise inclination of the ellipse relative to the x-axis.
       * @param {Number} x1
       * @param {Number} y1
       * @param {Number} x2
       * @param {Number} y2
       * @param {Number} radiusX
       * @param {Number} radiusY
       * @param {Number} rotation
       */
      arcTo: function(x1, y1, x2, y2, radiusX, radiusY, rotation) {
          if (!this.path) {
              this.beginPath();
          }
  
          this.path.arcTo(x1, y1, x2, y2, radiusX, radiusY, rotation);
          this.path.element = null;
      },
  
      /**
       * Adds the given point to the current subpath, connected to the previous one by a cubic Bézier
       * curve with the given control points.
       * @param {Number} x1
       * @param {Number} y1
       * @param {Number} x2
       * @param {Number} y2
       * @param {Number} x3
       * @param {Number} y3
       */
      bezierCurveTo: function(x1, y1, x2, y2, x3, y3) {
          if (!this.path) {
              this.beginPath();
          }
  
          this.path.bezierCurveTo(x1, y1, x2, y2, x3, y3);
          this.path.element = null;
      },
  
      /**
       * Strokes the given text at the given position. If a maximum width is provided, the text
       * will be scaled to fit that width if necessary.
       * @param {String} text
       * @param {Number} x
       * @param {Number} y
       */
      strokeText: function(text, x, y) {
          var element, tspan;
  
          text = String(text);
  
          if (this.strokeStyle) {
              element = this.getElement('text');
              tspan = this.surface.getSvgElement(element, 'tspan', 0);
  
              this.surface.setElementAttributes(element, {
                  "x": x,
                  "y": y,
                  "transform": this.matrix.toSvg(),
                  "stroke": this.strokeStyle,
                  "fill": "none",
                  "opacity": this.globalAlpha,
                  "stroke-opacity": this.strokeOpacity,
                  "style": "font: " + this.font,
                  "stroke-dasharray": this.lineDash.join(','),
                  "stroke-dashoffset": this.lineDashOffset
              });
  
              if (this.lineDash.length) {
                  this.surface.setElementAttributes(element, {
                      "stroke-dasharray": this.lineDash.join(','),
                      "stroke-dashoffset": this.lineDashOffset
                  });
              }
  
              if (tspan.dom.firstChild) {
                  tspan.dom.removeChild(tspan.dom.firstChild);
              }
  
              this.surface.setElementAttributes(tspan, {
                  "alignment-baseline": "alphabetic"
              });
  
              tspan.dom.appendChild(document.createTextNode(Ext.String.htmlDecode(text)));
          }
      },
  
      /**
       * Fills the given text at the given position. If a maximum width is provided, the text
       * will be scaled to fit that width if necessary.
       * @param {String} text
       * @param {Number} x
       * @param {Number} y
       */
      fillText: function(text, x, y) {
          var element, tspan;
  
          text = String(text);
  
          if (this.fillStyle) {
              element = this.getElement('text');
              tspan = this.surface.getSvgElement(element, 'tspan', 0);
  
              this.surface.setElementAttributes(element, {
                  "x": x,
                  "y": y,
                  "transform": this.matrix.toSvg(),
                  "fill": this.fillStyle,
                  "opacity": this.globalAlpha,
                  "fill-opacity": this.fillOpacity,
                  "style": "font: " + this.font
              });
  
              if (tspan.dom.firstChild) {
                  tspan.dom.removeChild(tspan.dom.firstChild);
              }
  
              this.surface.setElementAttributes(tspan, {
                  "alignment-baseline": "alphabetic"
              });
  
              tspan.dom.appendChild(document.createTextNode(Ext.String.htmlDecode(text)));
          }
      },
  
      /**
       * Draws the given image onto the canvas.
       * If the first argument isn't an img, canvas, or video element, throws a TypeMismatchError
       * exception. If the image has no image data, throws an InvalidStateError exception.
       * If the one of the source rectangle dimensions is zero, throws an IndexSizeError exception.
       * If the image isn't yet fully decoded, then nothing is drawn.
       * @param {HTMLElement} image
       * @param {Number} sx
       * @param {Number} sy
       * @param {Number} sw
       * @param {Number} sh
       * @param {Number} dx
       * @param {Number} dy
       * @param {Number} dw
       * @param {Number} dh
       */
      drawImage: function(image, sx, sy, sw, sh, dx, dy, dw, dh) {
          var me = this,
              element = me.getElement('image'),
              x = sx,
              y = sy,
              width = typeof sw === 'undefined' ? image.width : sw,
              height = typeof sh === 'undefined' ? image.height : sh,
              viewBox = null;
  
          if (typeof dh !== 'undefined') {
              viewBox = sx + " " + sy + " " + sw + " " + sh;
              x = dx;
              y = dy;
              width = dw;
              height = dh;
          }
  
          element.dom.setAttributeNS("http:/" + "/www.w3.org/1999/xlink", "href", image.src);
  
          me.surface.setElementAttributes(element, {
              viewBox: viewBox,
              x: x,
              y: y,
              width: width,
              height: height,
              opacity: me.globalAlpha,
              transform: me.matrix.toSvg()
          });
      },
  
      /**
       * Fills the subpaths of the current default path or the given path with the current fill style.
       */
      fill: function() {
          var me = this,
              path, fillGradient, element, bbox, fill;
  
          if (!me.path) {
              return;
          }
  
          if (me.fillStyle) {
              fillGradient = me.fillGradient;
              element = me.path.element;
              bbox = me.bbox;
  
              if (!element) {
                  path = me.path.toString();
                  element = me.path.element = me.getElement('path');
                  me.surface.setElementAttributes(element, {
                      "d": path,
                      "transform": me.matrix.toSvg()
                  });
              }
  
              if (fillGradient && bbox) {
                  // This indirectly calls ctx.createLinearGradient or ctx.createRadialGradient,
                  // depending on the type of gradient, and returns an instance of
                  // Ext.draw.engine.SvgContext.Gradient.
                  fill = fillGradient.generateGradient(me, bbox);
              }
              else {
                  fill = me.fillStyle;
              }
  
              me.surface.setElementAttributes(element, {
                  "fill": fill,
                  "fill-opacity": me.fillOpacity * me.globalAlpha
              });
          }
      },
  
      /**
       * Strokes the subpaths of the current default path or the given path with the current
       * stroke style.
       */
      stroke: function() {
          var me = this,
              path, strokeGradient, element, bbox, stroke;
  
          if (!me.path) {
              return;
          }
  
          if (me.strokeStyle) {
              strokeGradient = me.strokeGradient;
              element = me.path.element;
              bbox = me.bbox;
  
              if (!element || !me.path.svgString) {
                  path = me.path.toString();
  
                  if (!path) {
                      return;
                  }
  
                  element = me.path.element = me.getElement('path');
                  me.surface.setElementAttributes(element, {
                      "fill": "none",
                      "d": path,
                      "transform": me.matrix.toSvg()
                  });
              }
  
              if (strokeGradient && bbox) {
                  // This indirectly calls ctx.createLinearGradient or ctx.createRadialGradient,
                  // depending on the type of gradient, and returns an instance of
                  // Ext.draw.engine.SvgContext.Gradient.
                  stroke = strokeGradient.generateGradient(me, bbox);
              }
              else {
                  stroke = me.strokeStyle;
              }
  
              me.surface.setElementAttributes(element, {
                  "stroke": stroke,
                  "stroke-linecap": me.lineCap,
                  "stroke-linejoin": me.lineJoin,
                  "stroke-width": me.lineWidth,
                  "stroke-opacity": me.strokeOpacity * me.globalAlpha,
                  "stroke-dasharray": me.lineDash.join(','),
                  "stroke-dashoffset": me.lineDashOffset
              });
  
              if (me.lineDash.length) {
                  me.surface.setElementAttributes(element, {
                      "stroke-dasharray": me.lineDash.join(','),
                      "stroke-dashoffset": me.lineDashOffset
                  });
              }
          }
      },
  
      /**
       * @protected
       *
       * Note: After the method guarantees the transform matrix will be inverted.
       * @param {Object} attr The attribute object
       * @param {Boolean} [transformFillStroke] Indicate whether to transform fill and stroke.
       * If this is not given, then uses `attr.transformFillStroke` instead.
       */
      fillStroke: function(attr, transformFillStroke) {
          var ctx = this,
              fillStyle = ctx.fillStyle,
              strokeStyle = ctx.strokeStyle,
              fillOpacity = ctx.fillOpacity,
              strokeOpacity = ctx.strokeOpacity;
  
          if (transformFillStroke === undefined) {
              transformFillStroke = attr.transformFillStroke;
          }
  
          if (!transformFillStroke) {
              attr.inverseMatrix.toContext(ctx);
          }
  
          if (fillStyle && fillOpacity !== 0) {
              ctx.fill();
          }
  
          if (strokeStyle && strokeOpacity !== 0) {
              ctx.stroke();
          }
      },
  
      appendPath: function(path) {
          this.path = path.clone();
      },
  
      setLineDash: function(lineDash) {
          this.lineDash = lineDash;
      },
  
      getLineDash: function() {
          return this.lineDash;
      },
  
      /**
       * Returns an object that represents a linear gradient that paints along the line
       * given by the coordinates represented by the arguments.
       * @param {Number} x0
       * @param {Number} y0
       * @param {Number} x1
       * @param {Number} y1
       * @return {Ext.draw.engine.SvgContext.Gradient}
       */
      createLinearGradient: function(x0, y0, x1, y1) {
          var me = this,
              element = me.surface.getNextDef('linearGradient'),
              gradient;
  
          me.surface.setElementAttributes(element, {
              "x1": x0,
              "y1": y0,
              "x2": x1,
              "y2": y1,
              "gradientUnits": "userSpaceOnUse"
          });
          gradient = new Ext.draw.engine.SvgContext.Gradient(me, me.surface, element);
  
          return gradient;
      },
  
      /**
       * Returns a CanvasGradient object that represents a radial gradient that paints
       * along the cone given by the circles represented by the arguments.
       * If either of the radii are negative, throws an IndexSizeError exception.
       * @param {Number} x0
       * @param {Number} y0
       * @param {Number} r0
       * @param {Number} x1
       * @param {Number} y1
       * @param {Number} r1
       * @return {Ext.draw.engine.SvgContext.Gradient}
       */
      createRadialGradient: function(x0, y0, r0, x1, y1, r1) {
          var me = this,
              element = me.surface.getNextDef('radialGradient'),
              gradient;
  
          me.surface.setElementAttributes(element, {
              fx: x0,
              fy: y0,
              cx: x1,
              cy: y1,
              r: r1,
              gradientUnits: 'userSpaceOnUse'
          });
          gradient = new Ext.draw.engine.SvgContext.Gradient(me, me.surface, element, r0 / r1);
  
          return gradient;
      }
  });
  
  /**
   * @class Ext.draw.engine.SvgContext.Gradient
   *
   * A class that implements native CanvasGradient interface
   * (https://developer.mozilla.org/en/docs/Web/API/CanvasGradient)
   * and a `toString` method that returns the ID of the gradient.
   */
  Ext.define('Ext.draw.engine.SvgContext.Gradient', {
  
      // Gradients workflow in SVG engine:
      //
      // Inside the 'fill' & 'stroke' methods of the SVG Context
      // we check if the 'ctx.fillGradient' or 'ctx.strokeGradient'
      // objects exist.
      // These objects are instances of Ext.draw.gradient.Gradient
      // and are assigned to the ctx by the sprite's 'useAttributes' method,
      // if the sprite has any gradients.
      //
      // Additionally, we check if the 'ctx.bbox' object exists - the bounding box
      // for the gradients, set by the sprite's 'setGradientBBox' method.
      //
      // If we have both bbox and a valid instance of Ext.draw.gradient.Gradient,
      // the 'generateGradient' method of the instance is called,
      // which in turn calls 'ctx.createLinearGradient' or 'ctx.createRadialGradient'
      // depending on the type of the gradient represented by the instance.
      // These methods create a 'linearGradient' or 'radialGradient' SVG
      // node and wrap it into a Ext.draw.engine.SvgContext.Gradient instance.
      //
      // The Ext.draw.engine.SvgContext.Gradient instance is then used internally
      // by the Ext.draw.gradient.Gradient to add color 'stop' nodes
      // to the gradient node, and by the SVG context when the 'fill' or
      // 'stroke' attribute of a 'path' node is set to the Ext.draw.engine.SvgContext.Gradient
      // instance, which is implicitly converted to a string - a 'url(#id)' reference
      // to the gradient element wrapped by the instance.
  
      isGradient: true,
  
      constructor: function(ctx, surface, element, compression) {
          var me = this;
  
          me.ctx = ctx;
          me.surface = surface;
          me.element = element;
          me.position = 0;
          me.compression = compression || 0;
      },
  
      /**
       * Adds a color stop with the given color to the gradient at the given offset. 0.0 is the offset
       * at one end of the gradient, 1.0 is the offset at the other end.
       * @param {Number} offset
       * @param {String} color
       */
      addColorStop: function(offset, color) {
          var me = this,
              stop = me.surface.getSvgElement(me.element, 'stop', me.position++),
              compression = me.compression;
  
          me.surface.setElementAttributes(stop, {
              "offset": (((1 - compression) * offset + compression) * 100).toFixed(2) + '%',
              "stop-color": color,
              "stop-opacity": Ext.util.Color.fly(color).a.toFixed(15)
          });
      },
  
      toString: function() {
          var children = this.element.dom.childNodes;
  
          // Removing surplus stops in case existing gradient element with more stops was reused.
          while (children.length > this.position) {
              Ext.fly(children[children.length - 1]).destroy();
          }
  
          return 'url(#' + this.element.getId() + ')';
      }
  });
  