/**
* A sprite is a basic primitive from the charts package which represents a graphical
* object that can be drawn. Sprites are used extensively in the charts package to
* create the visual elements of each chart. You can also create a desired image by
* adding one or more sprites to a {@link Ext.draw.Container draw container}.
*
* The Sprite class itself is an abstract class and is not meant to be used directly.
* There are many different kinds of sprites available in the charts package that extend
* Ext.draw.sprite.Sprite. Each sprite type has various attributes that define how that
* sprite should look. For example, this is a {@link Ext.draw.sprite.Rect rect} sprite:
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 400,
* height: 400,
* sprites: [{
* type: 'rect',
* x: 50,
* y: 50,
* width: 100,
* height: 100,
* fillStyle: '#1F6D91'
* }]
* });
*
* By default, sprites are added to the default 'main' {@link Ext.draw.Surface surface}
* of the draw container. However, sprites may also be configured with a reference to a
* specific Ext.draw.Surface when set in the draw container's
* {@link Ext.draw.Container#cfg-sprites sprites} config. Specifying a surface
* other than 'main' will create a surface by that name if it does not already exist.
*
* @example
* Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 400,
* height: 400,
* sprites: [{
* type: 'rect',
* surface: 'anim', // a surface with id "anim" will be created automatically
* x: 50,
* y: 50,
* width: 100,
* height: 100,
* fillStyle: '#1F6D91'
* }]
* });
*
* The ability to have multiple surfaces is useful for performance (and battery life)
* reasons. Because changes to sprite attributes cause the whole surface (and all
* sprites in it) to re-render, it makes sense to group sprites by surface, so changes
* to one group of sprites will only trigger the surface they are in to re-render.
*
* You can add a sprite to an existing drawing by adding the sprite to a draw surface.
*
* @example
* var drawCt = Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 400,
* height: 400
* });
*
* // If the surface name is not specified then 'main' will be used
* var surface = drawCt.getSurface();
*
* surface.add({
* type: 'rect',
* x: 50,
* y: 50,
* width: 100,
* height: 100,
* fillStyle: '#1F6D91'
* });
*
* surface.renderFrame();
*
* **Note:** Changes to the sprites on a surface will be not be reflected in the DOM
* until you call the surface's {@link Ext.draw.Surface#method-renderFrame renderFrame}
* method. This must be done after adding, removing, or modifying sprites in order to
* see the changes on-screen.
*
* For information on configuring a sprite with an initial transformation see
* {@link #scaling}, {@link #rotation}, and {@link #translation}.
*
* For information on applying a transformation to an existing sprite see the
* Ext.draw.Matrix class.
*/
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,
$configStrict: false,
statics: {
//<debug>
/* eslint-disable max-len */
/**
* Debug rendering options:
*
* debug: {
* bbox: true, // renders the bounding box of the sprite
* xray: true // renders control points of the path (for Ext.draw.sprite.Path and descendants only)
* }
*
*/
debug: false,
/* eslint-enable max-len */
//</debug>
defaultHitTestOptions: {
fill: true,
stroke: true
}
},
inheritableStatics: {
def: {
processors: {
//<debug>
debug: 'default',
//</debug>
/**
* @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.
*/
// eslint-disable-next-line max-len
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, position offset, of the sprite on the x-axis.
*
* **Note:** Transform configs are *always* performed in the following
* order:
*
* 1. Scaling
* 2. Rotation
* 3. Translation
*
* See also: {@link #translation} and {@link #translationY}
*/
translationX: "number",
/**
* @cfg {Number} [translationY=0]
* The translation, position offset, of the sprite on the y-axis.
*
* **Note:** Transform configs are *always* performed in the following
* order:
*
* 1. Scaling
* 2. Rotation
* 3. Translation
*
* See also: {@link #translation} and {@link #translationX}
*/
translationY: "number",
/**
* @cfg {Number} [rotationRads=0]
* The angle of rotation of the sprite in radians.
*
* **Note:** Transform configs are *always* performed in the following
* order:
*
* 1. Scaling
* 2. Rotation
* 3. Translation
*
* See also: {@link #rotation}, {@link #rotationCenterX}, and
* {@link #rotationCenterY}
*/
rotationRads: "number",
/**
* @cfg {Number} [rotationCenterX=null]
* The central coordinate of the sprite's scale operation on the x-axis.
* Unless explicitly set, will default to the calculated center of the
* sprite along the x-axis.
*
* **Note:** Transform configs are *always* performed in the following
* order:
*
* 1. Scaling
* 2. Rotation
* 3. Translation
*
* See also: {@link #rotation}, {@link #rotationRads}, and
* {@link #rotationCenterY}
*/
rotationCenterX: "number",
/**
* @cfg {Number} [rotationCenterY=null]
* The central coordinate of the sprite's rotate operation on the y-axis.
* Unless explicitly set, will default to the calculated center of the
* sprite along the y-axis.
*
* **Note:** Transform configs are *always* performed in the following
* order:
*
* 1. Scaling
* 2. Rotation
* 3. Translation
*
* See also: {@link #rotation}, {@link #rotationRads}, and
* {@link #rotationCenterX}
*/
rotationCenterY: "number",
/**
* @cfg {Number} [scalingX=1] The scaling of the sprite on the x-axis.
* The number value represents a percentage by which to scale the
* sprite. **1** is equal to 100%, **2** would be 200%, etc.
*
* **Note:** Transform configs are *always* performed in the following
* order:
*
* 1. Scaling
* 2. Rotation
* 3. Translation
*
* See also: {@link #scaling}, {@link #scalingY},
* {@link #scalingCenterX}, and {@link #scalingCenterY}
*/
scalingX: "number",
/**
* @cfg {Number} [scalingY=1] The scaling of the sprite on the y-axis.
* The number value represents a percentage by which to scale the
* sprite. **1** is equal to 100%, **2** would be 200%, etc.
*
* **Note:** Transform configs are *always* performed in the following
* order:
*
* 1. Scaling
* 2. Rotation
* 3. Translation
*
* See also: {@link #scaling}, {@link #scalingX},
* {@link #scalingCenterX}, and {@link #scalingCenterY}
*/
scalingY: "number",
/**
* @cfg {Number} [scalingCenterX=null]
* The central coordinate of the sprite's scale operation on the x-axis.
*
* **Note:** Transform configs are *always* performed in the following
* order:
*
* 1. Scaling
* 2. Rotation
* 3. Translation
*
* See also: {@link #scaling}, {@link #scalingX},
* {@link #scalingY}, and {@link #scalingCenterY}
*/
scalingCenterX: "number",
/**
* @cfg {Number} [scalingCenterY=null]
* The central coordinate of the sprite's scale operation on the y-axis.
*
* **Note:** Transform configs are *always* performed in the following
* order:
*
* 1. Scaling
* 2. Rotation
* 3. Translation
*
* See also: {@link #scaling}, {@link #scalingX},
* {@link #scalingY}, and {@link #scalingCenterX}
*/
scalingCenterY: "number",
constrainGradients: "bool"
/**
* @cfg {Number/Object} rotation
* Applies an initial angle of rotation to the sprite. May be a number
* specifying the rotation in degrees. Or may be a config object using
* the below config options.
*
* **Note:** Rotation config options will be overridden by values set on
* the {@link #rotationRads}, {@link #rotationCenterX}, and
* {@link #rotationCenterY} configs.
*
* Ext.create({
* xtype: 'draw',
* renderTo: Ext.getBody(),
* width: 600,
* height: 400,
* sprites: [{
* type: 'rect',
* x: 50,
* y: 50,
* width: 100,
* height: 100,
* fillStyle: '#1F6D91',
* //rotation: 45
* rotation: {
* degrees: 45,
* //rads: Math.PI / 4,
* //centerX: 50,
* //centerY: 50
* }
* }]
* });
*
* **Note:** Transform configs are *always* performed in the following
* order:
*
* 1. Scaling
* 2. Rotation
* 3. Translation
*
* @cfg {Number} rotation.rads
* The angle in radians to rotate the sprite
*
* @cfg {Number} rotation.degrees
* The angle in degrees to rotate the sprite (is ignored if rads or
* {@link #rotationRads} is set
*
* @cfg {Number} rotation.centerX
* The central coordinate of the sprite's rotation on the x-axis.
* Unless explicitly set, will default to the calculated center of the
* sprite along the x-axis.
*
* @cfg {Number} rotation.centerY
* The central coordinate of the sprite's rotation on the y-axis.
* Unless explicitly set, will default to the calculated center of the
* sprite along the y-axis.
*/
/**
* @cfg {Number/Object} scaling
* Applies initial scaling to the sprite. May be a number specifying
* the amount to scale both the x and y-axis. The number value
* represents a percentage by which to scale the sprite. **1** is equal
* to 100%, **2** would be 200%, etc. Or may be a config object using
* the below config options.
*
* **Note:** Scaling config options will be overridden by values set on
* the {@link #scalingX}, {@link #scalingY}, {@link #scalingCenterX},
* and {@link #scalingCenterY} configs.
*
* Ext.create({
* xtype: 'draw',
* renderTo: Ext.getBody(),
* width: 600,
* height: 400,
* sprites: [{
* type: 'rect',
* x: 50,
* y: 50,
* width: 100,
* height: 100,
* fillStyle: '#1F6D91',
* //scaling: 2,
* scaling: {
* x: 2,
* y: 2
* //centerX: 100,
* //centerY: 100
* }
* }]
* });
*
* **Note:** Transform configs are *always* performed in the following
* order:
*
* 1. Scaling
* 2. Rotation
* 3. Translation
*
* @cfg {Number} scaling.x
* The amount by which to scale the sprite along the x-axis. The number
* value represents a percentage by which to scale the sprite. **1** is
* equal to 100%, **2** would be 200%, etc.
*
* @cfg {Number} scaling.y
* The amount by which to scale the sprite along the y-axis. The number
* value represents a percentage by which to scale the sprite. **1** is
* equal to 100%, **2** would be 200%, etc.
*
* @cfg scaling.centerX
* The central coordinate of the sprite's scaling on the x-axis. Unless
* explicitly set, will default to the calculated center of the sprite
* along the x-axis.
*
* @cfg {Number} scaling.centerY
* The central coordinate of the sprite's scaling on the y-axis. Unless
* explicitly set, will default to the calculated center of the sprite
* along the y-axis.
*/
/**
* @cfg {Object} translation
* Applies an initial translation, adjustment in x/y positioning, to the
* sprite.
*
* **Note:** Translation config options will be overridden by values set
* on the {@link #translationX} and {@link #translationY} configs.
*
* Ext.create({
* xtype: 'draw',
* renderTo: Ext.getBody(),
* width: 600,
* height: 400,
* sprites: [{
* type: 'rect',
* x: 50,
* y: 50,
* width: 100,
* height: 100,
* fillStyle: '#1F6D91',
* translation: {
* x: 50,
* y: 50
* }
* }]
* });
*
* **Note:** Transform configs are *always* performed in the following
* order:
*
* 1. Scaling
* 2. Rotation
* 3. Translation
*
* @cfg {Number} translation.x
* The amount to translate the sprite along the x-axis.
*
* @cfg {Number} translation.y
* The amount to translate the sprite along the y-axis.
*/
},
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: {
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' updater is meant to be called by subclasses when changes
// to attributes are expected to result in a change in sprite's dimensions.
bbox: 'bboxUpdater',
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...
*/
/**
* @cfg {Ext.draw.modifier.Animation} animation
* @accessor
*/
config: {
/**
* @private
* @cfg {Ext.draw.Surface/Ext.draw.sprite.Instancing/Ext.draw.sprite.Composite} parent
* The immediate parent of the sprite. Not necessarily a surface.
*/
parent: null,
/**
* @private
* @cfg {Ext.draw.Surface} surface
* The surface that this sprite is rendered into.
* This config is not meant to be used directly.
* Please use the {@link Ext.draw.Surface#add} method instead.
*/
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 superclassCfg = subClass.superclass.self.def.initialConfig,
ownCfg = data.inheritableStatics && data.inheritableStatics.def,
cfg;
// If sprite defines attributes of its own, merge that with those of its parent.
if (ownCfg) {
cfg = Ext.Object.merge({}, superclassCfg, ownCfg);
subClass.def = new Ext.draw.sprite.AttributeDefinition(cfg);
delete data.inheritableStatics.def;
}
else {
subClass.def = new Ext.draw.sprite.AttributeDefinition(superclassCfg);
}
subClass.def.spriteClass = subClass;
},
constructor: function(config) {
//<debug>
if (Ext.getClassName(this) === 'Ext.draw.sprite.Sprite') {
throw 'Ext.draw.sprite.Sprite is an abstract class';
}
//</debug>
// eslint-disable-next-line vars-on-top
var me = this,
attributeDefinition = me.self.def,
// It is important to get defaults (make sure
// 'defaults' config applier of the AttributeDefinition is called,
// since it is initialized lazily) before the attributes
// are initialized ('initializeAttributes' call).
defaults = attributeDefinition.getDefaults(),
processors = attributeDefinition.getProcessors(),
modifiers, name;
config = Ext.isObject(config) ? config : {};
me.id = config.id || Ext.id(null, 'ext-sprite-');
me.attr = {};
// Observable's constructor also calls the initConfig for us.
me.mixins.observable.constructor.apply(me, arguments);
modifiers = Ext.Array.from(config.modifiers, true);
me.createModifiers(modifiers);
me.initializeAttributes();
me.setAttributes(defaults, true);
//<debug>
for (name in config) {
if (name in processors && me['get' + name.charAt(0).toUpperCase() + name.substr(1)]) {
Ext.raise('The ' + me.$className +
' sprite has both a config and an attribute with the same name: ' + name + '.');
}
}
//</debug>
me.setAttributes(config);
},
updateSurface: function(surface, oldSurface) {
if (oldSurface) {
oldSurface.remove(this);
}
},
/**
* @private
* Current state of the sprite.
* Set to `true` if the sprite needs to be repainted.
* @cfg {Boolean} dirty
* @accessor
*/
getDirty: function() {
return this.attr.dirty;
},
setDirty: function(dirty) {
var parent;
// This could have been a regular attribute.
// Instead, it's a hidden one, which is initialized inside in the
// Target's modifier `prepareAttributes` method and is exposed
// as a config. The idea is to skip the modifier chain when
// we simply need to change the sprite's state and notify
// the sprite's parent.
this.attr.dirty = dirty;
if (dirty) {
parent = this.getParent();
if (parent) {
parent.setDirty(true);
}
}
},
addModifier: function(modifier, reinitializeAttributes) {
var me = this,
mods = me.modifiers,
animation = mods.animation,
target = mods.target,
type;
if (!(modifier instanceof Ext.draw.modifier.Modifier)) {
type = typeof modifier === 'string' ? modifier : modifier.type;
if (type && !mods[type]) {
mods[type] = modifier = Ext.factory(modifier, null, null, 'modifier');
}
}
modifier.setSprite(me);
if (modifier.preFx || modifier.config && modifier.config.preFx) {
if (animation._lower) {
animation._lower.setUpper(modifier);
}
modifier.setUpper(animation);
}
else {
target._lower.setUpper(modifier);
modifier.setUpper(target);
}
if (reinitializeAttributes) {
me.initializeAttributes();
}
return modifier;
},
createModifiers: function(modifiers) {
var me = this,
Modifier = Ext.draw.modifier,
animation = me.getInitialConfig().animation,
mods, i, ln;
// Create default modifiers.
me.modifiers = mods = {
target: new Modifier.Target({ sprite: me }),
animation: new Modifier.Animation(Ext.apply({ sprite: me }, animation))
};
// Link modifiers.
mods.animation.setUpper(mods.target);
for (i = 0, ln = modifiers.length; i < ln; i++) {
me.addModifier(modifiers[i], false);
}
return mods;
},
/**
* Returns the current animation instance.
* return {Ext.draw.modifier.Animation} The animation modifier used to animate the
* sprite
*/
getAnimation: function() {
return this.modifiers.animation;
},
/**
* Sets the animation config used by the sprite when animating the sprite's
* attributes and transformation properties.
*
* var drawCt = Ext.create({
* xtype: 'draw',
* renderTo: document.body,
* width: 400,
* height: 400,
* sprites: [{
* type: 'rect',
* x: 50,
* y: 50,
* width: 100,
* height: 100,
* fillStyle: '#1F6D91'
* }]
* });
*
* var rect = drawCt.getSurface().getItems()[0];
*
* rect.setAnimation({
* duration: 1000,
* easing: 'elasticOut'
* });
*
* Ext.defer(function () {
* rect.setAttributes({
* width: 250
* });
* }, 500);
*
* @param {Object} config The Ext.draw.modifier.Animation config for this sprite's
* animations.
*/
setAnimation: function(config) {
if (!this.isConfiguring) {
this.modifiers.animation.setConfig(config || { duration: 0 });
}
},
initializeAttributes: function() {
this.modifiers.target.prepareAttributes(this.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,
updaters = me.self.def.getUpdaters(),
any = false,
dirty = false,
pendingUpdaters, flags, updater, fn;
attr = attr || this.attr;
pendingUpdaters = attr.pendingUpdaters;
// 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];
fn = updaters[updater];
if (typeof fn === 'string') {
fn = me[fn];
}
if (fn) {
fn.call(me, attr, flags);
}
}
dirty = dirty || any;
} while (any);
delete me.callUpdaters; // Restore class method.
if (dirty) {
me.setDirty(true);
}
},
/**
* @private
*/
callUpdater: function(attr, updater, triggers) {
this.scheduleUpdater(attr, updater, triggers);
this.callUpdaters(attr);
},
/**
* @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 as an array of updaters to be called.
*/
scheduleUpdaters: function(attr, updaters, triggers) {
var updater, i, ln;
attr = attr || this.attr;
if (triggers) {
for (i = 0, ln = updaters.length; i < ln; i++) {
updater = updaters[i];
this.scheduleUpdater(attr, updater, triggers);
}
}
else {
for (updater in updaters) {
triggers = updaters[updater];
this.scheduleUpdater(attr, updater, triggers);
}
}
},
/**
* @private
* @param attr {Object} The attributes object (not necesseraly of a sprite,
* but of its instance).
* @param updater {String} Updater to be called.
* @param {String[]} [triggers] Attributes that triggered the update.
*/
scheduleUpdater: function(attr, updater, triggers) {
var pendingUpdaters;
triggers = triggers || [];
attr = attr || this.attr;
pendingUpdaters = attr.pendingUpdaters;
if (updater in pendingUpdaters) {
if (triggers.length) {
pendingUpdaters[updater] = Ext.Array.merge(pendingUpdaters[updater], triggers);
}
}
else {
pendingUpdaters[updater] = triggers;
}
},
/**
* Set attributes of the sprite.
* By default only the attributes that have processors will be set
* and all other attributes will be filtered out as a result of the
* normalization process.
* The normalization process can be skipped. In that case all the given
* attributes will be set unprocessed. This will result in better
* performance, but might also pollute the sprite's attributes with
* unwanted attributes or attributes with invalid values, if one is not
* careful. See also {@link #setAttributesBypassingNormalization}.
* If normalization is skipped, one may also chose to avoid copying
* the given object. This may result in even better performance, but
* only in cases where most of the attributes have values that are
* different from the old values, because copying additionally checks
* if the value has changed.
*
* @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.
* `bypassNormalization` should also be `true`. The content of object may be destroyed.
*/
setAttributes: function(changes, bypassNormalization, avoidCopy) {
var me = this,
changesToPush;
//<debug>
if (me.destroyed) {
Ext.Error.raise("Setting attributes of a destroyed sprite.");
}
//</debug>
if (bypassNormalization) {
if (avoidCopy) {
changesToPush = changes;
}
else {
changesToPush = Ext.apply({}, changes);
}
}
else {
changesToPush = me.self.def.normalize(changes);
}
me.modifiers.target.pushDown(me.attr, changesToPush);
},
/**
* Set attributes of the sprite, assuming the names and values have already been
* normalized.
*
* @deprecated 6.5.0 Use setAttributes directly 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);
},
/**
* @private
*/
bboxUpdater: 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;
// 'bbox' is not a standard attribute (in the sense that it doesn't have
// a processor = not explicitly declared and cannot be set by a user)
// and is calculated automatically by the 'getBBox' method.
// The 'bbox' attribute is created by the 'prepareAttributes' method
// of the Target modifier at construction time.
// Both plain and tranformed bounding boxes need to be updated.
// Mark them as such below.
attr.bbox.plain.dirty = true; // updated by the 'updatePlainBBox' method
// Before transformed bounding box can be updated,
// we must ensure that we have correct forward and inverse
// transformation matrices (which are also created by the Target modifier),
// so that they reflect the current state of the scaling, rotation
// and other transformation attributes.
// The 'applyTransformations' method does just that.
// The 'dirtyTransform' flag (another implicit attribute)
// is set to true when any of the transformation attributes change,
// to let us know that transformation matrices need to be updated.
attr.bbox.transform.dirty = true; // updated by the 'updateTransformedBBox' method
if (hasRotation && noRotationCenter || hasScaling && noScalingCenter) {
this.scheduleUpdater(attr, 'transform');
}
},
/**
* 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) {
// If tranformations are to be applied ('dirtyTransform' is true),
// then this will itself call the 'getBBox' method
// to get the plain untransformed bbox and calculate its center.
me.applyTransformations();
if (transform.dirty) {
me.updateTransformedBBox(transform, plain);
transform.dirty = false;
}
return transform;
}
return plain;
},
/**
* @method
* @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 (transformed bounding box) to populate.
* @param {Object} plain Untransformed bounding box.
*/
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) {
// Always (force) apply transformation to sprite instances,
// even if their 'dirtyTransform' flag is false.
// The 'dirtyTransform' flag of an instance may never be set to 'true', as the
// 'transform' updater won't ever be called for sprite instances that have
// the same transform attributes as their template, because there's nothing to update
// (an instance is simply a prototype chained template's 'attr' object, that only
// has own properties for attributes whose values are different).
// Making the modifier recognize transform attributes set on sprite instances
// (see Ext.draw.modifier.Modifier's 'pushDown' method, where attributes with
// same values are removed from the 'changes' object) and making sure their 'dirtyTransform'
// flag is set to 'true' is not a correct solution here, because of the way instances
// are rendered (see Ext.draw.sprite.Instancing's 'render' method) - there is no way
// an instance wounldn't want its 'applyTransformations' method called.
this.applyTransformations(this.isSpriteInstance);
// eslint-disable-next-line vars-on-top
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) && Ext.isNumber(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 from sprite's attributes.
* Transformations are applied in the following order: Scaling, Rotation, Translation.
* @param {Boolean} [force=false] Forces recalculation of transform matrices even when
* sprite's transform attributes supposedly haven't changed.
*/
applyTransformations: function(force) {
if (!force && !this.attr.dirtyTransform) {
return;
}
// eslint-disable-next-line vars-on-top
var me = this,
attr = me.attr,
center = me.getBBoxCenter(true),
centerX = center[0],
centerY = center[1],
tx = attr.translationX,
ty = 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),
tx_4, ty_4;
if (sx === 1 && sy === 1) {
scx = 0;
scy = 0;
}
if (rad === 0) {
rcx = 0;
rcy = 0;
}
// Translation component after steps 1-4 (see below).
// Saving it here to prevent double calculation.
tx_4 = scx * (1 - sx) - rcx;
ty_4 = scy * (1 - sy) - rcy;
/* eslint-disable max-len */
// The matrix below is a result of:
// (7) (6) (5) (4) (3) (2) (1)
// | 1 0 tx | | 1 0 rcx | | cos -sin 0 | | 1 0 -rcx | | 1 0 scx | | sx 0 0 | | 1 0 -scx |
// | 0 1 ty | * | 0 1 rcy | * | sin cos 0 | * | 0 1 -rcy | * | 0 1 scy | * | 0 sy 0 | * | 0 1 -scy |
// | 0 0 1 | | 0 0 1 | | 0 0 1 | | 0 0 1 | | 0 0 1 | | 0 0 0 | | 0 0 1 |
/* eslint-enable max-len */
attr.matrix.elements = [
cos * sx, sin * sx,
-sin * sy, cos * sy,
cos * tx_4 - sin * ty_4 + rcx + tx,
sin * tx_4 + cos * ty_4 + rcy + ty
];
attr.matrix.inverse(attr.inverseMatrix);
attr.dirtyTransform = false;
attr.bbox.transform.dirty = true;
},
/**
* Pre-multiplies the current transformation matrix of a sprite with the given matrix.
* If `isSplit` parameter is `true`, the resulting matrix is also split into
* individual components (scaling, rotation, translation) and corresponding sprite
* attributes are updated. The shearing component is not extracted.
* Note, that transformation attributes work as if transformations are applied to the
* local coordinate system of a sprite, while matrix transformations transform
* the global coordinate space or the surface grid.
* Since the `transform` method returns the sprite itself, calls to the method
* can be chained. And if updating sprite transformation attributes is desired,
* it can be achieved by setting the `isSplit` parameter of the last call to `true`.
* For example:
*
* sprite.transform(matrixA).transform(matrixB).transform(matrixC, true);
*
* See also: {@link #setTransform}
*
* @param {Ext.draw.Matrix/Number[]} matrix A transformation matrix or array of its elements.
* @param {Boolean} [isSplit=false] If 'true', transformation attributes are updated.
* @return {Ext.draw.sprite.Sprite} This sprite.
*/
transform: function(matrix, isSplit) {
var attr = this.attr,
spriteMatrix = attr.matrix,
elements;
if (matrix && matrix.isMatrix) {
elements = matrix.elements;
}
else {
elements = matrix;
}
//<debug>
if (!(Ext.isArray(elements) && elements.length === 6)) {
Ext.raise("An instance of Ext.draw.Matrix or an array of 6 numbers is expected.");
}
//</debug>
spriteMatrix.prepend.apply(spriteMatrix, elements.slice());
spriteMatrix.inverse(attr.inverseMatrix);
if (isSplit) {
this.updateTransformAttributes();
}
attr.dirtyTransform = false;
attr.bbox.transform.dirty = true;
this.setDirty(true);
return this;
},
/**
* @private
*/
updateTransformAttributes: function() {
var attr = this.attr,
split = attr.matrix.split();
attr.rotationRads = split.rotate;
attr.rotationCenterX = 0;
attr.rotationCenterY = 0;
attr.scalingX = split.scaleX;
attr.scalingY = split.scaleY;
attr.scalingCenterX = 0;
attr.scalingCenterY = 0;
attr.translationX = split.translateX;
attr.translationY = split.translateY;
},
/**
* Resets current transformation matrix of a sprite to the identify matrix.
* @param {Boolean} [isSplit=false] If 'true', transformation attributes are updated.
* @return {Ext.draw.sprite.Sprite} This sprite.
*/
resetTransform: function(isSplit) {
var attr = this.attr;
attr.matrix.reset();
attr.inverseMatrix.reset();
if (!isSplit) {
this.updateTransformAttributes();
}
attr.dirtyTransform = false;
attr.bbox.transform.dirty = true;
this.setDirty(true);
return this;
},
/**
* Resets current transformation matrix of a sprite to the identify matrix
* and pre-multiplies it with the given matrix.
* This is effectively the same as calling {@link #resetTransform},
* followed by {@link #transform} with the same arguments.
*
* See also: {@link #transform}
*
* 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().rotate(Math.PI, 100, 100);
*
* rect.setTransform(m);
* main.renderFrame();
*
* There may be times where the transformation you need to apply cannot easily be
* accomplished using the sprite’s convenience transform methods. Or, you may want
* to pass a matrix directly to the sprite in order to set a transformation. The
* `setTransform` method allows for this sort of advanced usage as well. The
* following tables show each transformation matrix used when applying
* transformations to a sprite.
*
* ### Translate
* <table style="text-align: center;">
* <tr>
* <td style="font-weight: normal;">1</td>
* <td style="font-weight: normal;">0</td>
* <td style="font-weight: normal;">tx</td>
* </tr>
* <tr>
* <td>0</td>
* <td>1</td>
* <td>ty</td>
* </tr>
* <tr>
* <td>0</td>
* <td>0</td>
* <td>1</td>
* </tr>
* </table>
*
* ### Rotate (θ is the angle of rotation)
* <table style="text-align: center;">
* <tr>
* <td style="font-weight: normal;">cos(θ)</td>
* <td style="font-weight: normal;">-sin(θ)</td>
* <td style="font-weight: normal;">0</td>
* </tr>
* <tr>
* <td>0</td>
* <td>cos(θ)</td>
* <td>0</td>
* </tr>
* <tr>
* <td>0</td>
* <td>0</td>
* <td>1</td>
* </tr>
* </table>
*
* ### Scale
* <table style="text-align: center;">
* <tr>
* <td style="font-weight: normal;">sx</td>
* <td style="font-weight: normal;">0</td>
* <td style="font-weight: normal;">0</td>
* </tr>
* <tr>
* <td>0</td>
* <td>cos(θ)</td>
* <td>0</td>
* </tr>
* <tr>
* <td>0</td>
* <td>0</td>
* <td>1</td>
* </tr>
* </table>
*
* ### Shear X _(λ is the distance on the x axis to shear by)_
* <table style="text-align: center;">
* <tr>
* <td style="font-weight: normal;">1</td>
* <td style="font-weight: normal;">λx</td>
* <td style="font-weight: normal;">0</td>
* </tr>
* <tr>
* <td>0</td>
* <td>1</td>
* <td>0</td>
* </tr>
* <tr>
* <td>0</td>
* <td>0</td>
* <td>1</td>
* </tr>
* </table>
*
* ### Shear Y (λ is the distance on the y axis to shear by)
* <table style="text-align: center;">
* <tr>
* <td style="font-weight: normal;">1</td>
* <td style="font-weight: normal;">0</td>
* <td style="font-weight: normal;">0</td>
* </tr>
* <tr>
* <td>λy</td>
* <td>1</td>
* <td>0</td>
* </tr>
* <tr>
* <td>0</td>
* <td>0</td>
* <td>1</td>
* </tr>
* </table>
*
* ### Skew X (θ is the angle to skew by)
* <table style="text-align: center;">
* <tr>
* <td style="font-weight: normal;">1</td>
* <td style="font-weight: normal;">tan(θ)</td>
* <td style="font-weight: normal;">0</td>
* </tr>
* <tr>
* <td>0</td>
* <td>1</td>
* <td>0</td>
* </tr>
* <tr>
* <td>0</td>
* <td>0</td>
* <td>1</td>
* </tr>
* </table>
*
* ### Skew Y (θ is the angle to skew by)
* <table style="text-align: center;">
* <tr>
* <td style="font-weight: normal;">1</td>
* <td style="font-weight: normal;">0</td>
* <td style="font-weight: normal;">0</td>
* </tr>
* <tr>
* <td>tan(θ)</td>
* <td>1</td>
* <td>0</td>
* </tr>
* <tr>
* <td>0</td>
* <td>0</td>
* <td>1</td>
* </tr>
* </table>
*
* Multiplying matrices for translation, rotation, scaling, and shearing / skewing
* any number of times in the desired order produces a single matrix for a composite
* transformation. You can use the product as a value for the `setTransform`method
* of a sprite:
*
* mySprite.setTransform([a, b, c, d, e, f]);
*
* Where `a`, `b`, `c`, `d`, `e`, `f` are numeric values that correspond to the
* following transformation matrix components:
*
* <table style="text-align: center;">
* <tr>
* <td style="font-weight: normal;">a</td>
* <td style="font-weight: normal;">c</td>
* <td style="font-weight: normal;">e</td>
* </tr>
* <tr>
* <td>b</td>
* <td>d</td>
* <td>f</td>
* </tr>
* <tr>
* <td>0</td>
* <td>0</td>
* <td>1</td>
* </tr>
* </table>
*
* @param {Ext.draw.Matrix/Number[]} matrix The transformation matrix to apply or its
* raw elements as an array.
* @param {Boolean} [isSplit=false] If `true`, transformation attributes are updated.
* @return {Ext.draw.sprite.Sprite} This sprite.
*/
setTransform: function(matrix, isSplit) {
this.resetTransform(true);
this.transform.call(this, matrix, isSplit);
return this;
},
/**
* @method
* Called before rendering.
*/
preRender: Ext.emptyFn,
/**
* @method
* This is where the actual sprite rendering happens by calling `ctx` methods.
* @param {Ext.draw.Surface} surface A draw container surface.
* @param {CanvasRenderingContext2D} ctx A context object that is API compatible with the native
* [CanvasRenderingContext2D](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D).
* @param {Number[]} surfaceClipRect The clip rect: [left, top, width, height].
* Not to be confused with the `surface.getRect()`, which represents the location
* and size of the surface in a draw container, in draw container coordinates.
* The clip rect on the other hand represents the portion of the surface that is being
* rendered, in surface coordinates.
*
* @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,
//<debug>
/**
* @private
* Renders the bounding box of transformed sprite.
*/
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();
},
//</debug>
/**
* 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, y, bbox, isBBoxHit;
// Meant to be overridden in subclasses for more precise hit testing.
// This version doesn't take any options and simply hit tests sprite's
// bounding box, if the sprite is visible.
if (this.isVisible()) {
x = point[0];
y = point[1];
bbox = this.getBBox();
isBBoxHit = bbox && x >= bbox.x && x <= (bbox.x + bbox.width) &&
y >= bbox.y && y <= (bbox.y + bbox.height);
if (isBBoxHit) {
return {
sprite: this
};
}
}
return null;
},
/**
* @private
* Checks if the sprite can be seen.
* This includes the `hidden` attribute check, alpha/opacity checks,
* fill/stroke color checks and surface/parent checks.
* The method doesn't check if the sprite is off-screen.
* @return {Boolean} Returns `true`, if the sprite can be seen.
*/
isVisible: function() {
var attr = this.attr,
parent = this.getParent(),
hasParent = parent && (parent.isSurface || parent.isVisible()),
isSeen = hasParent && !attr.hidden && attr.globalAlpha,
none1 = Ext.util.Color.NONE,
none2 = Ext.util.Color.RGBA_NONE,
hasFill = attr.fillOpacity && attr.fillStyle !== none1 && attr.fillStyle !== none2,
hasStroke = attr.strokeOpacity && attr.strokeStyle !== none1 &&
attr.strokeStyle !== none2,
result = isSeen && (hasFill || hasStroke);
return !!result;
},
repaint: function() {
var surface = this.getSurface();
if (surface) {
surface.renderFrame();
}
},
/**
* Removes this sprite from its surface.
* The sprite itself is not destroyed.
* @return {Ext.draw.sprite.Sprite} Returns the removed sprite or `null` otherwise.
*/
remove: function() {
var surface = this.getSurface();
if (surface && surface.isSurface) {
return surface.remove(this);
}
return null;
},
/**
* Removes the sprite and clears all listeners.
*/
destroy: function() {
var me = this,
modifier = me.modifiers.target,
currentModifier;
while (modifier) {
currentModifier = modifier;
modifier = modifier._lower;
currentModifier.destroy();
}
delete me.attr;
me.remove();
if (me.fireEvent('beforedestroy', me) !== false) {
me.fireEvent('destroy', me);
}
me.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 = new Ext.draw.sprite.AttributeDefinition(this.def);
this.def.spriteClass = this;
});