/** * @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() + ')'; }});