extjs-gpl
Version:
GPL licensed version of Sencha Ext JS
1,334 lines (1,324 loc) • 1.24 MB
JavaScript
/**
* @private
* @class Ext.draw.ContainerBase
*/
Ext.define('Ext.draw.ContainerBase', {
extend: 'Ext.panel.Panel',
requires: [
'Ext.window.Window'
],
/**
* @cfg {String} previewTitleText The text to place in Preview Chart window title.
*/
previewTitleText: 'Chart Preview',
/**
* @cfg {String} previewAltText The text to place in the Preview image alt attribute.
*/
previewAltText: 'Chart preview',
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.handleResize({
width: width,
height: height
});
},
preview: function() {
var image = this.getImage(),
items;
if (Ext.isIE8) {
return false;
}
if (image.type === 'svg-markup') {
items = {
xtype: 'container',
html: image.data
};
} else {
items = {
xtype: 'image',
mode: 'img',
cls: Ext.baseCSSPrefix + 'chart-image',
alt: this.previewAltText,
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));
}
}
}
};
}
new Ext.window.Window({
title: this.previewTitleText,
closeable: true,
renderTo: Ext.getBody(),
autoShow: true,
maximizeable: true,
maximized: true,
border: true,
layout: {
type: 'hbox',
pack: 'center',
align: 'middle'
},
items: {
xtype: 'container',
items: items
}
});
},
privates: {
getTargetEl: function() {
return this.innerElement;
},
reattachToBody: function() {
// This is to ensure charts work properly as grid column widgets.
var me = this;
if (me.pendingDetachSize) {
me.handleResize();
}
me.pendingDetachSize = false;
me.callParent();
}
}
});
/**
* @private
* @class Ext.draw.SurfaceBase (Classic)
*/
Ext.define('Ext.draw.SurfaceBase', {
extend: 'Ext.Widget',
getOwnerBody: function() {
return this.ownerCt.body;
}
});
/**
* @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;
}
return {
singleton: true,
attributeRe: /^url\(#([a-zA-Z\-]+)\)$/,
requires: [
'Ext.draw.Color'
],
color: {
parseInitial: function(color1, color2) {
if (Ext.isString(color1)) {
color1 = Ext.util.Color.create(color1);
}
if (Ext.isString(color2)) {
color2 = Ext.util.Color.create(color2);
}
if ((color1 && color1.isColor) && (color2 && color2.isColor)) {
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.util.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 (Ext.isNumber(f)) {
if (!Ext.isNumber(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;
} else {
target[i] = t;
}
}
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 (!(Ext.isNumber(nextX) && Ext.isNumber(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.
*/
beginUpdateIOS: Ext.os.is.iOS ? function() {
this.iosUpdateEl = Ext.getBody().createChild({
//<debug>
'data-sticky': true,
//</debug>
style: 'position: absolute; top: 0px; bottom: 0px; left: 0px; right: 0px; background: rgba(0,0,0,0.001); z-index: 100000'
});
} : Ext.emptyFn,
endUpdateIOS: function() {
this.iosUpdateEl = Ext.destroy(this.iosUpdateEl);
}
});
/**
* @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.util.Color.fly(color || Ext.util.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);
},
/**
* @method
* @protected
* Generates the gradient for the given context.
* @param {Ext.draw.engine.SvgContext} ctx The context.
* @param {Object} bbox
* @return {CanvasGradient/Ext.draw.engine.SvgContext.Gradient/Ext.util.Color.NONE}
*/
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": Ext.identityFn,
string: function(n) {
return String(n);
},
number: function(n) {
// Numbers as strings will be converted to numbers,
// null will be converted to 0.
if (Ext.isNumber(+n)) {
return n;
}
},
/**
* Normalize angle to the [-180,180) interval.
* @param n Angle in radians.
* @return {Number/undefined} Normalized angle or undefined.
*/
angle: function(n) {
if (Ext.isNumber(n)) {
n %= Math.PI * 2;
if (n < -Math.PI) {
n += Math.PI * 2;
} else 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 && n.isColor) {
return n.toString();
} else if (n && n.isGradient) {
return n;
} else if (!n) {
return Ext.util.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.util.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.util.Color.NONE;
}
},
limited: function(low, hi) {
return function(n) {
n = +n;
return Ext.isNumber(n) ? Math.min(Math.max(n, low), hi) : undefined;
};
},
limited01: function(n) {
n = +n;
return Ext.isNumber(n) ? Math.min(Math.max(n, 0), 1) : undefined;
},
/**
* 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: {
$value: {},
lazy: true
},
/**
* @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: {
// A plus side of lazy initialization is that the 'processors' and 'defaults' will
// only be applied for those sprite classes that are actually instantiated.
$value: {},
lazy: true
},
/**
* @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\-,]*)\)$/
},
// The sprite class for which AttributeDefinition instance is created.
spriteClass: null,
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 (typeof fn === 'string') {
match = fn.match(processorFactoryRe);
if (match) {
// enums(... , limited(... or something of that nature.
fn = defaultProcessor[match[1]].apply(defaultProcessor, match[2].split(','));
} else if (defaultProcessor[fn]) {
// Names of animation parsers match the names of attribute parsers.
animationProcessors[name] = fn;
anyAnimationProcessors = true;
fn = defaultProcessor[fn];
}
}
//<debug>
if (!Ext.isFunction(fn)) {
Ext.raise(this.spriteClass.$className + ": processor '" + name + "' has not been found.");
}
//</debug>
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 processors = this.getProcessors(),
aliases = this.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 processors = this.getProcessors(),
aliases = this.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();
// This will NOT update the transformation matrix of a sprite
// with the given elements. It will attempt to extract the
// individual transformation attributes from the transformation matrix
// elements provided. Then the extracted attributes will be used by
// the sprite's 'applyTransformations' method to calculate
// the transformation matrix of the sprite.
// It's not possible to recover all the information from the given
// transformation matrix elements. Shearing and centers of rotation
// and scaling are not recovered.
// Ideally, this should work like sprite.transform([elements], true),
// i.e. update the transformation matrix of a sprite directly,
// without attempting to update sprite's transformation attributes.
// But we are not changing the behavior (just yet) for compatibility
// reasons.
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);
}
});
/**
* Ext.draw.Matix is a utility class used to calculate
* [affine transformation](http://en.wikipedia.org/wiki/Affine_transformation) matrix.
* The matrix class is used to apply transformations to existing
* {@link Ext.draw.sprite.Sprite sprites} using a number of convenience transform
* methods.
*
* Transformations configured directly on a sprite are processed in the following order:
* scaling, rotation, and translation. The matrix class offers additional flexibility.
* Once a sprite is created, you can use the matrix class's transform methods as many
* times as needed and in any order you choose.
*
* To demonstrate, we'll start with a simple {@link Ext.draw.sprite.Rect rect} sprite
* with the intent of rotating it 180 degrees with the bottom right corner being the
* center of rotation. To begin, let's look at the initial, untransformed sprite:
*
* @example
* var drawContainer = new Ext.draw.Container({
* renderTo: Ext.getBody(),
* width: 380,
* height: 380,
* sprites: [{
* type: 'rect',
* width: 100,
* height: 100,
* fillStyle: 'red'
* }]
* });
*
* Next, we'll use the {@link #rotate} and {@link #translate} methods from our matrix
* class to position the rect sprite.
*
* @example
* var drawContainer = new Ext.draw.Container({
* renderTo: Ext.getBody(),
* width: 380,
* height: 380,
* sprites: [{
* type: 'rect',
* width: 100,
* height: 100,
* fillStyle: 'red'
* }]
* });
*
* var main = drawContainer.getSurface();
* var rect = main.getItems()[0];
*
* var m = new Ext.draw.Matrix().translate(100, 100).
* rotate(Math.PI).
* translate(-100, - 100);
*
* rect.setTransform(m);
* main.renderFrame();
*
* In the previous example we perform the following steps in order to achieve our
* desired rotated output:
*
* - translate the rect to the right and down by 100
* - rotate by 180 degrees
* - translate the rect to the right and down by 100
*
* **Note:** A couple of things to note at this stage; 1) the rotation center point is
* the upper left corner of the sprite by default and 2) with transformations, the
* sprite itself isn't transformed, but rather the entire coordinate p