/** * 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: { defaultHitTestOptions: { fill: true, stroke: true }, //<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> }, 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;});