/** * A Surface is an interface to render methods inside a draw {@link Ext.draw.Component}. * A Surface contains methods to render sprites, get bounding boxes of sprites, add * sprites to the canvas, initialize other graphic components, etc. One of the most used * methods for this class is the `add` method, to add Sprites to the surface. * * Most of the Surface methods are abstract and they have a concrete implementation * in VML or SVG engines. * * A Surface instance can be accessed as a property of a draw component. For example: * * drawComponent.getSurface('main').add({ * type: 'circle', * fill: '#ffc', * radius: 100, * x: 100, * y: 100 * }); * * The configuration object passed in the `add` method is the same as described in the {@link Ext.draw.sprite.Sprite} * class documentation. * * ## Example * * drawComponent.getSurface('main').add([ * { * type: 'circle', * radius: 10, * fill: '#f00', * x: 10, * y: 10, * group: 'circles' * }, * { * type: 'circle', * radius: 10, * fill: '#0f0', * x: 50, * y: 50, * group: 'circles' * }, * { * type: 'circle', * radius: 10, * fill: '#00f', * x: 100, * y: 100, * group: 'circles' * }, * { * type: 'rect', * radius: 10, * x: 10, * y: 10, * group: 'rectangles' * }, * { * type: 'rect', * radius: 10, * x: 50, * y: 50, * group: 'rectangles' * }, * { * type: 'rect', * radius: 10, * x: 100, * y: 100, * group: 'rectangles' * } * ]); * */ Ext.define('Ext.draw.Surface', { extend: 'Ext.Component', xtype: 'surface', requires: [ 'Ext.draw.sprite.*', 'Ext.draw.gradient.*', 'Ext.draw.sprite.AttributeDefinition', 'Ext.draw.Matrix', 'Ext.draw.Draw', 'Ext.draw.Group' ], uses: [ "Ext.draw.engine.Canvas" ], defaultIdPrefix: 'ext-surface-', /** * The reported device pixel density. */ devicePixelRatio: window.devicePixelRatio || 1, statics: { /** * Stably sort the list of sprites by their zIndex. * TODO: Improve the performance. Reduce gc impact. * @param list */ stableSort: function (list) { if (list.length < 2) { return; } var keys = {}, sortedKeys, result = [], i, ln, zIndex; for (i = 0, ln = list.length; i < ln; i++) { zIndex = list[i].attr.zIndex; if (!keys[zIndex]) { keys[zIndex] = [list[i]]; } else { keys[zIndex].push(list[i]); } } sortedKeys = Object.keys(keys).sort(function (a, b) {return a - b;}); for (i = 0, ln = sortedKeys.length; i < ln; i++) { result.push.apply(result, keys[sortedKeys[i]]); } for (i = 0, ln = list.length; i < ln; i++) { list[i] = result[i]; } } }, config: { /** * @cfg {Array} * The region of the surface related to its component. */ region: null, /** * @cfg {Object} * The config of a background sprite of current surface */ background: null, /** * @cfg {Ext.draw.Group} * The default group of the surfaces. */ items: [], /** * @cfg {Array} * An array of groups. */ groups: [], /** * @cfg {Boolean} * Indicates whether the surface needs redraw. */ dirty: false }, dirtyPredecessor: 0, constructor: function (config) { var me = this; me.predecessors = []; me.successors = []; me.pendingRenderFrame = false; me.callSuper([config]); me.matrix = new Ext.draw.Matrix(); me.inverseMatrix = me.matrix.inverse(me.inverseMatrix); me.resetTransform(); }, /** * Round the number to align to the pixels on device. * @param num The number to align. * @return {Number} The resultant alignment. */ roundPixel: function (num) { return Math.round(this.devicePixelRatio * num) / this.devicePixelRatio; }, /** * Mark the surface to render after another surface is updated. * @param surface The surface to wait for. */ waitFor: function (surface) { var me = this, predecessors = me.predecessors; if (!Ext.Array.contains(predecessors, surface)) { predecessors.push(surface); surface.successors.push(me); if (surface._dirty) { me.dirtyPredecessor++; } } }, setDirty: function (dirty) { if (this._dirty !== dirty) { var successors = this.successors, successor, i, ln = successors.length; for (i = 0; i < ln; i++) { successor = successors[i]; if (dirty) { successor.dirtyPredecessor++; successor.setDirty(true); } else { successor.dirtyPredecessor--; if (successor.dirtyPredecessor === 0 && successor.pendingRenderFrame) { successor.renderFrame(); } } } this._dirty = dirty; } }, applyElement: function (newElement, oldElement) { if (oldElement) { oldElement.set(newElement); } else { oldElement = Ext.Element.create(newElement); } this.setDirty(true); return oldElement; }, applyBackground: function (background, oldBackground) { this.setDirty(true); if (Ext.isString(background)) { background = { fillStyle: background }; } return Ext.factory(background, Ext.draw.sprite.Rect, oldBackground); }, applyRegion: function (region, oldRegion) { if (oldRegion && region[0] === oldRegion[0] && region[1] === oldRegion[1] && region[2] === oldRegion[2] && region[3] === oldRegion[3]) { return; } if (Ext.isArray(region)) { return [region[0], region[1], region[2], region[3]]; } else if (Ext.isObject(region)) { return [ region.x || region.left, region.y || region.top, region.width || (region.right - region.left), region.height || (region.bottom - region.top) ]; } }, updateRegion: function (region) { var me = this, l = region[0], t = region[1], r = l + region[2], b = t + region[3], background = this.getBackground(), element = me.element; element.setBox({ top: Math.floor(t), left: Math.floor(l), width: Math.ceil(r - Math.floor(l)), height: Math.ceil(b - Math.floor(t)) }); if (background) { background.setAttributes({ x: 0, y: 0, width: Math.ceil(r - Math.floor(l)), height: Math.ceil(b - Math.floor(t)) }); } me.setDirty(true); }, /** * Reset the matrix of the surface. */ resetTransform: function () { this.matrix.set(1, 0, 0, 1, 0, 0); this.inverseMatrix.set(1, 0, 0, 1, 0, 0); this.setDirty(true); }, updateComponent: function (component, oldComponent) { if (component) { component.element.dom.appendChild(this.element.dom); } }, /** * Add a Sprite to the surface. * You can put any number of object as parameter. * See {@link Ext.draw.sprite.Sprite} for the configuration object to be passed into this method. * * For example: * * drawComponent.surface.add({ * type: 'circle', * fill: '#ffc', * radius: 100, * x: 100, * y: 100 * }); * */ add: function () { var me = this, args = Array.prototype.slice.call(arguments), argIsArray = Ext.isArray(args[0]), results = [], sprite, items, i, ln, group, groups; items = argIsArray ? args[0] : args; for (i = 0, ln = items.length; i < ln; i++) { sprite = items[i]; if (!sprite) { continue; } sprite = me.prepareItems(args[0])[i]; groups = sprite.group; if (groups.length) { for (i = 0, ln = groups.length; i < ln; i++) { group = groups[i]; me.getGroup(group).add(sprite); } } me.getItems().add(sprite); results.push(sprite); sprite.setParent(this); me.onAdd(sprite); } me.dirtyZIndex = true; me.setDirty(true); if (!argIsArray && results.length === 1) { return results[0]; } else { return results; } }, /** * @protected * Invoked when a sprite is adding to the surface. * @param {Ext.draw.sprite.Sprite} sprite The sprite to be added. */ onAdd: Ext.emptyFn, /** * Remove a given sprite from the surface, optionally destroying the sprite in the process. * You can also call the sprite own `remove` method. * * For example: * * drawComponent.surface.remove(sprite); * // or... * sprite.remove(); * * @param {Ext.draw.sprite.Sprite} sprite * @param {Boolean} destroySprite */ remove: function (sprite, destroySprite) { if (sprite) { if (destroySprite === true) { sprite.destroy(); } else { this.getGroups().each(function (item) { item.remove(sprite); }); this.getItems().remove(sprite); } this.dirtyZIndex = true; this.setDirty(true); } }, /** * Remove all sprites from the surface, optionally destroying the sprites in the process. * * For example: * * drawComponent.surface.removeAll(); * */ removeAll: function () { this.getItems().clear(); this.dirtyZIndex = true; }, // @private applyItems: function (items, oldItems) { var result; if (items instanceof Ext.draw.Group) { result = items; } else { result = new Ext.draw.Group({surface: this}); result.autoDestroy = true; result.addAll(this.prepareItems(items)); } this.setDirty(true); return result; }, /** * @private * Initialize and apply defaults to surface items. */ prepareItems: function (items) { items = [].concat(items); // Make sure defaults are applied and item is initialized var me = this, item, i, ln, j, removeSprite = function (sprite) { this.remove(sprite, false); }; for (i = 0, ln = items.length; i < ln; i++) { item = items[i]; if (!(item instanceof Ext.draw.sprite.Sprite)) { // Temporary, just take in configs... item = items[i] = me.createItem(item); } for (j = 0; j < item.group.length; j++) { me.getGroup(item.group[i]).add(item); } item.on('beforedestroy', removeSprite, me); } return items; }, applyGroups: function (groups, oldGroups) { var result; if (groups instanceof Ext.util.MixedCollection) { result = groups; } else { result = new Ext.util.MixedCollection(); result.addAll(groups); } if (oldGroups) { oldGroups.each(function (group) { if (!result.contains()) { group.destroy(); } }); oldGroups.destroy(); } this.setDirty(true); return result; }, /** * @deprecated Do not use groups directly * Returns a new group or an existent group associated with the current surface. * The group returned is a {@link Ext.draw.Group} group. * * For example: * * var spriteGroup = drawComponent.surface.getGroup('someGroupId'); * * @param {String} id The unique identifier of the group. * @return {Ext.draw.Group} The group. */ getGroup: function (id) { var group; if (typeof id === "string") { group = this.getGroups().get(id); if (!group) { group = this.createGroup(id); } } else { group = id; } return group; }, /** * @private * @deprecated Do not use groups directly * @param id * @return {Ext.draw.Group} The group. */ createGroup: function (id) { var group = this.getGroups().get(id); if (!group) { group = new Ext.draw.Group({surface: this}); group.id = id || Ext.id(null, 'ext-surface-group-'); this.getGroups().add(group); } this.setDirty(true); return group; }, /** * @private * @deprecated Do not use groups directly * @param group */ removeGroup: function (group) { if (Ext.isString(group)) { group = this.getGroups().get(group); } if (group) { this.getGroups().remove(group); group.destroy(); } this.setDirty(true); }, /** * @private Creates an item and appends it to the surface. Called * as an internal method when calling `add`. */ createItem: function (config) { var sprite = Ext.create(config.xclass || 'sprite.' + config.type, config); return sprite; }, /** * @deprecated Use the `sprite.getBBox(isWithoutTransform)` directly. * @param sprite * @param isWithoutTransform * @return {Object} */ getBBox: function (sprite, isWithoutTransform) { return sprite.getBBox(isWithoutTransform); }, /** * Empty the surface content (without touching the sprites.) */ clear: Ext.emptyFn, /** * @private * Order the items by their z-index if any of that has been changed since last sort. */ orderByZIndex: function () { var me = this, items = me.getItems().items, dirtyZIndex = false, i, ln; if (me.getDirty()) { for (i = 0, ln = items.length; i < ln; i++) { if (items[i].attr.dirtyZIndex) { dirtyZIndex = true; break; } } if (dirtyZIndex) { // sort by zIndex Ext.draw.Surface.stableSort(items); this.setDirty(true); } for (i = 0, ln = items.length; i < ln; i++) { items[i].attr.dirtyZIndex = false; } } }, /** * Force the element to redraw. */ repaint: function () { var me = this; me.repaint = Ext.emptyFn; setTimeout(function () { delete me.repaint; me.element.repaint(); }, 1); }, /** * Triggers the re-rendering of the canvas. */ renderFrame: function () { if (!this.element) { return; } if (this.dirtyPredecessor > 0) { this.pendingRenderFrame = true; } var me = this, region = this.getRegion(), background = me.getBackground(), items = me.getItems().items, item, i, ln; // Cannot render before the surface is placed. if (!region) { return; } // This will also check the dirty flags of the sprites. me.orderByZIndex(); if (me.getDirty()) { me.clear(); me.clearTransform(); if (background) { me.renderSprite(background); } for (i = 0, ln = items.length; i < ln; i++) { item = items[i]; item.applyTransformations(); if (false === me.renderSprite(item)) { return; } item.attr.textPositionCount = me.textPosition; } me.setDirty(false); } }, /** * @private * Renders a single sprite into the surface. * Do not call it from outside `renderFrame` method. * * @param {Ext.draw.sprite.Sprite} sprite The Sprite to be rendered. * @return {Boolean} returns `false` to stop the rendering to continue. */ renderSprite: Ext.emptyFn, /** * @private * Clears the current transformation state on the surface. */ clearTransform: Ext.emptyFn, /** * Returns 'true' if the surface is dirty. * @return {Boolean} 'true' if the surface is dirty */ getDirty: function () { return this._dirty; }, /** * Destroys the surface. This is done by removing all components from it and * also removing its reference to a DOM element. * * For example: * * drawComponent.surface.destroy(); */ destroy: function () { var me = this; me.removeAll(); me.setBackground(null); me.setGroups([]); me.getGroups().destroy(); me.predecessors = null; me.successors = null; me.callSuper(); } });