/**
* @private
* @class Ext.draw.ContainerBase
*/
Ext.define('Ext.draw.ContainerBase', {
extend: 'Ext.panel.Panel',
requires: [
'Ext.window.Window'
],
layout: 'container',
// Adds a listener to this draw container's element. If the element does not yet exist
// addition of the listener will be deferred until onRender. Useful when listeners
// need to be attached during initConfig.
addElementListener: function() {
var me = this,
args = arguments;
if (me.rendered) {
me.el.on.apply(me.el, args);
} else {
me.on('render', function() {
me.el.on.apply(me.el, args);
});
}
},
removeElementListener: function() {
var me = this,
args = arguments;
if (me.rendered) {
me.el.un.apply(me.el, args);
}
},
afterRender: function() {
this.callParent(arguments);
this.initAnimator();
},
getItems: function() {
var me = this,
items = me.items;
if (!items || !items.isMixedCollection) {
// getItems may be called before initItems has run and created the items
// collection, so we have to create it here just in case (this can happen
// if getItems is called during initConfig)
me.initItems();
}
return me.items;
},
onRender: function() {
this.callParent(arguments);
this.element = this.el;
this.innerElement = this.body;
},
setItems: function(items) {
this.items = items;
return items;
},
setSurfaceSize: function(width, height) {
this.resizeHandler({
width: width,
height: height
});
this.renderFrame();
},
onResize: function(width, height, oldWidth, oldHeight) {
var me = this;
me.callParent([
width,
height,
oldWidth,
oldHeight
]);
me.setBodySize({
width: width,
height: height
});
},
preview: function() {
var image = this.getImage();
new Ext.window.Window({
title: 'Chart Preview',
closeable: true,
renderTo: Ext.getBody(),
autoShow: true,
maximizeable: true,
maximized: true,
border: true,
layout: {
type: 'hbox',
pack: 'center',
align: 'middle'
},
items: {
xtype: 'container',
items: {
xtype: 'image',
mode: 'img',
cls: Ext.baseCSSPrefix + 'chart-image',
src: image.data,
listeners: {
afterrender: function() {
var me = this,
img = me.imgEl.dom,
ratio = image.type === 'svg' ? 1 : (window['devicePixelRatio'] || 1),
size;
if (!img.naturalWidth || !img.naturalHeight) {
img.onload = function() {
var width = img.naturalWidth,
height = img.naturalHeight;
me.setWidth(Math.floor(width / ratio));
me.setHeight(Math.floor(height / ratio));
};
} else {
size = me.getSize();
me.setWidth(Math.floor(size.width / ratio));
me.setHeight(Math.floor(size.height / ratio));
}
}
}
}
}
});
},
privates: {
getTargetEl: function() {
return this.innerElement;
}
}
});
/**
* @private
* @class Ext.draw.SurfaceBase
*/
Ext.define('Ext.draw.SurfaceBase', {
extend: 'Ext.Widget'
});
/**
* Represents an RGB color and provides helper functions on it e.g. to get
* color components in HSL color space.
*/
Ext.define('Ext.draw.Color', {
statics: {
colorToHexRe: /(.*?)rgb\((\d+),\s*(\d+),\s*(\d+)\)/,
rgbToHexRe: /\s*rgb\((\d+),\s*(\d+),\s*(\d+)\)/,
rgbaToHexRe: /\s*rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\.\d]+)\)/,
hexRe: /\s*#([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)\s*/,
// Note that 'none' ia an invalid color string.
// When assigned to the fillStyle/strokeStyle/shadowColor properties
// of a Canvas context, those properties won't change their values.
NONE: 'none',
RGBA_NONE: 'rgba(0, 0, 0, 0)'
},
isColor: true,
/**
* @cfg {Number} lightnessFactor
*
* The default factor to compute the lighter or darker color.
*/
lightnessFactor: 0.2,
/**
* @constructor
* @param {Number} red Red component (0..255)
* @param {Number} green Green component (0..255)
* @param {Number} blue Blue component (0..255)
* @param {Number} [alpha=1] (optional) Alpha component (0..1)
*/
constructor: function(red, green, blue, alpha) {
this.setRGB(red, green, blue, alpha);
},
setRGB: function(red, green, blue, alpha) {
var me = this;
me.r = Math.min(255, Math.max(0, red));
me.g = Math.min(255, Math.max(0, green));
me.b = Math.min(255, Math.max(0, blue));
if (alpha === undefined) {
me.a = 1;
} else {
me.a = Math.min(1, Math.max(0, alpha));
}
},
/**
* Returns the gray value (0 to 255) of the color.
*
* The gray value is calculated using the formula r*0.3 + g*0.59 + b*0.11.
*
* @return {Number}
*/
getGrayscale: function() {
// http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale
return this.r * 0.3 + this.g * 0.59 + this.b * 0.11;
},
/**
* Get the equivalent HSL components of the color.
* @return {Number[]}
*/
getHSL: function() {
var me = this,
r = me.r / 255,
g = me.g / 255,
b = me.b / 255,
max = Math.max(r, g, b),
min = Math.min(r, g, b),
delta = max - min,
h,
s = 0,
l = 0.5 * (max + min);
// min==max means achromatic (hue is undefined)
if (min !== max) {
s = (l <= 0.5) ? delta / (max + min) : delta / (2 - max - min);
if (r === max) {
h = 60 * (g - b) / delta;
} else if (g === max) {
h = 120 + 60 * (b - r) / delta;
} else {
h = 240 + 60 * (r - g) / delta;
}
if (h < 0) {
h += 360;
}
if (h >= 360) {
h -= 360;
}
}
return [
h,
s,
l
];
},
/**
* Get the equivalent HSV components of the color.
* @return {Number[]}
*/
getHSV: function() {
var me = this,
r = me.r / 255,
g = me.g / 255,
b = me.b / 255,
max = Math.max(r, g, b),
min = Math.min(r, g, b),
C = max - min,
h,
s = 0,
v = max;
// min == max means achromatic (hue is undefined)
if (min != max) {
s = v ? C / v : 0;
if (r === max) {
h = 60 * (g - b) / C;
} else if (g === max) {
h = 60 * (b - r) / C + 120;
} else {
h = 60 * (r - g) / C + 240;
}
if (h < 0) {
h += 360;
}
if (h >= 360) {
h -= 360;
}
}
return [
h,
s,
v
];
},
/**
* Set current color based on the specified HSL values.
*
* @param {Number} h Hue component [0..360)
* @param {Number} s Saturation component [0..1]
* @param {Number} l Lightness component [0..1]
* @return {Ext.draw.Color}
*/
setHSL: function(h, s, l) {
var me = this,
abs = Math.abs,
c, x, m;
h = (h % 360 + 360) % 360;
s = s > 1 ? 1 : s < 0 ? 0 : s;
l = l > 1 ? 1 : l < 0 ? 0 : l;
if (s === 0 || h === null) {
l *= 255;
me.setRGB(l, l, l);
} else {
// http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL
h /= 60;
c = s * (1 - abs(2 * l - 1));
// chroma
x = c * (1 - abs(h % 2 - 1));
// second largest component
m = l - c / 2;
// lightness adjustment
m *= 255;
c *= 255;
x *= 255;
switch (Math.floor(h)) {
case 0:
me.setRGB(c + m, x + m, m);
break;
case 1:
me.setRGB(x + m, c + m, m);
break;
case 2:
me.setRGB(m, c + m, x + m);
break;
case 3:
me.setRGB(m, x + m, c + m);
break;
case 4:
me.setRGB(x + m, m, c + m);
break;
case 5:
me.setRGB(c + m, m, x + m);
break;
}
}
return me;
},
/**
* Set current color based on the specified HSV values.
*
* @param {Number} h Hue component [0..360)
* @param {Number} s Saturation component [0..1]
* @param {Number} v Value component [0..1]
* @return {Ext.draw.Color}
*/
setHSV: function(h, s, v) {
var me = this,
c, x, m;
h = (h % 360 + 360) % 360;
s = s > 1 ? 1 : s < 0 ? 0 : s;
v = v > 1 ? 1 : v < 0 ? 0 : v;
if (s === 0 || h === null) {
v *= 255;
me.setRGB(v, v, v);
} else {
// http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSV
h /= 60;
c = v * s;
// chroma
x = c * (1 - Math.abs(h % 2 - 1));
// second largest component
m = v - c;
// value adjustment
m *= 255;
c *= 255;
x *= 255;
switch (Math.floor(h)) {
case 0:
me.setRGB(c + m, x + m, m);
break;
case 1:
me.setRGB(x + m, c + m, m);
break;
case 2:
me.setRGB(m, c + m, x + m);
break;
case 3:
me.setRGB(m, x + m, c + m);
break;
case 4:
me.setRGB(x + m, m, c + m);
break;
case 5:
me.setRGB(c + m, m, x + m);
break;
}
}
return me;
},
/**
* Returns a new color that is lighter than this color in the HSL color space.
* @param {Number} [factor=0.2] Lighter factor (0..1).
* @return {Ext.draw.Color}
*/
createLighter: function(factor) {
if (!factor && factor !== 0) {
factor = this.lightnessFactor;
}
var hsl = this.getHSL();
hsl[2] = Ext.Number.constrain(hsl[2] + factor, 0, 1);
return Ext.draw.Color.fromHSL(hsl[0], hsl[1], hsl[2]);
},
/**
* Returns a new color that is darker than this color in the HSL color space.
* @param {Number} [factor=0.2] Darker factor (0..1).
* @return {Ext.draw.Color}
*/
createDarker: function(factor) {
if (!factor && factor !== 0) {
factor = this.lightnessFactor;
}
return this.createLighter(-factor);
},
/**
* toString() returns a color in hex format ('#rrggbb') if the alpha is 1. If the
* alpha is less than one, toString() returns the color in RGBA format ('rgba(255,0,0,0.3)').
*
* @return {String}
*/
toString: function() {
var me = this,
round = Math.round;
if (me.a === 1) {
var r = round(me.r).toString(16),
g = round(me.g).toString(16),
b = round(me.b).toString(16);
r = (r.length === 1) ? '0' + r : r;
g = (g.length === 1) ? '0' + g : g;
b = (b.length === 1) ? '0' + b : b;
return [
'#',
r,
g,
b
].join('');
} else {
return 'rgba(' + [
round(me.r),
round(me.g),
round(me.b),
me.a === 0 ? 0 : me.a.toFixed(15)
].join(', ') + ')';
}
},
// Even though things like 'rgba(0,0,0,0)' will probably get converted to
// 'rgba(0, 0, 0, 0)' when assigned to ctx.fillStyle or ctx.strokeStyle,
// we can't be sure this is the case for every browser, so for consistency
// with the Ext.draw.Color.RGBA_NONE (which is used a lot for checks)
// we join using the ', ' and not ',' here.
/**
* Convert a color to hexadecimal format.
*
* @param {String/Array} color The color value (i.e 'rgb(255, 255, 255)', 'color: #ffffff').
* Can also be an Array, in this case the function handles the first member.
* @return {String} The color in hexadecimal format.
*/
toHex: function(color) {
if (Ext.isArray(color)) {
color = color[0];
}
if (!Ext.isString(color)) {
return '';
}
if (color.substr(0, 1) === '#') {
return color;
}
var digits = Ext.draw.Color.colorToHexRe.exec(color);
if (Ext.isArray(digits)) {
var red = parseInt(digits[2], 10),
green = parseInt(digits[3], 10),
blue = parseInt(digits[4], 10),
rgb = blue | (green << 8) | (red << 16);
return digits[1] + '#' + ("000000" + rgb.toString(16)).slice(-6);
} else {
return '';
}
},
/**
* Parse the string and set the current color.
*
* Supported formats:
*
* + '#rrggbb'
* + '#rgb', 'rgb(r,g,b)'
* + 'rgba(r,g,b,a)'
* + supported CSS color names (e.g., 'black', 'white', etc).
*
* If the string is not recognized, setFromString returns rgba(0,0,0,0).
*
* @param {String} Color Color as string.
* @return this
*/
setFromString: function(str) {
var values, r, g, b,
a = 1,
parse = parseInt;
if (str === Ext.draw.Color.NONE) {
this.r = this.g = this.b = this.a = 0;
return this;
}
if ((str.length === 4 || str.length === 7) && str.substr(0, 1) === '#') {
values = str.match(Ext.draw.Color.hexRe);
if (values) {
r = parse(values[1], 16) >> 0;
g = parse(values[2], 16) >> 0;
b = parse(values[3], 16) >> 0;
if (str.length === 4) {
r += (r * 16);
g += (g * 16);
b += (b * 16);
}
}
} else if ((values = str.match(Ext.draw.Color.rgbToHexRe))) {
r = +values[1];
g = +values[2];
b = +values[3];
} else if ((values = str.match(Ext.draw.Color.rgbaToHexRe))) {
r = +values[1];
g = +values[2];
b = +values[3];
a = +values[4];
} else {
if (Ext.draw.Color.ColorList.hasOwnProperty(str.toLowerCase())) {
return this.setFromString(Ext.draw.Color.ColorList[str.toLowerCase()]);
}
}
if (typeof r === 'undefined') {
return this;
}
this.r = r;
this.g = g;
this.b = b;
this.a = a;
return this;
}
}, function() {
var flyColor = new this();
this.addStatics({
/**
* Returns a flyweight instance of Ext.draw.Color.
*
* Can be called with either a CSS color string or with separate
* arguments for red, green, blue, alpha.
*
* @param {Number/String} red Red component (0..255) or CSS color string.
* @param {Number} [green] Green component (0..255)
* @param {Number} [blue] Blue component (0..255)
* @param {Number} [alpha=1] Alpha component (0..1)
* @return {Ext.draw.Color}
* @static
*/
fly: function(r, g, b, a) {
switch (arguments.length) {
case 1:
flyColor.setFromString(r);
break;
case 3:
case 4:
flyColor.setRGB(r, g, b, a);
break;
default:
return null;
}
return flyColor;
},
ColorList: {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgreen: '#006400',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
gold: '#ffd700',
goldenrod: '#daa520',
gray: '#808080',
green: '#008000',
greenyellow: '#adff2f',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavender: '#e6e6fa',
lavenderblush: '#fff0f5',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgrey: '#d3d3d3',
lightgreen: '#90ee90',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370d8',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#d87093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
red: '#ff0000',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd32'
},
/**
* Create a new color based on the specified HSL values.
*
* @param {Number} h Hue component [0..360)
* @param {Number} s Saturation component [0..1]
* @param {Number} l Lightness component [0..1]
* @return {Ext.draw.Color}
* @static
*/
fromHSL: function(h, s, l) {
return (new this(0, 0, 0, 0)).setHSL(h, s, l);
},
/**
* Create a new color based on the specified HSV values.
*
* @param {Number} h Hue component [0..360)
* @param {Number} s Saturation component [0..1]
* @param {Number} l Value component [0..1]
* @return {Ext.draw.Color}
* @static
*/
fromHSV: function(h, s, v) {
return (new this(0, 0, 0, 0)).setHSL(h, s, v);
},
/**
* Parse the string and create a new color.
*
* Supported formats:
*
* + '#rrggbb'
* + '#rgb', 'rgb(r,g,b)'
* + 'rgba(r,g,b,a)'
* + supported CSS color names (e.g., 'black', 'white', etc).
*
* If the string is not recognized, fromString returns rgba(0,0,0,0).
*
* @param {String} Color Color as string.
* @return {Ext.draw.Color}
* @static
*/
fromString: function(string) {
return (new this(0, 0, 0, 0)).setFromString(string);
},
/**
* Convenience method for creating a color.
*
* Can be called with several different combinations of arguments:
*
* // Ext.draw.Color is returned unchanged.
* Ext.draw.Color.create(new Ext.draw.color(255, 0, 0, 0));
*
* // CSS color string.
* Ext.draw.Color.create("red");
*
* // Array of red, green, blue, alpha
* Ext.draw.Color.create([255, 0, 0, 0]);
*
* // Separate arguments of red, green, blue, alpha
* Ext.draw.Color.create(255, 0, 0, 0);
*
* // Returns black when no arguments given.
* Ext.draw.Color.create();
*
* @param {Ext.draw.Color/String/Number[]/Number} [red] Red component (0..255),
* CSS color string or array of all components.
* @param {Number} [green] Green component (0..255)
* @param {Number} [blue] Blue component (0..255)
* @param {Number} [alpha=1] Alpha component (0..1)
* @return {Ext.draw.Color}
* @static
*/
create: function(arg) {
if (arg instanceof this) {
return arg;
} else if (Ext.isArray(arg)) {
return new Ext.draw.Color(arg[0], arg[1], arg[2], arg[3]);
} else if (Ext.isString(arg)) {
return Ext.draw.Color.fromString(arg);
} else if (arguments.length > 2) {
return new Ext.draw.Color(arguments[0], arguments[1], arguments[2], arguments[3]);
} else {
return new Ext.draw.Color(0, 0, 0, 0);
}
}
});
});
/**
* @private
* @class Ext.draw.sprite.AnimationParser
*
* Computes an intermidiate value between two values of the same type for use in animations.
* Can have pre- and post- processor functions if the values need to be processed
* before an intermidiate value can be computed (parseInitial), or the computed value
* needs to be processed before it can be used as a valid attribute value (serve).
*/
Ext.define('Ext.draw.sprite.AnimationParser', function() {
function compute(from, to, delta) {
return from + (to - from) * delta;
}
function isNotNumber(n) {
return isNaN(parseFloat(n)) || !isFinite(n);
}
return {
singleton: true,
attributeRe: /^url\(#([a-zA-Z\-]+)\)$/,
requires: [
'Ext.draw.Color'
],
color: {
parseInitial: function(color1, color2) {
if (Ext.isString(color1)) {
color1 = Ext.draw.Color.create(color1);
}
if (Ext.isString(color2)) {
color2 = Ext.draw.Color.create(color2);
}
if ((color1 instanceof Ext.draw.Color) && (color2 instanceof Ext.draw.Color)) {
return [
[
color1.r,
color1.g,
color1.b,
color1.a
],
[
color2.r,
color2.g,
color2.b,
color2.a
]
];
} else {
return [
color1 || color2,
color2 || color1
];
}
},
compute: function(from, to, delta) {
if (!Ext.isArray(from) || !Ext.isArray(to)) {
return to || from;
} else {
return [
compute(from[0], to[0], delta),
compute(from[1], to[1], delta),
compute(from[2], to[2], delta),
compute(from[3], to[3], delta)
];
}
},
serve: function(array) {
var color = Ext.draw.Color.fly(array[0], array[1], array[2], array[3]);
return color.toString();
}
},
number: {
parse: function(n) {
return n === null ? null : +n;
},
compute: function(from, to, delta) {
if (!Ext.isNumber(from) || !Ext.isNumber(to)) {
return to || from;
} else {
return compute(from, to, delta);
}
}
},
angle: {
parseInitial: function(from, to) {
if (to - from > Math.PI) {
to -= Math.PI * 2;
} else if (to - from < -Math.PI) {
to += Math.PI * 2;
}
return [
from,
to
];
},
compute: function(from, to, delta) {
if (!Ext.isNumber(from) || !Ext.isNumber(to)) {
return to || from;
} else {
return compute(from, to, delta);
}
}
},
path: {
parseInitial: function(from, to) {
var fromStripes = from.toStripes(),
toStripes = to.toStripes(),
i, j,
fromLength = fromStripes.length,
toLength = toStripes.length,
fromStripe, toStripe, length,
lastStripe = toStripes[toLength - 1],
endPoint = [
lastStripe[lastStripe.length - 2],
lastStripe[lastStripe.length - 1]
];
for (i = fromLength; i < toLength; i++) {
fromStripes.push(fromStripes[fromLength - 1].slice(0));
}
for (i = toLength; i < fromLength; i++) {
toStripes.push(endPoint.slice(0));
}
length = fromStripes.length;
toStripes.path = to;
toStripes.temp = new Ext.draw.Path();
for (i = 0; i < length; i++) {
fromStripe = fromStripes[i];
toStripe = toStripes[i];
fromLength = fromStripe.length;
toLength = toStripe.length;
toStripes.temp.commands.push('M');
for (j = toLength; j < fromLength; j += 6) {
toStripe.push(endPoint[0], endPoint[1], endPoint[0], endPoint[1], endPoint[0], endPoint[1]);
}
lastStripe = toStripes[toStripes.length - 1];
endPoint = [
lastStripe[lastStripe.length - 2],
lastStripe[lastStripe.length - 1]
];
for (j = fromLength; j < toLength; j += 6) {
fromStripe.push(endPoint[0], endPoint[1], endPoint[0], endPoint[1], endPoint[0], endPoint[1]);
}
for (i = 0; i < toStripe.length; i++) {
toStripe[i] -= fromStripe[i];
}
for (i = 2; i < toStripe.length; i += 6) {
toStripes.temp.commands.push('C');
}
}
return [
fromStripes,
toStripes
];
},
compute: function(fromStripes, toStripes, delta) {
if (delta >= 1) {
return toStripes.path;
}
var i = 0,
ln = fromStripes.length,
j = 0,
ln2, from, to,
temp = toStripes.temp.params,
pos = 0;
for (; i < ln; i++) {
from = fromStripes[i];
to = toStripes[i];
ln2 = from.length;
for (j = 0; j < ln2; j++) {
temp[pos++] = to[j] * delta + from[j];
}
}
return toStripes.temp;
}
},
data: {
compute: function(from, to, delta, target) {
var lf = from.length - 1,
lt = to.length - 1,
len = Math.max(lf, lt),
f, t, i;
if (!target || target === from) {
target = [];
}
target.length = len + 1;
for (i = 0; i <= len; i++) {
f = from[Math.min(i, lf)];
t = to[Math.min(i, lt)];
if (isNotNumber(f)) {
target[i] = t;
} else {
if (isNotNumber(t)) {
// This may not give the desired visual result during
// animation (after all, we don't know what the target
// value should be, if it wasn't given to us), but it's
// better than spitting out a bunch of NaNs in the target
// array, when transitioning from a non-empty to an empty
// array.
t = 0;
}
target[i] = (t - f) * delta + f;
}
}
return target;
}
},
text: {
compute: function(from, to, delta) {
return from.substr(0, Math.round(from.length * (1 - delta))) + to.substr(Math.round(to.length * (1 - delta)));
}
},
limited: 'number',
limited01: 'number'
};
});
(function() {
if (!Ext.global.Float32Array) {
// Typed Array polyfill
var Float32Array = function(array) {
if (typeof array === 'number') {
this.length = array;
} else if ('length' in array) {
this.length = array.length;
for (var i = 0,
len = array.length; i < len; i++) {
this[i] = +array[i];
}
}
};
Float32Array.prototype = [];
Ext.global.Float32Array = Float32Array;
}
})();
/**
* Utility class providing mathematics functionalities through all the draw package.
*/
Ext.define('Ext.draw.Draw', {
singleton: true,
radian: Math.PI / 180,
pi2: Math.PI * 2,
/**
* @deprecated Please use the {@link Ext#identityFn} instead.
* Function that returns its first element.
* @param {Mixed} a
* @return {Mixed}
*/
reflectFn: function(a) {
return a;
},
/**
* Converting degrees to radians.
* @param {Number} degrees
* @return {Number}
*/
rad: function(degrees) {
return (degrees % 360) * this.radian;
},
/**
* Converting radians to degrees.
* @param {Number} radian
* @return {Number}
*/
degrees: function(radian) {
return (radian / this.radian) % 360;
},
/**
*
* @param {Object} bbox1
* @param {Object} bbox2
* @param {Number} [padding]
* @return {Boolean}
*/
isBBoxIntersect: function(bbox1, bbox2, padding) {
padding = padding || 0;
return (Math.max(bbox1.x, bbox2.x) - padding > Math.min(bbox1.x + bbox1.width, bbox2.x + bbox2.width)) || (Math.max(bbox1.y, bbox2.y) - padding > Math.min(bbox1.y + bbox1.height, bbox2.y + bbox2.height));
},
/**
* Checks if a point is within a bounding box.
* @param x
* @param y
* @param bbox
* @return {Boolean}
*/
isPointInBBox: function(x, y, bbox) {
return !!bbox && x >= bbox.x && x <= (bbox.x + bbox.width) && y >= bbox.y && y <= (bbox.y + bbox.height);
},
/**
* Natural cubic spline interpolation.
* This algorithm runs in linear time.
*
* @param {Array} points Array of numbers.
*/
spline: function(points) {
var i, j,
ln = points.length,
nd, d, y, ny,
r = 0,
zs = new Float32Array(points.length),
result = new Float32Array(points.length * 3 - 2);
zs[0] = 0;
zs[ln - 1] = 0;
for (i = 1; i < ln - 1; i++) {
zs[i] = (points[i + 1] + points[i - 1] - 2 * points[i]) - zs[i - 1];
r = 1 / (4 - r);
zs[i] *= r;
}
for (i = ln - 2; i > 0; i--) {
r = 3.732050807568877 + 48.248711305964385 / (-13.928203230275537 + Math.pow(0.07179676972449123, i));
zs[i] -= zs[i + 1] * r;
}
ny = points[0];
nd = ny - zs[0];
for (i = 0 , j = 0; i < ln - 1; j += 3) {
y = ny;
d = nd;
i++;
ny = points[i];
nd = ny - zs[i];
result[j] = y;
result[j + 1] = (nd + 2 * d) / 3;
result[j + 2] = (nd * 2 + d) / 3;
}
result[j] = ny;
return result;
},
/**
* @private
*
* Calculates bezier curve control anchor points for a particular point in a path, with a
* smoothing curve applied. The smoothness of the curve is controlled by the 'value' parameter.
* Note that this algorithm assumes that the line being smoothed is normalized going from left
* to right; it makes special adjustments assuming this orientation.
*
* @param {Number} prevX X coordinate of the previous point in the path
* @param {Number} prevY Y coordinate of the previous point in the path
* @param {Number} curX X coordinate of the current point in the path
* @param {Number} curY Y coordinate of the current point in the path
* @param {Number} nextX X coordinate of the next point in the path
* @param {Number} nextY Y coordinate of the next point in the path
* @param {Number} value A value to control the smoothness of the curve; this is used to
* divide the distance between points, so a value of 2 corresponds to
* half the distance between points (a very smooth line) while higher values
* result in less smooth curves. Defaults to 4.
* @return {Object} Object containing x1, y1, x2, y2 bezier control anchor points; x1 and y1
* are the control point for the curve toward the previous path point, and
* x2 and y2 are the control point for the curve toward the next path point.
*/
getAnchors: function(prevX, prevY, curX, curY, nextX, nextY, value) {
value = value || 4;
var PI = Math.PI,
halfPI = PI / 2,
abs = Math.abs,
sin = Math.sin,
cos = Math.cos,
atan = Math.atan,
control1Length, control2Length, control1Angle, control2Angle, control1X, control1Y, control2X, control2Y, alpha;
// Find the length of each control anchor line, by dividing the horizontal distance
// between points by the value parameter.
control1Length = (curX - prevX) / value;
control2Length = (nextX - curX) / value;
// Determine the angle of each control anchor line. If the middle point is a vertical
// turnaround then we force it to a flat horizontal angle to prevent the curve from
// dipping above or below the middle point. Otherwise we use an angle that points
// toward the previous/next target point.
if ((curY >= prevY && curY >= nextY) || (curY <= prevY && curY <= nextY)) {
control1Angle = control2Angle = halfPI;
} else {
control1Angle = atan((curX - prevX) / abs(curY - prevY));
if (prevY < curY) {
control1Angle = PI - control1Angle;
}
control2Angle = atan((nextX - curX) / abs(curY - nextY));
if (nextY < curY) {
control2Angle = PI - control2Angle;
}
}
// Adjust the calculated angles so they point away from each other on the same line
alpha = halfPI - ((control1Angle + control2Angle) % (PI * 2)) / 2;
if (alpha > halfPI) {
alpha -= PI;
}
control1Angle += alpha;
control2Angle += alpha;
// Find the control anchor points from the angles and length
control1X = curX - control1Length * sin(control1Angle);
control1Y = curY + control1Length * cos(control1Angle);
control2X = curX + control2Length * sin(control2Angle);
control2Y = curY + control2Length * cos(control2Angle);
// One last adjustment, make sure that no control anchor point extends vertically past
// its target prev/next point, as that results in curves dipping above or below and
// bending back strangely. If we find this happening we keep the control angle but
// reduce the length of the control line so it stays within bounds.
if ((curY > prevY && control1Y < prevY) || (curY < prevY && control1Y > prevY)) {
control1X += abs(prevY - control1Y) * (control1X - curX) / (control1Y - curY);
control1Y = prevY;
}
if ((curY > nextY && control2Y < nextY) || (curY < nextY && control2Y > nextY)) {
control2X -= abs(nextY - control2Y) * (control2X - curX) / (control2Y - curY);
control2Y = nextY;
}
return {
x1: control1X,
y1: control1Y,
x2: control2X,
y2: control2Y
};
},
/**
* Given coordinates of the points, calculates coordinates of a Bezier curve that goes through them.
* @param dataX x-coordinates of the points.
* @param dataY y-coordinates of the points.
* @param value A value to control the smoothness of the curve.
* @return {Object} Object holding two arrays, for x and y coordinates of the curve.
*/
smooth: function(dataX, dataY, value) {
var ln = dataX.length,
prevX, prevY, curX, curY, nextX, nextY, x, y,
smoothX = [],
smoothY = [],
i, anchors;
for (i = 0; i < ln - 1; i++) {
prevX = dataX[i];
prevY = dataY[i];
if (i === 0) {
x = prevX;
y = prevY;
smoothX.push(x);
smoothY.push(y);
if (ln === 1) {
break;
}
}
curX = dataX[i + 1];
curY = dataY[i + 1];
nextX = dataX[i + 2];
nextY = dataY[i + 2];
if (isNaN(nextX) || isNaN(nextY)) {
smoothX.push(x, curX, curX);
smoothY.push(y, curY, curY);
break;
}
anchors = this.getAnchors(prevX, prevY, curX, curY, nextX, nextY, value);
smoothX.push(x, anchors.x1, curX);
smoothY.push(y, anchors.y1, curY);
x = anchors.x2;
y = anchors.y2;
}
return {
smoothX: smoothX,
smoothY: smoothY
};
},
/**
* @method
* @private
* Work around for iOS.
* Nested 3d-transforms seems to prevent the redraw inside it until some event is fired.
*/
updateIOS: Ext.os.is.iOS ? function() {
var el = Ext.getBody().createChild({
style: 'position: absolute; top: 0px; bottom: 0px; left: 0px; right: 0px; background: rgba(0,0,0,0.001); z-index: 100000'
});
Ext.draw.Animator.schedule(function() {
el.destroy();
});
} : Ext.emptyFn
});
/**
* @class Ext.draw.gradient.Gradient
*
* Creates a gradient.
*/
Ext.define('Ext.draw.gradient.Gradient', {
requires: [
'Ext.draw.Color'
],
isGradient: true,
config: {
/**
* Defines the stops of the gradient.
*/
stops: []
},
applyStops: function(newStops) {
var stops = [],
ln = newStops.length,
i, stop, color;
for (i = 0; i < ln; i++) {
stop = newStops[i];
color = stop.color;
if (!(color && color.isColor)) {
color = Ext.draw.Color.fly(color || Ext.draw.Color.NONE);
}
stops.push({
offset: Math.min(1, Math.max(0, 'offset' in stop ? stop.offset : stop.position || 0)),
color: color.toString()
});
}
stops.sort(function(a, b) {
return a.offset - b.offset;
});
return stops;
},
onClassExtended: function(subClass, member) {
if (!member.alias && member.type) {
member.alias = 'gradient.' + member.type;
}
},
constructor: function(config) {
this.initConfig(config);
},
/**
* @protected
* Generates the gradient for the given context.
* @param {Ext.draw.engine.SvgContext} ctx The context.
* @param {Object} bbox
* @return {Object}
*/
generateGradient: Ext.emptyFn
});
/**
* @class Ext.draw.gradient.GradientDefinition
*
* A global map of all gradient configs.
*/
Ext.define('Ext.draw.gradient.GradientDefinition', {
singleton: true,
urlStringRe: /^url\(#([\w\-]+)\)$/,
gradients: {},
add: function(gradients) {
var store = this.gradients,
i, n, gradient;
for (i = 0 , n = gradients.length; i < n; i++) {
gradient = gradients[i];
if (Ext.isString(gradient.id)) {
store[gradient.id] = gradient;
}
}
},
get: function(str) {
var store = this.gradients,
match = str.match(this.urlStringRe),
gradient;
if (match && match[1] && (gradient = store[match[1]])) {
return gradient || str;
}
return str;
}
});
/**
* @private
* @class Ext.draw.sprite.AttributeParser
*
* Parsers used for sprite attributes if they are {@link Ext.draw.sprite.AttributeDefinition#normalize normalized}
* (default) when being {@link Ext.draw.sprite.Sprite#setAttributes set}.
*
* Methods of the singleton correpond either to the processor functions themselves or processor factories.
*/
Ext.define('Ext.draw.sprite.AttributeParser', {
singleton: true,
attributeRe: /^url\(#([a-zA-Z\-]+)\)$/,
requires: [
'Ext.draw.Color',
'Ext.draw.gradient.GradientDefinition'
],
"default": function(n) {
return n;
},
string: function(n) {
return String(n);
},
number: function(n) {
if (!isNaN(n)) {
return n;
}
},
angle: function(n) {
if (!isNaN(n)) {
n %= Math.PI * 2;
if (n < -Math.PI) {
n += Math.PI * 2;
}
if (n > Math.PI) {
n -= Math.PI * 2;
}
return n;
}
},
data: function(n) {
if (Ext.isArray(n)) {
return n.slice();
} else if (n instanceof Float32Array) {
return new Float32Array(n);
}
},
bool: function(n) {
return !!n;
},
color: function(n) {
if (n instanceof Ext.draw.Color) {
return n.toString();
} else if (n instanceof Ext.draw.gradient.Gradient) {
return n;
} else if (!n) {
return Ext.draw.Color.NONE;
} else if (Ext.isString(n)) {
if (n.substr(0, 3) === 'url') {
n = Ext.draw.gradient.GradientDefinition.get(n);
if (Ext.isString(n)) {
return n;
}
} else {
return Ext.draw.Color.fly(n).toString();
}
}
if (n.type === 'linear') {
return Ext.create('Ext.draw.gradient.Linear', n);
} else if (n.type === 'radial') {
return Ext.create('Ext.draw.gradient.Radial', n);
} else if (n.type === 'pattern') {
return Ext.create('Ext.draw.gradient.Pattern', n);
} else {
return Ext.draw.Color.NONE;
}
},
limited: function(low, hi) {
return function(n) {
return isNaN(n) ? undefined : Math.min(Math.max(+n, low), hi);
};
},
limited01: function(n) {
return isNaN(n) ? undefined : Math.min(Math.max(+n, 0), 1);
},
/**
* Generates a function that checks if a value matches
* one of the given attributes.
* @return {Function}
*/
enums: function() {
var enums = {},
args = Array.prototype.slice.call(arguments, 0),
i, ln;
for (i = 0 , ln = args.length; i < ln; i++) {
enums[args[i]] = true;
}
return function(n) {
return n in enums ? n : undefined;
};
}
});
/**
* @private
* Flyweight object to process the attributes of a sprite.
* A single instance of the AttributeDefinition is created per sprite class.
* See `onClassCreated` and `onClassExtended` callbacks
* of the {@link Ext.draw.sprite.Sprite} for more info.
*/
Ext.define('Ext.draw.sprite.AttributeDefinition', {
requires: [
'Ext.draw.sprite.AttributeParser',
'Ext.draw.sprite.AnimationParser'
],
config: {
/**
* @cfg {Object} defaults Defines the default values of attributes.
*/
defaults: {},
/**
* @cfg {Object} aliases Defines the alternative names for attributes.
*/
aliases: {},
/**
* @cfg {Object} animationProcessors Defines the process used to animate between attributes.
* One doesn't have to define animation processors for sprite attributes that use
* predefined {@link #processors} from the {@link Ext.draw.sprite.AttributeParser} singleton.
* For such attributes matching animation processors from the {@link Ext.draw.sprite.AnimationParser}
* singleton will be used automatically.
* However, if you have a custom processor for an attribute that should support
* animation, you must provide a corresponding animation processor for it here.
* For more information on animation processors please see {@link Ext.draw.sprite.AnimationParser}
* documentation.
*/
animationProcessors: {},
/**
* @cfg {Object} processors Defines the preprocessing used on the attributes.
* One can define a custom processor function here or use the name of a predefined
* processor from the {@link Ext.draw.sprite.AttributeParser} singleton.
*/
processors: {},
/**
* @cfg {Object} dirtyTriggers
* @deprecated Use the {@link #triggers} config instead.
*/
dirtyTriggers: {},
/**
* @cfg {Object} triggers Defines which updaters have to be called when an attribute is changed.
* For example, the config below indicates that the 'size' updater
* of a {@link Ext.draw.sprite.Square square} sprite has to be called
* when the 'size' attribute changes.
*
* triggers: {
* size: 'size' // Use comma-separated values here if multiple updaters have to be called.
* } // Note that the order is _not_ guaranteed.
*
* If any of the updaters to be called (triggered by the {@link Ext.draw.sprite.Sprite#setAttributes call)
* set attributes themselves and those attributes have triggers defined for them,
* then their updaters will be called after all current updaters finish execution.
*
* The updater functions themselves are defined in the {@link #updaters} config,
* aside from the 'canvas' updater, which doesn't have to be defined and acts as a flag,
* indicating that this attribute should be applied to a Canvas context (or whatever emulates it).
* @since 5.1.0
*/
triggers: {},
/**
* @cfg {Object} updaters Defines the postprocessing used by the attribute.
* Inside the updater function 'this' refers to the sprite that the attributes belong to.
* In case of an instancing sprite 'this' will refer to the instancing template.
* The two parameters passed to the updater function are the attributes object
* of the sprite or instance, and the names of attributes that triggered this updater call.
*
* The example below shows how the 'size' updater changes other attributes
* of a {@link Ext.draw.sprite.Square square} sprite sprite when its 'size' attribute changes.
*
* updaters: {
* size: function (attr) {
* var size = attr.size;
* this.setAttributes({ // Changes to these attributes will trigger the 'path' updater.
* x: attr.x - size,
* y: attr.y - size,
* height: 2 * size,
* width: 2 * size
* });
* }
* }
*/
updaters: {}
},
inheritableStatics: {
/**
* @private
* Processor declaration in the form of 'processorFactory(argument1,argument2,...)'.
* E.g.: {@link Ext.draw.sprite.AttributeParser#enums enums},
* {@link Ext.draw.sprite.AttributeParser#limited limited}.
*/
processorFactoryRe: /^(\w+)\(([\w\-,]*)\)$/
},
constructor: function(config) {
var me = this;
me.initConfig(config);
},
applyDefaults: function(defaults, oldDefaults) {
oldDefaults = Ext.apply(oldDefaults || {}, this.normalize(defaults));
return oldDefaults;
},
applyAliases: function(aliases, oldAliases) {
return Ext.apply(oldAliases || {}, aliases);
},
applyProcessors: function(processors, oldProcessors) {
this.getAnimationProcessors();
// Apply custom animation processors first.
var result = oldProcessors || {},
defaultProcessor = Ext.draw.sprite.AttributeParser,
processorFactoryRe = this.self.processorFactoryRe,
animationProcessors = {},
anyAnimationProcessors, name, match, fn;
for (name in processors) {
fn = processors[name];
if (!Ext.isFunction(fn)) {
if (Ext.isString(fn)) {
match = fn.match(processorFactoryRe);
if (match) {
fn = defaultProcessor[match[1]].apply(defaultProcessor, match[2].split(','));
} else {
// Names of animation parsers match the names of attribute parsers.
animationProcessors[name] = fn;
anyAnimationProcessors = true;
fn = defaultProcessor[fn];
}
} else {
continue;
}
}
result[name] = fn;
}
if (anyAnimationProcessors) {
this.setAnimationProcessors(animationProcessors);
}
return result;
},
applyAnimationProcessors: function(animationProcessors, oldAnimationProcessors) {
var parser = Ext.draw.sprite.AnimationParser,
name, item;
if (!oldAnimationProcessors) {
oldAnimationProcessors = {};
}
for (name in animationProcessors) {
item = animationProcessors[name];
if (item === 'none') {
oldAnimationProcessors[name] = null;
} else if (Ext.isString(item) && !(name in oldAnimationProcessors)) {
if (item in parser) {
// The while loop is used to resolve aliases, e.g. `num: 'number'`,
// where `number` maps to a parser object or is an alias too.
while (Ext.isString(parser[item])) {
item = parser[item];
}
oldAnimationProcessors[name] = parser[item];
}
} else if (Ext.isObject(item)) {
oldAnimationProcessors[name] = item;
}
}
return oldAnimationProcessors;
},
updateDirtyTriggers: function(dirtyTriggers) {
this.setTriggers(dirtyTriggers);
},
applyTriggers: function(triggers, oldTriggers) {
if (!oldTriggers) {
oldTriggers = {};
}
for (var name in triggers) {
oldTriggers[name] = triggers[name].split(',');
}
return oldTriggers;
},
applyUpdaters: function(updaters, oldUpdaters) {
return Ext.apply(oldUpdaters || {}, updaters);
},
batchedNormalize: function(batchedChanges, keepUnrecognized) {
if (!batchedChanges) {
return {};
}
var definition = this,
processors = definition.getProcessors(),
aliases = definition.getAliases(),
translation = batchedChanges.translation || batchedChanges.translate,
normalized = {},
i, ln, name, val, rotation, scaling, matrix, subVal, split;
if ('rotation' in batchedChanges) {
rotation = batchedChanges.rotation;
} else {
rotation = ('rotate' in batchedChanges) ? batchedChanges.rotate : undefined;
}
if ('scaling' in batchedChanges) {
scaling = batchedChanges.scaling;
} else {
scaling = ('scale' in batchedChanges) ? batchedChanges.scale : undefined;
}
if (typeof scaling !== 'undefined') {
if (Ext.isNumber(scaling)) {
normalized.scalingX = scaling;
normalized.scalingY = scaling;
} else {
if ('x' in scaling) {
normalized.scalingX = scaling.x;
}
if ('y' in scaling) {
normalized.scalingY = scaling.y;
}
if ('centerX' in scaling) {
normalized.scalingCenterX = scaling.centerX;
}
if ('centerY' in scaling) {
normalized.scalingCenterY = scaling.centerY;
}
}
}
if (typeof rotation !== 'undefined') {
if (Ext.isNumber(rotation)) {
rotation = Ext.draw.Draw.rad(rotation);
normalized.rotationRads = rotation;
} else {
if ('rads' in rotation) {
normalized.rotationRads = rotation.rads;
} else if ('degrees' in rotation) {
if (Ext.isArray(rotation.degrees)) {
normalized.rotationRads = Ext.Array.map(rotation.degrees, function(deg) {
return Ext.draw.Draw.rad(deg);
});
} else {
normalized.rotationRads = Ext.draw.Draw.rad(rotation.degrees);
}
}
if ('centerX' in rotation) {
normalized.rotationCenterX = rotation.centerX;
}
if ('centerY' in rotation) {
normalized.rotationCenterY = rotation.centerY;
}
}
}
if (typeof translation !== 'undefined') {
if ('x' in translation) {
normalized.translationX = translation.x;
}
if ('y' in translation) {
normalized.translationY = translation.y;
}
}
if ('matrix' in batchedChanges) {
matrix = Ext.draw.Matrix.create(batchedChanges.matrix);
split = matrix.split();
normalized.matrix = matrix;
normalized.rotationRads = split.rotation;
normalized.rotationCenterX = 0;
normalized.rotationCenterY = 0;
normalized.scalingX = split.scaleX;
normalized.scalingY = split.scaleY;
normalized.scalingCenterX = 0;
normalized.scalingCenterY = 0;
normalized.translationX = split.translateX;
normalized.translationY = split.translateY;
}
for (name in batchedChanges) {
val = batchedChanges[name];
if (typeof val === 'undefined') {
continue;
} else if (Ext.isArray(val)) {
if (name in aliases) {
name = aliases[name];
}
if (name in processors) {
normalized[name] = [];
for (i = 0 , ln = val.length; i < ln; i++) {
subVal = processors[name].call(this, val[i]);
if (typeof subVal !== 'undefined') {
normalized[name][i] = subVal;
}
}
} else if (keepUnrecognized) {
normalized[name] = val;
}
} else {
if (name in aliases) {
name = aliases[name];
}
if (name in processors) {
val = processors[name].call(this, val);
if (typeof val !== 'undefined') {
normalized[name] = val;
}
} else if (keepUnrecognized) {
normalized[name] = val;
}
}
}
return normalized;
},
/**
* Normalizes the changes given via their processors before they are applied as attributes.
*
* @param {Object} changes The changes given.
* @param {Boolean} keepUnrecognized If 'true', unknown attributes will be passed through as normalized values.
* @return {Object} The normalized values.
*/
normalize: function(changes, keepUnrecognized) {
if (!changes) {
return {};
}
var definition = this,
processors = definition.getProcessors(),
aliases = definition.getAliases(),
translation = changes.translation || changes.translate,
normalized = {},
name, val, rotation, scaling, matrix, split;
if ('rotation' in changes) {
rotation = changes.rotation;
} else {
rotation = ('rotate' in changes) ? changes.rotate : undefined;
}
if ('scaling' in changes) {
scaling = changes.scaling;
} else {
scaling = ('scale' in changes) ? changes.scale : undefined;
}
if (translation) {
if ('x' in translation) {
normalized.translationX = translation.x;
}
if ('y' in translation) {
normalized.translationY = translation.y;
}
}
if (typeof scaling !== 'undefined') {
if (Ext.isNumber(scaling)) {
normalized.scalingX = scaling;
normalized.scalingY = scaling;
} else {
if ('x' in scaling) {
normalized.scalingX = scaling.x;
}
if ('y' in scaling) {
normalized.scalingY = scaling.y;
}
if ('centerX' in scaling) {
normalized.scalingCenterX = scaling.centerX;
}
if ('centerY' in scaling) {
normalized.scalingCenterY = scaling.centerY;
}
}
}
if (typeof rotation !== 'undefined') {
if (Ext.isNumber(rotation)) {
rotation = Ext.draw.Draw.rad(rotation);
normalized.rotationRads = rotation;
} else {
if ('rads' in rotation) {
normalized.rotationRads = rotation.rads;
} else if ('degrees' in rotation) {
normalized.rotationRads = Ext.draw.Draw.rad(rotation.degrees);
}
if ('centerX' in rotation) {
normalized.rotationCenterX = rotation.centerX;
}
if ('centerY' in rotation) {
normalized.rotationCenterY = rotation.centerY;
}
}
}
if ('matrix' in changes) {
matrix = Ext.draw.Matrix.create(changes.matrix);
split = matrix.split();
normalized.matrix = matrix;
normalized.rotationRads = split.rotation;
normalized.rotationCenterX = 0;
normalized.rotationCenterY = 0;
normalized.scalingX = split.scaleX;
normalized.scalingY = split.scaleY;
normalized.scalingCenterX = 0;
normalized.scalingCenterY = 0;
normalized.translationX = split.translateX;
normalized.translationY = split.translateY;
}
for (name in changes) {
val = changes[name];
if (typeof val === 'undefined') {
continue;
}
if (name in aliases) {
name = aliases[name];
}
if (name in processors) {
val = processors[name].call(this, val);
if (typeof val !== 'undefined') {
normalized[name] = val;
}
} else if (keepUnrecognized) {
normalized[name] = val;
}
}
return normalized;
},
setBypassingNormalization: function(attr, modifierStack, changes) {
return modifierStack.pushDown(attr, changes);
},
set: function(attr, modifierStack, changes) {
changes = this.normalize(changes);
return this.setBypassingNormalization(attr, modifierStack, changes);
}
});
/**
* Utility class to calculate [affine transformation](http://en.wikipedia.org/wiki/Affine_transformation) matrix.
*
* This class is compatible with [SVGMatrix](http://www.w3.org/TR/SVG11/coords.html#InterfaceSVGMatrix) except:
*
* 1. Ext.draw.Matrix is not read only.
* 2. Using Number as its components rather than floats.
*
* Using this class helps to reduce the severe numeric
* [problem with HTML Canvas and SVG transformation](http://stackoverflow.com/questions/8784405/large-numbers-in-html-canvas-translate-result-in-strange-behavior)
*
* There's also no way to get current transformation matrix [in Canvas](http://stackoverflow.com/questions/7395813/html5-canvas-get-transform-matrix).
*/
Ext.define('Ext.draw.Matrix', {
isMatrix: true,
statics: {
/**
* @static
* Return the affine matrix that transform two points (x0, y0) and (x1, y1) to (x0p, y0p) and (x1p, y1p)
* @param {Number} x0
* @param {Number} y0
* @param {Number} x1
* @param {Number} y1
* @param {Number} x0p
* @param {Number} y0p
* @param {Number} x1p
* @param {Number} y1p
*/
createAffineMatrixFromTwoPair: function(x0, y0, x1, y1, x0p, y0p, x1p, y1p) {
var dx = x1 - x0,
dy = y1 - y0,
dxp = x1p - x0p,
dyp = y1p - y0p,
r = 1 / (dx * dx + dy * dy),
a = dx * dxp + dy * dyp,
b = dxp * dy - dx * dyp,
c = -a * x0 - b * y0,
f = b * x0 - a * y0;
return new this(a * r, -b * r, b * r, a * r, c * r + x0p, f * r + y0p);
},
/**
* @static
* Return the affine matrix that transform two points (x0, y0) and (x1, y1) to (x0p, y0p) and (x1p, y1p)
* @param {Number} x0
* @param {Number} y0
* @param {Number} x1
* @param {Number} y1
* @param {Number} x0p
* @param {Number} y0p
* @param {Number} x1p
* @param {Number} y1p
*/
createPanZoomFromTwoPair: function(x0, y0, x1, y1, x0p, y0p, x1p, y1p) {
if (arguments.length === 2) {
return this.createPanZoomFromTwoPair.apply(this, x0.concat(y0));
}
var dx = x1 - x0,
dy = y1 - y0,
cx = (x0 + x1) * 0.5,
cy = (y0 + y1) * 0.5,
dxp = x1p - x0p,
dyp = y1p - y0p,
cxp = (x0p + x1p) * 0.5,
cyp = (y0p + y1p) * 0.5,
r = dx * dx + dy * dy,
rp = dxp * dxp + dyp * dyp,
scale = Math.sqrt(rp / r);
return new this(scale, 0, 0, scale, cxp - scale * cx, cyp - scale * cy);
},
/**
* @static
* Create a flyweight to wrap the given array.
* The flyweight will directly refer the object and the elements can be changed by other methods.
*
* Do not hold the instance of flyweight matrix.
*
* @param {Array} elements
* @return {Ext.draw.Matrix}
*/
fly: (function() {
var flyMatrix = null,
simplefly = function(elements) {
flyMatrix.elements = elements;
return flyMatrix;
};
return function(elements) {
if (!flyMatrix) {
flyMatrix = new Ext.draw.Matrix();
}
flyMatrix.elements = elements;
Ext.draw.Matrix.fly = simplefly;
return flyMatrix;
};
})(),
/**
* @static
* Create a matrix from `mat`. If `mat` is already a matrix, returns it.
* @param {Mixed} mat
* @return {Ext.draw.Matrix}
*/
create: function(mat) {
if (mat instanceof this) {
return mat;
}
return new this(mat);
}
},
/**
* Create an affine transform matrix.
*
* @param {Number} xx Coefficient from x to x
* @param {Number} xy Coefficient from x to y
* @param {Number} yx Coefficient from y to x
* @param {Number} yy Coefficient from y to y
* @param {Number} dx Offset of x
* @param {Number} dy Offset of y
*/
constructor: function(xx, xy, yx, yy, dx, dy) {
if (xx && xx.length === 6) {
this.elements = xx.slice();
} else if (xx !== undefined) {
this.elements = [
xx,
xy,
yx,
yy,
dx,
dy
];
} else {
this.elements = [
1,
0,
0,
1,
0,
0
];
}
},
/**
* Prepend a matrix onto the current.
*
* __Note:__ The given transform will come after the current one.
*
* @param {Number} xx Coefficient from x to x.
* @param {Number} xy Coefficient from x to y.
* @param {Number} yx Coefficient from y to x.
* @param {Number} yy Coefficient from y to y.
* @param {Number} dx Offset of x.
* @param {Number} dy Offset of y.
* @return {Ext.draw.Matrix} this
*/
prepend: function(xx, xy, yx, yy, dx, dy) {
var elements = this.elements,
xx0 = elements[0],
xy0 = elements[1],
yx0 = elements[2],
yy0 = elements[3],
dx0 = elements[4],
dy0 = elements[5];
elements[0] = xx * xx0 + yx * xy0;
elements[1] = xy * xx0 + yy * xy0;
elements[2] = xx * yx0 + yx * yy0;
elements[3] = xy * yx0 + yy * yy0;
elements[4] = xx * dx0 + yx * dy0 + dx;
elements[5] = xy * dx0 + yy * dy0 + dy;
return this;
},
/**
* Prepend a matrix onto the current.
*
* __Note:__ The given transform will come after the current one.
* @param {Ext.draw.Matrix} matrix
* @return {Ext.draw.Matrix} this
*/
prependMatrix: function(matrix) {
return this.prepend.apply(this, matrix.elements);
},
/**
* Postpend a matrix onto the current.
*
* __Note:__ The given transform will come before the current one.
*
* @param {Number} xx Coefficient from x to x.
* @param {Number} xy Coefficient from x to y.
* @param {Number} yx Coefficient from y to x.
* @param {Number} yy Coefficient from y to y.
* @param {Number} dx Offset of x.
* @param {Number} dy Offset of y.
* @return {Ext.draw.Matrix} this
*/
append: function(xx, xy, yx, yy, dx, dy) {
var elements = this.elements,
xx0 = elements[0],
xy0 = elements[1],
yx0 = elements[2],
yy0 = elements[3],
dx0 = elements[4],
dy0 = elements[5];
elements[0] = xx * xx0 + xy * yx0;
elements[1] = xx * xy0 + xy * yy0;
elements[2] = yx * xx0 + yy * yx0;
elements[3] = yx * xy0 + yy * yy0;
elements[4] = dx * xx0 + dy * yx0 + dx0;
elements[5] = dx * xy0 + dy * yy0 + dy0;
return this;
},
/**
* Postpend a matrix onto the current.
*
* __Note:__ The given transform will come before the current one.
*
* @param {Ext.draw.Matrix} matrix
* @return {Ext.draw.Matrix} this
*/
appendMatrix: function(matrix) {
return this.append.apply(this, matrix.elements);
},
/**
* Set the elements of a Matrix
* @param {Number} xx
* @param {Number} xy
* @param {Number} yx
* @param {Number} yy
* @param {Number} dx
* @param {Number} dy
* @return {Ext.draw.Matrix} this
*/
set: function(xx, xy, yx, yy, dx, dy) {
var elements = this.elements;
elements[0] = xx;
elements[1] = xy;
elements[2] = yx;
elements[3] = yy;
elements[4] = dx;
elements[5] = dy;
return this;
},
/**
* Return a new matrix represents the opposite transformation of the current one.
*
* @param {Ext.draw.Matrix} [target] A target matrix. If present, it will receive
* the result of inversion to avoid creating a new object.
*
* @return {Ext.draw.Matrix}
*/
inverse: function(target) {
var elements = this.elements,
a = elements[0],
b = elements[1],
c = elements[2],
d = elements[3],
e = elements[4],
f = elements[5],
rDim = 1 / (a * d - b * c);
a *= rDim;
b *= rDim;
c *= rDim;
d *= rDim;
if (target) {
target.set(d, -b, -c, a, c * f - d * e, b * e - a * f);
return target;
} else {
return new Ext.draw.Matrix(d, -b, -c, a, c * f - d * e, b * e - a * f);
}
},
/**
* Translate the matrix.
*
* @param {Number} x
* @param {Number} y
* @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
* @return {Ext.draw.Matrix} this
*/
translate: function(x, y, prepend) {
if (prepend) {
return this.prepend(1, 0, 0, 1, x, y);
} else {
return this.append(1, 0, 0, 1, x, y);
}
},
/**
* Scale the matrix.
*
* @param {Number} sx
* @param {Number} sy
* @param {Number} scx
* @param {Number} scy
* @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
* @return {Ext.draw.Matrix} this
*/
scale: function(sx, sy, scx, scy, prepend) {
var me = this;
// null or undefined
if (sy == null) {
sy = sx;
}
if (scx === undefined) {
scx = 0;
}
if (scy === undefined) {
scy = 0;
}
if (prepend) {
return me.prepend(sx, 0, 0, sy, scx - scx * sx, scy - scy * sy);
} else {
return me.append(sx, 0, 0, sy, scx - scx * sx, scy - scy * sy);
}
},
/**
* Rotate the matrix.
*
* @param {Number} angle Radians to rotate
* @param {Number|null} rcx Center of rotation.
* @param {Number|null} rcy Center of rotation.
* @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
* @return {Ext.draw.Matrix} this
*/
rotate: function(angle, rcx, rcy, prepend) {
var me = this,
cos = Math.cos(angle),
sin = Math.sin(angle);
rcx = rcx || 0;
rcy = rcy || 0;
if (prepend) {
return me.prepend(cos, sin, -sin, cos, rcx - cos * rcx + rcy * sin, rcy - cos * rcy - rcx * sin);
} else {
return me.append(cos, sin, -sin, cos, rcx - cos * rcx + rcy * sin, rcy - cos * rcy - rcx * sin);
}
},
/**
* Rotate the matrix by the angle of a vector.
*
* @param {Number} x
* @param {Number} y
* @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
* @return {Ext.draw.Matrix} this
*/
rotateFromVector: function(x, y, prepend) {
var me = this,
d = Math.sqrt(x * x + y * y),
cos = x / d,
sin = y / d;
if (prepend) {
return me.prepend(cos, sin, -sin, cos, 0, 0);
} else {
return me.append(cos, sin, -sin, cos, 0, 0);
}
},
/**
* Clone this matrix.
* @return {Ext.draw.Matrix}
*/
clone: function() {
return new Ext.draw.Matrix(this.elements);
},
/**
* Horizontally flip the matrix
* @return {Ext.draw.Matrix} this
*/
flipX: function() {
return this.append(-1, 0, 0, 1, 0, 0);
},
/**
* Vertically flip the matrix
* @return {Ext.draw.Matrix} this
*/
flipY: function() {
return this.append(1, 0, 0, -1, 0, 0);
},
/**
* Skew the matrix
* @param {Number} angle
* @return {Ext.draw.Matrix} this
*/
skewX: function(angle) {
return this.append(1, Math.tan(angle), 0, -1, 0, 0);
},
/**
* Skew the matrix
* @param {Number} angle
* @return {Ext.draw.Matrix} this
*/
skewY: function(angle) {
return this.append(1, 0, Math.tan(angle), -1, 0, 0);
},
/**
* Reset the matrix to identical.
* @return {Ext.draw.Matrix} this
*/
reset: function() {
return this.set(1, 0, 0, 1, 0, 0);
},
/**
* @private
* Split Matrix to `{{devicePixelRatio,c,0},{b,devicePixelRatio,0},{0,0,1}}.{{xx,0,dx},{0,yy,dy},{0,0,1}}`
* @return {Object} Object with b,c,d=devicePixelRatio,xx,yy,dx,dy
*/
precisionCompensate: function(devicePixelRatio, comp) {
var elements = this.elements,
x2x = elements[0],
x2y = elements[1],
y2x = elements[2],
y2y = elements[3],
newDx = elements[4],
newDy = elements[5],
r = x2y * y2x - x2x * y2y;
comp.b = devicePixelRatio * x2y / x2x;
comp.c = devicePixelRatio * y2x / y2y;
comp.d = devicePixelRatio;
comp.xx = x2x / devicePixelRatio;
comp.yy = y2y / devicePixelRatio;
comp.dx = (newDy * x2x * y2x - newDx * x2x * y2y) / r / devicePixelRatio;
comp.dy = (newDx * x2y * y2y - newDy * x2x * y2y) / r / devicePixelRatio;
},
/**
* @private
* Split Matrix to `{{1,c,0},{b,d,0},{0,0,1}}.{{xx,0,dx},{0,xx,dy},{0,0,1}}`
* @return {Object} Object with b,c,d,xx,yy=xx,dx,dy
*/
precisionCompensateRect: function(devicePixelRatio, comp) {
var elements = this.elements,
x2x = elements[0],
x2y = elements[1],
y2x = elements[2],
y2y = elements[3],
newDx = elements[4],
newDy = elements[5],
yxOnXx = y2x / x2x;
comp.b = devicePixelRatio * x2y / x2x;
comp.c = devicePixelRatio * yxOnXx;
comp.d = devicePixelRatio * y2y / x2x;
comp.xx = x2x / devicePixelRatio;
comp.yy = x2x / devicePixelRatio;
comp.dx = (newDy * y2x - newDx * y2y) / (x2y * yxOnXx - y2y) / devicePixelRatio;
comp.dy = -(newDy * x2x - newDx * x2y) / (x2y * yxOnXx - y2y) / devicePixelRatio;
},
/**
* Transform point returning the x component of the result.
* @param {Number} x
* @param {Number} y
* @return {Number} x component of the result.
*/
x: function(x, y) {
var elements = this.elements;
return x * elements[0] + y * elements[2] + elements[4];
},
/**
* Transform point returning the y component of the result.
* @param {Number} x
* @param {Number} y
* @return {Number} y component of the result.
*/
y: function(x, y) {
var elements = this.elements;
return x * elements[1] + y * elements[3] + elements[5];
},
/**
* @private
* @param {Number} i
* @param {Number} j
* @return {String}
*/
get: function(i, j) {
return +this.elements[i + j * 2].toFixed(4);
},
/**
* Transform a point to a new array.
* @param {Array} point
* @return {Array}
*/
transformPoint: function(point) {
var elements = this.elements;
return [
point[0] * elements[0] + point[1] * elements[2] + elements[4],
point[0] * elements[1] + point[1] * elements[3] + elements[5]
];
},
/**
* @param {Object} bbox Given as `{x: Number, y: Number, width: Number, height: Number}`.
* @param {Number} [radius]
* @param {Object} [target] Optional target object to recieve the result.
* Recommended to use it for better gc.
*
* @return {Object} Object with x, y, width and height.
*/
transformBBox: function(bbox, radius, target) {
var elements = this.elements,
l = bbox.x,
t = bbox.y,
w0 = bbox.width * 0.5,
h0 = bbox.height * 0.5,
xx = elements[0],
xy = elements[1],
yx = elements[2],
yy = elements[3],
cx = l + w0,
cy = t + h0,
w, h, scales;
if (radius) {
w0 -= radius;
h0 -= radius;
scales = [
Math.sqrt(elements[0] * elements[0] + elements[2] * elements[2]),
Math.sqrt(elements[1] * elements[1] + elements[3] * elements[3])
];
w = Math.abs(w0 * xx) + Math.abs(h0 * yx) + Math.abs(scales[0] * radius);
h = Math.abs(w0 * xy) + Math.abs(h0 * yy) + Math.abs(scales[1] * radius);
} else {
w = Math.abs(w0 * xx) + Math.abs(h0 * yx);
h = Math.abs(w0 * xy) + Math.abs(h0 * yy);
}
if (!target) {
target = {};
}
target.x = cx * xx + cy * yx + elements[4] - w;
target.y = cx * xy + cy * yy + elements[5] - h;
target.width = w + w;
target.height = h + h;
return target;
},
/**
* Transform a list for points.
*
* __Note:__ will change the original list but not points inside it.
* @param {Array} list
* @return {Array} list
*/
transformList: function(list) {
var elements = this.elements,
xx = elements[0],
yx = elements[2],
dx = elements[4],
xy = elements[1],
yy = elements[3],
dy = elements[5],
ln = list.length,
p, i;
for (i = 0; i < ln; i++) {
p = list[i];
list[i] = [
p[0] * xx + p[1] * yx + dx,
p[0] * xy + p[1] * yy + dy
];
}
return list;
},
/**
* Determines whether this matrix is an identity matrix (no transform).
* @return {Boolean}
*/
isIdentity: function() {
var elements = this.elements;
return elements[0] === 1 && elements[1] === 0 && elements[2] === 0 && elements[3] === 1 && elements[4] === 0 && elements[5] === 0;
},
/**
* Determines if this matrix has the same values as another matrix.
* @param {Ext.draw.Matrix} matrix
* @return {Boolean}
*/
equals: function(matrix) {
var elements = this.elements,
elements2 = matrix.elements;
return elements[0] === elements2[0] && elements[1] === elements2[1] && elements[2] === elements2[2] && elements[3] === elements2[3] && elements[4] === elements2[4] && elements[5] === elements2[5];
},
/**
* Create an array of elements by horizontal order (xx,yx,dx,yx,yy,dy).
* @return {Array}
*/
toArray: function() {
var elements = this.elements;
return [
elements[0],
elements[2],
elements[4],
elements[1],
elements[3],
elements[5]
];
},
/**
* Create an array of elements by vertical order (xx,xy,yx,yy,dx,dy).
* @return {Array|String}
*/
toVerticalArray: function() {
return this.elements.slice();
},
/**
* Get an array of elements.
* The numbers are rounded to keep only 4 decimals.
* @return {Array}
*/
toString: function() {
var me = this;
return [
me.get(0, 0),
me.get(0, 1),
me.get(1, 0),
me.get(1, 1),
me.get(2, 0),
me.get(2, 1)
].join(',');
},
/**
* Apply the matrix to a drawing context.
* @param {Object} ctx
* @return {Ext.draw.Matrix} this
*/
toContext: function(ctx) {
ctx.transform.apply(ctx, this.elements);
return this;
},
/**
* Return a string that can be used as transform attribute in SVG.
* @return {String}
*/
toSvg: function() {
var elements = this.elements;
// The reason why we cannot use `.join` is the `1e5` form is not accepted in svg.
return "matrix(" + elements[0].toFixed(9) + ',' + elements[1].toFixed(9) + ',' + elements[2].toFixed(9) + ',' + elements[3].toFixed(9) + ',' + elements[4].toFixed(9) + ',' + elements[5].toFixed(9) + ")";
},
/**
* Get the x scale of the matrix.
* @return {Number}
*/
getScaleX: function() {
var elements = this.elements;
return Math.sqrt(elements[0] * elements[0] + elements[2] * elements[2]);
},
/**
* Get the y scale of the matrix.
* @return {Number}
*/
getScaleY: function() {
var elements = this.elements;
return Math.sqrt(elements[1] * elements[1] + elements[3] * elements[3]);
},
/**
* Get x-to-x component of the matrix
* @return {Number}
*/
getXX: function() {
return this.elements[0];
},
/**
* Get x-to-y component of the matrix.
* @return {Number}
*/
getXY: function() {
return this.elements[1];
},
/**
* Get y-to-x component of the matrix.
* @return {Number}
*/
getYX: function() {
return this.elements[2];
},
/**
* Get y-to-y component of the matrix.
* @return {Number}
*/
getYY: function() {
return this.elements[3];
},
/**
* Get offset x component of the matrix.
* @return {Number}
*/
getDX: function() {
return this.elements[4];
},
/**
* Get offset y component of the matrix.
* @return {Number}
*/
getDY: function() {
return this.elements[5];
},
/**
* Split a transformation matrix into Scale, Rotate, Translate components.
* @return {Object}
*/
split: function() {
var el = this.elements,
xx = el[0],
xy = el[1],
yx = el[2],
yy = el[3],
out = {
translateX: el[4],
translateY: el[5]
};
out.scaleX = Ext.Number.sign(xx) * Math.sqrt(xx * xx + yx * yx);
out.scaleY = Ext.Number.sign(yy) * Math.sqrt(xy * xy + yy * yy);
out.rotation = Math.atan2(xy, yy);
return out;
}
}, function() {
function registerName(properties, name, i) {
properties[name] = {
get: function() {
return this.elements[i];
},
set: function(val) {
this.elements[i] = val;
}
};
}
// Compatibility with SVGMatrix.
if (Object.defineProperties) {
var properties = {};
/**
* @property {Number} a Get x-to-x component of the matrix. Avoid using it for performance consideration.
* Use {@link #getXX} instead.
*/
registerName(properties, 'a', 0);
registerName(properties, 'b', 1);
registerName(properties, 'c', 2);
registerName(properties, 'd', 3);
registerName(properties, 'e', 4);
registerName(properties, 'f', 5);
Object.defineProperties(this.prototype, properties);
}
/**
* Performs matrix multiplication. This matrix is post-multiplied by another matrix.
*
* __Note:__ The given transform will come before the current one.
*
* @method
* @param {Ext.draw.Matrix} matrix
* @return {Ext.draw.Matrix} this
*/
this.prototype.multiply = this.prototype.appendMatrix;
});
/**
* @class Ext.draw.modifier.Modifier
*
* Each sprite has a stack of modifiers. The resulting attributes of sprite is
* the content of the stack top. When setting attributes to a sprite,
* changes will be pushed-down though the stack of modifiers and pop-back the
* additive changes; When modifier is triggered to change the attribute of a
* sprite, it will pop-up the changes to the top.
*/
Ext.define('Ext.draw.modifier.Modifier', {
mixins: {
observable: 'Ext.mixin.Observable'
},
config: {
/**
* @cfg {Ext.draw.modifier.Modifier} previous Previous modifier that receives
* the push-down changes.
*/
previous: null,
/**
* @cfg {Ext.draw.modifier.Modifier} next Next modifier that receives the
* pop-up changes.
*/
next: null,
/**
* @cfg {Ext.draw.sprite.Sprite} sprite The sprite to which the modifier belongs.
*/
sprite: null
},
constructor: function(config) {
this.mixins.observable.constructor.call(this, config);
},
updateNext: function(next) {
if (next) {
next.setPrevious(this);
}
},
updatePrevious: function(prev) {
if (prev) {
prev.setNext(this);
}
},
/**
* Validate attribute set before use.
*
* @param {Object} attr The attribute to be validated. Note that it may be already initialized, so do
* not override properties that have already been used.
*/
prepareAttributes: function(attr) {
if (this._previous) {
this._previous.prepareAttributes(attr);
}
},
/**
* Invoked when changes need to be popped up to the top.
* @param {Object} attributes The source attributes.
* @param {Object} changes The changes to be popped up.
*/
popUp: function(attributes, changes) {
if (this._next) {
this._next.popUp(attributes, changes);
} else {
Ext.apply(attributes, changes);
}
},
/**
* Invoked when changes need to be pushed down to the sprite.
* @param {Object} attr The source attributes.
* @param {Object} changes The changes to make. This object might be changed unexpectedly inside the method.
* @return {Mixed}
*/
pushDown: function(attr, changes) {
if (this._previous) {
return this._previous.pushDown(attr, changes);
} else {
for (var name in changes) {
if (changes[name] === attr[name]) {
delete changes[name];
}
}
return changes;
}
}
});
/**
* @class Ext.draw.modifier.Target
* @extends Ext.draw.modifier.Modifier
*
* This is the destination (top) modifier that has to be put at
* the top of the modifier stack.
*
* The Target modifier figures out which updaters have to be called
* for the changed set of attributes and makes the sprite and its instances (if any)
* call them.
*
*/
Ext.define('Ext.draw.modifier.Target', {
requires: [
'Ext.draw.Matrix'
],
extend: 'Ext.draw.modifier.Modifier',
alias: 'modifier.target',
statics: {
uniqueId: 0
},
/**
* @inheritdoc
*/
prepareAttributes: function(attr) {
var previous = this.getPrevious();
if (previous) {
previous.prepareAttributes(attr);
}
attr.attributeId = 'attribute-' + Ext.draw.modifier.Target.uniqueId++;
if (!attr.hasOwnProperty('canvasAttributes')) {
attr.bbox = {
plain: {
dirty: true
},
transform: {
dirty: true
}
};
attr.dirty = true;
/*
Maps updaters that have to be called to the attributes that triggered the update.
It is basically a reversed `triggers` map (see Ext.draw.sprite.AttributeDefinition),
but only for those attributes that have changed.
Pending updaters are called by the Ext.draw.sprite.Sprite.callUpdaters method.
The 'canvas' updater is a special kind of updater that is not actually a function
but a flag indicating that the attribute should be applied directly to a canvas
context.
*/
attr.pendingUpdaters = {};
/*
Holds the attributes that triggered the canvas update (attr.pendingUpdaters.canvas).
Canvas attributes are applied directly to a canvas context
by the sprite.useAttributes method.
*/
attr.canvasAttributes = {};
attr.matrix = new Ext.draw.Matrix();
attr.inverseMatrix = new Ext.draw.Matrix();
}
},
/**
* @private
* Applies changes to sprite/instance attributes and determines which updaters
* have to be called as a result of attributes change.
* @param {Object} attr The source attributes.
* @param {Object} changes The modifier changes.
*/
applyChanges: function(attr, changes) {
Ext.apply(attr, changes);
var sprite = this.getSprite(),
pendingUpdaters = attr.pendingUpdaters,
triggers = sprite.self.def.getTriggers(),
updaters, instances, instance, name, hasChanges, canvasAttributes, i, j, ln;
for (name in changes) {
hasChanges = true;
if ((updaters = triggers[name])) {
sprite.scheduleUpdaters(attr, updaters, [
name
]);
}
if (attr.template && changes.removeFromInstance && changes.removeFromInstance[name]) {
delete attr[name];
}
}
if (!hasChanges) {
return;
}
// This can prevent sub objects to set duplicated attributes to context.
if (pendingUpdaters.canvas) {
canvasAttributes = pendingUpdaters.canvas;
delete pendingUpdaters.canvas;
for (i = 0 , ln = canvasAttributes.length; i < ln; i++) {
name = canvasAttributes[i];
attr.canvasAttributes[name] = attr[name];
}
}
// If the attributes of an instancing sprite template are being modified here,
// then spread the pending updaters to the instances (template's children).
if (attr.hasOwnProperty('children')) {
instances = attr.children;
for (i = 0 , ln = instances.length; i < ln; i++) {
instance = instances[i];
Ext.apply(instance.pendingUpdaters, pendingUpdaters);
if (canvasAttributes) {
for (j = 0; j < canvasAttributes.length; j++) {
name = canvasAttributes[j];
instance.canvasAttributes[name] = instance[name];
}
}
sprite.callUpdaters(instance);
}
}
sprite.setDirty(true);
sprite.callUpdaters(attr);
},
/**
* @inheritdoc
*/
popUp: function(attr, changes) {
this.applyChanges(attr, changes);
},
/**
* @inheritdoc
*/
pushDown: function(attr, changes) {
var previous = this.getPrevious();
if (previous) {
changes = previous.pushDown(attr, changes);
}
this.applyChanges(attr, changes);
return changes;
}
});
(function() {
var pow = Math.pow,
sin = Math.sin,
cos = Math.cos,
sqrt = Math.sqrt,
pi = Math.PI,
easings, addEasing, poly, createPoly, easing, i, l;
//create polynomial easing equations
poly = [
'quad',
'cubic',
'quart',
'quint'
];
//create other easing equations
easings = {
pow: function(p, x) {
return pow(p, x[0] || 6);
},
expo: function(p) {
return pow(2, 8 * (p - 1));
},
circ: function(p) {
return 1 - sqrt(1 - p * p);
},
sine: function(p) {
return 1 - sin((1 - p) * pi / 2);
},
back: function(p, n) {
n = n || 1.616;
return p * p * ((n + 1) * p - n);
},
bounce: function(p) {
var value;
for (var a = 0,
b = 1; 1; a += b , b /= 2) {
if (p >= (7 - 4 * a) / 11) {
value = b * b - pow((11 - 6 * a - 11 * p) / 4, 2);
break;
}
}
return value;
},
elastic: function(p, x) {
return pow(2, 10 * --p) * cos(20 * p * pi * (x || 1) / 3);
}
};
//Add easeIn, easeOut, easeInOut options to all easing equations.
addEasing = function(easing, params) {
params = params && params.length ? params : [
params
];
return Ext.apply(easing, {
easeIn: function(pos) {
return easing(pos, params);
},
easeOut: function(pos) {
return 1 - easing(1 - pos, params);
},
easeInOut: function(pos) {
return (pos <= 0.5) ? easing(2 * pos, params) / 2 : (2 - easing(2 * (1 - pos), params)) / 2;
}
});
};
//Append the polynomial equations with easing support to the EasingPrototype.
createPoly = function(times) {
return function(p) {
return pow(p, times);
};
};
for (i = 0 , l = poly.length; i < l; ++i) {
easings[poly[i]] = createPoly(i + 2);
}
//Add linear interpolator
easings.linear = function(x) {
return x;
};
for (easing in easings) {
if (easings.hasOwnProperty(easing)) {
addEasing(easings[easing]);
}
}
/**
* @class
* Contains transition equations such as `Quad`, `Cubic`, `Quart`, `Quint`,
* `Expo`, `Circ`, `Pow`, `Sine`, `Back`, `Bounce`, `Elastic`, etc.
*
* Contains transition equations such as `Quad`, `Cubic`, `Quart`, `Quint`, `Expo`, `Circ`, `Pow`, `Sine`, `Back`, `Bounce`, `Elastic`, etc.
* Each transition also contains methods for applying this function as ease in, ease out or ease in and out accelerations.
*
* var fx = Ext.create('Ext.draw.fx.Sprite', {
* sprite: sprite,
* duration: 1000,
* easing: 'backOut'
* });
*/
Ext.define('Ext.draw.TimingFunctions', {
singleton: true,
easingMap: {
linear: easings.linear,
easeIn: easings.quad.easeIn,
easeOut: easings.quad.easeOut,
easeInOut: easings.quad.easeInOut,
backIn: easings.back,
backOut: function(x, n) {
return 1 - easings.back(1 - x, n);
},
backInOut: function(x, n) {
if (x < 0.5) {
return easings.back(x * 2, n) * 0.5;
} else {
return 1 - easings.back((1 - x) * 2, n) * 0.5;
}
},
elasticIn: function(x, n) {
return 1 - easings.elastic(1 - x, n);
},
elasticOut: easings.elastic,
bounceIn: easings.bounce,
bounceOut: function(x) {
return 1 - easings.bounce(1 - x);
}
}
}, function() {
Ext.apply(this, easings);
});
})();
/**
* @class Ext.draw.Animator
*
* Singleton class that manages the animation pool.
*/
Ext.define('Ext.draw.Animator', {
uses: [
'Ext.draw.Draw'
],
singleton: true,
frameCallbacks: {},
frameCallbackId: 0,
scheduled: 0,
frameStartTimeOffset: Ext.now(),
animations: [],
running: false,
/**
* Cross platform `animationTime` implementation.
* @return {Number}
*/
animationTime: function() {
return Ext.AnimationQueue.frameStartTime - this.frameStartTimeOffset;
},
/**
* Adds an animated object to the animation pool.
*
* @param {Object} animation The animation descriptor to add to the pool.
*/
add: function(animation) {
var me = this;
if (!me.contains(animation)) {
me.animations.push(animation);
me.ignite();
if ('fireEvent' in animation) {
animation.fireEvent('animationstart', animation);
}
}
},
/**
* Removes an animation from the pool.
* TODO: This is broken when called within `step` method.
* @param {Object} animation The animation to remove from the pool.
*/
remove: function(animation) {
var me = this,
animations = me.animations,
i = 0,
l = animations.length;
for (; i < l; ++i) {
if (animations[i] === animation) {
animations.splice(i, 1);
if ('fireEvent' in animation) {
animation.fireEvent('animationend', animation);
}
return;
}
}
},
/**
* Returns `true` or `false` whether it contains the given animation or not.
*
* @param {Object} animation The animation to check for.
* @return {Boolean}
*/
contains: function(animation) {
return Ext.Array.indexOf(this.animations, animation) > -1;
},
/**
* Returns `true` or `false` whether the pool is empty or not.
* @return {Boolean}
*/
empty: function() {
return this.animations.length === 0;
},
/**
* Given a frame time it will filter out finished animations from the pool.
*
* @param {Number} frameTime The frame's start time, in milliseconds.
*/
step: function(frameTime) {
var me = this,
animations = me.animations,
animation,
i = 0,
ln = animations.length;
for (; i < ln; i++) {
animation = animations[i];
animation.step(frameTime);
if (!animation.animating) {
animations.splice(i, 1);
i--;
ln--;
if (animation.fireEvent) {
animation.fireEvent('animationend');
}
}
}
},
/**
* Register a one-time callback that will be called at the next frame.
* @param {Function/String} callback
* @param {Object} scope
* @return {String} The ID of the scheduled callback.
*/
schedule: function(callback, scope) {
scope = scope || this;
var id = 'frameCallback' + (this.frameCallbackId++);
if (Ext.isString(callback)) {
callback = scope[callback];
}
Ext.draw.Animator.frameCallbacks[id] = {
fn: callback,
scope: scope,
once: true
};
this.scheduled++;
Ext.draw.Animator.ignite();
return id;
},
/**
* Register a one-time callback that will be called at the next frame,
* if that callback (with a matching function and scope) isn't already scheduled.
* @param {Function/String} callback
* @param {Object} scope
* @return {String/null} The ID of the scheduled callback or null, if that callback has already been scheduled.
*/
scheduleIf: function(callback, scope) {
scope = scope || this;
var frameCallbacks = Ext.draw.Animator.frameCallbacks,
cb, id;
if (Ext.isString(callback)) {
callback = scope[callback];
}
for (id in frameCallbacks) {
cb = frameCallbacks[id];
if (cb.once && cb.fn === callback && cb.scope === scope) {
return null;
}
}
return this.schedule(callback, scope);
},
/**
* Cancel a registered one-time callback
* @param {String} id
*/
cancel: function(id) {
if (Ext.draw.Animator.frameCallbacks[id] && Ext.draw.Animator.frameCallbacks[id].once) {
this.scheduled--;
delete Ext.draw.Animator.frameCallbacks[id];
}
},
/**
* Register a recursive callback that will be called at every frame.
*
* @param {Function} callback
* @param {Object} scope
* @return {String}
*/
addFrameCallback: function(callback, scope) {
scope = scope || this;
if (Ext.isString(callback)) {
callback = scope[callback];
}
var id = 'frameCallback' + (this.frameCallbackId++);
Ext.draw.Animator.frameCallbacks[id] = {
fn: callback,
scope: scope
};
return id;
},
/**
* Unregister a recursive callback.
* @param {String} id
*/
removeFrameCallback: function(id) {
delete Ext.draw.Animator.frameCallbacks[id];
},
/**
* @private
*/
fireFrameCallbacks: function() {
var callbacks = this.frameCallbacks,
id, fn, cb;
for (id in callbacks) {
cb = callbacks[id];
fn = cb.fn;
if (Ext.isString(fn)) {
fn = cb.scope[fn];
}
fn.call(cb.scope);
if (callbacks[id] && cb.once) {
this.scheduled--;
delete callbacks[id];
}
}
},
handleFrame: function() {
this.step(this.animationTime());
this.fireFrameCallbacks();
if (!this.scheduled && this.empty()) {
Ext.AnimationQueue.stop(this.handleFrame, this);
this.running = false;
}
},
ignite: function() {
if (!this.running) {
this.running = true;
Ext.AnimationQueue.start(this.handleFrame, this);
Ext.draw.Draw.updateIOS();
}
}
});
/**
* The Animation modifier.
*
* Sencha Charts allow users to use transitional animation on sprites. Simply set the duration
* and easing in the animation modifier, then all the changes to the sprites will be animated.
*
* Also, you can use different durations and easing functions on different attributes by using
* {@link #customDurations} and {@link #customEasings}.
*
* By default, an animation modifier will be created during the initialization of a sprite.
* You can get the animation modifier of a sprite via `sprite.fx`.
*
*/
Ext.define('Ext.draw.modifier.Animation', {
requires: [
'Ext.draw.TimingFunctions',
'Ext.draw.Animator'
],
extend: 'Ext.draw.modifier.Modifier',
alias: 'modifier.animation',
config: {
/**
* @cfg {Function} easing
* Default easing function.
*/
easing: function(x) {
return x;
},
/**
* @cfg {Number} duration
* Default duration time (ms).
*/
duration: 0,
/**
* @cfg {Object} customEasings Overrides the default easing function for defined attributes. E.g.:
*
* // Assuming the sprite the modifier is applied to is a 'circle'.
* customEasings: {
* r: 'easeOut',
* 'fillStyle,strokeStyle': 'linear',
* 'cx,cy': function (p, n) {
* p = 1 - p;
* n = n || 1.616;
* return 1 - p * p * ((n + 1) * p - n);
* }
* }
*/
customEasings: {},
/**
* @cfg {Object} customDurations Overrides the default duration for defined attributes. E.g.:
*
* // Assuming the sprite the modifier is applied to is a 'circle'.
* customDurations: {
* r: 1000,
* 'fillStyle,strokeStyle': 2000,
* 'cx,cy': 1000
* }
*/
customDurations: {},
/**
* @deprecated Use {@link #customDurations} instead.
*/
customDuration: null
},
constructor: function() {
this.anyAnimation = false;
this.anySpecialAnimations = false;
this.animating = 0;
this.animatingPool = [];
this.callParent(arguments);
},
/**
* @inheritdoc
*/
prepareAttributes: function(attr) {
if (!attr.hasOwnProperty('timers')) {
attr.animating = false;
attr.timers = {};
// The animationOriginal object is used to hold the target values for the
// attributes while they are being animated from source to target values.
// The animationOriginal is pushed down to the lower level modifiers,
// instead of the actual attr object, to hide the fact that the
// attributes are being animated.
attr.animationOriginal = Ext.Object.chain(attr);
attr.animationOriginal.prototype = attr;
}
if (this._previous) {
this._previous.prepareAttributes(attr.animationOriginal);
}
},
updateSprite: function(sprite) {
this.setConfig(sprite.config.fx);
},
updateDuration: function(duration) {
this.anyAnimation = duration > 0;
},
applyEasing: function(easing) {
if (typeof easing === 'string') {
return Ext.draw.TimingFunctions.easingMap[easing];
} else {
return easing;
}
},
applyCustomEasings: function(newEasings, oldEasings) {
oldEasings = oldEasings || {};
var any, key, attrs, easing, i, ln;
for (key in newEasings) {
any = true;
easing = newEasings[key];
attrs = key.split(',');
if (typeof easing === 'string') {
easing = Ext.draw.TimingFunctions.easingMap[easing];
}
for (i = 0 , ln = attrs.length; i < ln; i++) {
oldEasings[attrs[i]] = easing;
}
}
if (any) {
this.anySpecialAnimations = any;
}
return oldEasings;
},
/**
* Set special easings on the given attributes. E.g.:
*
* circleSprite.fx.setEasingOn('r', 'elasticIn');
*
* @param {String/Array} attrs The source attribute(s).
* @param {String} easing The special easings.
*/
setEasingOn: function(attrs, easing) {
attrs = Ext.Array.from(attrs).slice();
var customEasings = {},
ln = attrs.length,
i = 0;
for (; i < ln; i++) {
customEasings[attrs[i]] = easing;
}
this.setCustomEasings(customEasings);
},
/**
* Remove special easings on the given attributes.
* @param {String/Array} attrs The source attribute(s).
*/
clearEasingOn: function(attrs) {
attrs = Ext.Array.from(attrs, true);
var i = 0,
ln = attrs.length;
for (; i < ln; i++) {
delete this._customEasings[attrs[i]];
}
},
applyCustomDurations: function(newDurations, oldDurations) {
oldDurations = oldDurations || {};
var any, key, duration, attrs, i, ln;
for (key in newDurations) {
any = true;
duration = newDurations[key];
attrs = key.split(',');
for (i = 0 , ln = attrs.length; i < ln; i++) {
oldDurations[attrs[i]] = duration;
}
}
if (any) {
this.anySpecialAnimations = any;
}
return oldDurations;
},
/**
* @private
* @deprecated
* @since 5.0.1.
*/
applyCustomDuration: function(newDuration, oldDuration) {
if (newDuration) {
this.getCustomDurations();
this.setCustomDurations(newDuration);
Ext.log.warn("'customDuration' config is deprecated. Use 'customDurations' config instead.");
}
},
/**
* Set special duration on the given attributes. E.g.:
*
* rectSprite.fx.setDurationOn('height', 2000);
*
* @param {String/Array} attrs The source attributes.
* @param {Number} duration The special duration.
*/
setDurationOn: function(attrs, duration) {
attrs = Ext.Array.from(attrs).slice();
var customDurations = {},
i = 0,
ln = attrs.length;
for (; i < ln; i++) {
customDurations[attrs[i]] = duration;
}
this.setCustomDurations(customDurations);
},
/**
* Remove special easings on the given attributes.
* @param {Object} attrs The source attributes.
*/
clearDurationOn: function(attrs) {
attrs = Ext.Array.from(attrs, true);
var i = 0,
ln = attrs.length;
for (; i < ln; i++) {
delete this._customDurations[attrs[i]];
}
},
/**
* @private
* Initializes Animator for the animation.
* @param {Object} attr The source attributes.
* @param {Boolean} animating The animating flag.
*/
setAnimating: function(attr, animating) {
var me = this,
pool = me.animatingPool;
if (attr.animating !== animating) {
attr.animating = animating;
if (animating) {
pool.push(attr);
if (me.animating === 0) {
Ext.draw.Animator.add(me);
}
me.animating++;
} else {
for (var i = pool.length; i--; ) {
if (pool[i] === attr) {
pool.splice(i, 1);
}
}
me.animating = pool.length;
}
}
},
/**
* @private
* Set the attr with given easing and duration.
* @param {Object} attr The attributes collection.
* @param {Object} changes The changes that popped up from lower modifier.
* @return {Object} The changes to pop up.
*/
setAttrs: function(attr, changes) {
var timers = attr.timers,
parsers = this._sprite.self.def._animationProcessors,
defaultEasing = this._easing,
defaultDuration = this._duration,
customDurations = this._customDurations,
customEasings = this._customEasings,
anySpecial = this.anySpecialAnimations,
any = this.anyAnimation || anySpecial,
animationOriginal = attr.animationOriginal,
ignite = false,
timer, name, newValue, startValue, parser, easing, duration;
if (!any) {
// If there is no animation enabled
// When applying changes to attributes, simply stop current animation
// and set the value.
for (name in changes) {
if (attr[name] === changes[name]) {
delete changes[name];
} else {
attr[name] = changes[name];
}
delete animationOriginal[name];
delete timers[name];
}
return changes;
} else {
// If any animation
for (name in changes) {
newValue = changes[name];
startValue = attr[name];
if (newValue !== startValue && startValue !== undefined && startValue !== null && (parser = parsers[name])) {
// If this property is animating.
// Figure out the desired duration and easing.
easing = defaultEasing;
duration = defaultDuration;
if (anySpecial) {
// Deducing the easing function and duration
if (name in customEasings) {
easing = customEasings[name];
}
if (name in customDurations) {
duration = customDurations[name];
}
}
// Transitions betweens color and gradient or between gradients are not supported.
if (startValue && startValue.isGradient || newValue && newValue.isGradient) {
duration = 0;
}
// If the property is animating
if (duration) {
if (!timers[name]) {
timers[name] = {};
}
timer = timers[name];
timer.start = 0;
timer.easing = easing;
timer.duration = duration;
timer.compute = parser.compute;
timer.serve = parser.serve || Ext.identityFn;
timer.remove = changes.removeFromInstance && changes.removeFromInstance[name];
if (parser.parseInitial) {
var initial = parser.parseInitial(startValue, newValue);
timer.source = initial[0];
timer.target = initial[1];
} else if (parser.parse) {
timer.source = parser.parse(startValue);
timer.target = parser.parse(newValue);
} else {
timer.source = startValue;
timer.target = newValue;
}
// The animation started. Change to originalVal.
animationOriginal[name] = newValue;
delete changes[name];
ignite = true;
continue;
} else {
delete animationOriginal[name];
}
} else {
delete animationOriginal[name];
}
// If the property is not animating.
delete timers[name];
}
}
if (ignite && !attr.animating) {
this.setAnimating(attr, true);
}
return changes;
},
/**
* @private
*
* Update attributes to current value according to current animation time.
* This method will not affect the values of lower layers, but may delete a
* value from it.
* @param {Object} attr The source attributes.
* @return {Object} The changes to pop up.
*/
updateAttributes: function(attr) {
if (!attr.animating) {
return {};
}
var changes = {},
any = false,
timers = attr.timers,
animationOriginal = attr.animationOriginal,
now = Ext.draw.Animator.animationTime(),
name, timer, delta;
// If updated in the same frame, return.
if (attr.lastUpdate === now) {
return {};
}
for (name in timers) {
timer = timers[name];
if (!timer.start) {
timer.start = now;
delta = 0;
} else {
delta = (now - timer.start) / timer.duration;
}
if (delta >= 1) {
changes[name] = animationOriginal[name];
delete animationOriginal[name];
if (timers[name].remove) {
changes.removeFromInstance = changes.removeFromInstance || {};
changes.removeFromInstance[name] = true;
}
delete timers[name];
} else {
changes[name] = timer.serve(timer.compute(timer.source, timer.target, timer.easing(delta), attr[name]));
any = true;
}
}
attr.lastUpdate = now;
this.setAnimating(attr, any);
return changes;
},
/**
* @inheritdoc
*/
pushDown: function(attr, changes) {
changes = this.callParent([
attr.animationOriginal,
changes
]);
return this.setAttrs(attr, changes);
},
/**
* @inheritdoc
*/
popUp: function(attr, changes) {
attr = attr.prototype;
changes = this.setAttrs(attr, changes);
if (this._next) {
return this._next.popUp(attr, changes);
} else {
return Ext.apply(attr, changes);
}
},
// This is called as an animated object in `Ext.draw.Animator`.
step: function(frameTime) {
var me = this,
pool = me.animatingPool.slice(),
attributes, i, ln;
for (i = 0 , ln = pool.length; i < ln; i++) {
attributes = pool[i];
var changes = this.updateAttributes(attributes),
name;
// Looking for anything in changes
//noinspection LoopStatementThatDoesntLoopJS
for (name in changes) {
if (this._next) {
this._next.popUp(attributes, changes);
}
break;
}
}
},
/**
* Stop all animations affected by this modifier.
*/
stop: function() {
this.step();
var me = this,
pool = me.animatingPool,
i, ln;
for (i = 0 , ln = pool.length; i < ln; i++) {
pool[i].animating = false;
}
me.animatingPool.length = 0;
me.animating = 0;
Ext.draw.Animator.remove(me);
},
destroy: function() {
var me = this;
me.animatingPool.length = 0;
me.animating = 0;
}
});
/**
* @class Ext.draw.modifier.Highlight
* @extends Ext.draw.modifier.Modifier
*
* Highlight is a modifier that will override sprite attributes
* with {@link Ext.draw.modifier.Highlight#highlightStyle highlightStyle} attributes
* when sprite's `highlighted` attribute is true.
*/
Ext.define('Ext.draw.modifier.Highlight', {
extend: 'Ext.draw.modifier.Modifier',
alias: 'modifier.highlight',
config: {
/**
* @cfg {Boolean} enabled 'true' if the highlight is applied.
*/
enabled: false,
/**
* @cfg {Object} highlightStyle The style attributes of the highlight modifier.
*/
highlightStyle: null
},
preFx: true,
applyHighlightStyle: function(style, oldStyle) {
oldStyle = oldStyle || {};
if (this.getSprite()) {
Ext.apply(oldStyle, this.getSprite().self.def.normalize(style));
} else {
Ext.apply(oldStyle, style);
}
return oldStyle;
},
/**
* @inheritdoc
*/
prepareAttributes: function(attr) {
if (!attr.hasOwnProperty('highlightOriginal')) {
attr.highlighted = false;
attr.highlightOriginal = Ext.Object.chain(attr);
attr.highlightOriginal.prototype = attr;
// A list of attributes that should be removed from a sprite instance
// when it is unhighlighted.
attr.highlightOriginal.removeFromInstance = {};
}
if (this._previous) {
this._previous.prepareAttributes(attr.highlightOriginal);
}
},
updateSprite: function(sprite, oldSprite) {
if (sprite) {
if (this.getHighlightStyle()) {
this._highlightStyle = sprite.self.def.normalize(this.getHighlightStyle());
}
this.setHighlightStyle(sprite.config.highlight);
}
// Add highlight related attributes to sprite's attribute definition.
// TODO: Unfortunately this will affect all sprites of the same type,
// TODO: even those without the highlight modifier.
sprite.self.def.setConfig({
defaults: {
highlighted: false
},
processors: {
highlighted: 'bool'
}
});
this.setSprite(sprite);
},
/**
* Filter out modifier changes that override highlightStyle or source attributes.
* @param {Object} attr The source attributes.
* @param {Object} changes The modifier changes.
* @return {*} The filtered changes.
*/
filterChanges: function(attr, changes) {
var me = this,
highlightOriginal = attr.highlightOriginal,
style = me.getHighlightStyle(),
name;
if (attr.highlighted) {
// TODO: Remove changes that match highlightStyle attribute names.
// TODO: Backup such changes to highlightOriginal before removing.
for (name in changes) {
if (style.hasOwnProperty(name)) {
// If sprite is highlighted, then stash the changes
// to the `highlightStyle` attributes made by lower level modifiers
// to apply them later when sprite is unhighlighted.
highlightOriginal[name] = changes[name];
delete changes[name];
}
}
}
// TODO: Remove changes (except the 'highlighted' flag) that match the original values. Why?
for (name in changes) {
if (name !== 'highlighted' && highlightOriginal[name] === changes[name]) {
delete changes[name];
}
}
return changes;
},
/**
* @inheritdoc
*/
pushDown: function(attr, changes) {
var highlightStyle = this.getHighlightStyle(),
highlightOriginal = attr.highlightOriginal,
removeFromInstance = highlightOriginal.removeFromInstance,
highlighted, name, tplAttr, timer;
if (changes.hasOwnProperty('highlighted')) {
highlighted = changes.highlighted;
// Hide `highlighted` and `highlightStyle` from underlying modifiers.
delete changes.highlighted;
if (this._previous) {
changes = this._previous.pushDown(highlightOriginal, changes);
}
changes = this.filterChanges(attr, changes);
if (highlighted !== attr.highlighted) {
if (highlighted) {
// Switching ON.
// At this time, original should be empty.
for (name in highlightStyle) {
// Remember the values of attributes to revert back to them on unhighlight.
if (name in changes) {
// Remember value set by lower level modifiers.
highlightOriginal[name] = changes[name];
} else {
// Remember the original value.
// If this is a sprite instance and it doesn't have its own
// 'name' attribute, (i.e. inherits template's attribute value)
// than we have to get the value for the 'name' attribute from
// the template's 'animationOriginal' object instead of its
// 'attr' object (which is the prototype of the instance),
// because the 'name' attribute of the template may be animating.
// Check out the prepareAttributes method of the Animation
// modifier for more details on the 'animationOriginal' object.
tplAttr = attr.template && attr.template.ownAttr;
if (tplAttr && !attr.prototype.hasOwnProperty(name)) {
removeFromInstance[name] = true;
highlightOriginal[name] = tplAttr.animationOriginal[name];
} else {
// Even if a sprite instance has its own property, it may
// still have to be removed from the instance after
// unhighlighting is done.
// Consider a situation where an instance doesn't originally
// have its own attribute (that is used for highlighting and
// unhighlighting). It will however have that attribute as
// its own when the highlight/unhighlight animation is in
// progress, until the attribute is removed from the instance
// when the unhighlighting is done.
// So in a scenario where the instance is highlighted, then
// unhighlighted (i.e. starts animating back to its original
// value) and then highlighted again before the unhighlight
// animation is done, we should still mark the attribute
// for removal from the instance, if it was our original
// intention. To tell if it was, we can check the timer
// for the attribute and see if the 'remove' flag is set.
timer = highlightOriginal.timers[name];
if (timer && timer.remove) {
removeFromInstance[name] = true;
}
highlightOriginal[name] = attr[name];
}
}
if (highlightOriginal[name] !== highlightStyle[name]) {
changes[name] = highlightStyle[name];
}
}
} else {
// Switching OFF.
for (name in highlightStyle) {
if (!(name in changes)) {
changes[name] = highlightOriginal[name];
}
delete highlightOriginal[name];
}
changes.removeFromInstance = changes.removeFromInstance || {};
// Let the higher lever animation modifier know which attributes
// should be removed from instance when the animation is done.
Ext.apply(changes.removeFromInstance, removeFromInstance);
highlightOriginal.removeFromInstance = {};
}
changes.highlighted = highlighted;
}
} else {
if (this._previous) {
changes = this._previous.pushDown(highlightOriginal, changes);
}
changes = this.filterChanges(attr, changes);
}
return changes;
},
/**
* @inheritdoc
*/
popUp: function(attr, changes) {
changes = this.filterChanges(attr, changes);
Ext.draw.modifier.Modifier.prototype.popUp.call(this, attr, changes);
}
});
/**
* A sprite is an object rendered in a drawing {@link Ext.draw.Surface}.
* The Sprite class itself is an abstract class and is not meant to be used directly.
* Every sprite in the Draw and Chart packages is a subclass of the Ext.draw.sprite.Sprite.
* The standard Sprite subclasses are:
*
* * {@link Ext.draw.sprite.Path} - A sprite that represents a path.
* * {@link Ext.draw.sprite.Rect} - A sprite that represents a rectangle.
* * {@link Ext.draw.sprite.Circle} - A sprite that represents a circle.
* * {@link Ext.draw.sprite.Sector} - A sprite representing a pie slice.
* * {@link Ext.draw.sprite.Arc} - A sprite that represents a circular arc.
* * {@link Ext.draw.sprite.Ellipse} - A sprite that represents an ellipse.
* * {@link Ext.draw.sprite.EllipticalArc} - A sprite that represents an elliptical arc.
* * {@link Ext.draw.sprite.Text} - A sprite that represents text.
* * {@link Ext.draw.sprite.Image} - A sprite that represents an image.
* * {@link Ext.draw.sprite.Instancing} - A sprite that represents multiple instances based on the given template.
* * {@link Ext.draw.sprite.Composite} - Represents a group of sprites.
*
* Sprites can be created with a reference to a {@link Ext.draw.Surface}
*
* var drawContainer = Ext.create('Ext.draw.Container', {
* // ...
* });
*
* var sprite = Ext.create('Ext.draw.sprite.Sprite', {
* type: 'circle',
* fill: '#ff0',
* surface: drawContainer.getSurface('main'),
* radius: 5
* });
*
* Sprites can also be added to the surface as a configuration object:
*
* var sprite = drawContainer.getSurface('main').add({
* type: 'circle',
* fill: '#ff0',
* radius: 5
* });
*/
Ext.define('Ext.draw.sprite.Sprite', {
alias: 'sprite.sprite',
mixins: {
observable: 'Ext.mixin.Observable'
},
requires: [
'Ext.draw.Draw',
'Ext.draw.gradient.Gradient',
'Ext.draw.sprite.AttributeDefinition',
'Ext.draw.modifier.Target',
'Ext.draw.modifier.Animation',
'Ext.draw.modifier.Highlight'
],
isSprite: true,
inheritableStatics: {
def: {
processors: {
/**
* @cfg {String} [strokeStyle="none"] The color of the stroke (a CSS color value).
*/
strokeStyle: "color",
/**
* @cfg {String} [fillStyle="none"] The color of the shape (a CSS color value).
*/
fillStyle: "color",
/**
* @cfg {Number} [strokeOpacity=1] The opacity of the stroke. Limited from 0 to 1.
*/
strokeOpacity: "limited01",
/**
* @cfg {Number} [fillOpacity=1] The opacity of the fill. Limited from 0 to 1.
*/
fillOpacity: "limited01",
/**
* @cfg {Number} [lineWidth=1] The width of the line stroke.
*/
lineWidth: "number",
/**
* @cfg {String} [lineCap="butt"] The style of the line caps.
*/
lineCap: "enums(butt,round,square)",
/**
* @cfg {String} [lineJoin="miter"] The style of the line join.
*/
lineJoin: "enums(round,bevel,miter)",
/**
* @cfg {Array} [lineDash=[]]
* An even number of non-negative numbers specifying a dash/space sequence.
* Note that while this is supported in IE8 (VML engine), the behavior is
* different from Canvas and SVG. Please refer to this document for details:
* http://msdn.microsoft.com/en-us/library/bb264085(v=vs.85).aspx
* Although IE9 and IE10 have Canvas support, the 'lineDash'
* attribute is not supported in those browsers.
*/
lineDash: "data",
/**
* @cfg {Number} [lineDashOffset=0]
* A number specifying how far into the line dash sequence drawing commences.
*/
lineDashOffset: "number",
/**
* @cfg {Number} [miterLimit=10]
* Sets the distance between the inner corner and the outer corner where two lines meet.
*/
miterLimit: "number",
/**
* @cfg {String} [shadowColor="none"] The color of the shadow (a CSS color value).
*/
shadowColor: "color",
/**
* @cfg {Number} [shadowOffsetX=0] The offset of the sprite's shadow on the x-axis.
*/
shadowOffsetX: "number",
/**
* @cfg {Number} [shadowOffsetY=0] The offset of the sprite's shadow on the y-axis.
*/
shadowOffsetY: "number",
/**
* @cfg {Number} [shadowBlur=0] The amount blur used on the shadow.
*/
shadowBlur: "number",
/**
* @cfg {Number} [globalAlpha=1] The opacity of the sprite. Limited from 0 to 1.
*/
globalAlpha: "limited01",
/**
* @cfg {String} [globalCompositeOperation=source-over]
* Indicates how source images are drawn onto a destination image.
* globalCompositeOperation attribute is not supported by the SVG and VML (excanvas) engines.
*/
globalCompositeOperation: "enums(source-over,destination-over,source-in,destination-in,source-out,destination-out,source-atop,destination-atop,lighter,xor,copy)",
/**
* @cfg {Boolean} [hidden=false] Determines whether or not the sprite is hidden.
*/
hidden: "bool",
/**
* @cfg {Boolean} [transformFillStroke=false]
* Determines whether the fill and stroke are affected by sprite transformations.
*/
transformFillStroke: "bool",
/**
* @cfg {Number} [zIndex=0]
* The stacking order of the sprite.
*/
zIndex: "number",
/**
* @cfg {Number} [translationX=0]
* The translation of the sprite on the x-axis.
*/
translationX: "number",
/**
* @cfg {Number} [translationY=0]
* The translation of the sprite on the y-axis.
*/
translationY: "number",
/**
* @cfg {Number} [rotationRads=0]
* The angle of rotation of the sprite in radians.
*/
rotationRads: "number",
/**
* @cfg {Number} [rotationCenterX=null]
* The central coordinate of the sprite's scale operation on the x-axis.
*/
rotationCenterX: "number",
/**
* @cfg {Number} [rotationCenterY=null]
* The central coordinate of the sprite's rotate operation on the y-axis.
*/
rotationCenterY: "number",
/**
* @cfg {Number} [scalingX=1] The scaling of the sprite on the x-axis.
*/
scalingX: "number",
/**
* @cfg {Number} [scalingY=1] The scaling of the sprite on the y-axis.
*/
scalingY: "number",
/**
* @cfg {Number} [scalingCenterX=null]
* The central coordinate of the sprite's scale operation on the x-axis.
*/
scalingCenterX: "number",
/**
* @cfg {Number} [scalingCenterY=null]
* The central coordinate of the sprite's scale operation on the y-axis.
*/
scalingCenterY: "number",
constrainGradients: "bool"
},
aliases: {
"stroke": "strokeStyle",
"fill": "fillStyle",
"color": "fillStyle",
"stroke-width": "lineWidth",
"stroke-linecap": "lineCap",
"stroke-linejoin": "lineJoin",
"stroke-miterlimit": "miterLimit",
"text-anchor": "textAlign",
"opacity": "globalAlpha",
translateX: "translationX",
translateY: "translationY",
rotateRads: "rotationRads",
rotateCenterX: "rotationCenterX",
rotateCenterY: "rotationCenterY",
scaleX: "scalingX",
scaleY: "scalingY",
scaleCenterX: "scalingCenterX",
scaleCenterY: "scalingCenterY"
},
defaults: {
hidden: false,
zIndex: 0,
strokeStyle: "none",
fillStyle: "none",
lineWidth: 1,
lineDash: [],
lineDashOffset: 0,
lineCap: "butt",
lineJoin: "miter",
miterLimit: 10,
shadowColor: "none",
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowBlur: 0,
globalAlpha: 1,
strokeOpacity: 1,
fillOpacity: 1,
transformFillStroke: false,
translationX: 0,
translationY: 0,
rotationRads: 0,
rotationCenterX: null,
rotationCenterY: null,
scalingX: 1,
scalingY: 1,
scalingCenterX: null,
scalingCenterY: null,
constrainGradients: false
},
triggers: {
hidden: "canvas",
zIndex: "zIndex",
globalAlpha: "canvas",
globalCompositeOperation: "canvas",
transformFillStroke: "canvas",
strokeStyle: "canvas",
fillStyle: "canvas",
strokeOpacity: "canvas",
fillOpacity: "canvas",
lineWidth: "canvas",
lineCap: "canvas",
lineJoin: "canvas",
lineDash: "canvas",
lineDashOffset: "canvas",
miterLimit: "canvas",
shadowColor: "canvas",
shadowOffsetX: "canvas",
shadowOffsetY: "canvas",
shadowBlur: "canvas",
translationX: "transform",
translationY: "transform",
rotationRads: "transform",
rotationCenterX: "transform",
rotationCenterY: "transform",
scalingX: "transform",
scalingY: "transform",
scalingCenterX: "transform",
scalingCenterY: "transform",
constrainGradients: "canvas"
},
updaters: {
bbox: function(attr) {
var hasRotation = attr.rotationRads !== 0,
hasScaling = attr.scalingX !== 1 || attr.scalingY !== 1,
noRotationCenter = attr.rotationCenterX === null || attr.rotationCenterY === null,
noScalingCenter = attr.scalingCenterX === null || attr.scalingCenterY === null;
attr.bbox.plain.dirty = true;
attr.bbox.transform.dirty = true;
if (hasRotation && noRotationCenter || hasScaling && noScalingCenter) {
this.scheduleUpdaters(attr, {
transform: []
});
}
},
zIndex: function(attr) {
attr.dirtyZIndex = true;
},
transform: function(attr) {
attr.dirtyTransform = true;
attr.bbox.transform.dirty = true;
}
}
}
},
/**
* @property {Object} attr
* The visual attributes of the sprite, e.g. strokeStyle, fillStyle, lineWidth...
*/
attr: {},
config: {
parent: null,
/**
* @cfg {Ext.draw.Surface} surface
* The surface that this sprite is rendered into.
*/
surface: null
},
onClassExtended: function(subClass, data) {
// The `def` here is no longer a config, but an instance
// of the AttributeDefinition class created with that config,
// which can now be retrieved from `initialConfig`.
var initCfg = subClass.superclass.self.def.initialConfig,
cfg;
// If sprite defines attributes of its own, merge that with those of its parent.
if (data.inheritableStatics && data.inheritableStatics.def) {
cfg = Ext.merge({}, initCfg, data.inheritableStatics.def);
subClass.def = Ext.create('Ext.draw.sprite.AttributeDefinition', cfg);
delete data.inheritableStatics.def;
} else {
subClass.def = Ext.create('Ext.draw.sprite.AttributeDefinition', initCfg);
}
},
constructor: function(config) {
if (Ext.getClassName(this) === 'Ext.draw.sprite.Sprite') {
throw 'Ext.draw.sprite.Sprite is an abstract class';
}
var me = this;
config = Ext.isObject(config) ? config : {};
me.id = config.id || Ext.id(null, 'ext-sprite-');
me.attr = {};
me.mixins.observable.constructor.apply(me, arguments);
var modifiers = Ext.Array.from(config.modifiers, true);
me.prepareModifiers(modifiers);
me.initializeAttributes();
me.setAttributes(me.self.def.getDefaults(), true);
var processors = me.self.def.getProcessors();
for (var name in config) {
if (name in processors && me['get' + name.charAt(0).toUpperCase() + name.substr(1)]) {
Ext.Error.raise('The ' + me.$className + ' sprite has both a config and an attribute with the same name: ' + name + '.');
}
}
me.setAttributes(config);
},
getDirty: function() {
return this.attr.dirty;
},
setDirty: function(dirty) {
if ((this.attr.dirty = dirty)) {
if (this._parent) {
this._parent.setDirty(true);
}
}
},
addModifier: function(modifier, reinitializeAttributes) {
var me = this;
if (!(modifier instanceof Ext.draw.modifier.Modifier)) {
modifier = Ext.factory(modifier, null, null, 'modifier');
}
modifier.setSprite(this);
if (modifier.preFx || modifier.config && modifier.config.preFx) {
if (me.fx.getPrevious()) {
me.fx.getPrevious().setNext(modifier);
}
modifier.setNext(me.fx);
} else {
me.topModifier.getPrevious().setNext(modifier);
modifier.setNext(me.topModifier);
}
if (reinitializeAttributes) {
me.initializeAttributes();
}
return modifier;
},
prepareModifiers: function(additionalModifiers) {
// Set defaults
var me = this,
i, ln;
me.topModifier = new Ext.draw.modifier.Target({
sprite: me
});
// Link modifiers
me.fx = new Ext.draw.modifier.Animation({
sprite: me
});
me.fx.setNext(me.topModifier);
for (i = 0 , ln = additionalModifiers.length; i < ln; i++) {
me.addModifier(additionalModifiers[i], false);
}
},
initializeAttributes: function() {
var me = this;
me.topModifier.prepareAttributes(me.attr);
},
/**
* @private
* Calls updaters triggered by changes to sprite attributes.
* @param attr The attributes of a sprite or its instance.
*/
callUpdaters: function(attr) {
var me = this,
pendingUpdaters = attr.pendingUpdaters,
updaters = me.self.def.getUpdaters(),
any = false,
dirty = false,
flags, updater;
// If updaters set sprite attributes that trigger other updaters,
// those updaters are not called right away, but wait until all current
// updaters are called (till the next do/while loop iteration).
me.callUpdaters = Ext.emptyFn;
// Hide class method from the instance.
do {
any = false;
for (updater in pendingUpdaters) {
any = true;
flags = pendingUpdaters[updater];
delete pendingUpdaters[updater];
if (updaters[updater]) {
updaters[updater].call(me, attr, flags);
}
}
dirty = dirty || any;
} while (any);
delete me.callUpdaters;
// Restore class method.
if (dirty) {
me.setDirty(true);
}
},
/**
* @private
* Schedules specified updaters to be called.
* Updaters are called implicitly as a result of a change to sprite attributes.
* But sometimes it may be required to call an updater without setting an attribute,
* and without messing up the updater call order (by calling the updater immediately).
* For example:
*
* updaters: {
* onDataX: function (attr) {
* this.processDataX();
* // Process data Y every time data X is processed.
* // Call the onDataY updater as if changes to dataY attribute itself
* // triggered the update.
* this.scheduleUpdaters(attr, {onDataY: ['dataY']});
* // Alternatively:
* // this.scheduleUpdaters(attr, ['onDataY'], ['dataY']);
* }
* }
*
* @param {Object} attr The attributes object (not necesseraly of a sprite, but of its instance).
* @param {Object/String[]} updaters A map of updaters to be called to attributes that triggered the update.
* @param {String[]} [triggers] Attributes that triggered the update. An optional parameter.
* If used, the `updaters` parameter will be treated an array of updaters to be called.
*/
scheduleUpdaters: function(attr, updaters, triggers) {
var pendingUpdaters = attr.pendingUpdaters,
updater;
function schedule() {
if (updater in pendingUpdaters) {
if (triggers.length) {
pendingUpdaters[updater] = Ext.Array.merge(pendingUpdaters[updater], triggers);
}
} else {
pendingUpdaters[updater] = triggers;
}
}
if (triggers) {
for (var i = 0,
ln = updaters.length; i < ln; i++) {
updater = updaters[i];
schedule();
}
} else {
for (updater in updaters) {
triggers = updaters[updater];
schedule();
}
}
},
/**
* Set attributes of the sprite.
*
* @param {Object} changes The content of the change.
* @param {Boolean} [bypassNormalization] `true` to avoid normalization of the given changes.
* @param {Boolean} [avoidCopy] `true` to avoid copying the `changes` object.
* The content of object may be destroyed.
*/
setAttributes: function(changes, bypassNormalization, avoidCopy) {
//if (changes && 'fillStyle' in changes) {
// console.groupCollapsed('set fillStyle', this.getId(), this.attr.part);
// console.trace();
// console.groupEnd();
//}
var attr = this.attr;
if (bypassNormalization) {
if (avoidCopy) {
this.topModifier.pushDown(attr, changes);
} else {
this.topModifier.pushDown(attr, Ext.apply({}, changes));
}
} else {
this.topModifier.pushDown(attr, this.self.def.normalize(changes));
}
},
/**
* Set attributes of the sprite, assuming the names and values have already been
* normalized.
*
* @deprecated Use setAttributes directy with bypassNormalization argument being `true`.
* @param {Object} changes The content of the change.
* @param {Boolean} [avoidCopy] `true` to avoid copying the `changes` object.
* The content of object may be destroyed.
*/
setAttributesBypassingNormalization: function(changes, avoidCopy) {
return this.setAttributes(changes, true, avoidCopy);
},
/**
* Returns the bounding box for the given Sprite as calculated with the Canvas engine.
*
* @param {Boolean} [isWithoutTransform] Whether to calculate the bounding box with the current transforms or not.
*/
getBBox: function(isWithoutTransform) {
var me = this,
attr = me.attr,
bbox = attr.bbox,
plain = bbox.plain,
transform = bbox.transform;
if (plain.dirty) {
me.updatePlainBBox(plain);
plain.dirty = false;
}
if (isWithoutTransform) {
return plain;
} else {
me.applyTransformations();
if (transform.dirty) {
me.updateTransformedBBox(transform, plain);
transform.dirty = false;
}
return transform;
}
},
/**
* @protected
* Subclass will fill the plain object with `x`, `y`, `width`, `height` information of the plain bounding box of
* this sprite.
*
* @param {Object} plain Target object.
*/
updatePlainBBox: Ext.emptyFn,
/**
* @protected
* Subclass will fill the plain object with `x`, `y`, `width`, `height` information of the transformed
* bounding box of this sprite.
*
* @param {Object} transform Target object.
* @param {Object} plain Auxiliary object providing information of plain object.
*/
updateTransformedBBox: function(transform, plain) {
this.attr.matrix.transformBBox(plain, 0, transform);
},
/**
* Subclass can rewrite this function to gain better performance.
* @param {Boolean} isWithoutTransform
* @return {Array}
*/
getBBoxCenter: function(isWithoutTransform) {
var bbox = this.getBBox(isWithoutTransform);
if (bbox) {
return [
bbox.x + bbox.width * 0.5,
bbox.y + bbox.height * 0.5
];
} else {
return [
0,
0
];
}
},
/**
* Hide the sprite.
* @return {Ext.draw.sprite.Sprite} this
* @chainable
*/
hide: function() {
this.attr.hidden = true;
this.setDirty(true);
return this;
},
/**
* Show the sprite.
* @return {Ext.draw.sprite.Sprite} this
* @chainable
*/
show: function() {
this.attr.hidden = false;
this.setDirty(true);
return this;
},
/**
* Applies sprite's attributes to the given context.
* @param {Object} ctx Context to apply sprite's attributes to.
* @param {Array} rect The rect of the context to be affected by gradients.
*/
useAttributes: function(ctx, rect) {
this.applyTransformations();
var attr = this.attr,
canvasAttributes = attr.canvasAttributes,
strokeStyle = canvasAttributes.strokeStyle,
fillStyle = canvasAttributes.fillStyle,
lineDash = canvasAttributes.lineDash,
lineDashOffset = canvasAttributes.lineDashOffset,
id;
if (strokeStyle) {
if (strokeStyle.isGradient) {
ctx.strokeStyle = 'black';
ctx.strokeGradient = strokeStyle;
} else {
ctx.strokeGradient = false;
}
}
if (fillStyle) {
if (fillStyle.isGradient) {
ctx.fillStyle = 'black';
ctx.fillGradient = fillStyle;
} else {
ctx.fillGradient = false;
}
}
if (lineDash) {
ctx.setLineDash(lineDash);
}
// Only set lineDashOffset to contexts that support the property (excludes VML).
if (Ext.isNumber(lineDashOffset + ctx.lineDashOffset)) {
ctx.lineDashOffset = lineDashOffset;
}
for (id in canvasAttributes) {
if (canvasAttributes[id] !== undefined && canvasAttributes[id] !== ctx[id]) {
ctx[id] = canvasAttributes[id];
}
}
this.setGradientBBox(ctx, rect);
},
setGradientBBox: function(ctx, rect) {
var attr = this.attr;
if (attr.constrainGradients) {
ctx.setGradientBBox({
x: rect[0],
y: rect[1],
width: rect[2],
height: rect[3]
});
} else {
ctx.setGradientBBox(this.getBBox(attr.transformFillStroke));
}
},
/**
* @private
*
* Calculates forward and inverse transform matrices.
* @param {Boolean} force Forces recalculation of transform matrices even when sprite's transform attributes supposedly haven't changed.
*/
applyTransformations: function(force) {
if (!force && !this.attr.dirtyTransform) {
return;
}
var me = this,
attr = me.attr,
center = me.getBBoxCenter(true),
centerX = center[0],
centerY = center[1],
x = attr.translationX,
y = attr.translationY,
sx = attr.scalingX,
sy = attr.scalingY === null ? attr.scalingX : attr.scalingY,
scx = attr.scalingCenterX === null ? centerX : attr.scalingCenterX,
scy = attr.scalingCenterY === null ? centerY : attr.scalingCenterY,
rad = attr.rotationRads,
rcx = attr.rotationCenterX === null ? centerX : attr.rotationCenterX,
rcy = attr.rotationCenterY === null ? centerY : attr.rotationCenterY,
cos = Math.cos(rad),
sin = Math.sin(rad);
if (sx === 1 && sy === 1) {
scx = 0;
scy = 0;
}
if (rad === 0) {
rcx = 0;
rcy = 0;
}
attr.matrix.elements = [
cos * sx,
sin * sy,
-sin * sx,
cos * sy,
scx + (rcx - cos * rcx - scx + rcy * sin) * sx + x,
scy + (rcy - cos * rcy - scy + rcx * -sin) * sy + y
];
attr.matrix.inverse(attr.inverseMatrix);
attr.dirtyTransform = false;
attr.bbox.transform.dirty = true;
},
/**
* Called before rendering.
*/
preRender: Ext.emptyFn,
/**
* Render method.
* @param {Ext.draw.Surface} surface The surface.
* @param {Object} ctx A context object compatible with CanvasRenderingContext2D.
* @param {Array} rect The clip rect (or called dirty rect) of the current rendering. Not to be confused
* with `surface.getRect()`.
*
* @return {*} returns `false` to stop rendering in this frame.
* All the sprites that haven't been rendered will have their dirty flag untouched.
*/
render: Ext.emptyFn,
/**
* Performs a hit test on the sprite.
* @param {Array} point A two-item array containing x and y coordinates of the point.
* @param {Object} options Hit testing options.
* @return {Object} A hit result object that contains more information about what
* exactly was hit or null if nothing was hit.
*/
hitTest: function(point, options) {
var x = point[0],
y = point[1],
bbox = this.getBBox();
if (bbox && x >= bbox.left && x <= bbox.right && y >= bbox.top && y <= bbox.bottom) {
return {
sprite: this
};
}
return null;
},
repaint: function() {
var surface = this.getSurface();
if (surface) {
surface.renderFrame();
}
},
/**
* Removes the sprite and clears all listeners.
*/
destroy: function() {
var me = this,
modifier = me.topModifier,
curr;
while (modifier) {
curr = modifier;
modifier = modifier.getPrevious();
curr.destroy();
}
delete me.attr;
me.destroy = Ext.emptyFn;
if (me.fireEvent('beforedestroy', me) !== false) {
me.fireEvent('destroy', me);
}
this.callParent();
}
}, function() {
// onClassCreated
// Create one AttributeDefinition instance per sprite class when a class is created
// and replace the `def` config with the instance that was created using that config.
// Here we only create an AttributeDefinition instance for the base Sprite class,
// attribute definitions for subclasses are created inside onClassExtended method.
this.def = Ext.create('Ext.draw.sprite.AttributeDefinition', this.def);
});
/**
* Class representing a path.
* Designed to be compatible with [CanvasPathMethods](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#canvaspathmethods)
* and will hopefully be replaced by the browsers' implementation of the Path object.
*/
Ext.define('Ext.draw.Path', {
requires: [
'Ext.draw.Draw'
],
statics: {
pathRe: /,?([achlmqrstvxz]),?/gi,
pathRe2: /-/gi,
pathSplitRe: /\s|,/g
},
svgString: '',
/**
* Create a path from pathString.
* @constructor
* @param {String} pathString
*/
constructor: function(pathString) {
var me = this;
me.commands = [];
// Stores command letters from the SVG path data ('d' attribute).
me.params = [];
// Stores command parameters from the SVG path data.
// All command parameters are actually point coordinates as the only commands used
// are the M, L, C, Z. This makes path transformations and hit testing easier.
// Arcs are approximated using cubic Bezier curves, H and S commands are translated
// to L commands and relative commands are translated to their absolute versions.
me.cursor = null;
me.startX = 0;
me.startY = 0;
if (pathString) {
me.fromSvgString(pathString);
}
},
/**
* Clear the path.
*/
clear: function() {
var me = this;
me.params.length = 0;
me.commands.length = 0;
me.cursor = null;
me.startX = 0;
me.startY = 0;
me.dirt();
},
/**
* @private
*/
dirt: function() {
this.svgString = '';
},
/**
* Move to a position.
* @param {Number} x
* @param {Number} y
*/
moveTo: function(x, y) {
var me = this;
if (!me.cursor) {
me.cursor = [
x,
y
];
}
me.params.push(x, y);
me.commands.push('M');
me.startX = x;
me.startY = y;
me.cursor[0] = x;
me.cursor[1] = y;
me.dirt();
},
/**
* A straight line to a position.
* @param {Number} x
* @param {Number} y
*/
lineTo: function(x, y) {
var me = this;
if (!me.cursor) {
me.cursor = [
x,
y
];
me.params.push(x, y);
me.commands.push('M');
} else {
me.params.push(x, y);
me.commands.push('L');
}
me.cursor[0] = x;
me.cursor[1] = y;
me.dirt();
},
/**
* A cubic bezier curve to a position.
* @param {Number} cx1
* @param {Number} cy1
* @param {Number} cx2
* @param {Number} cy2
* @param {Number} x
* @param {Number} y
*/
bezierCurveTo: function(cx1, cy1, cx2, cy2, x, y) {
var me = this;
if (!me.cursor) {
me.moveTo(cx1, cy1);
}
me.params.push(cx1, cy1, cx2, cy2, x, y);
me.commands.push('C');
me.cursor[0] = x;
me.cursor[1] = y;
me.dirt();
},
/**
* A quadratic bezier curve to a position.
* @param {Number} cx
* @param {Number} cy
* @param {Number} x
* @param {Number} y
*/
quadraticCurveTo: function(cx, cy, x, y) {
var me = this;
if (!me.cursor) {
me.moveTo(cx, cy);
}
me.bezierCurveTo((2 * cx + me.cursor[0]) / 3, (2 * cy + me.cursor[1]) / 3, (2 * cx + x) / 3, (2 * cy + y) / 3, x, y);
},
/**
* Close this path with a straight line.
*/
closePath: function() {
var me = this;
if (me.cursor) {
me.commands.push('Z');
me.dirt();
}
},
/**
* Create a elliptic arc curve compatible with SVG's arc to instruction.
*
* The curve start from (`x1`, `y1`) and ends at (`x2`, `y2`). The ellipse
* has radius `rx` and `ry` and a rotation of `rotation`.
* @param {Number} x1
* @param {Number} y1
* @param {Number} x2
* @param {Number} y2
* @param {Number} [rx]
* @param {Number} [ry]
* @param {Number} [rotation]
*/
arcTo: function(x1, y1, x2, y2, rx, ry, rotation) {
var me = this;
if (ry === undefined) {
ry = rx;
}
if (rotation === undefined) {
rotation = 0;
}
if (!me.cursor) {
me.moveTo(x1, y1);
return;
}
if (rx === 0 || ry === 0) {
me.lineTo(x1, y1);
return;
}
x2 -= x1;
y2 -= y1;
var x0 = me.cursor[0] - x1,
y0 = me.cursor[1] - y1,
area = x2 * y0 - y2 * x0,
cos, sin, xx, yx, xy, yy,
l0 = Math.sqrt(x0 * x0 + y0 * y0),
l2 = Math.sqrt(x2 * x2 + y2 * y2),
dist, cx, cy;
// cos rx, -sin ry , x1 - cos rx x1 + ry sin y1
// sin rx, cos ry, -rx sin x1 + y1 - cos ry y1
if (area === 0) {
me.lineTo(x1, y1);
return;
}
if (ry !== rx) {
cos = Math.cos(rotation);
sin = Math.sin(rotation);
xx = cos / rx;
yx = sin / ry;
xy = -sin / rx;
yy = cos / ry;
var temp = xx * x0 + yx * y0;
y0 = xy * x0 + yy * y0;
x0 = temp;
temp = xx * x2 + yx * y2;
y2 = xy * x2 + yy * y2;
x2 = temp;
} else {
x0 /= rx;
y0 /= ry;
x2 /= rx;
y2 /= ry;
}
cx = x0 * l2 + x2 * l0;
cy = y0 * l2 + y2 * l0;
dist = 1 / (Math.sin(Math.asin(Math.abs(area) / (l0 * l2)) * 0.5) * Math.sqrt(cx * cx + cy * cy));
cx *= dist;
cy *= dist;
var k0 = (cx * x0 + cy * y0) / (x0 * x0 + y0 * y0),
k2 = (cx * x2 + cy * y2) / (x2 * x2 + y2 * y2);
var cosStart = x0 * k0 - cx,
sinStart = y0 * k0 - cy,
cosEnd = x2 * k2 - cx,
sinEnd = y2 * k2 - cy,
startAngle = Math.atan2(sinStart, cosStart),
endAngle = Math.atan2(sinEnd, cosEnd);
if (area > 0) {
if (endAngle < startAngle) {
endAngle += Math.PI * 2;
}
} else {
if (startAngle < endAngle) {
startAngle += Math.PI * 2;
}
}
if (ry !== rx) {
cx = cos * cx * rx - sin * cy * ry + x1;
cy = sin * cy * ry + cos * cy * ry + y1;
me.lineTo(cos * rx * cosStart - sin * ry * sinStart + cx, sin * rx * cosStart + cos * ry * sinStart + cy);
me.ellipse(cx, cy, rx, ry, rotation, startAngle, endAngle, area < 0);
} else {
cx = cx * rx + x1;
cy = cy * ry + y1;
me.lineTo(rx * cosStart + cx, ry * sinStart + cy);
me.ellipse(cx, cy, rx, ry, rotation, startAngle, endAngle, area < 0);
}
},
/**
* Create an elliptic arc.
*
* See [the whatwg reference of ellipse](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-ellipse).
*
* @param {Number} cx
* @param {Number} cy
* @param {Number} radiusX
* @param {Number} radiusY
* @param {Number} rotation
* @param {Number} startAngle
* @param {Number} endAngle
* @param {Number} anticlockwise
*/
ellipse: function(cx, cy, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise) {
var me = this,
params = me.params,
start = params.length,
count, i, j;
if (endAngle - startAngle >= Math.PI * 2) {
me.ellipse(cx, cy, radiusX, radiusY, rotation, startAngle, startAngle + Math.PI, anticlockwise);
me.ellipse(cx, cy, radiusX, radiusY, rotation, startAngle + Math.PI, endAngle, anticlockwise);
return;
}
if (!anticlockwise) {
if (endAngle < startAngle) {
endAngle += Math.PI * 2;
}
count = me.approximateArc(params, cx, cy, radiusX, radiusY, rotation, startAngle, endAngle);
} else {
if (startAngle < endAngle) {
startAngle += Math.PI * 2;
}
count = me.approximateArc(params, cx, cy, radiusX, radiusY, rotation, endAngle, startAngle);
for (i = start , j = params.length - 2; i < j; i += 2 , j -= 2) {
var temp = params[i];
params[i] = params[j];
params[j] = temp;
temp = params[i + 1];
params[i + 1] = params[j + 1];
params[j + 1] = temp;
}
}
if (!me.cursor) {
me.cursor = [
params[params.length - 2],
params[params.length - 1]
];
me.commands.push('M');
} else {
me.cursor[0] = params[params.length - 2];
me.cursor[1] = params[params.length - 1];
me.commands.push('L');
}
for (i = 2; i < count; i += 6) {
me.commands.push('C');
}
me.dirt();
},
/**
* Create an circular arc.
*
* @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) {
this.ellipse(x, y, radius, radius, 0, startAngle, endAngle, anticlockwise);
},
/**
* Draw a rectangle and close it.
*
* @param {Number} x
* @param {Number} y
* @param {Number} width
* @param {Number} height
*/
rect: function(x, y, width, height) {
if (width == 0 || height == 0) {
return;
}
var me = this;
me.moveTo(x, y);
me.lineTo(x + width, y);
me.lineTo(x + width, y + height);
me.lineTo(x, y + height);
me.closePath();
},
/**
* @private
* @param {Array} result
* @param {Number} cx
* @param {Number} cy
* @param {Number} rx
* @param {Number} ry
* @param {Number} phi
* @param {Number} theta1
* @param {Number} theta2
* @return {Number}
*/
approximateArc: function(result, cx, cy, rx, ry, phi, theta1, theta2) {
var cosPhi = Math.cos(phi),
sinPhi = Math.sin(phi),
cosTheta1 = Math.cos(theta1),
sinTheta1 = Math.sin(theta1),
xx = cosPhi * cosTheta1 * rx - sinPhi * sinTheta1 * ry,
yx = -cosPhi * sinTheta1 * rx - sinPhi * cosTheta1 * ry,
xy = sinPhi * cosTheta1 * rx + cosPhi * sinTheta1 * ry,
yy = -sinPhi * sinTheta1 * rx + cosPhi * cosTheta1 * ry,
rightAngle = Math.PI / 2,
count = 2,
exx = xx,
eyx = yx,
exy = xy,
eyy = yy,
rho = 0.547443256150549,
temp, y1, x3, y3, x2, y2;
theta2 -= theta1;
if (theta2 < 0) {
theta2 += Math.PI * 2;
}
result.push(xx + cx, xy + cy);
while (theta2 >= rightAngle) {
result.push(exx + eyx * rho + cx, exy + eyy * rho + cy, exx * rho + eyx + cx, exy * rho + eyy + cy, eyx + cx, eyy + cy);
count += 6;
theta2 -= rightAngle;
temp = exx;
exx = eyx;
eyx = -temp;
temp = exy;
exy = eyy;
eyy = -temp;
}
if (theta2) {
y1 = (0.3294738052815987 + 0.012120855841304373 * theta2) * theta2;
x3 = Math.cos(theta2);
y3 = Math.sin(theta2);
x2 = x3 + y1 * y3;
y2 = y3 - y1 * x3;
result.push(exx + eyx * y1 + cx, exy + eyy * y1 + cy, exx * x2 + eyx * y2 + cx, exy * x2 + eyy * y2 + cy, exx * x3 + eyx * y3 + cx, exy * x3 + eyy * y3 + cy);
count += 6;
}
return count;
},
/**
* [http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes](http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes)
* @param {Number} rx
* @param {Number} ry
* @param {Number} rotation Differ from svg spec, this is radian.
* @param {Number} fA
* @param {Number} fS
* @param {Number} x2
* @param {Number} y2
*/
arcSvg: function(rx, ry, rotation, fA, fS, x2, y2) {
if (rx < 0) {
rx = -rx;
}
if (ry < 0) {
ry = -ry;
}
var me = this,
x1 = me.cursor[0],
y1 = me.cursor[1],
hdx = (x1 - x2) / 2,
hdy = (y1 - y2) / 2,
cosPhi = Math.cos(rotation),
sinPhi = Math.sin(rotation),
xp = hdx * cosPhi + hdy * sinPhi,
yp = -hdx * sinPhi + hdy * cosPhi,
ratX = xp / rx,
ratY = yp / ry,
lambda = ratX * ratX + ratY * ratY,
cx = (x1 + x2) * 0.5,
cy = (y1 + y2) * 0.5,
cpx = 0,
cpy = 0;
if (lambda >= 1) {
lambda = Math.sqrt(lambda);
rx *= lambda;
ry *= lambda;
} else // me gives lambda == cpx == cpy == 0;
{
lambda = Math.sqrt(1 / lambda - 1);
if (fA === fS) {
lambda = -lambda;
}
cpx = lambda * rx * ratY;
cpy = -lambda * ry * ratX;
cx += cosPhi * cpx - sinPhi * cpy;
cy += sinPhi * cpx + cosPhi * cpy;
}
var theta1 = Math.atan2((yp - cpy) / ry, (xp - cpx) / rx),
deltaTheta = Math.atan2((-yp - cpy) / ry, (-xp - cpx) / rx) - theta1;
if (fS) {
if (deltaTheta <= 0) {
deltaTheta += Math.PI * 2;
}
} else {
if (deltaTheta >= 0) {
deltaTheta -= Math.PI * 2;
}
}
me.ellipse(cx, cy, rx, ry, rotation, theta1, theta1 + deltaTheta, 1 - fS);
},
/**
* Feed the path from svg path string.
* @param {String} pathString
*/
fromSvgString: function(pathString) {
if (!pathString) {
return;
}
var me = this,
parts,
paramCounts = {
a: 7,
c: 6,
h: 1,
l: 2,
m: 2,
q: 4,
s: 4,
t: 2,
v: 1,
z: 0,
A: 7,
C: 6,
H: 1,
L: 2,
M: 2,
Q: 4,
S: 4,
T: 2,
V: 1,
Z: 0
},
lastCommand = '',
lastControlX, lastControlY,
lastX = 0,
lastY = 0,
part = false,
i, partLength, relative;
// Split the string to items.
if (Ext.isString(pathString)) {
parts = pathString.replace(Ext.draw.Path.pathRe, " $1 ").replace(Ext.draw.Path.pathRe2, " -").split(Ext.draw.Path.pathSplitRe);
} else if (Ext.isArray(pathString)) {
parts = pathString.join(',').split(Ext.draw.Path.pathSplitRe);
}
// Remove empty entries
for (i = 0 , partLength = 0; i < parts.length; i++) {
if (parts[i] !== '') {
parts[partLength++] = parts[i];
}
}
parts.length = partLength;
me.clear();
for (i = 0; i < parts.length; ) {
lastCommand = part;
part = parts[i];
relative = (part.toUpperCase() !== part);
i++;
switch (part) {
case 'M':
me.moveTo(lastX = +parts[i], lastY = +parts[i + 1]);
i += 2;
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
i += 2;
};
break;
case 'L':
me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
i += 2;
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX = +parts[i], lastY = +parts[i + 1]);
i += 2;
};
break;
case 'A':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.arcSvg(+parts[i], +parts[i + 1], +parts[i + 2] * Math.PI / 180, +parts[i + 3], +parts[i + 4], lastX = +parts[i + 5], lastY = +parts[i + 6]);
i += 7;
};
break;
case 'C':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.bezierCurveTo(+parts[i], +parts[i + 1], lastControlX = +parts[i + 2], lastControlY = +parts[i + 3], lastX = +parts[i + 4], lastY = +parts[i + 5]);
i += 6;
};
break;
case 'Z':
me.closePath();
break;
case 'm':
me.moveTo(lastX += +parts[i], lastY += +parts[i + 1]);
i += 2;
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
i += 2;
};
break;
case 'l':
me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
i += 2;
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX += +parts[i], lastY += +parts[i + 1]);
i += 2;
};
break;
case 'a':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.arcSvg(+parts[i], +parts[i + 1], +parts[i + 2] * Math.PI / 180, +parts[i + 3], +parts[i + 4], lastX += +parts[i + 5], lastY += +parts[i + 6]);
i += 7;
};
break;
case 'c':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.bezierCurveTo(lastX + (+parts[i]), lastY + (+parts[i + 1]), lastControlX = lastX + (+parts[i + 2]), lastControlY = lastY + (+parts[i + 3]), lastX += +parts[i + 4], lastY += +parts[i + 5]);
i += 6;
};
break;
case 'z':
me.closePath();
break;
case 's':
if (!(lastCommand === 'c' || lastCommand === 'C' || lastCommand === 's' || lastCommand === 'S')) {
lastControlX = lastX;
lastControlY = lastY;
};
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.bezierCurveTo(lastX + lastX - lastControlX, lastY + lastY - lastControlY, lastControlX = lastX + (+parts[i]), lastControlY = lastY + (+parts[i + 1]), lastX += +parts[i + 2], lastY += +parts[i + 3]);
i += 4;
};
break;
case 'S':
if (!(lastCommand === 'c' || lastCommand === 'C' || lastCommand === 's' || lastCommand === 'S')) {
lastControlX = lastX;
lastControlY = lastY;
};
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.bezierCurveTo(lastX + lastX - lastControlX, lastY + lastY - lastControlY, lastControlX = +parts[i], lastControlY = +parts[i + 1], lastX = (+parts[i + 2]), lastY = (+parts[i + 3]));
i += 4;
};
break;
case 'q':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.quadraticCurveTo(lastControlX = lastX + (+parts[i]), lastControlY = lastY + (+parts[i + 1]), lastX += +parts[i + 2], lastY += +parts[i + 3]);
i += 4;
};
break;
case 'Q':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.quadraticCurveTo(lastControlX = +parts[i], lastControlY = +parts[i + 1], lastX = +parts[i + 2], lastY = +parts[i + 3]);
i += 4;
};
break;
case 't':
if (!(lastCommand === 'q' || lastCommand === 'Q' || lastCommand === 't' || lastCommand === 'T')) {
lastControlX = lastX;
lastControlY = lastY;
};
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.quadraticCurveTo(lastControlX = lastX + lastX - lastControlX, lastControlY = lastY + lastY - lastControlY, lastX += +parts[i + 1], lastY += +parts[i + 2]);
i += 2;
};
break;
case 'T':
if (!(lastCommand === 'q' || lastCommand === 'Q' || lastCommand === 't' || lastCommand === 'T')) {
lastControlX = lastX;
lastControlY = lastY;
};
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.quadraticCurveTo(lastControlX = lastX + lastX - lastControlX, lastControlY = lastY + lastY - lastControlY, lastX = (+parts[i + 1]), lastY = (+parts[i + 2]));
i += 2;
};
break;
case 'h':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX += +parts[i], lastY);
i++;
};
break;
case 'H':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX = +parts[i], lastY);
i++;
};
break;
case 'v':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX, lastY += +parts[i]);
i++;
};
break;
case 'V':
while (i < partLength && !paramCounts.hasOwnProperty(parts[i])) {
me.lineTo(lastX, lastY = +parts[i]);
i++;
};
break;
}
}
},
/**
* Clone this path.
* @return {Ext.draw.Path}
*/
clone: function() {
var me = this,
path = new Ext.draw.Path();
path.params = me.params.slice(0);
path.commands = me.commands.slice(0);
path.cursor = me.cursor ? me.cursor.slice(0) : null;
path.startX = me.startX;
path.startY = me.startY;
path.svgString = me.svgString;
return path;
},
/**
* Transform the current path by a matrix.
* @param {Ext.draw.Matrix} matrix
*/
transform: function(matrix) {
if (matrix.isIdentity()) {
return;
}
var xx = matrix.getXX(),
yx = matrix.getYX(),
dx = matrix.getDX(),
xy = matrix.getXY(),
yy = matrix.getYY(),
dy = matrix.getDY(),
params = this.params,
i = 0,
ln = params.length,
x, y;
for (; i < ln; i += 2) {
x = params[i];
y = params[i + 1];
params[i] = x * xx + y * yx + dx;
params[i + 1] = x * xy + y * yy + dy;
}
this.dirt();
},
/**
* Get the bounding box of this matrix.
* @param {Object} [target] Optional object to receive the result.
*
* @return {Object} Object with x, y, width and height
*/
getDimension: function(target) {
if (!target) {
target = {};
}
if (!this.commands || !this.commands.length) {
target.x = 0;
target.y = 0;
target.width = 0;
target.height = 0;
return target;
}
target.left = Infinity;
target.top = Infinity;
target.right = -Infinity;
target.bottom = -Infinity;
var i = 0,
j = 0,
commands = this.commands,
params = this.params,
ln = commands.length,
x, y;
for (; i < ln; i++) {
switch (commands[i]) {
case 'M':
case 'L':
x = params[j];
y = params[j + 1];
target.left = Math.min(x, target.left);
target.top = Math.min(y, target.top);
target.right = Math.max(x, target.right);
target.bottom = Math.max(y, target.bottom);
j += 2;
break;
case 'C':
this.expandDimension(target, x, y, params[j], params[j + 1], params[j + 2], params[j + 3], x = params[j + 4], y = params[j + 5]);
j += 6;
break;
}
}
target.x = target.left;
target.y = target.top;
target.width = target.right - target.left;
target.height = target.bottom - target.top;
return target;
},
/**
* Get the bounding box as if the path is transformed by a matrix.
*
* @param {Ext.draw.Matrix} matrix
* @param {Object} [target] Optional object to receive the result.
*
* @return {Object} An object with x, y, width and height.
*/
getDimensionWithTransform: function(matrix, target) {
if (!this.commands || !this.commands.length) {
if (!target) {
target = {};
}
target.x = 0;
target.y = 0;
target.width = 0;
target.height = 0;
return target;
}
target.left = Infinity;
target.top = Infinity;
target.right = -Infinity;
target.bottom = -Infinity;
var xx = matrix.getXX(),
yx = matrix.getYX(),
dx = matrix.getDX(),
xy = matrix.getXY(),
yy = matrix.getYY(),
dy = matrix.getDY(),
i = 0,
j = 0,
commands = this.commands,
params = this.params,
ln = commands.length,
x, y;
for (; i < ln; i++) {
switch (commands[i]) {
case 'M':
case 'L':
x = params[j] * xx + params[j + 1] * yx + dx;
y = params[j] * xy + params[j + 1] * yy + dy;
target.left = Math.min(x, target.left);
target.top = Math.min(y, target.top);
target.right = Math.max(x, target.right);
target.bottom = Math.max(y, target.bottom);
j += 2;
break;
case 'C':
this.expandDimension(target, x, y, params[j] * xx + params[j + 1] * yx + dx, params[j] * xy + params[j + 1] * yy + dy, params[j + 2] * xx + params[j + 3] * yx + dx, params[j + 2] * xy + params[j + 3] * yy + dy, x = params[j + 4] * xx + params[j + 5] * yx + dx, y = params[j + 4] * xy + params[j + 5] * yy + dy);
j += 6;
break;
}
}
if (!target) {
target = {};
}
target.x = target.left;
target.y = target.top;
target.width = target.right - target.left;
target.height = target.bottom - target.top;
return target;
},
/**
* @private
* Expand the rect by the bbox of a bezier curve.
*
* @param {Object} target
* @param {Number} x1
* @param {Number} y1
* @param {Number} cx1
* @param {Number} cy1
* @param {Number} cx2
* @param {Number} cy2
* @param {Number} x2
* @param {Number} y2
*/
expandDimension: function(target, x1, y1, cx1, cy1, cx2, cy2, x2, y2) {
var me = this,
l = target.left,
r = target.right,
t = target.top,
b = target.bottom,
dim = me.dim || (me.dim = []);
me.curveDimension(x1, cx1, cx2, x2, dim);
l = Math.min(l, dim[0]);
r = Math.max(r, dim[1]);
me.curveDimension(y1, cy1, cy2, y2, dim);
t = Math.min(t, dim[0]);
b = Math.max(b, dim[1]);
target.left = l;
target.right = r;
target.top = t;
target.bottom = b;
},
/**
* @private
* Determine the curve
* @param {Number} a
* @param {Number} b
* @param {Number} c
* @param {Number} d
* @param {Number} dim
*/
curveDimension: function(a, b, c, d, dim) {
var qa = 3 * (-a + 3 * (b - c) + d),
qb = 6 * (a - 2 * b + c),
qc = -3 * (a - b),
x, y,
min = Math.min(a, d),
max = Math.max(a, d),
delta;
if (qa === 0) {
if (qb === 0) {
dim[0] = min;
dim[1] = max;
return;
} else {
x = -qc / qb;
if (0 < x && x < 1) {
y = this.interpolate(a, b, c, d, x);
min = Math.min(min, y);
max = Math.max(max, y);
}
}
} else {
delta = qb * qb - 4 * qa * qc;
if (delta >= 0) {
delta = Math.sqrt(delta);
x = (delta - qb) / 2 / qa;
if (0 < x && x < 1) {
y = this.interpolate(a, b, c, d, x);
min = Math.min(min, y);
max = Math.max(max, y);
}
if (delta > 0) {
x -= delta / qa;
if (0 < x && x < 1) {
y = this.interpolate(a, b, c, d, x);
min = Math.min(min, y);
max = Math.max(max, y);
}
}
}
}
dim[0] = min;
dim[1] = max;
},
/**
* @private
*
* Returns `a * (1 - t) ^ 3 + 3 * b (1 - t) ^ 2 * t + 3 * c (1 - t) * t ^ 3 + d * t ^ 3`.
*
* @param {Number} a
* @param {Number} b
* @param {Number} c
* @param {Number} d
* @param {Number} t
* @return {Number}
*/
interpolate: function(a, b, c, d, t) {
if (t === 0) {
return a;
}
if (t === 1) {
return d;
}
var rate = (1 - t) / t;
return t * t * t * (d + rate * (3 * c + rate * (3 * b + rate * a)));
},
/**
* Reconstruct path from cubic bezier curve stripes.
* @param {Array} stripes
*/
fromStripes: function(stripes) {
var me = this,
i = 0,
ln = stripes.length,
j, ln2, stripe;
me.clear();
for (; i < ln; i++) {
stripe = stripes[i];
me.params.push.apply(me.params, stripe);
me.commands.push('M');
for (j = 2 , ln2 = stripe.length; j < ln2; j += 6) {
me.commands.push('C');
}
}
if (!me.cursor) {
me.cursor = [];
}
me.cursor[0] = me.params[me.params.length - 2];
me.cursor[1] = me.params[me.params.length - 1];
me.dirt();
},
/**
* Convert path to bezier curve stripes.
* @param {Array} [target] The optional array to receive the result.
* @return {Array}
*/
toStripes: function(target) {
var stripes = target || [],
curr, x, y, lastX, lastY, startX, startY, i, j,
commands = this.commands,
params = this.params,
ln = commands.length;
for (i = 0 , j = 0; i < ln; i++) {
switch (commands[i]) {
case 'M':
curr = [
startX = lastX = params[j++],
startY = lastY = params[j++]
];
stripes.push(curr);
break;
case 'L':
x = params[j++];
y = params[j++];
curr.push((lastX + lastX + x) / 3, (lastY + lastY + y) / 3, (lastX + x + x) / 3, (lastY + y + y) / 3, lastX = x, lastY = y);
break;
case 'C':
curr.push(params[j++], params[j++], params[j++], params[j++], lastX = params[j++], lastY = params[j++]);
break;
case 'Z':
x = startX;
y = startY;
curr.push((lastX + lastX + x) / 3, (lastY + lastY + y) / 3, (lastX + x + x) / 3, (lastY + y + y) / 3, lastX = x, lastY = y);
break;
}
}
return stripes;
},
/**
* @private
* Update cache for svg string of this path.
*/
updateSvgString: function() {
var result = [],
commands = this.commands,
params = this.params,
ln = commands.length,
i = 0,
j = 0;
for (; i < ln; i++) {
switch (commands[i]) {
case 'M':
result.push('M' + params[j] + ',' + params[j + 1]);
j += 2;
break;
case 'L':
result.push('L' + params[j] + ',' + params[j + 1]);
j += 2;
break;
case 'C':
result.push('C' + params[j] + ',' + params[j + 1] + ' ' + params[j + 2] + ',' + params[j + 3] + ' ' + params[j + 4] + ',' + params[j + 5]);
j += 6;
break;
case 'Z':
result.push('Z');
break;
}
}
this.svgString = result.join('');
},
/**
* Return an svg path string for this path.
* @return {String}
*/
toString: function() {
if (!this.svgString) {
this.updateSvgString();
}
return this.svgString;
}
});
Ext.define('Ext.draw.overrides.Path', {
override: 'Ext.draw.Path',
// An arbitrary point outside the path used for hit testing with ray casting method.
rayOrigin: {
x: -10000,
y: -10000
},
/**
* Tests whether the given point is inside the path.
* @param {Number} x
* @param {Number} y
* @return {Boolean}
* @member Ext.draw.Path
*/
isPointInPath: function(x, y) {
var me = this,
commands = me.commands,
solver = Ext.draw.PathUtil,
origin = me.rayOrigin,
params = me.params,
ln = commands.length,
firstX = null,
firstY = null,
lastX = 0,
lastY = 0,
count = 0,
i, j;
for (i = 0 , j = 0; i < ln; i++) {
switch (commands[i]) {
case 'M':
if (firstX !== null) {
if (solver.linesIntersection(firstX, firstY, lastX, lastY, origin.x, origin.y, x, y)) {
count += 1;
}
};
firstX = lastX = params[j];
firstY = lastY = params[j + 1];
j += 2;
break;
case 'L':
if (solver.linesIntersection(lastX, lastY, params[j], params[j + 1], origin.x, origin.y, x, y)) {
count += 1;
};
lastX = params[j];
lastY = params[j + 1];
j += 2;
break;
case 'C':
count += solver.cubicLineIntersections(lastX, params[j], params[j + 2], params[j + 4], lastY, params[j + 1], params[j + 3], params[j + 5], origin.x, origin.y, x, y).length;
lastX = params[j + 4];
lastY = params[j + 5];
j += 6;
break;
case 'Z':
if (firstX !== null) {
if (solver.linesIntersection(firstX, firstY, lastX, lastY, origin.x, origin.y, x, y)) {
count += 1;
}
};
break;
}
}
return count % 2 === 1;
},
/**
* Tests whether the given point is on the path.
* @param {Number} x
* @param {Number} y
* @return {Boolean}
* @member Ext.draw.Path
*/
isPointOnPath: function(x, y) {
var me = this,
commands = me.commands,
solver = Ext.draw.PathUtil,
params = me.params,
ln = commands.length,
firstX = null,
firstY = null,
lastX = 0,
lastY = 0,
i, j;
for (i = 0 , j = 0; i < ln; i++) {
switch (commands[i]) {
case 'M':
if (firstX !== null) {
if (solver.pointOnLine(firstX, firstY, lastX, lastY, x, y)) {
return true;
}
};
firstX = lastX = params[j];
firstY = lastY = params[j + 1];
j += 2;
break;
case 'L':
if (solver.pointOnLine(lastX, lastY, params[j], params[j + 1], x, y)) {
return true;
};
lastX = params[j];
lastY = params[j + 1];
j += 2;
break;
case 'C':
if (solver.pointOnCubic(lastX, params[j], params[j + 2], params[j + 4], lastY, params[j + 1], params[j + 3], params[j + 5], x, y)) {
return true;
};
lastX = params[j + 4];
lastY = params[j + 5];
j += 6;
break;
case 'Z':
if (firstX !== null) {
if (solver.pointOnLine(firstX, firstY, lastX, lastY, x, y)) {
return true;
}
};
break;
}
}
return false;
},
/**
* Calculates the points where the given segment intersects the path.
* If four parameters are given then the segment is considered to be a line segment,
* where given parameters are the coordinates of the start and end points.
* If eight parameters are given then the segment is considered to be
* a cubic Bezier curve segment, where given parameters are the
* coordinates of its edge points and control points.
* @param x1
* @param y1
* @param x2
* @param y2
* @param x3
* @param y3
* @param x4
* @param y4
* @return {Array}
* @member Ext.draw.Path
*/
getSegmentIntersections: function(x1, y1, x2, y2, x3, y3, x4, y4) {
var me = this,
count = arguments.length,
solver = Ext.draw.PathUtil,
commands = me.commands,
params = me.params,
ln = commands.length,
firstX = null,
firstY = null,
lastX = 0,
lastY = 0,
intersections = [],
i, j, points;
for (i = 0 , j = 0; i < ln; i++) {
switch (commands[i]) {
case 'M':
if (firstX !== null) {
switch (count) {
case 4:
points = solver.linesIntersection(firstX, firstY, lastX, lastY, x1, y1, x2, y2);
if (points) {
intersections.push(points);
};
break;
case 8:
points = solver.cubicLineIntersections(x1, x2, x3, x4, y1, y2, y3, y4, firstX, firstY, lastX, lastY);
intersections.push.apply(intersections, points);
break;
}
};
firstX = lastX = params[j];
firstY = lastY = params[j + 1];
j += 2;
break;
case 'L':
switch (count) {
case 4:
points = solver.linesIntersection(lastX, lastY, params[j], params[j + 1], x1, y1, x2, y2);
if (points) {
intersections.push(points);
};
break;
case 8:
points = solver.cubicLineIntersections(x1, x2, x3, x4, y1, y2, y3, y4, lastX, lastY, params[j], params[j + 1]);
intersections.push.apply(intersections, points);
break;
};
lastX = params[j];
lastY = params[j + 1];
j += 2;
break;
case 'C':
switch (count) {
case 4:
points = solver.cubicLineIntersections(lastX, params[j], params[j + 2], params[j + 4], lastY, params[j + 1], params[j + 3], params[j + 5], x1, y1, x2, y2);
intersections.push.apply(intersections, points);
break;
case 8:
points = solver.cubicsIntersections(lastX, params[j], params[j + 2], params[j + 4], lastY, params[j + 1], params[j + 3], params[j + 5], x1, x2, x3, x4, y1, y2, y3, y4);
intersections.push.apply(intersections, points);
break;
};
lastX = params[j + 4];
lastY = params[j + 5];
j += 6;
break;
case 'Z':
if (firstX !== null) {
switch (count) {
case 4:
points = solver.linesIntersection(firstX, firstY, lastX, lastY, x1, y1, x2, y2);
if (points) {
intersections.push(points);
};
break;
case 8:
points = solver.cubicLineIntersections(x1, x2, x3, x4, y1, y2, y3, y4, firstX, firstY, lastX, lastY);
intersections.push.apply(intersections, points);
break;
}
};
break;
}
}
return intersections;
},
getIntersections: function(path) {
var me = this,
commands = me.commands,
params = me.params,
ln = commands.length,
firstX = null,
firstY = null,
lastX = 0,
lastY = 0,
intersections = [],
i, j, points;
for (i = 0 , j = 0; i < ln; i++) {
switch (commands[i]) {
case 'M':
if (firstX !== null) {
points = path.getSegmentIntersections.call(path, firstX, firstY, lastX, lastY);
intersections.push.apply(intersections, points);
};
firstX = lastX = params[j];
firstY = lastY = params[j + 1];
j += 2;
break;
case 'L':
points = path.getSegmentIntersections.call(path, lastX, lastY, params[j], params[j + 1]);
intersections.push.apply(intersections, points);
lastX = params[j];
lastY = params[j + 1];
j += 2;
break;
case 'C':
points = path.getSegmentIntersections.call(path, lastX, lastY, params[j], params[j + 1], params[j + 2], params[j + 3], params[j + 4], params[j + 5]);
intersections.push.apply(intersections, points);
lastX = params[j + 4];
lastY = params[j + 5];
j += 6;
break;
case 'Z':
if (firstX !== null) {
points = path.getSegmentIntersections.call(path, firstX, firstY, lastX, lastY);
intersections.push.apply(intersections, points);
};
break;
}
}
return intersections;
}
});
/**
* @class Ext.draw.sprite.Path
* @extends Ext.draw.sprite.Sprite
*
* A sprite that represents a path.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'path',
* path: 'M20,30 c0,-50 75,50 75,0 c0,-50 -75,50 -75,0',
* fillStyle: '#1F6D91'
* }]
* });
*/
Ext.define('Ext.draw.sprite.Path', {
extend: 'Ext.draw.sprite.Sprite',
requires: [
'Ext.draw.Draw',
'Ext.draw.Path'
],
alias: [
'sprite.path',
'Ext.draw.Sprite'
],
type: 'path',
isPath: true,
statics: {
/**
* Debug rendering options:
*
* debug: {
* bbox: true, // renders the bounding box of the path
* xray: true // renders control points of the path
* }
*
*/
debug: false
},
inheritableStatics: {
def: {
processors: {
/**
* @cfg {String} path The SVG based path string used by the sprite.
*/
path: function(n, o) {
if (!(n instanceof Ext.draw.Path)) {
n = new Ext.draw.Path(n);
}
return n;
},
debug: 'default'
},
aliases: {
d: 'path'
},
triggers: {
path: 'bbox'
},
updaters: {
path: function(attr) {
var path = attr.path;
if (!path || path.bindAttr !== attr) {
path = new Ext.draw.Path();
path.bindAttr = attr;
attr.path = path;
}
path.clear();
this.updatePath(path, attr);
this.scheduleUpdaters(attr, {
bbox: [
'path'
]
});
}
}
}
},
updatePlainBBox: function(plain) {
if (this.attr.path) {
this.attr.path.getDimension(plain);
}
},
updateTransformedBBox: function(transform) {
if (this.attr.path) {
this.attr.path.getDimensionWithTransform(this.attr.matrix, transform);
}
},
render: function(surface, ctx) {
var mat = this.attr.matrix,
attr = this.attr;
if (!attr.path || attr.path.params.length === 0) {
return;
}
mat.toContext(ctx);
ctx.appendPath(attr.path);
ctx.fillStroke(attr);
var debug = this.statics().debug || attr.debug;
if (debug) {
debug.bbox && this.renderBBox(surface, ctx);
debug.xray && this.renderXRay(surface, ctx);
}
},
renderBBox: function(surface, ctx) {
var bbox = this.getBBox();
ctx.beginPath();
ctx.moveTo(bbox.x, bbox.y);
ctx.lineTo(bbox.x + bbox.width, bbox.y);
ctx.lineTo(bbox.x + bbox.width, bbox.y + bbox.height);
ctx.lineTo(bbox.x, bbox.y + bbox.height);
ctx.closePath();
ctx.strokeStyle = 'red';
ctx.strokeOpacity = 1;
ctx.lineWidth = 0.5;
ctx.stroke();
},
renderXRay: function(surface, ctx) {
var attr = this.attr,
mat = attr.matrix,
imat = attr.inverseMatrix,
path = attr.path,
commands = path.commands,
params = path.params,
ln = commands.length,
twoPi = Math.PI * 2,
size = 2,
i, j;
mat.toContext(ctx);
ctx.beginPath();
for (i = 0 , j = 0; i < ln; i++) {
switch (commands[i]) {
case 'M':
ctx.moveTo(params[j] - size, params[j + 1] - size);
ctx.rect(params[j] - size, params[j + 1] - size, size * 2, size * 2);
j += 2;
break;
case 'L':
ctx.moveTo(params[j] - size, params[j + 1] - size);
ctx.rect(params[j] - size, params[j + 1] - size, size * 2, size * 2);
j += 2;
break;
case 'C':
ctx.moveTo(params[j] + size, params[j + 1]);
ctx.arc(params[j], params[j + 1], size, 0, twoPi, true);
j += 2;
ctx.moveTo(params[j] + size, params[j + 1]);
ctx.arc(params[j], params[j + 1], size, 0, twoPi, true);
j += 2;
ctx.moveTo(params[j] + size * 2, params[j + 1]);
ctx.rect(params[j] - size, params[j + 1] - size, size * 2, size * 2);
j += 2;
break;
default:
}
}
imat.toContext(ctx);
ctx.strokeStyle = 'black';
ctx.strokeOpacity = 1;
ctx.lineWidth = 1;
ctx.stroke();
mat.toContext(ctx);
ctx.beginPath();
for (i = 0 , j = 0; i < ln; i++) {
switch (commands[i]) {
case 'M':
ctx.moveTo(params[j], params[j + 1]);
j += 2;
break;
case 'L':
ctx.moveTo(params[j], params[j + 1]);
j += 2;
break;
case 'C':
ctx.lineTo(params[j], params[j + 1]);
j += 2;
ctx.moveTo(params[j], params[j + 1]);
j += 2;
ctx.lineTo(params[j], params[j + 1]);
j += 2;
break;
default:
}
}
imat.toContext(ctx);
ctx.lineWidth = 0.5;
ctx.stroke();
},
/**
* Update the path.
* @param {Ext.draw.Path} path An empty path to draw on using path API.
* @param {Object} attr The attribute object. Note: DO NOT use the `sprite.attr` instead of this
* if you want to work with instancing.
*/
updatePath: function(path, attr) {}
});
/**
* @class Ext.draw.overrides.sprite.Path
*/
Ext.define('Ext.draw.overrides.sprite.Path', {
override: 'Ext.draw.sprite.Path',
requires: [
'Ext.draw.Color'
],
/**
* Tests whether the given point is inside the path.
* @param x
* @param y
* @return {Boolean}
* @member Ext.draw.sprite.Path
*/
isPointInPath: function(x, y) {
var attr = this.attr;
if (attr.fillStyle === Ext.draw.Color.RGBA_NONE) {
return this.isPointOnPath(x, y);
}
var path = attr.path,
matrix = attr.matrix,
params, result;
if (!matrix.isIdentity()) {
params = path.params.slice(0);
path.transform(attr.matrix);
}
result = path.isPointInPath(x, y);
if (params) {
path.params = params;
}
return result;
},
/**
* Tests whether the given point is on the path.
* @param x
* @param y
* @return {Boolean}
* @member Ext.draw.sprite.Path
*/
isPointOnPath: function(x, y) {
var attr = this.attr,
path = attr.path,
matrix = attr.matrix,
params, result;
if (!matrix.isIdentity()) {
params = path.params.slice(0);
path.transform(attr.matrix);
}
result = path.isPointOnPath(x, y);
if (params) {
path.params = params;
}
return result;
},
/**
* @inheritdoc
*/
hitTest: function(point, options) {
var me = this,
attr = me.attr,
path = attr.path,
bbox = me.getBBox(),
matrix = attr.matrix,
x = point[0],
y = point[1],
hasFill = attr.fillStyle !== Ext.draw.Color.NONE && attr.fillStyle !== Ext.draw.Color.RGBA_NONE,
bboxHit = bbox && x >= bbox.x && x <= (bbox.x + bbox.width) && y >= bbox.y && y <= (bbox.y + bbox.height),
result = null,
params;
if (!bboxHit) {
return result;
}
if (!matrix.isIdentity()) {
params = path.params.slice(0);
path.transform(attr.matrix);
}
if (options.fill && options.stroke) {
if (hasFill) {
if (path.isPointInPath(x, y)) {
result = {
sprite: me
};
}
} else {
if (path.isPointInPath(x, y) || path.isPointOnPath(x, y)) {
result = {
sprite: me
};
}
}
} else if (options.stroke && !options.fill) {
if (path.isPointOnPath(x, y)) {
result = {
sprite: me
};
}
} else if (options.fill && !options.stroke) {
if (path.isPointInPath(x, y)) {
result = {
sprite: me
};
}
}
if (params) {
path.params = params;
}
return result;
},
/**
* Returns all points where this sprite intersects the given sprite.
* The given sprite must be an instance of the {@link Ext.draw.sprite.Path} class
* or its subclass.
* @param path
* @return {Array}
* @member Ext.draw.sprite.Path
*/
getIntersections: function(path) {
if (!(path.isSprite && path.isPath)) {
return [];
}
var aAttr = this.attr,
bAttr = path.attr,
aPath = aAttr.path,
bPath = bAttr.path,
aMatrix = aAttr.matrix,
bMatrix = bAttr.matrix,
aParams, bParams, intersections;
if (!aMatrix.isIdentity()) {
aParams = aPath.params.slice(0);
aPath.transform(aAttr.matrix);
}
if (!bMatrix.isIdentity()) {
bParams = bPath.params.slice(0);
bPath.transform(bAttr.matrix);
}
intersections = aPath.getIntersections(bPath);
if (aParams) {
aPath.params = aParams;
}
if (bParams) {
bPath.params = bParams;
}
return intersections;
}
});
/**
* @class Ext.draw.sprite.Circle
* @extends Ext.draw.sprite.Path
*
* A sprite that represents a circle.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'circle',
* cx: 100,
* cy: 100,
* r: 50,
* fillStyle: '#1F6D91'
* }]
* });
*/
Ext.define('Ext.draw.sprite.Circle', {
extend: 'Ext.draw.sprite.Path',
alias: 'sprite.circle',
type: 'circle',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [cx=0] The center coordinate of the sprite on the x-axis.
*/
cx: 'number',
/**
* @cfg {Number} [cy=0] The center coordinate of the sprite on the y-axis.
*/
cy: 'number',
/**
* @cfg {Number} [r=0] The radius of the sprite.
*/
r: 'number'
},
aliases: {
radius: 'r',
x: 'cx',
y: 'cy',
centerX: 'cx',
centerY: 'cy'
},
defaults: {
cx: 0,
cy: 0,
r: 4
},
triggers: {
cx: 'path',
cy: 'path',
r: 'path'
}
}
},
updatePlainBBox: function(plain) {
var attr = this.attr,
cx = attr.cx,
cy = attr.cy,
r = attr.r;
plain.x = cx - r;
plain.y = cy - r;
plain.width = r + r;
plain.height = r + r;
},
updateTransformedBBox: function(transform) {
var attr = this.attr,
cx = attr.cx,
cy = attr.cy,
r = attr.r,
matrix = attr.matrix,
scaleX = matrix.getScaleX(),
scaleY = matrix.getScaleY(),
w, h;
w = scaleX * r;
h = scaleY * r;
transform.x = matrix.x(cx, cy) - w;
transform.y = matrix.y(cx, cy) - h;
transform.width = w + w;
transform.height = h + h;
},
updatePath: function(path, attr) {
path.arc(attr.cx, attr.cy, attr.r, 0, Math.PI * 2, false);
}
});
/**
* @class Ext.draw.sprite.Arc
* @extend Ext.draw.sprite.Circle
*
* A sprite that represents a circular arc.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'arc',
* cx: 100,
* cy: 100,
* r: 80,
* fillStyle: '#1F6D91',
* startAngle: 0,
* endAngle: Math.PI,
* anticlockwise: true
* }]
* });
*/
Ext.define('Ext.draw.sprite.Arc', {
extend: 'Ext.draw.sprite.Circle',
alias: 'sprite.arc',
type: 'arc',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [startAngle=0] The beginning angle of the arc.
*/
startAngle: 'number',
/**
* @cfg {Number} [endAngle=Math.PI*2] The ending angle of the arc.
*/
endAngle: 'number',
/**
* @cfg {Boolean} [anticlockwise=false] Determines whether or not the arc is drawn clockwise.
*/
anticlockwise: 'bool'
},
aliases: {
from: 'startAngle',
to: 'endAngle',
start: 'startAngle',
end: 'endAngle'
},
defaults: {
startAngle: 0,
endAngle: Math.PI * 2,
anticlockwise: false
},
triggers: {
startAngle: 'path',
endAngle: 'path',
anticlockwise: 'path'
}
}
},
updatePath: function(path, attr) {
path.arc(attr.cx, attr.cy, attr.r, attr.startAngle, attr.endAngle, attr.anticlockwise);
}
});
/**
* A sprite that represents an arrow.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'arrow',
* translationX: 100,
* translationY: 100,
* size: 40,
* fillStyle: '#30BDA7'
* }]
* });
*/
Ext.define('Ext.draw.sprite.Arrow', {
extend: 'Ext.draw.sprite.Path',
alias: 'sprite.arrow',
inheritableStatics: {
def: {
processors: {
x: 'number',
y: 'number',
/**
* @cfg {Number} [size=4] The size of the sprite.
* Meant to be comparable to the size of a circle sprite with the same radius.
*/
size: 'number'
},
defaults: {
x: 0,
y: 0,
size: 4
},
triggers: {
x: 'path',
y: 'path',
size: 'path'
}
}
},
updatePath: function(path, attr) {
var s = attr.size * 1.5,
x = attr.x - attr.lineWidth / 2,
y = attr.y;
path.fromSvgString('M'.concat(x - s * 0.7, ',', y - s * 0.4, 'l', [
s * 0.6,
0,
0,
-s * 0.4,
s,
s * 0.8,
-s,
s * 0.8,
0,
-s * 0.4,
-s * 0.6,
0
], 'z'));
}
});
/**
* @class Ext.draw.sprite.Composite
* @extends Ext.draw.sprite.Sprite
*
* Represents a group of sprites.
*/
Ext.define('Ext.draw.sprite.Composite', {
extend: 'Ext.draw.sprite.Sprite',
alias: 'sprite.composite',
type: 'composite',
isComposite: true,
config: {
sprites: []
},
constructor: function() {
this.sprites = [];
this.sprites.map = {};
this.callParent(arguments);
},
/**
* Adds a sprite to the composite.
* @param {Ext.draw.sprite.Sprite|Object} sprite
*/
add: function(sprite) {
if (!sprite) {
return null;
}
if (!sprite.isSprite) {
sprite = Ext.create('sprite.' + sprite.type, sprite);
sprite.setParent(this);
sprite.setSurface(this.getSurface());
}
var me = this,
attr = me.attr,
oldTransformations = sprite.applyTransformations;
sprite.applyTransformations = function() {
if (sprite.attr.dirtyTransform) {
attr.dirtyTransform = true;
attr.bbox.plain.dirty = true;
attr.bbox.transform.dirty = true;
}
oldTransformations.call(sprite);
};
me.sprites.push(sprite);
me.sprites.map[sprite.id] = sprite.getId();
attr.bbox.plain.dirty = true;
attr.bbox.transform.dirty = true;
return sprite;
},
updateSurface: function(surface) {
for (var i = 0,
ln = this.sprites.length; i < ln; i++) {
this.sprites[i].setSurface(surface);
}
},
/**
* Adds a list of sprites to the composite.
* @param {Ext.draw.sprite.Sprite[]|Object[]|Ext.draw.sprite.Sprite|Object} sprites
*/
addAll: function(sprites) {
if (sprites.isSprite || sprites.type) {
this.add(sprites);
} else if (Ext.isArray(sprites)) {
var i = 0;
while (i < sprites.length) {
this.add(sprites[i++]);
}
}
},
/**
* Updates the bounding box of the composite, which contains the bounding box of all sprites in the composite.
*/
updatePlainBBox: function(plain) {
var me = this,
left = Infinity,
right = -Infinity,
top = Infinity,
bottom = -Infinity,
sprite, bbox, i, ln;
for (i = 0 , ln = me.sprites.length; i < ln; i++) {
sprite = me.sprites[i];
sprite.applyTransformations();
bbox = sprite.getBBox();
if (left > bbox.x) {
left = bbox.x;
}
if (right < bbox.x + bbox.width) {
right = bbox.x + bbox.width;
}
if (top > bbox.y) {
top = bbox.y;
}
if (bottom < bbox.y + bbox.height) {
bottom = bbox.y + bbox.height;
}
}
plain.x = left;
plain.y = top;
plain.width = right - left;
plain.height = bottom - top;
},
/**
* Renders all sprites contained in the composite to the surface.
*/
render: function(surface, ctx, rect) {
var mat = this.attr.matrix,
i, ln;
mat.toContext(ctx);
for (i = 0 , ln = this.sprites.length; i < ln; i++) {
surface.renderSprite(this.sprites[i], rect);
}
}
});
/**
* A sprite that represents a cross.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'cross',
* translationX: 100,
* translationY: 100,
* size: 40,
* fillStyle: '#1F6D91'
* }]
* });
*/
Ext.define('Ext.draw.sprite.Cross', {
extend: 'Ext.draw.sprite.Path',
alias: 'sprite.cross',
inheritableStatics: {
def: {
processors: {
x: 'number',
y: 'number',
/**
* @cfg {Number} [size=4] The size of the sprite.
* Meant to be comparable to the size of a circle sprite with the same radius.
*/
size: 'number'
},
defaults: {
x: 0,
y: 0,
size: 4
},
triggers: {
x: 'path',
y: 'path',
size: 'path'
}
}
},
updatePath: function(path, attr) {
var s = attr.size / 1.7,
x = attr.x - attr.lineWidth / 2,
y = attr.y;
path.fromSvgString('M'.concat(x - s, ',', y, 'l', [
-s,
-s,
s,
-s,
s,
s,
s,
-s,
s,
s,
-s,
s,
s,
s,
-s,
s,
-s,
-s,
-s,
s,
-s,
-s,
'z'
]));
}
});
/**
* A sprite that represents a diamond.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'diamond',
* translationX: 100,
* translationY: 100,
* size: 40,
* fillStyle: '#1F6D91'
* }]
* });
*/
Ext.define('Ext.draw.sprite.Diamond', {
extend: 'Ext.draw.sprite.Path',
alias: 'sprite.diamond',
inheritableStatics: {
def: {
processors: {
x: 'number',
y: 'number',
/**
* @cfg {Number} [size=4] The size of the sprite.
* Meant to be comparable to the size of a circle sprite with the same radius.
*/
size: 'number'
},
defaults: {
x: 0,
y: 0,
size: 4
},
triggers: {
x: 'path',
y: 'path',
size: 'path'
}
}
},
updatePath: function(path, attr) {
var s = attr.size * 1.25,
x = attr.x - attr.lineWidth / 2,
y = attr.y;
path.fromSvgString([
'M',
x,
y - s,
'l',
s,
s,
-s,
s,
-s,
-s,
s,
-s,
'z'
]);
}
});
/**
* @class Ext.draw.sprite.Ellipse
* @extends Ext.draw.sprite.Path
*
* A sprite that represents an ellipse.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'ellipse',
* cx: 100,
* cy: 100,
* rx: 80,
* ry: 50,
* fillStyle: '#1F6D91'
* }]
* });
*/
Ext.define("Ext.draw.sprite.Ellipse", {
extend: "Ext.draw.sprite.Path",
alias: 'sprite.ellipse',
type: 'ellipse',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [cx=0] The center coordinate of the sprite on the x-axis.
*/
cx: "number",
/**
* @cfg {Number} [cy=0] The center coordinate of the sprite on the y-axis.
*/
cy: "number",
/**
* @cfg {Number} [rx=1] The radius of the sprite on the x-axis.
*/
rx: "number",
/**
* @cfg {Number} [ry=1] The radius of the sprite on the y-axis.
*/
ry: "number",
/**
* @cfg {Number} [axisRotation=0] The rotation of the sprite about its axis.
*/
axisRotation: "number"
},
aliases: {
radius: "r",
x: "cx",
y: "cy",
centerX: "cx",
centerY: "cy",
radiusX: "rx",
radiusY: "ry"
},
defaults: {
cx: 0,
cy: 0,
rx: 1,
ry: 1,
axisRotation: 0
},
triggers: {
cx: 'path',
cy: 'path',
rx: 'path',
ry: 'path',
axisRotation: 'path'
}
}
},
updatePlainBBox: function(plain) {
var attr = this.attr,
cx = attr.cx,
cy = attr.cy,
rx = attr.rx,
ry = attr.ry;
plain.x = cx - rx;
plain.y = cy - ry;
plain.width = rx + rx;
plain.height = ry + ry;
},
updateTransformedBBox: function(transform) {
var attr = this.attr,
cx = attr.cx,
cy = attr.cy,
rx = attr.rx,
ry = attr.ry,
rxy = ry / rx,
matrix = attr.matrix.clone(),
xx, xy, yx, yy, dx, dy, w, h;
matrix.append(1, 0, 0, rxy, 0, cy * (1 - rxy));
xx = matrix.getXX();
yx = matrix.getYX();
dx = matrix.getDX();
xy = matrix.getXY();
yy = matrix.getYY();
dy = matrix.getDY();
w = Math.sqrt(xx * xx + yx * yx) * rx;
h = Math.sqrt(xy * xy + yy * yy) * rx;
transform.x = cx * xx + cy * yx + dx - w;
transform.y = cx * xy + cy * yy + dy - h;
transform.width = w + w;
transform.height = h + h;
},
updatePath: function(path, attr) {
path.ellipse(attr.cx, attr.cy, attr.rx, attr.ry, attr.axisRotation, 0, Math.PI * 2, false);
}
});
/**
* @class Ext.draw.sprite.EllipticalArc
* @extends Ext.draw.sprite.Ellipse
*
* A sprite that represents an elliptical arc.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'ellipticalArc',
* cx: 100,
* cy: 100,
* rx: 80,
* ry: 50,
* fillStyle: '#1F6D91',
* startAngle: 0,
* endAngle: Math.PI,
* anticlockwise: true
* }]
* });
*/
Ext.define('Ext.draw.sprite.EllipticalArc', {
extend: 'Ext.draw.sprite.Ellipse',
alias: 'sprite.ellipticalArc',
type: 'ellipticalArc',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [startAngle=0] The beginning angle of the arc.
*/
startAngle: 'number',
/**
* @cfg {Number} [endAngle=Math.PI*2] The ending angle of the arc.
*/
endAngle: 'number',
/**
* @cfg {Boolean} [anticlockwise=false] Determines whether or not the arc is drawn clockwise.
*/
anticlockwise: 'bool'
},
aliases: {
from: 'startAngle',
to: 'endAngle',
start: 'startAngle',
end: 'endAngle'
},
defaults: {
startAngle: 0,
endAngle: Math.PI * 2,
anticlockwise: false
},
triggers: {
startAngle: 'path',
endAngle: 'path',
anticlockwise: 'path'
}
}
},
updatePath: function(path, attr) {
path.ellipse(attr.cx, attr.cy, attr.rx, attr.ry, attr.axisRotation, attr.startAngle, attr.endAngle, attr.anticlockwise);
}
});
/**
* @class Ext.draw.sprite.Rect
* @extends Ext.draw.sprite.Path
*
* A sprite that represents a rectangle.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'rect',
* x: 50,
* y: 50,
* width: 100,
* height: 100,
* fillStyle: '#1F6D91'
* }]
* });
*/
Ext.define('Ext.draw.sprite.Rect', {
extend: 'Ext.draw.sprite.Path',
alias: 'sprite.rect',
type: 'rect',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [x=0] The position of the sprite on the x-axis.
*/
x: 'number',
/**
* @cfg {Number} [y=0] The position of the sprite on the y-axis.
*/
y: 'number',
/**
* @cfg {Number} [width=8] The width of the sprite.
*/
width: 'number',
/**
* @cfg {Number} [height=8] The height of the sprite.
*/
height: 'number',
/**
* @cfg {Number} [radius=0] The radius of the rounded corners.
*/
radius: 'number'
},
aliases: {},
triggers: {
x: 'path',
y: 'path',
width: 'path',
height: 'path',
radius: 'path'
},
defaults: {
x: 0,
y: 0,
width: 8,
height: 8,
radius: 0
}
}
},
updatePlainBBox: function(plain) {
var attr = this.attr;
plain.x = attr.x;
plain.y = attr.y;
plain.width = attr.width;
plain.height = attr.height;
},
updateTransformedBBox: function(transform, plain) {
this.attr.matrix.transformBBox(plain, this.attr.radius, transform);
},
updatePath: function(path, attr) {
var x = attr.x,
y = attr.y,
width = attr.width,
height = attr.height,
radius = Math.min(attr.radius, Math.abs(attr.height) * 0.5, Math.abs(attr.width) * 0.5);
if (radius === 0) {
path.rect(x, y, width, height);
} else {
path.moveTo(x + radius, y);
path.arcTo(x + width, y, x + width, y + height, radius);
path.arcTo(x + width, y + height, x, y + height, radius);
path.arcTo(x, y + height, x, y, radius);
path.arcTo(x, y, x + radius, y, radius);
}
}
});
/**
* @class Ext.draw.sprite.Image
* @extends Ext.draw.sprite.Rect
*
* A sprite that represents an image.
*/
Ext.define("Ext.draw.sprite.Image", {
extend: "Ext.draw.sprite.Rect",
alias: 'sprite.image',
type: 'image',
statics: {
imageLoaders: {}
},
inheritableStatics: {
def: {
processors: {
/**
* @cfg {String} [src=''] The image source of the sprite.
*/
src: 'string'
},
defaults: {
src: '',
/**
* @cfg {Number} [width=null] The width of the image.
* For consistent image size on all devices the width must be explicitly set.
* Otherwise the natural image width devided by the device pixel ratio
* (for a crisp looking image) will be used as the width of the sprite.
*/
width: null,
/**
* @cfg {Number} [height=null] The height of the image.
* For consistent image size on all devices the height must be explicitly set.
* Otherwise the natural image height devided by the device pixel ratio
* (for a crisp looking image) will be used as the height of the sprite.
*/
height: null
}
}
},
render: function(surface, ctx) {
var me = this,
attr = me.attr,
mat = attr.matrix,
src = attr.src,
x = attr.x,
y = attr.y,
width = attr.width,
height = attr.height,
loadingStub = Ext.draw.sprite.Image.imageLoaders[src],
imageLoader, image, i;
if (loadingStub && loadingStub.done) {
mat.toContext(ctx);
image = loadingStub.image;
ctx.drawImage(image, x, y, width || (image.naturalWidth || image.width) / surface.devicePixelRatio, height || (image.naturalHeight || image.height) / surface.devicePixelRatio);
} else if (!loadingStub) {
imageLoader = new Image();
loadingStub = Ext.draw.sprite.Image.imageLoaders[src] = {
image: imageLoader,
done: false,
pendingSprites: [
me
],
pendingSurfaces: [
surface
]
};
imageLoader.width = width;
imageLoader.height = height;
imageLoader.onload = function() {
if (!loadingStub.done) {
loadingStub.done = true;
for (i = 0; i < loadingStub.pendingSprites.length; i++) {
loadingStub.pendingSprites[i].setDirty(true);
}
for (i in loadingStub.pendingSurfaces) {
loadingStub.pendingSurfaces[i].renderFrame();
}
}
};
imageLoader.src = src;
} else {
Ext.Array.include(loadingStub.pendingSprites, me);
Ext.Array.include(loadingStub.pendingSurfaces, surface);
}
}
});
/**
* @class Ext.draw.sprite.Instancing
* @extends Ext.draw.sprite.Sprite
*
* Sprite that represents multiple instances based on the given template.
*/
Ext.define('Ext.draw.sprite.Instancing', {
extend: 'Ext.draw.sprite.Sprite',
alias: 'sprite.instancing',
type: 'instancing',
isInstancing: true,
config: {
/**
* @cfg {Object} [template=null] The sprite template used by all instances.
*/
template: null
},
instances: null,
applyTemplate: function(template) {
if (!Ext.isObject(template)) {
Ext.Error.raise("A template of an instancing sprite must either be " + "a sprite instance or a valid config object from which a template " + "sprite will be created.");
} else if (template.isInstancing || template.isComposite) {
Ext.Error.raise("Can't use an instancing or composite sprite " + "as a template for an instancing sprite.");
}
if (!template.isSprite) {
if (!template.xclass && !template.type) {
// For compatibility with legacy charts.
template.type = 'circle';
}
template = Ext.create(template.xclass || 'sprite.' + template.type, template);
}
template.setParent(this);
return template;
},
updateTemplate: function(template, oldTemplate) {
if (oldTemplate) {
delete oldTemplate.ownAttr;
}
template.setSurface(this.getSurface());
// ownAttr is used to get a reference to the template's attributes
// when one of the instances is rendering, as at that moment the template's
// attributes (template.attr) are the instance's attributes.
template.ownAttr = template.attr;
template.attr.children = this.instances = [];
this.position = 0;
},
updateSurface: function(surface) {
var template = this.getTemplate();
if (template) {
template.setSurface(surface);
}
},
get: function(index) {
return this.instances[index];
},
getCount: function() {
return this.instances.length;
},
clearAll: function() {
this.instances.length = 0;
this.position = 0;
},
/**
* Creates a new sprite instance.
*
* @param {Object} config The configuration of the instance.
* @param {Boolean} [bypassNormalization] 'true' to bypass attribute normalization.
* @param {Boolean} [avoidCopy] 'true' to avoid copying the `config` object.
* @return {Object} The attributes of the instance.
*/
createInstance: function(config, bypassNormalization, avoidCopy) {
var template = this.getTemplate(),
originalAttr = template.attr,
attr = Ext.Object.chain(originalAttr);
template.topModifier.prepareAttributes(attr);
template.attr = attr;
template.setAttributes(config, bypassNormalization, avoidCopy);
attr.template = template;
this.instances.push(attr);
template.attr = originalAttr;
this.position++;
return attr;
},
/**
* Not supported.
*
* @return {null}
*/
getBBox: function() {
return null;
},
/**
* Returns the bounding box for the instance at the given index.
*
* @param {Number} index The index of the instance.
* @param {Boolean} [isWithoutTransform] 'true' to not apply sprite transforms to the bounding box.
* @return {Object} The bounding box for the instance.
*/
getBBoxFor: function(index, isWithoutTransform) {
var template = this.getTemplate(),
originalAttr = template.attr,
bbox;
template.attr = this.instances[index];
bbox = template.getBBox(isWithoutTransform);
template.attr = originalAttr;
return bbox;
},
render: function(surface, ctx, clipRect, rect) {
if (!this.getTemplate()) {
Ext.Error.raise('An instancing sprite must have a template.');
}
var me = this,
template = me.getTemplate(),
mat = me.attr.matrix,
originalAttr = template.attr,
instances = me.instances,
i,
ln = me.position;
mat.toContext(ctx);
template.preRender(surface, ctx, clipRect, rect);
template.useAttributes(ctx, rect);
for (i = 0; i < ln; i++) {
if (instances[i].dirtyZIndex) {
break;
}
}
for (i = 0; i < ln; i++) {
if (instances[i].hidden) {
continue;
}
ctx.save();
template.attr = instances[i];
template.useAttributes(ctx, rect);
template.render(surface, ctx, clipRect, rect);
ctx.restore();
}
template.attr = originalAttr;
},
/**
* Sets the attributes for the instance at the given index.
*
* @param {Number} index the index of the instance
* @param {Object} changes the attributes to change
* @param {Boolean} [bypassNormalization] 'true' to avoid attribute normalization
*/
setAttributesFor: function(index, changes, bypassNormalization) {
var template = this.getTemplate(),
originalAttr = template.attr,
attr = this.instances[index];
if (!attr) {
return;
}
template.attr = attr;
if (bypassNormalization) {
changes = Ext.apply({}, changes);
} else {
changes = template.self.def.normalize(changes);
}
template.topModifier.pushDown(attr, changes);
template.attr = originalAttr;
},
destroy: function() {
this.callParent();
this.instances.length = 0;
this.instances = null;
if (this.getTemplate()) {
this.getTemplate().destroy();
}
}
});
/**
* A sprite that represents a line.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'line',
* fromX: 20,
* fromY: 20,
* toX: 120,
* toY: 120,
* strokeStyle: '#1F6D91',
* lineWidth: 3
* }]
* });
*/
Ext.define('Ext.draw.sprite.Line', {
extend: 'Ext.draw.sprite.Sprite',
alias: 'sprite.line',
type: 'line',
inheritableStatics: {
def: {
processors: {
fromX: 'number',
fromY: 'number',
toX: 'number',
toY: 'number'
},
defaults: {
fromX: 0,
fromY: 0,
toX: 1,
toY: 1,
strokeStyle: 'black'
},
aliases: {
x1: 'fromX',
y1: 'fromY',
x2: 'toX',
y2: 'toY'
}
}
},
updatePlainBBox: function(plain) {
var attr = this.attr,
fromX = Math.min(attr.fromX, attr.toX),
fromY = Math.min(attr.fromY, attr.toY),
toX = Math.max(attr.fromX, attr.toX),
toY = Math.max(attr.fromY, attr.toY);
plain.x = fromX;
plain.y = fromY;
plain.width = toX - fromX;
plain.height = toY - fromY;
},
render: function(surface, ctx) {
var attr = this.attr,
matrix = this.attr.matrix;
matrix.toContext(ctx);
ctx.beginPath();
ctx.moveTo(attr.fromX, attr.fromY);
ctx.lineTo(attr.toX, attr.toY);
ctx.stroke();
}
});
/**
* A sprite that represents a plus.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'plus',
* translationX: 100,
* translationY: 100,
* size: 40,
* fillStyle: '#1F6D91'
* }]
* });
*/
Ext.define('Ext.draw.sprite.Plus', {
extend: 'Ext.draw.sprite.Path',
alias: 'sprite.plus',
inheritableStatics: {
def: {
processors: {
x: 'number',
y: 'number',
/**
* @cfg {Number} [size=4] The size of the sprite.
* Meant to be comparable to the size of a circle sprite with the same radius.
*/
size: 'number'
},
defaults: {
x: 0,
y: 0,
size: 4
},
triggers: {
x: 'path',
y: 'path',
size: 'path'
}
}
},
updatePath: function(path, attr) {
var s = attr.size / 1.3,
x = attr.x - attr.lineWidth / 2,
y = attr.y;
path.fromSvgString('M'.concat(x - s / 2, ',', y - s / 2, 'l', [
0,
-s,
s,
0,
0,
s,
s,
0,
0,
s,
-s,
0,
0,
s,
-s,
0,
0,
-s,
-s,
0,
0,
-s,
'z'
]));
}
});
/**
* @class Ext.draw.sprite.Sector
* @extends Ext.draw.sprite.Path
*
* A sprite representing a pie slice.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'sector',
* centerX: 100,
* centerY: 100,
* startAngle: -2.355,
* endAngle: -.785,
* endRho: 50,
* fillStyle: '#1F6D91'
* }]
* });
*/
Ext.define('Ext.draw.sprite.Sector', {
extend: 'Ext.draw.sprite.Path',
alias: 'sprite.sector',
type: 'sector',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [centerX=0] The center coordinate of the sprite on the x-axis.
*/
centerX: 'number',
/**
* @cfg {Number} [centerY=0] The center coordinate of the sprite on the y-axis.
*/
centerY: 'number',
/**
* @cfg {Number} [startAngle=0] The starting angle of the sprite.
*/
startAngle: 'number',
/**
* @cfg {Number} [endAngle=0] The ending angle of the sprite.
*/
endAngle: 'number',
/**
* @cfg {Number} [startRho=0] The starting point of the radius of the sprite.
*/
startRho: 'number',
/**
* @cfg {Number} [endRho=150] The ending point of the radius of the sprite.
*/
endRho: 'number',
/**
* @cfg {Number} [margin=0] The margin of the sprite from the center of pie.
*/
margin: 'number'
},
aliases: {
rho: 'endRho'
},
triggers: {
centerX: 'path,bbox',
centerY: 'path,bbox',
startAngle: 'path,bbox',
endAngle: 'path,bbox',
startRho: 'path,bbox',
endRho: 'path,bbox',
margin: 'path,bbox'
},
defaults: {
centerX: 0,
centerY: 0,
startAngle: 0,
endAngle: 0,
startRho: 0,
endRho: 150,
margin: 0,
path: 'M 0,0'
}
}
},
getMidAngle: function() {
return this.midAngle || 0;
},
updatePath: function(path, attr) {
var startAngle = Math.min(attr.startAngle, attr.endAngle),
endAngle = Math.max(attr.startAngle, attr.endAngle),
midAngle = this.midAngle = (startAngle + endAngle) * 0.5,
margin = attr.margin,
centerX = attr.centerX,
centerY = attr.centerY,
startRho = Math.min(attr.startRho, attr.endRho),
endRho = Math.max(attr.startRho, attr.endRho);
if (margin) {
centerX += margin * Math.cos(midAngle);
centerY += margin * Math.sin(midAngle);
}
path.moveTo(centerX + startRho * Math.cos(startAngle), centerY + startRho * Math.sin(startAngle));
path.lineTo(centerX + endRho * Math.cos(startAngle), centerY + endRho * Math.sin(startAngle));
path.arc(centerX, centerY, endRho, startAngle, endAngle, false);
path.lineTo(centerX + startRho * Math.cos(endAngle), centerY + startRho * Math.sin(endAngle));
path.arc(centerX, centerY, startRho, endAngle, startAngle, true);
}
});
/**
* A sprite that represents a square.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'square',
* x: 100,
* y: 100,
* size: 50,
* fillStyle: '#1F6D91'
* }]
* });
*/
Ext.define('Ext.draw.sprite.Square', {
extend: 'Ext.draw.sprite.Rect',
alias: 'sprite.square',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [size=4] The size of the sprite.
* Meant to be comparable to the size of a circle sprite with the same radius.
*/
size: 'number'
},
defaults: {
size: 4
},
triggers: {
size: 'size'
},
updaters: {
size: function(attr) {
var size = attr.size,
halfLineWidth = attr.lineWidth / 2;
this.setAttributes({
x: attr.x - size - halfLineWidth,
y: attr.y - size,
height: 2 * size,
width: 2 * size
});
}
}
}
}
});
/**
* Utility class to provide a way to *approximately* measure the dimension of text
* without a drawing context.
*/
Ext.define('Ext.draw.TextMeasurer', {
singleton: true,
requires: [
'Ext.util.TextMetrics'
],
measureDiv: null,
measureCache: {},
/**
* @cfg {Boolean} [precise=false]
* This singleton tries not to make use of the Ext.util.TextMetrics because it is
* several times slower than TextMeasurer's own solution. TextMetrics is more precise
* though, so if you have a case where the error is too big, you may want to set
* this config to `true` to get perfect results at the expense of performance.
* Note: defaults to `true` in IE8.
*/
precise: Ext.isIE8,
measureDivTpl: {
tag: 'div',
style: {
overflow: 'hidden',
position: 'relative',
'float': 'left',
// 'float' is a reserved word. Don't unquote, or it will break the CMD build.
width: 0,
height: 0
},
// Tell the spec runner to ignore this element when checking if the dom is clean.
'data-sticky': true,
children: {
tag: 'div',
style: {
display: 'block',
position: 'absolute',
x: -100000,
y: -100000,
padding: 0,
margin: 0,
'z-index': -100000,
'white-space': 'nowrap'
}
}
},
/**
* @private Measure the size of a text with specific font by using DOM to measure it.
* Could be very expensive therefore should be used lazily.
* @param {String} text
* @param {String} font
* @return {Object} An object with `width` and `height` properties.
* @return {Number} return.width
* @return {Number} return.height
*/
actualMeasureText: function(text, font) {
var me = Ext.draw.TextMeasurer,
measureDiv = me.measureDiv,
FARAWAY = 100000,
size;
if (!measureDiv) {
var parent = Ext.Element.create({
// Tell the spec runner to ignore this element when checking if the dom is clean.
'data-sticky': true,
style: {
"overflow": "hidden",
"position": "relative",
"float": "left",
// DO NOT REMOVE THE QUOTE OR IT WILL BREAK COMPRESSOR
"width": 0,
"height": 0
}
});
me.measureDiv = measureDiv = Ext.Element.create({
style: {
"position": 'absolute',
"x": FARAWAY,
"y": FARAWAY,
"z-index": -FARAWAY,
"white-space": "nowrap",
"display": 'block',
"padding": 0,
"margin": 0
}
});
Ext.getBody().appendChild(parent);
parent.appendChild(measureDiv);
}
if (font) {
measureDiv.setStyle({
font: font,
lineHeight: 'normal'
});
}
measureDiv.setText('(' + text + ')');
size = measureDiv.getSize();
measureDiv.setText('()');
size.width -= measureDiv.getSize().width;
return size;
},
/**
* Measure a single-line text with specific font.
* This will split the text into characters and add up their size.
* That may *not* be the exact size of the text as it is displayed.
* @param {String} text
* @param {String} font
* @return {Object} An object with `width` and `height` properties.
* @return {Number} return.width
* @return {Number} return.height
*/
measureTextSingleLine: function(text, font) {
if (this.precise) {
return this.preciseMeasureTextSingleLine(text, font);
}
text = text.toString();
var cache = this.measureCache,
chars = text.split(''),
width = 0,
height = 0,
cachedItem, charactor, i, ln, size;
if (!cache[font]) {
cache[font] = {};
}
cache = cache[font];
if (cache[text]) {
return cache[text];
}
for (i = 0 , ln = chars.length; i < ln; i++) {
charactor = chars[i];
if (!(cachedItem = cache[charactor])) {
size = this.actualMeasureText(charactor, font);
cachedItem = cache[charactor] = size;
}
width += cachedItem.width;
height = Math.max(height, cachedItem.height);
}
return cache[text] = {
width: width,
height: height
};
},
// A more precise but slower version of the measureTextSingleLine method.
preciseMeasureTextSingleLine: function(text, font) {
text = text.toString();
var measureDiv = this.measureDiv || (this.measureDiv = Ext.getBody().createChild(this.measureDivTpl).down('div'));
measureDiv.setStyle({
font: font || ''
});
return Ext.util.TextMetrics.measure(measureDiv, text);
},
/**
* Measure a text with specific font.
* This will split the text to lines and add up their size.
* That may *not* be the exact size of the text as it is displayed.
* @param {String} text
* @param {String} font
* @return {Object} An object with `width`, `height` and `sizes` properties.
* @return {Number} return.width
* @return {Number} return.height
* @return {Object} return.sizes Results of individual line measurements, in case of multiline text.
*/
measureText: function(text, font) {
var lines = text.split('\n'),
ln = lines.length,
height = 0,
width = 0,
line, i, sizes;
if (ln === 1) {
return this.measureTextSingleLine(text, font);
}
sizes = [];
for (i = 0; i < ln; i++) {
line = this.measureTextSingleLine(lines[i], font);
sizes.push(line);
height += line.height;
width = Math.max(width, line.width);
}
return {
width: width,
height: height,
sizes: sizes
};
}
});
/**
* @class Ext.draw.sprite.Text
* @extends Ext.draw.sprite.Sprite
*
* A sprite that represents text.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'text',
* x: 50,
* y: 50,
* text: 'Sencha',
* fontSize: 30,
* fillStyle: '#1F6D91'
* }]
* });
*/
Ext.define('Ext.draw.sprite.Text', {
extend: 'Ext.draw.sprite.Sprite',
requires: [
'Ext.draw.TextMeasurer',
'Ext.draw.Color'
],
alias: 'sprite.text',
type: 'text',
lineBreakRe: /\n/g,
statics: {
/**
* Debug rendering options:
*
* debug: {
* bbox: true // renders the bounding box of the text sprite
* }
*
*/
debug: false
},
inheritableStatics: {
shortHand1Re: /'(.*)'/g,
shortHand2Re: / /g,
shortHand3Re: /\s*,\s*/g,
shortHand4Re: /\$\$\$\$/g,
def: {
animationProcessors: {
text: 'text'
},
processors: {
/**
* @cfg {Number} [x=0]
* The position of the sprite on the x-axis.
*/
x: 'number',
/**
* @cfg {Number} [y=0]
* The position of the sprite on the y-axis.
*/
y: 'number',
/**
* @cfg {String} [text='']
* The text represented in the sprite.
*/
text: 'string',
/**
* @cfg {String/Number} [fontSize='10px']
* The size of the font displayed.
*/
fontSize: function(n) {
if (!isNaN(n)) {
return +n + 'px';
} else if (n.match(Ext.dom.Element.unitRe)) {
return n;
}
},
/**
* @cfg {String} [fontStyle='']
* The style of the font displayed. {normal, italic, oblique}
*/
fontStyle: 'enums(,italic,oblique)',
/**
* @cfg {String} [fontVariant='']
* The variant of the font displayed. {normal, small-caps}
*/
fontVariant: 'enums(,small-caps)',
/**
* @cfg {String} [fontWeight='']
* The weight of the font displayed. {normal, bold, bolder, lighter}
*/
fontWeight: (function(fontWeights) {
return function(n) {
if (!n) {
return '';
} else if (n === 'normal') {
return '';
} else if (!isNaN(n)) {
n = +n;
if (100 <= n && n <= 900) {
return n;
}
} else if (n in fontWeights) {
return n;
}
};
})({
normal: true,
bold: true,
bolder: true,
lighter: true
}),
/**
* @cfg {String} [fontFamily='sans-serif']
* The family of the font displayed.
*/
fontFamily: 'string',
/**
* @cfg {String} [textAlign='start']
* The alignment of the text displayed. {left, right, center, start, end}
*/
textAlign: (function(textAligns) {
return function(n) {
return textAligns[n] || 'center';
};
})({
start: 'start',
left: 'start',
center: 'center',
middle: 'center',
end: 'end',
right: 'end'
}),
/**
* @cfg {String} [textBaseline="alphabetic"]
* The baseline of the text displayed. {top, hanging, middle, alphabetic, ideographic, bottom}
*/
textBaseline: (function(textBaselines) {
return function(n) {
return textBaselines[n] || 'alphabetic';
};
})({
top: 'top',
hanging: 'hanging',
middle: 'middle',
center: 'middle',
alphabetic: 'alphabetic',
ideographic: 'ideographic',
bottom: 'bottom'
}),
/**
* @cfg {String} [font='10px sans-serif']
* The font displayed.
*/
font: "string",
debug: 'default'
},
aliases: {
'font-size': 'fontSize',
'font-family': 'fontFamily',
'font-weight': 'fontWeight',
'font-variant': 'fontVariant',
'text-anchor': 'textAlign'
},
defaults: {
fontStyle: '',
fontVariant: '',
fontWeight: '',
fontSize: '10px',
fontFamily: 'sans-serif',
font: '10px sans-serif',
textBaseline: 'alphabetic',
textAlign: 'start',
strokeStyle: 'rgba(0, 0, 0, 0)',
fillStyle: '#000',
x: 0,
y: 0,
text: ''
},
triggers: {
fontStyle: 'font,bbox',
fontVariant: 'font,bbox',
fontWeight: 'font,bbox',
fontSize: 'font,bbox',
fontFamily: 'font,bbox',
font: 'font-short-hand,bbox,canvas',
textBaseline: 'bbox',
textAlign: 'bbox',
x: 'bbox',
y: 'bbox',
text: 'bbox'
},
updaters: {
'font-short-hand': (function(dispatcher) {
return function(attrs) {
// TODO: Do this according to http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
var value = attrs.font,
parts, part, i, ln, dispKey;
value = value.replace(Ext.draw.sprite.Text.shortHand1Re, function(a, arg1) {
return arg1.replace(Ext.draw.sprite.Text.shortHand2Re, '$$$$');
});
value = value.replace(Ext.draw.sprite.Text.shortHand3Re, ',');
parts = value.split(' ');
attrs = {};
for (i = 0 , ln = parts.length; i < ln; i++) {
part = parts[i];
dispKey = dispatcher[part];
if (dispKey) {
attrs[dispKey] = part;
} else if (part.match(Ext.dom.Element.unitRe)) {
attrs.fontSize = part;
} else {
attrs.fontFamily = part.replace(Ext.draw.sprite.Text.shortHand4Re, ' ');
}
}
this.setAttributes(attrs, true);
};
})({
'italic': 'fontStyle',
'oblique': 'fontStyle',
'bold': 'fontWeight',
'bolder': 'fontWeight',
'lighter': 'fontWeight',
'100': 'fontWeight',
'200': 'fontWeight',
'300': 'fontWeight',
'400': 'fontWeight',
'500': 'fontWeight',
'600': 'fontWeight',
'700': 'fontWeight',
'800': 'fontWeight',
'900': 'fontWeight',
'small-caps': 'fontVariant'
}),
font: function(attrs) {
var font = '';
if (attrs.fontWeight) {
font += attrs.fontWeight + ' ';
}
if (attrs.fontStyle) {
font += attrs.fontStyle + ' ';
}
if (attrs.fontVariant) {
font += attrs.fontVariant + ' ';
}
if (attrs.fontSize) {
font += attrs.fontSize + ' ';
}
if (attrs.fontFamily) {
font += attrs.fontFamily;
}
this.setAttributes({
font: font
}, true);
}
}
}
},
constructor: function(config) {
if (config && config.font) {
config = Ext.clone(config);
for (var key in config) {
if (key !== 'font' && key.indexOf('font') === 0) {
delete config[key];
}
}
}
Ext.draw.sprite.Sprite.prototype.constructor.call(this, config);
},
// Overriding the getBBox method of the abstract sprite here to always
// recalculate the bounding box of the text in flipped RTL mode
// because in that case the position of the sprite depends not just on
// the value of its 'x' attribute, but also on the width of the surface
// the sprite belongs to.
getBBox: function(isWithoutTransform) {
var me = this,
plain = me.attr.bbox.plain,
surface = me.getSurface();
if (!surface) {
Ext.Error.raise("The sprite does not belong to a surface.");
}
if (plain.dirty) {
me.updatePlainBBox(plain);
plain.dirty = false;
}
if (surface.getInherited().rtl && surface.getFlipRtlText()) {
// Since sprite's attributes haven't actually changed at this point,
// and we just want to update the position of its bbox
// based on surface's width, there's no reason to perform
// expensive text measurement operation here,
// so we can use the result of the last measurement instead.
me.updatePlainBBox(plain, true);
}
return me.callParent([
isWithoutTransform
]);
},
rtlAlignments: {
start: 'end',
center: 'center',
end: 'start'
},
updatePlainBBox: function(plain, useOldSize) {
var me = this,
attr = me.attr,
x = attr.x,
y = attr.y,
dx = [],
font = attr.font,
text = attr.text,
baseline = attr.textBaseline,
alignment = attr.textAlign,
size = (useOldSize && me.oldSize) ? me.oldSize : (me.oldSize = Ext.draw.TextMeasurer.measureText(text, font)),
surface = me.getSurface(),
isRtl = surface.getInherited().rtl,
flipRtlText = isRtl && surface.getFlipRtlText(),
rect = surface.getRect(),
sizes = size.sizes,
blockHeight = size.height,
blockWidth = size.width,
ln = sizes ? sizes.length : 0,
lineWidth,
i = 0;
// To get consistent results in all browsers we don't apply textAlign and textBaseline
// attributes of the sprite to context, so text is always left aligned and has an alphabetic baseline.
// Instead we have to calculate the horizontal offset of each line based on sprite's textAlign,
// and the vertical offset of the bounding box based on sprite's textBaseline.
// These offsets are then used by the sprite's 'render' method to position text properly.
switch (baseline) {
case 'hanging':
case 'top':
break;
case 'ideographic':
case 'bottom':
y -= blockHeight;
break;
case 'alphabetic':
y -= blockHeight * 0.8;
break;
case 'middle':
y -= blockHeight * 0.5;
break;
}
if (flipRtlText) {
x = rect[2] - rect[0] - x;
alignment = me.rtlAlignments[alignment];
}
switch (alignment) {
case 'start':
if (isRtl) {
for (; i < ln; i++) {
lineWidth = sizes[i].width;
dx.push(-(blockWidth - lineWidth));
}
};
break;
case 'end':
x -= blockWidth;
if (isRtl) {
break;
};
for (; i < ln; i++) {
lineWidth = sizes[i].width;
dx.push(blockWidth - lineWidth);
};
break;
case 'center':
x -= blockWidth * 0.5;
for (; i < ln; i++) {
lineWidth = sizes[i].width;
dx.push((isRtl ? -1 : 1) * (blockWidth - lineWidth) * 0.5);
};
break;
}
attr.textAlignOffsets = dx;
plain.x = x;
plain.y = y;
plain.width = blockWidth;
plain.height = blockHeight;
},
setText: function(text) {
this.setAttributes({
text: text
}, true);
},
setElementStyles: function(element, styles) {
var stylesCache = element.stylesCache || (element.stylesCache = {}),
style = element.dom.style,
name;
for (name in styles) {
if (stylesCache[name] !== styles[name]) {
stylesCache[name] = style[name] = styles[name];
}
}
},
renderBBox: function(surface, ctx) {
var bbox = this.getBBox(true);
ctx.beginPath();
ctx.moveTo(bbox.x, bbox.y);
ctx.lineTo(bbox.x + bbox.width, bbox.y);
ctx.lineTo(bbox.x + bbox.width, bbox.y + bbox.height);
ctx.lineTo(bbox.x, bbox.y + bbox.height);
ctx.closePath();
ctx.strokeStyle = 'red';
ctx.strokeOpacity = 1;
ctx.lineWidth = 0.5;
ctx.stroke();
},
render: function(surface, ctx, rect) {
var me = this,
attr = me.attr,
mat = Ext.draw.Matrix.fly(attr.matrix.elements.slice(0)),
bbox = me.getBBox(true),
dx = attr.textAlignOffsets,
none = Ext.draw.Color.RGBA_NONE,
x, y, i, lines, lineHeight;
if (attr.text.length === 0) {
return;
}
lines = attr.text.split('\n');
lineHeight = bbox.height / lines.length;
// Simulate textBaseline and textAlign.
x = attr.bbox.plain.x;
// lineHeight * 0.78 is the approximate distance between the top and the alphabetic baselines
y = attr.bbox.plain.y + lineHeight * 0.78;
mat.toContext(ctx);
if (surface.getInherited().rtl) {
// Canvas element in RTL mode automatically flips text alignment.
// Here we compensate for that change.
// So text is still positioned and aligned as in the LTR mode,
// but the direction of the text is RTL.
x += attr.bbox.plain.width;
}
for (i = 0; i < lines.length; i++) {
if (ctx.fillStyle !== none) {
ctx.fillText(lines[i], x + (dx[i] || 0), y + lineHeight * i);
}
if (ctx.strokeStyle !== none) {
ctx.strokeText(lines[i], x + (dx[i] || 0), y + lineHeight * i);
}
}
var debug = me.statics().debug || attr.debug;
if (debug) {
debug.bbox && me.renderBBox(surface, ctx);
}
}
});
/**
* A veritical line sprite. The x and y configs set the center of the line with the size
* value determining the height of the line (the line will be twice the height of 'size'
* since 'size' is added to above and below 'y' to set the line endpoints).
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'tick',
* x: 20,
* y: 40,
* size: 10,
* strokeStyle: '#388FAD',
* lineWidth: 2
* }]
* });
*/
Ext.define('Ext.draw.sprite.Tick', {
extend: 'Ext.draw.sprite.Line',
alias: 'sprite.tick',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Object} x The position of the center of the sprite on the x-axis.
*/
x: 'number',
/**
* @cfg {Object} y The position of the center of the sprite on the y-axis.
*/
y: 'number',
/**
* @cfg {Number} [size=4] The size of the sprite.
* Meant to be comparable to the size of a circle sprite with the same radius.
*/
size: 'number'
},
defaults: {
x: 0,
y: 0,
size: 4
},
triggers: {
x: 'tick',
y: 'tick',
size: 'tick'
},
updaters: {
tick: function(attr) {
var size = attr.size * 1.5,
halfLineWidth = attr.lineWidth / 2,
x = attr.x,
y = attr.y;
this.setAttributes({
fromX: x - halfLineWidth,
fromY: y - size,
toX: x - halfLineWidth,
toY: y + size
});
}
}
}
}
});
/**
* A sprite that represents a triangle.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'triangle',
* size: 50,
* translationX: 100,
* translationY: 100,
* fillStyle: '#1F6D91'
* }]
* });
*
*/
Ext.define('Ext.draw.sprite.Triangle', {
extend: 'Ext.draw.sprite.Path',
alias: 'sprite.triangle',
inheritableStatics: {
def: {
processors: {
x: 'number',
y: 'number',
/**
* @cfg {Number} [size=4] The size of the sprite.
* Meant to be comparable to the size of a circle sprite with the same radius.
*/
size: 'number'
},
defaults: {
x: 0,
y: 0,
size: 4
},
triggers: {
x: 'path',
y: 'path',
size: 'path'
}
}
},
updatePath: function(path, attr) {
var s = attr.size * 2.2,
x = attr.x,
y = attr.y;
path.fromSvgString('M'.concat(x, ',', y, 'm0-', s * 0.58, 'l', s * 0.5, ',', s * 0.87, '-', s, ',0z'));
}
});
/**
* Linear gradient.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'circle',
* cx: 100,
* cy: 100,
* r: 100,
* fillStyle: {
* type: 'linear',
* degrees: 180,
* stops: [{
* offset: 0,
* color: '#1F6D91'
* }, {
* offset: 1,
* color: '#90BCC9'
* }]
* }
* }]
* });
*/
Ext.define('Ext.draw.gradient.Linear', {
extend: 'Ext.draw.gradient.Gradient',
requires: [
'Ext.draw.Color'
],
type: 'linear',
config: {
/**
* @cfg {Number}
* The angle of rotation of the gradient in degrees.
*/
degrees: 0,
/**
* @cfg {Number}
* The angle of rotation of the gradient in radians.
*/
radians: 0
},
applyRadians: function(radians, oldRadians) {
if (Ext.isNumber(radians)) {
return radians;
}
return oldRadians;
},
applyDegrees: function(degrees, oldDegrees) {
if (Ext.isNumber(degrees)) {
return degrees;
}
return oldDegrees;
},
updateRadians: function(radians) {
this.setDegrees(Ext.draw.Draw.degrees(radians));
},
updateDegrees: function(degrees) {
this.setRadians(Ext.draw.Draw.rad(degrees));
},
/**
* @inheritdoc
*/
generateGradient: function(ctx, bbox) {
var angle = this.getRadians(),
cos = Math.cos(angle),
sin = Math.sin(angle),
w = bbox.width,
h = bbox.height,
cx = bbox.x + w * 0.5,
cy = bbox.y + h * 0.5,
stops = this.getStops(),
ln = stops.length,
gradient, l, i;
if (!isNaN(cx) && !isNaN(cy) && h > 0 && w > 0) {
l = (Math.sqrt(h * h + w * w) * Math.abs(Math.cos(angle - Math.atan(h / w)))) / 2;
gradient = ctx.createLinearGradient(cx + cos * l, cy + sin * l, cx - cos * l, cy - sin * l);
for (i = 0; i < ln; i++) {
gradient.addColorStop(stops[i].offset, stops[i].color);
}
return gradient;
}
return Ext.draw.Color.NONE;
}
});
/**
* Radial gradient.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 600,
* height: 400,
* sprites: [{
* type: 'circle',
* cx: 100,
* cy: 100,
* r: 100,
* fillStyle: {
* type: 'radial',
* start: {
* x: 0,
* y: 0,
* r: 0
* },
* end: {
* x: 0,
* y: 0,
* r: 1
* },
* stops: [{
* offset: 0,
* color: '#90BCC9'
* }, {
* offset: 1,
* color: '#1F6D91'
* }]
* }
* }]
* });
*/
Ext.define('Ext.draw.gradient.Radial', {
extend: 'Ext.draw.gradient.Gradient',
type: 'radial',
config: {
/**
* @cfg {Object} start The starting circle of the gradient.
*/
start: {
x: 0,
y: 0,
r: 0
},
/**
* @cfg {Object} end The ending circle of the gradient.
*/
end: {
x: 0,
y: 0,
r: 1
}
},
applyStart: function(newStart, oldStart) {
if (!oldStart) {
return newStart;
}
var circle = {
x: oldStart.x,
y: oldStart.y,
r: oldStart.r
};
if ('x' in newStart) {
circle.x = newStart.x;
} else if ('centerX' in newStart) {
circle.x = newStart.centerX;
}
if ('y' in newStart) {
circle.y = newStart.y;
} else if ('centerY' in newStart) {
circle.y = newStart.centerY;
}
if ('r' in newStart) {
circle.r = newStart.r;
} else if ('radius' in newStart) {
circle.r = newStart.radius;
}
return circle;
},
applyEnd: function(newEnd, oldEnd) {
if (!oldEnd) {
return newEnd;
}
var circle = {
x: oldEnd.x,
y: oldEnd.y,
r: oldEnd.r
};
if ('x' in newEnd) {
circle.x = newEnd.x;
} else if ('centerX' in newEnd) {
circle.x = newEnd.centerX;
}
if ('y' in newEnd) {
circle.y = newEnd.y;
} else if ('centerY' in newEnd) {
circle.y = newEnd.centerY;
}
if ('r' in newEnd) {
circle.r = newEnd.r;
} else if ('radius' in newEnd) {
circle.r = newEnd.radius;
}
return circle;
},
/**
* @inheritdoc
*/
generateGradient: function(ctx, bbox) {
var start = this.getStart(),
end = this.getEnd(),
w = bbox.width * 0.5,
h = bbox.height * 0.5,
x = bbox.x + w,
y = bbox.y + h,
gradient = ctx.createRadialGradient(x + start.x * w, y + start.y * h, start.r * Math.max(w, h), x + end.x * w, y + end.y * h, end.r * Math.max(w, h)),
stops = this.getStops(),
ln = stops.length,
i;
for (i = 0; i < ln; i++) {
gradient.addColorStop(stops[i].offset, stops[i].color);
}
return gradient;
}
});
/**
* A Surface is an interface to render methods inside a draw {@link Ext.draw.Container}.
* A Surface contains methods to render sprites, get bounding boxes of sprites, add
* sprites to the canvas, initialize other graphic components, etc. One of the most used
* methods for this class is the `add` method, to add Sprites to the surface.
*
* Most of the Surface methods are abstract and they have a concrete implementation
* in Canvas or SVG engines.
*
* A Surface instance can be accessed as a property of a draw container. For example:
*
* drawContainer.getSurface('main').add({
* type: 'circle',
* fill: '#ffc',
* radius: 100,
* x: 100,
* y: 100
* });
* drawContainer.renderFrame();
*
* The configuration object passed in the `add` method is the same as described in the {@link Ext.draw.sprite.Sprite}
* class documentation.
*
* ## Example
*
* drawContainer.getSurface('main').add([
* {
* type: 'circle',
* radius: 10,
* fill: '#f00',
* x: 10,
* y: 10
* },
* {
* type: 'circle',
* radius: 10,
* fill: '#0f0',
* x: 50,
* y: 50
* },
* {
* type: 'circle',
* radius: 10,
* fill: '#00f',
* x: 100,
* y: 100
* },
* {
* type: 'rect',
* radius: 10,
* x: 10,
* y: 10
* },
* {
* type: 'rect',
* radius: 10,
* x: 50,
* y: 50
* },
* {
* type: 'rect',
* radius: 10,
* x: 100,
* y: 100
* }
* ]);
* drawContainer.renderFrame();
*
*/
Ext.define('Ext.draw.Surface', {
extend: 'Ext.draw.SurfaceBase',
xtype: 'surface',
requires: [
'Ext.draw.sprite.*',
'Ext.draw.gradient.*',
'Ext.draw.sprite.AttributeDefinition',
'Ext.draw.Matrix',
'Ext.draw.Draw'
],
uses: [
'Ext.draw.engine.Canvas'
],
/**
* The reported device pixel density.
*/
devicePixelRatio: window.devicePixelRatio || 1,
deprecated: {
'5.1.0': {
statics: {
methods: {
/**
* @deprecated 5.1.0
* Stably sort the list of sprites by their zIndex.
* Deprecated, use the {@link Ext.Array#sort} method instead.
* @param {Array} list
* @return {Array} Sorted array.
*/
stableSort: function(list) {
return Ext.Array.sort(list, function(a, b) {
return a.attr.zIndex - b.attr.zIndex;
});
}
}
}
}
},
config: {
cls: Ext.baseCSSPrefix + 'surface',
/**
* @cfg {Array}
* The [x, y, width, height] rect of the surface related to its container.
*/
rect: null,
/**
* @cfg {Object}
* Background sprite config of the surface.
*/
background: null,
/**
* @cfg {Array}
* Array of sprite instances.
*/
items: [],
/**
* @cfg {Boolean}
* Indicates whether the surface needs to redraw.
*/
dirty: false,
/**
* @cfg {Boolean} flipRtlText
* If the surface is in the RTL mode, text will render with the RTL direction,
* but the alignment and position of the text won't change by default.
* Setting this config to 'true' will get text alignment and its position
* within a surface mirrored.
*/
flipRtlText: false
},
isSurface: true,
dirtyPredecessor: 0,
constructor: function(config) {
var me = this;
me.predecessors = [];
me.successors = [];
// The `pendingRenderFrame` flag is used to indicate that `predecessors` (surfaces that should render first)
// are dirty, and to call `renderFrame` when all `predecessors` have their `renderFrame` called
// (i.e. not dirty anymore).
me.pendingRenderFrame = false;
me.map = {};
me.callParent([
config
]);
me.matrix = new Ext.draw.Matrix();
me.inverseMatrix = me.matrix.inverse(me.inverseMatrix);
me.resetTransform();
},
/**
* Round the number to align to the pixels on device.
* @param {Number} num The number to align.
* @return {Number} The resultant alignment.
*/
roundPixel: function(num) {
return Math.round(this.devicePixelRatio * num) / this.devicePixelRatio;
},
/**
* Mark the surface to render after another surface is updated.
* @param {Ext.draw.Surface} surface The surface to wait for.
*/
waitFor: function(surface) {
var me = this,
predecessors = me.predecessors;
if (!Ext.Array.contains(predecessors, surface)) {
predecessors.push(surface);
surface.successors.push(me);
if (surface._dirty) {
me.dirtyPredecessor++;
}
}
},
setDirty: function(dirty) {
if (this._dirty !== dirty) {
var successors = this.successors,
successor, i,
ln = successors.length;
for (i = 0; i < ln; i++) {
successor = successors[i];
if (dirty) {
successor.dirtyPredecessor++;
successor.setDirty(true);
} else {
successor.dirtyPredecessor--;
if (successor.dirtyPredecessor === 0 && successor.pendingRenderFrame) {
successor.renderFrame();
}
}
}
this._dirty = dirty;
}
},
applyElement: function(newElement, oldElement) {
if (oldElement) {
oldElement.set(newElement);
} else {
oldElement = Ext.Element.create(newElement);
}
this.setDirty(true);
return oldElement;
},
applyBackground: function(background, oldBackground) {
this.setDirty(true);
if (Ext.isString(background)) {
background = {
fillStyle: background
};
}
return Ext.factory(background, Ext.draw.sprite.Rect, oldBackground);
},
applyRect: function(rect, oldRect) {
if (oldRect && rect[0] === oldRect[0] && rect[1] === oldRect[1] && rect[2] === oldRect[2] && rect[3] === oldRect[3]) {
return;
}
if (Ext.isArray(rect)) {
return [
rect[0],
rect[1],
rect[2],
rect[3]
];
} else if (Ext.isObject(rect)) {
return [
rect.x || rect.left,
rect.y || rect.top,
rect.width || (rect.right - rect.left),
rect.height || (rect.bottom - rect.top)
];
}
},
updateRect: function(rect) {
var me = this,
l = rect[0],
t = rect[1],
r = l + rect[2],
b = t + rect[3],
background = me.getBackground(),
element = me.element;
element.setLocalXY(Math.floor(l), Math.floor(t));
element.setSize(Math.ceil(r - Math.floor(l)), Math.ceil(b - Math.floor(t)));
if (background) {
background.setAttributes({
x: 0,
y: 0,
width: Math.ceil(r - Math.floor(l)),
height: Math.ceil(b - Math.floor(t))
});
}
me.setDirty(true);
},
/**
* Reset the matrix of the surface.
*/
resetTransform: function() {
this.matrix.set(1, 0, 0, 1, 0, 0);
this.inverseMatrix.set(1, 0, 0, 1, 0, 0);
this.setDirty(true);
},
/**
* Get the sprite by id or index.
* It will first try to find a sprite with the given id, otherwise will try to use the id as an index.
* @param {String|Number} id
* @return {Ext.draw.sprite.Sprite}
*/
get: function(id) {
return this.map[id] || this.items[id];
},
/**
* Add a Sprite to the surface.
* You can put any number of object as parameter.
* See {@link Ext.draw.sprite.Sprite} for the configuration object to be passed into this method.
*
* For example:
*
* drawContainer.getSurface().add({
* type: 'circle',
* fill: '#ffc',
* radius: 100,
* x: 100,
* y: 100
* });
* drawContainer.renderFrame();
*
*/
add: function() {
var me = this,
args = Array.prototype.slice.call(arguments),
argIsArray = Ext.isArray(args[0]),
results = [],
sprite, sprites, items, i, ln;
items = Ext.Array.clean(argIsArray ? args[0] : args);
if (!items.length) {
return results;
}
sprites = me.prepareItems(items);
for (i = 0 , ln = sprites.length; i < ln; i++) {
sprite = sprites[i];
me.map[sprite.getId()] = sprite;
results.push(sprite);
sprite.setParent(me);
sprite.setSurface(me);
me.onAdd(sprite);
}
items = me.getItems();
if (items) {
items.push.apply(items, results);
}
me.dirtyZIndex = true;
me.setDirty(true);
if (!argIsArray && results.length === 1) {
return results[0];
} else {
return results;
}
},
/**
* @protected
* Invoked when a sprite is added to the surface.
* @param {Ext.draw.sprite.Sprite} sprite The sprite to be added.
*/
onAdd: Ext.emptyFn,
/**
* Remove a given sprite from the surface, optionally destroying the sprite in the process.
* You can also call the sprite own `remove` method.
*
* For example:
*
* drawContainer.surface.remove(sprite);
* // or...
* sprite.remove();
*
* @param {Ext.draw.sprite.Sprite} sprite
* @param {Boolean} [destroySprite=false]
*/
remove: function(sprite, destroySprite) {
if (sprite) {
delete this.map[sprite.getId()];
if (destroySprite) {
sprite.destroy();
} else {
sprite.setParent(null);
sprite.setSurface(null);
Ext.Array.remove(this.getItems(), sprite);
}
this.dirtyZIndex = true;
this.setDirty(true);
}
},
/**
* Remove all sprites from the surface, optionally destroying the sprites in the process.
*
* For example:
*
* drawContainer.getSurface('main').removeAll();
*
* @param {Boolean} [destroySprites=false]
*/
removeAll: function(destroySprites) {
var items = this.getItems(),
i = items.length,
item;
if (destroySprites) {
while (i > 0) {
items[--i].destroy();
}
} else {
while (i > 0) {
i--;
item = items[i];
item.setParent(null);
item.setSurface(null);
}
}
items.length = 0;
this.map = {};
this.dirtyZIndex = true;
},
// @private
applyItems: function(items) {
if (this.getItems()) {
this.removeAll(true);
}
return Ext.Array.from(this.add(items));
},
/**
* @private
* Initialize and apply defaults to surface items.
*/
prepareItems: function(items) {
items = [].concat(items);
// Make sure defaults are applied and item is initialized
var me = this,
item, i, ln, j,
removeSprite = function(sprite) {
this.remove(sprite, false);
};
for (i = 0 , ln = items.length; i < ln; i++) {
item = items[i];
if (!(item instanceof Ext.draw.sprite.Sprite)) {
// Temporary, just take in configs...
item = items[i] = me.createItem(item);
}
item.on('beforedestroy', removeSprite, me);
}
return items;
},
/**
* @private Creates an item and appends it to the surface. Called
* as an internal method when calling `add`.
*/
createItem: function(config) {
return Ext.create(config.xclass || 'sprite.' + config.type, config);
},
/**
* Return the minimal bounding box that contains all the sprites bounding boxes in the given list of sprites.
* @param {Ext.draw.sprite.Sprite[]|Ext.draw.sprite.Sprite} sprites
* @param {Boolean} [isWithoutTransform=false]
* @return {{x: Number, y: Number, width: number, height: number}}
*/
getBBox: function(sprites, isWithoutTransform) {
var sprites = Ext.Array.from(sprites),
left = Infinity,
right = -Infinity,
top = Infinity,
bottom = -Infinity,
sprite, bbox, i, ln;
for (i = 0 , ln = sprites.length; i < ln; i++) {
sprite = sprites[i];
bbox = sprite.getBBox(isWithoutTransform);
if (left > bbox.x) {
left = bbox.x;
}
if (right < bbox.x + bbox.width) {
right = bbox.x + bbox.width;
}
if (top > bbox.y) {
top = bbox.y;
}
if (bottom < bbox.y + bbox.height) {
bottom = bbox.y + bbox.height;
}
}
return {
x: left,
y: top,
width: right - left,
height: bottom - top
};
},
emptyRect: [
0,
0,
0,
0
],
// Converts event's page coordinates into surface coordinates.
// Note: surface's x-coordinates always go LTR, regardless of RTL mode.
getEventXY: function(e) {
var me = this,
isRtl = me.getInherited().rtl,
pageXY = e.getXY(),
// Event position in page coordinates.
container = me.el.up(),
xy = container.getXY(),
// Surface container position in page coordinates.
rect = me.getRect() || me.emptyRect,
// Surface position in surface container coordinates (LTR).
result = [],
width;
if (isRtl) {
width = container.getWidth();
// The line below is actually a simplified form of
// rect[2] - (pageXY[0] - xy[0] - (width - (rect[0] + rect[2]))).
result[0] = xy[0] - pageXY[0] - rect[0] + width;
} else {
result[0] = pageXY[0] - xy[0] - rect[0];
}
result[1] = pageXY[1] - xy[1] - rect[1];
return result;
},
/**
* Empty the surface content (without touching the sprites.)
*/
clear: Ext.emptyFn,
/**
* @private
* Order the items by their z-index if any of that has been changed since last sort.
*/
orderByZIndex: function() {
var me = this,
items = me.getItems(),
dirtyZIndex = false,
i, ln;
if (me.getDirty()) {
for (i = 0 , ln = items.length; i < ln; i++) {
if (items[i].attr.dirtyZIndex) {
dirtyZIndex = true;
break;
}
}
if (dirtyZIndex) {
// sort by zIndex
Ext.Array.sort(items, function(a, b) {
return a.attr.zIndex - b.attr.zIndex;
});
this.setDirty(true);
}
for (i = 0 , ln = items.length; i < ln; i++) {
items[i].attr.dirtyZIndex = false;
}
}
},
/**
* Force the element to redraw.
*/
repaint: function() {
var me = this;
me.repaint = Ext.emptyFn;
Ext.defer(function() {
delete me.repaint;
me.element.repaint();
}, 1);
},
/**
* Triggers the re-rendering of the canvas.
*/
renderFrame: function() {
if (!this.element) {
return;
}
if (this.dirtyPredecessor > 0) {
this.pendingRenderFrame = true;
return;
}
var me = this,
rect = this.getRect(),
background = me.getBackground(),
items = me.getItems(),
item, i, ln;
// Cannot render before the surface is placed.
if (!rect) {
return;
}
// This will also check the dirty flags of the sprites.
me.orderByZIndex();
if (me.getDirty()) {
me.clear();
me.clearTransform();
if (background) {
me.renderSprite(background);
}
for (i = 0 , ln = items.length; i < ln; i++) {
item = items[i];
if (false === me.renderSprite(item)) {
return;
}
item.attr.textPositionCount = me.textPosition;
}
me.setDirty(false);
}
},
/**
* @private
* Renders a single sprite into the surface.
* Do not call it from outside `renderFrame` method.
*
* @param {Ext.draw.sprite.Sprite} sprite The Sprite to be rendered.
* @return {Boolean} returns `false` to stop the rendering to continue.
*/
renderSprite: Ext.emptyFn,
/**
* @method flatten
* Flattens the given drawing surfaces into a single image
* and returns an object containing the data (in the DataURL format)
* and the type (e.g. 'png' or 'svg') of that image.
* @param {Object} size The size of the final image.
* @param {Number} size.width
* @param {Number} size.height
* @param {Ext.draw.Surface[]} surfaces The surfaces to flatten.
* @return {Object}
* @return {String} return.data The DataURL of the flattened image.
* @return {String} return.type The type of the image.
*
*/
/**
* @private
* Clears the current transformation state on the surface.
*/
clearTransform: Ext.emptyFn,
/**
* Returns 'true' if the surface is dirty.
* @return {Boolean} 'true' if the surface is dirty
*/
getDirty: function() {
return this._dirty;
},
/**
* Destroys the surface. This is done by removing all components from it and
* also removing its reference to a DOM element.
*
* For example:
*
* drawContainer.surface.destroy();
*/
destroy: function() {
var me = this;
me.removeAll();
me.setBackground(null);
me.predecessors = null;
me.successors = null;
me.callParent();
}
});
Ext.define('Ext.draw.overrides.Surface', {
override: 'Ext.draw.Surface',
hitOptions: {
fill: true,
stroke: true
},
/**
* Performs a hit test on all sprites in the surface, returning the first matching one.
* @param {Array} point A two-item array containing x and y coordinates of the point.
* @param {Object} options Hit testing options.
* @return {Object} A hit result object that contains more information about what
* exactly was hit or null if nothing was hit.
* @member Ext.draw.Surface
*/
hitTest: function(point, options) {
var me = this,
sprites = me.getItems(),
i, sprite, result;
options = options || me.hitOptions;
for (i = sprites.length - 1; i >= 0; i--) {
sprite = sprites[i];
if (sprite.hitTest) {
result = sprite.hitTest(point, options);
if (result) {
return result;
}
}
}
return null;
},
/**
* Performs a hit test on all sprites in the surface, returning the first matching one.
* Since hit testing is typically performed on mouse events, this convenience method
* converts event's page coordinates to surface coordinates before calling {@link #hitTest}.
* @param {Array} point An event object.
* @param {Object} options Hit testing options.
* @return {Object} A hit result object that contains more information about what
* exactly was hit or null if nothing was hit.
* @member Ext.draw.Surface
*/
hitTestEvent: function(event, options) {
var xy = this.getEventXY(event);
return this.hitTest(xy, options);
}
});
/**
* @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",
"lineDash": [],
"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) {
this.surface = SvgSurface;
this.state = [];
this.matrix = new Ext.draw.Matrix();
this.path = null;
this.clear();
},
/**
* Clears the context.
*/
clear: function() {
this.group = this.surface.mainGroup;
this.position = 0;
this.path = null;
},
/**
* @private
* @param {String} tag
* @return {*}
*/
getElement: function(tag) {
return this.surface.getSvgElement(this.group, tag, this.position++);
},
/**
* @private
*
* Destroys the DOM element and all associated gradients.
*
* @param element {HTMLElement|Ext.dom.Element|String} DOM element.
*/
removeElement: function(element) {
var element = Ext.fly(element),
fill, stroke, fillMatch, strokeMatch, gradients, gradient, key;
if (!element) {
return;
}
if (element.dom.tagName === 'g') {
gradients = element.dom.gradients;
for (key in gradients) {
gradients[key].destroy();
}
} else {
fill = element.getAttribute('fill');
stroke = element.getAttribute('stroke');
fillMatch = fill && fill.match(this.urlStringRe);
strokeMatch = stroke && stroke.match(this.urlStringRe);
if (fillMatch && fillMatch[1]) {
gradient = Ext.fly(fillMatch[1]);
if (gradient) {
gradient.destroy();
}
}
if (strokeMatch && strokeMatch[1]) {
gradient = Ext.fly(strokeMatch[1]);
if (gradient) {
gradient.destroy();
}
}
}
element.destroy();
},
/**
* 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(),
children = this.group.dom.childNodes,
key, i;
// Removing extra DOM elements that were not reused.
while (children.length > this.position) {
this.removeElement(children[children.length - 1]);
}
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 = this.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) {
if (this.path) {
var 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) {
text = String(text);
if (this.strokeStyle) {
var 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) {
text = String(text);
if (this.fillStyle) {
var 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() {
if (!this.path) {
return;
}
if (this.fillStyle) {
var path,
fillGradient = this.fillGradient,
bbox = this.bbox,
element = this.path.element;
if (!element) {
path = this.path.toString();
element = this.path.element = this.getElement('path');
this.surface.setElementAttributes(element, {
"d": path,
"transform": this.matrix.toSvg()
});
}
this.surface.setElementAttributes(element, {
"fill": fillGradient && bbox ? fillGradient.generateGradient(this, bbox) : this.fillStyle,
"fill-opacity": this.fillOpacity * this.globalAlpha
});
}
},
/**
* Strokes the subpaths of the current default path or the given path with the current stroke style.
*/
stroke: function() {
if (!this.path) {
return;
}
if (this.strokeStyle) {
var path,
strokeGradient = this.strokeGradient,
bbox = this.bbox,
element = this.path.element;
if (!element || !this.path.svgString) {
path = this.path.toString();
if (!path) {
return;
}
element = this.path.element = this.getElement('path');
this.surface.setElementAttributes(element, {
"fill": "none",
"d": path,
"transform": this.matrix.toSvg()
});
}
this.surface.setElementAttributes(element, {
"stroke": strokeGradient && bbox ? strokeGradient.generateGradient(this, bbox) : this.strokeStyle,
"stroke-linecap": this.lineCap,
"stroke-linejoin": this.lineJoin,
"stroke-width": this.lineWidth,
"stroke-opacity": this.strokeOpacity * this.globalAlpha,
"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
});
}
}
},
/**
* @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'),
gradients = me.group.dom.gradients || (me.group.dom.gradients = {}),
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);
gradients[element.dom.id] = gradient;
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'),
gradients = me.group.dom.gradients || (me.group.dom.gradients = {}),
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);
gradients[element.dom.id] = gradient;
return gradient;
}
});
/**
* @class Ext.draw.engine.SvgContext.Gradient
*/
Ext.define("Ext.draw.engine.SvgContext.Gradient", {
statics: {
map: {}
},
constructor: function(ctx, surface, element, compression) {
var map = this.statics().map,
oldInstance;
// Because of the way Ext.draw.engine.Svg.getNextDef works,
// there is no guarantee that an existing DOM element from the 'defs' section won't be used
// for the 'element' param.
oldInstance = map[element.dom.id];
if (oldInstance) {
oldInstance.element = null;
}
map[element.dom.id] = this;
this.ctx = ctx;
this.surface = surface;
this.element = element;
this.position = 0;
this.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 stop = this.surface.getSvgElement(this.element, 'stop', this.position++),
compression = this.compression;
this.surface.setElementAttributes(stop, {
"offset": (((1 - compression) * offset + compression) * 100).toFixed(2) + '%',
"stop-color": color,
"stop-opacity": Ext.draw.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() + ')';
},
destroy: function() {
var map = this.statics().map,
element = this.element;
if (element) {
delete map[element.dom.id];
element.destroy();
}
this.callParent();
}
});
/**
* @class Ext.draw.engine.Svg
* @extends Ext.draw.Surface
*
* SVG engine.
*/
Ext.define('Ext.draw.engine.Svg', {
extend: 'Ext.draw.Surface',
requires: [
'Ext.draw.engine.SvgContext'
],
statics: {
BBoxTextCache: {}
},
config: {
/**
* Nothing needs to be done in high precision mode.
*/
highPrecision: false
},
getElementConfig: function() {
//TODO:ps In the Ext world, use renderTpl to create the children
return {
reference: 'element',
style: {
position: 'absolute'
},
children: [
{
reference: 'innerElement',
style: {
width: '100%',
height: '100%',
position: 'relative'
},
children: [
{
tag: 'svg',
reference: 'svgElement',
namespace: "http://www.w3.org/2000/svg",
width: '100%',
height: '100%',
version: 1.1
}
]
}
]
};
},
constructor: function(config) {
var me = this;
me.callParent([
config
]);
me.mainGroup = me.createSvgNode("g");
me.defElement = me.createSvgNode("defs");
// me.svgElement is assigned in element creation of Ext.Component.
me.svgElement.appendChild(me.mainGroup);
me.svgElement.appendChild(me.defElement);
me.ctx = new Ext.draw.engine.SvgContext(me);
},
/**
* Creates a DOM element under the SVG namespace of the given type.
* @param {String} type The type of the SVG DOM element.
* @return {*} The created element.
*/
createSvgNode: function(type) {
var node = document.createElementNS("http://www.w3.org/2000/svg", type);
return Ext.get(node);
},
/**
* @private
* Returns the SVG DOM element at the given position. If it does not already exist or is a different element tag
* it will be created and inserted into the DOM.
* @param {Ext.dom.Element} group The parent DOM element.
* @param {String} tag The SVG element tag.
* @param {Number} position The position of the element in the DOM.
* @return {Ext.dom.Element} The SVG element.
*/
getSvgElement: function(group, tag, position) {
var element;
if (group.dom.childNodes.length > position) {
element = group.dom.childNodes[position];
if (element.tagName === tag) {
return Ext.get(element);
} else {
Ext.destroy(element);
}
}
element = Ext.get(this.createSvgNode(tag));
if (position === 0) {
group.insertFirst(element);
} else {
element.insertAfter(Ext.fly(group.dom.childNodes[position - 1]));
}
element.cache = {};
return element;
},
/**
* @private
* Applies attributes to the given element.
* @param {Ext.dom.Element} element The DOM element to be applied.
* @param {Object} attributes The attributes to apply to the element.
*/
setElementAttributes: function(element, attributes) {
var dom = element.dom,
cache = element.cache,
name, value;
for (name in attributes) {
value = attributes[name];
if (cache[name] !== value) {
cache[name] = value;
dom.setAttribute(name, value);
}
}
},
/**
* @private
* Gets the next reference element under the SVG 'defs' tag.
* @param {String} tagName The type of reference element.
* @return {Ext.dom.Element} The reference element.
*/
getNextDef: function(tagName) {
return this.getSvgElement(this.defElement, tagName, this.defPosition++);
},
/**
* @inheritdoc
*/
clearTransform: function() {
var me = this;
me.mainGroup.set({
transform: me.matrix.toSvg()
});
},
/**
* @inheritdoc
*/
clear: function() {
this.ctx.clear();
this.defPosition = 0;
},
/**
* @inheritdoc
*/
renderSprite: function(sprite) {
var me = this,
rect = me.getRect(),
ctx = me.ctx;
if (sprite.attr.hidden || sprite.attr.opacity === 0) {
ctx.save();
ctx.restore();
return;
}
sprite.element = ctx.save();
sprite.preRender(this);
sprite.useAttributes(ctx, rect);
if (false === sprite.render(this, ctx, [
0,
0,
rect[2],
rect[3]
])) {
return false;
}
sprite.setDirty(false);
ctx.restore();
},
flatten: function(size, surfaces) {
var svg = '',
className = Ext.getClassName(this),
surface, rect, i;
svg += '';
return {
data: 'data:image/svg+xml;utf8,' + encodeURIComponent(svg),
type: 'svg'
};
},
/**
* @private
* Serializes an SVG DOM element and its children recursively into a string.
* @param {Object} node DOM element to serialize.
* @return {String}
*/
serializeNode: function(node) {
var result = '',
i, n, attr, child;
if (node.nodeType === document.TEXT_NODE) {
return node.nodeValue;
}
result += '<' + node.nodeName;
if (node.attributes.length) {
for (i = 0 , n = node.attributes.length; i < n; i++) {
attr = node.attributes[i];
result += ' ' + attr.name + '="' + attr.value + '"';
}
}
result += '>';
if (node.childNodes && node.childNodes.length) {
for (i = 0 , n = node.childNodes.length; i < n; i++) {
child = node.childNodes[i];
result += this.serializeNode(child);
}
}
result += '' + node.nodeName + '>';
return result;
},
/**
* Destroys the Canvas element and prepares it for Garbage Collection.
*/
destroy: function(path, matrix, band) {
var me = this;
me.ctx.destroy();
me.mainGroup.destroy();
delete me.mainGroup;
delete me.ctx;
me.callParent(arguments);
},
remove: function(sprite, destroySprite) {
if (sprite && sprite.element) {
//if sprite has an associated svg element remove it from the surface
if (this.ctx) {
this.ctx.removeElement(sprite.element);
} else {
sprite.element.destroy();
}
sprite.element = null;
}
this.callParent(arguments);
}
});
// @define Ext.draw.engine.excanvas
/**
* @class Ext.draw.engine.excanvas
* @private
* @define Ext.draw.engine.excanvas
*/
Ext.draw || (Ext.draw = {});
Ext.draw.engine || (Ext.draw.engine = {});
Ext.draw.engine.excanvas = true;
// Copyright 2006 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Known Issues:
//
// * Patterns only support repeat.
// * Radial gradient are not implemented. The VML version of these look very
// different from the canvas one.
// * Clipping paths are not implemented.
// * Coordsize. The width and height attribute have higher priority than the
// width and height style values which isn't correct.
// * Painting mode isn't implemented.
// * Canvas width/height should is using content-box by default. IE in
// Quirks mode will draw the canvas using border-box. Either change your
// doctype to HTML5
// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
// or use Box Sizing Behavior from WebFX
// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
// * Non uniform scaling does not correctly scale strokes.
// * Optimize. There is always room for speed improvements.
// Only add this code if we do not already have a canvas implementation
if (!document.createElement('canvas').getContext) {
(function() {
// alias some functions to make (compiled) code shorter
var m = Math;
var mr = m.round;
var ms = m.sin;
var mc = m.cos;
var abs = m.abs;
var sqrt = m.sqrt;
// this is used for sub pixel precision
var Z = 10;
var Z2 = Z / 2;
var IE_VERSION = +navigator.userAgent.match(/MSIE ([\d.]+)?/)[1];
/**
* This funtion is assigned to the elements as element.getContext().
* @this {HTMLElement}
* @return {CanvasRenderingContext2D_}
*/
function getContext() {
return this.context_ || (this.context_ = new CanvasRenderingContext2D_(this));
}
var slice = Array.prototype.slice;
/**
* Binds a function to an object. The returned function will always use the
* passed in {@code obj} as {@code this}.
*
* Example:
*
* g = bind(f, obj, a, b)
* g(c, d) // will do f.call(obj, a, b, c, d)
*
* @param {Function} f The function to bind the object to
* @param {Object} obj The object that should act as this when the function
* is called
* @param {*} var_args Rest arguments that will be used as the initial
* arguments when the function is called
* @return {Function} A new function that has bound this
*/
function bind(f, obj, var_args) {
var a = slice.call(arguments, 2);
return function() {
return f.apply(obj, a.concat(slice.call(arguments)));
};
}
function encodeHtmlAttribute(s) {
return String(s).replace(/&/g, '&').replace(/"/g, '"');
}
function addNamespace(doc, prefix, urn) {
Ext.onReady(function() {
if (!doc.namespaces[prefix]) {
doc.namespaces.add(prefix, urn, '#default#VML');
}
});
}
function addNamespacesAndStylesheet(doc) {
addNamespace(doc, 'g_vml_', 'urn:schemas-microsoft-com:vml');
addNamespace(doc, 'g_o_', 'urn:schemas-microsoft-com:office:office');
// Setup default CSS. Only add one style sheet per document
if (!doc.styleSheets['ex_canvas_']) {
var ss = doc.createStyleSheet();
ss.owningElement.id = 'ex_canvas_';
ss.cssText = 'canvas{display:inline-block;overflow:hidden;' + // default size is 300x150 in Gecko and Opera
'text-align:left;width:300px;height:150px}';
}
}
// Add namespaces and stylesheet at startup.
addNamespacesAndStylesheet(document);
var G_vmlCanvasManager_ = {
init: function(opt_doc) {
var doc = opt_doc || document;
// Create a dummy element so that IE will allow canvas elements to be
// recognized.
doc.createElement('canvas');
doc.attachEvent('onreadystatechange', bind(this.init_, this, doc));
},
init_: function(doc) {
// find all canvas elements
var els = doc.getElementsByTagName('canvas');
for (var i = 0; i < els.length; i++) {
this.initElement(els[i]);
}
},
/**
* Public initializes a canvas element so that it can be used as canvas
* element from now on. This is called automatically before the page is
* loaded but if you are creating elements using createElement you need to
* make sure this is called on the element.
* @param {HTMLElement} el The canvas element to initialize.
* @return {HTMLElement} the element that was created.
*/
initElement: function(el) {
if (!el.getContext) {
el.getContext = getContext;
// Add namespaces and stylesheet to document of the element.
addNamespacesAndStylesheet(el.ownerDocument);
// Remove fallback content. There is no way to hide text nodes so we
// just remove all childNodes. We could hide all elements and remove
// text nodes but who really cares about the fallback content.
el.innerHTML = '';
// do not use inline function because that will leak memory
el.attachEvent('onpropertychange', onPropertyChange);
el.attachEvent('onresize', onResize);
var attrs = el.attributes;
if (attrs.width && attrs.width.specified) {
// TODO: use runtimeStyle and coordsize
// el.getContext().setWidth_(attrs.width.nodeValue);
el.style.width = attrs.width.nodeValue + 'px';
} else {
el.width = el.clientWidth;
}
if (attrs.height && attrs.height.specified) {
// TODO: use runtimeStyle and coordsize
// el.getContext().setHeight_(attrs.height.nodeValue);
el.style.height = attrs.height.nodeValue + 'px';
} else {
el.height = el.clientHeight;
}
}
//el.getContext().setCoordsize_()
return el;
}
};
function onPropertyChange(e) {
var el = e.srcElement;
switch (e.propertyName) {
case 'width':
el.getContext().clearRect();
el.style.width = el.attributes.width.nodeValue + 'px';
// In IE8 this does not trigger onresize.
el.firstChild.style.width = el.clientWidth + 'px';
break;
case 'height':
el.getContext().clearRect();
el.style.height = el.attributes.height.nodeValue + 'px';
el.firstChild.style.height = el.clientHeight + 'px';
break;
}
}
function onResize(e) {
var el = e.srcElement;
if (el.firstChild) {
el.firstChild.style.width = el.clientWidth + 'px';
el.firstChild.style.height = el.clientHeight + 'px';
}
}
G_vmlCanvasManager_.init();
// precompute "00" to "FF"
var decToHex = [];
for (var i = 0; i < 16; i++) {
for (var j = 0; j < 16; j++) {
decToHex[i * 16 + j] = i.toString(16) + j.toString(16);
}
}
function createMatrixIdentity() {
return [
[
1,
0,
0
],
[
0,
1,
0
],
[
0,
0,
1
]
];
}
function matrixMultiply(m1, m2) {
var result = createMatrixIdentity();
for (var x = 0; x < 3; x++) {
for (var y = 0; y < 3; y++) {
var sum = 0;
for (var z = 0; z < 3; z++) {
sum += m1[x][z] * m2[z][y];
}
result[x][y] = sum;
}
}
return result;
}
function copyState(o1, o2) {
o2.fillStyle = o1.fillStyle;
o2.lineCap = o1.lineCap;
o2.lineJoin = o1.lineJoin;
o2.lineDash = o1.lineDash;
o2.lineWidth = o1.lineWidth;
o2.miterLimit = o1.miterLimit;
o2.shadowBlur = o1.shadowBlur;
o2.shadowColor = o1.shadowColor;
o2.shadowOffsetX = o1.shadowOffsetX;
o2.shadowOffsetY = o1.shadowOffsetY;
o2.strokeStyle = o1.strokeStyle;
o2.globalAlpha = o1.globalAlpha;
o2.font = o1.font;
o2.textAlign = o1.textAlign;
o2.textBaseline = o1.textBaseline;
o2.arcScaleX_ = o1.arcScaleX_;
o2.arcScaleY_ = o1.arcScaleY_;
o2.lineScale_ = o1.lineScale_;
}
var colorData = {
aliceblue: '#F0F8FF',
antiquewhite: '#FAEBD7',
aquamarine: '#7FFFD4',
azure: '#F0FFFF',
beige: '#F5F5DC',
bisque: '#FFE4C4',
black: '#000000',
blanchedalmond: '#FFEBCD',
blueviolet: '#8A2BE2',
brown: '#A52A2A',
burlywood: '#DEB887',
cadetblue: '#5F9EA0',
chartreuse: '#7FFF00',
chocolate: '#D2691E',
coral: '#FF7F50',
cornflowerblue: '#6495ED',
cornsilk: '#FFF8DC',
crimson: '#DC143C',
cyan: '#00FFFF',
darkblue: '#00008B',
darkcyan: '#008B8B',
darkgoldenrod: '#B8860B',
darkgray: '#A9A9A9',
darkgreen: '#006400',
darkgrey: '#A9A9A9',
darkkhaki: '#BDB76B',
darkmagenta: '#8B008B',
darkolivegreen: '#556B2F',
darkorange: '#FF8C00',
darkorchid: '#9932CC',
darkred: '#8B0000',
darksalmon: '#E9967A',
darkseagreen: '#8FBC8F',
darkslateblue: '#483D8B',
darkslategray: '#2F4F4F',
darkslategrey: '#2F4F4F',
darkturquoise: '#00CED1',
darkviolet: '#9400D3',
deeppink: '#FF1493',
deepskyblue: '#00BFFF',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1E90FF',
firebrick: '#B22222',
floralwhite: '#FFFAF0',
forestgreen: '#228B22',
gainsboro: '#DCDCDC',
ghostwhite: '#F8F8FF',
gold: '#FFD700',
goldenrod: '#DAA520',
grey: '#808080',
greenyellow: '#ADFF2F',
honeydew: '#F0FFF0',
hotpink: '#FF69B4',
indianred: '#CD5C5C',
indigo: '#4B0082',
ivory: '#FFFFF0',
khaki: '#F0E68C',
lavender: '#E6E6FA',
lavenderblush: '#FFF0F5',
lawngreen: '#7CFC00',
lemonchiffon: '#FFFACD',
lightblue: '#ADD8E6',
lightcoral: '#F08080',
lightcyan: '#E0FFFF',
lightgoldenrodyellow: '#FAFAD2',
lightgreen: '#90EE90',
lightgrey: '#D3D3D3',
lightpink: '#FFB6C1',
lightsalmon: '#FFA07A',
lightseagreen: '#20B2AA',
lightskyblue: '#87CEFA',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#B0C4DE',
lightyellow: '#FFFFE0',
limegreen: '#32CD32',
linen: '#FAF0E6',
magenta: '#FF00FF',
mediumaquamarine: '#66CDAA',
mediumblue: '#0000CD',
mediumorchid: '#BA55D3',
mediumpurple: '#9370DB',
mediumseagreen: '#3CB371',
mediumslateblue: '#7B68EE',
mediumspringgreen: '#00FA9A',
mediumturquoise: '#48D1CC',
mediumvioletred: '#C71585',
midnightblue: '#191970',
mintcream: '#F5FFFA',
mistyrose: '#FFE4E1',
moccasin: '#FFE4B5',
navajowhite: '#FFDEAD',
oldlace: '#FDF5E6',
olivedrab: '#6B8E23',
orange: '#FFA500',
orangered: '#FF4500',
orchid: '#DA70D6',
palegoldenrod: '#EEE8AA',
palegreen: '#98FB98',
paleturquoise: '#AFEEEE',
palevioletred: '#DB7093',
papayawhip: '#FFEFD5',
peachpuff: '#FFDAB9',
peru: '#CD853F',
pink: '#FFC0CB',
plum: '#DDA0DD',
powderblue: '#B0E0E6',
rosybrown: '#BC8F8F',
royalblue: '#4169E1',
saddlebrown: '#8B4513',
salmon: '#FA8072',
sandybrown: '#F4A460',
seagreen: '#2E8B57',
seashell: '#FFF5EE',
sienna: '#A0522D',
skyblue: '#87CEEB',
slateblue: '#6A5ACD',
slategray: '#708090',
slategrey: '#708090',
snow: '#FFFAFA',
springgreen: '#00FF7F',
steelblue: '#4682B4',
tan: '#D2B48C',
thistle: '#D8BFD8',
tomato: '#FF6347',
turquoise: '#40E0D0',
violet: '#EE82EE',
wheat: '#F5DEB3',
whitesmoke: '#F5F5F5',
yellowgreen: '#9ACD32'
};
function getRgbHslContent(styleString) {
var start = styleString.indexOf('(', 3);
var end = styleString.indexOf(')', start + 1);
var parts = styleString.substring(start + 1, end).split(',');
// add alpha if needed
if (parts.length != 4 || styleString.charAt(3) != 'a') {
parts[3] = 1;
}
return parts;
}
function percent(s) {
return parseFloat(s) / 100;
}
function clamp(v, min, max) {
return Math.min(max, Math.max(min, v));
}
function hslToRgb(parts) {
var r, g, b, h, s, l;
h = parseFloat(parts[0]) / 360 % 360;
if (h < 0) {
h++;
}
s = clamp(percent(parts[1]), 0, 1);
l = clamp(percent(parts[2]), 0, 1);
if (s == 0) {
r = g = b = l;
} else // achromatic
{
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hueToRgb(p, q, h + 1 / 3);
g = hueToRgb(p, q, h);
b = hueToRgb(p, q, h - 1 / 3);
}
return '#' + decToHex[Math.floor(r * 255)] + decToHex[Math.floor(g * 255)] + decToHex[Math.floor(b * 255)];
}
function hueToRgb(m1, m2, h) {
if (h < 0) {
h++;
}
if (h > 1) {
h--;
}
if (6 * h < 1) {
return m1 + (m2 - m1) * 6 * h;
}
else if (2 * h < 1) {
return m2;
}
else if (3 * h < 2) {
return m1 + (m2 - m1) * (2 / 3 - h) * 6;
}
else {
return m1;
}
}
var processStyleCache = {};
function processStyle(styleString) {
if (styleString in processStyleCache) {
return processStyleCache[styleString];
}
var str,
alpha = 1;
styleString = String(styleString);
if (styleString.charAt(0) == '#') {
str = styleString;
} else if (/^rgb/.test(styleString)) {
var parts = getRgbHslContent(styleString);
var str = '#',
n;
for (var i = 0; i < 3; i++) {
if (parts[i].indexOf('%') != -1) {
n = Math.floor(percent(parts[i]) * 255);
} else {
n = +parts[i];
}
str += decToHex[clamp(n, 0, 255)];
}
alpha = +parts[3];
} else if (/^hsl/.test(styleString)) {
var parts = getRgbHslContent(styleString);
str = hslToRgb(parts);
alpha = parts[3];
} else {
str = colorData[styleString] || styleString;
}
return processStyleCache[styleString] = {
color: str,
alpha: alpha
};
}
var DEFAULT_STYLE = {
style: 'normal',
variant: 'normal',
weight: 'normal',
size: 10,
family: 'sans-serif'
};
// Internal text style cache
var fontStyleCache = {};
function processFontStyle(styleString) {
if (fontStyleCache[styleString]) {
return fontStyleCache[styleString];
}
var el = document.createElement('div');
var style = el.style;
try {
style.font = styleString;
} catch (ex) {}
// Ignore failures to set to invalid font.
return fontStyleCache[styleString] = {
style: style.fontStyle || DEFAULT_STYLE.style,
variant: style.fontVariant || DEFAULT_STYLE.variant,
weight: style.fontWeight || DEFAULT_STYLE.weight,
size: style.fontSize || DEFAULT_STYLE.size,
family: style.fontFamily || DEFAULT_STYLE.family
};
}
function getComputedStyle(style, element) {
var computedStyle = {};
for (var p in style) {
computedStyle[p] = style[p];
}
// Compute the size
var canvasFontSize = parseFloat(element.currentStyle.fontSize),
fontSize = parseFloat(style.size);
if (typeof style.size == 'number') {
computedStyle.size = style.size;
} else if (style.size.indexOf('px') != -1) {
computedStyle.size = fontSize;
} else if (style.size.indexOf('em') != -1) {
computedStyle.size = canvasFontSize * fontSize;
} else if (style.size.indexOf('%') != -1) {
computedStyle.size = (canvasFontSize / 100) * fontSize;
} else if (style.size.indexOf('pt') != -1) {
computedStyle.size = fontSize / 0.75;
} else {
computedStyle.size = canvasFontSize;
}
// Different scaling between normal text and VML text. This was found using
// trial and error to get the same size as non VML text.
computedStyle.size *= 0.981;
return computedStyle;
}
function buildStyle(style) {
return style.style + ' ' + style.variant + ' ' + style.weight + ' ' + style.size + 'px ' + style.family;
}
var lineCapMap = {
'butt': 'flat',
'round': 'round'
};
function processLineCap(lineCap) {
return lineCapMap[lineCap] || 'square';
}
/**
* @class CanvasRenderingContext2D_
* This class implements CanvasRenderingContext2D interface as described by
* the WHATWG.
* @param {HTMLElement} canvasElement The element that the 2D context should
* be associated with
* @private
*/
//
function CanvasRenderingContext2D_(canvasElement) {
this.m_ = createMatrixIdentity();
this.mStack_ = [];
this.aStack_ = [];
this.currentPath_ = [];
// Canvas context properties
this.strokeStyle = '#000';
this.fillStyle = '#000';
this.lineWidth = 1;
this.lineJoin = 'miter';
this.lineDash = [];
this.lineCap = 'butt';
this.miterLimit = Z * 1;
this.globalAlpha = 1;
this.font = '10px sans-serif';
this.textAlign = 'left';
this.textBaseline = 'alphabetic';
this.canvas = canvasElement;
var cssText = 'width:' + canvasElement.clientWidth + 'px;height:' + canvasElement.clientHeight + 'px;overflow:hidden;position:absolute';
var el = canvasElement.ownerDocument.createElement('div');
el.style.cssText = cssText;
canvasElement.appendChild(el);
var overlayEl = el.cloneNode(false);
// Use a non transparent background.
overlayEl.style.backgroundColor = 'red';
overlayEl.style.filter = 'alpha(opacity=0)';
canvasElement.appendChild(overlayEl);
this.element_ = el;
this.arcScaleX_ = 1;
this.arcScaleY_ = 1;
this.lineScale_ = 1;
}
var contextPrototype = CanvasRenderingContext2D_.prototype;
contextPrototype.clearRect = function() {
if (this.textMeasureEl_) {
this.textMeasureEl_.removeNode(true);
this.textMeasureEl_ = null;
}
this.element_.innerHTML = '';
};
contextPrototype.beginPath = function() {
// TODO: Branch current matrix so that save/restore has no effect
// as per safari docs.
this.currentPath_ = [];
};
contextPrototype.moveTo = function(aX, aY) {
var p = getCoords(this, aX, aY);
this.currentPath_.push({
type: 'moveTo',
x: p.x,
y: p.y
});
this.currentX_ = p.x;
this.currentY_ = p.y;
};
contextPrototype.lineTo = function(aX, aY) {
var p = getCoords(this, aX, aY);
this.currentPath_.push({
type: 'lineTo',
x: p.x,
y: p.y
});
this.currentX_ = p.x;
this.currentY_ = p.y;
};
contextPrototype.bezierCurveTo = function(aCP1x, aCP1y, aCP2x, aCP2y, aX, aY) {
var p = getCoords(this, aX, aY);
var cp1 = getCoords(this, aCP1x, aCP1y);
var cp2 = getCoords(this, aCP2x, aCP2y);
bezierCurveTo(this, cp1, cp2, p);
};
// Helper function that takes the already fixed cordinates.
function bezierCurveTo(self, cp1, cp2, p) {
self.currentPath_.push({
type: 'bezierCurveTo',
cp1x: cp1.x,
cp1y: cp1.y,
cp2x: cp2.x,
cp2y: cp2.y,
x: p.x,
y: p.y
});
self.currentX_ = p.x;
self.currentY_ = p.y;
}
contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
// the following is lifted almost directly from
// http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes
var cp = getCoords(this, aCPx, aCPy);
var p = getCoords(this, aX, aY);
var cp1 = {
x: this.currentX_ + 2 / 3 * (cp.x - this.currentX_),
y: this.currentY_ + 2 / 3 * (cp.y - this.currentY_)
};
var cp2 = {
x: cp1.x + (p.x - this.currentX_) / 3,
y: cp1.y + (p.y - this.currentY_) / 3
};
bezierCurveTo(this, cp1, cp2, p);
};
contextPrototype.arc = function(aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise) {
aRadius *= Z;
var arcType = aClockwise ? 'at' : 'wa';
var xStart = aX + mc(aStartAngle) * aRadius - Z2;
var yStart = aY + ms(aStartAngle) * aRadius - Z2;
var xEnd = aX + mc(aEndAngle) * aRadius - Z2;
var yEnd = aY + ms(aEndAngle) * aRadius - Z2;
// IE won't render arches drawn counter clockwise if xStart == xEnd.
if (xStart == xEnd && !aClockwise) {
xStart += 0.125;
}
// Offset xStart by 1/80 of a pixel. Use something
// that can be represented in binary
var p = getCoords(this, aX, aY);
var pStart = getCoords(this, xStart, yStart);
var pEnd = getCoords(this, xEnd, yEnd);
this.currentPath_.push({
type: arcType,
x: p.x,
y: p.y,
radius: aRadius,
xStart: pStart.x,
yStart: pStart.y,
xEnd: pEnd.x,
yEnd: pEnd.y
});
};
contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
this.moveTo(aX, aY);
this.lineTo(aX + aWidth, aY);
this.lineTo(aX + aWidth, aY + aHeight);
this.lineTo(aX, aY + aHeight);
this.closePath();
};
contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
var oldPath = this.currentPath_;
this.beginPath();
this.moveTo(aX, aY);
this.lineTo(aX + aWidth, aY);
this.lineTo(aX + aWidth, aY + aHeight);
this.lineTo(aX, aY + aHeight);
this.closePath();
this.stroke();
this.currentPath_ = oldPath;
};
contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
var oldPath = this.currentPath_;
this.beginPath();
this.moveTo(aX, aY);
this.lineTo(aX + aWidth, aY);
this.lineTo(aX + aWidth, aY + aHeight);
this.lineTo(aX, aY + aHeight);
this.closePath();
this.fill();
this.currentPath_ = oldPath;
};
contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
var gradient = new CanvasGradient_('gradient');
gradient.x0_ = aX0;
gradient.y0_ = aY0;
gradient.x1_ = aX1;
gradient.y1_ = aY1;
return gradient;
};
contextPrototype.createRadialGradient = function(aX0, aY0, aR0, aX1, aY1, aR1) {
var gradient = new CanvasGradient_('gradientradial');
gradient.x0_ = aX0;
gradient.y0_ = aY0;
gradient.r0_ = aR0;
gradient.x1_ = aX1;
gradient.y1_ = aY1;
gradient.r1_ = aR1;
return gradient;
};
contextPrototype.drawImage = function(image, var_args) {
var dx, dy, dw, dh, sx, sy, sw, sh;
// to find the original width we overide the width and height
var oldRuntimeWidth = image.runtimeStyle.width;
var oldRuntimeHeight = image.runtimeStyle.height;
image.runtimeStyle.width = 'auto';
image.runtimeStyle.height = 'auto';
// get the original size
var w = image.width;
var h = image.height;
// and remove overides
image.runtimeStyle.width = oldRuntimeWidth;
image.runtimeStyle.height = oldRuntimeHeight;
if (arguments.length == 3) {
dx = arguments[1];
dy = arguments[2];
sx = sy = 0;
sw = dw = w;
sh = dh = h;
} else if (arguments.length == 5) {
dx = arguments[1];
dy = arguments[2];
dw = arguments[3];
dh = arguments[4];
sx = sy = 0;
sw = w;
sh = h;
} else if (arguments.length == 9) {
sx = arguments[1];
sy = arguments[2];
sw = arguments[3];
sh = arguments[4];
dx = arguments[5];
dy = arguments[6];
dw = arguments[7];
dh = arguments[8];
} else {
throw Error('Invalid number of arguments');
}
var d = getCoords(this, dx, dy);
var vmlStr = [];
var W = 10;
var H = 10;
var m = this.m_;
vmlStr.push('