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