X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/draw/Surface.js diff --git a/src/draw/Surface.js b/src/draw/Surface.js new file mode 100644 index 00000000..2283c576 --- /dev/null +++ b/src/draw/Surface.js @@ -0,0 +1,758 @@ +/** + * @class Ext.draw.Surface + * @extends Object + * + * 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.surface.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} + * class documentation. + * + * ### Listeners + * + * You can also add event listeners to the surface using the `Observable` listener syntax. Supported events are: + * + * - mousedown + * - mouseup + * - mouseover + * - mouseout + * - mousemove + * - mouseenter + * - mouseleave + * - click + * + * For example: + * + drawComponent.surface.on({ + 'mousemove': function() { + console.log('moving the mouse over the surface'); + } + }); + */ +Ext.define('Ext.draw.Surface', { + + /* Begin Definitions */ + + mixins: { + observable: 'Ext.util.Observable' + }, + + requires: ['Ext.draw.CompositeSprite'], + uses: ['Ext.draw.engine.Svg', 'Ext.draw.engine.Vml'], + + separatorRe: /[, ]+/, + + statics: { + /** + * Create and return a new concrete Surface instance appropriate for the current environment. + * @param {Object} config Initial configuration for the Surface instance + * @param {Array} enginePriority Optional order of implementations to use; the first one that is + * available in the current environment will be used. Defaults to + * ['Svg', 'Vml']. + */ + create: function(config, enginePriority) { + enginePriority = enginePriority || ['Svg', 'Vml']; + + var i = 0, + len = enginePriority.length, + surfaceClass; + + for (; i < len; i++) { + if (Ext.supports[enginePriority[i]]) { + return Ext.create('Ext.draw.engine.' + enginePriority[i], config); + } + } + return false; + } + }, + + /* End Definitions */ + + // @private + availableAttrs: { + blur: 0, + "clip-rect": "0 0 1e9 1e9", + cursor: "default", + cx: 0, + cy: 0, + 'dominant-baseline': 'auto', + fill: "none", + "fill-opacity": 1, + font: '10px "Arial"', + "font-family": '"Arial"', + "font-size": "10", + "font-style": "normal", + "font-weight": 400, + gradient: "", + height: 0, + hidden: false, + href: "http://sencha.com/", + opacity: 1, + path: "M0,0", + radius: 0, + rx: 0, + ry: 0, + scale: "1 1", + src: "", + stroke: "#000", + "stroke-dasharray": "", + "stroke-linecap": "butt", + "stroke-linejoin": "butt", + "stroke-miterlimit": 0, + "stroke-opacity": 1, + "stroke-width": 1, + target: "_blank", + text: "", + "text-anchor": "middle", + title: "Ext Draw", + width: 0, + x: 0, + y: 0, + zIndex: 0 + }, + + /** + * @cfg {Number} height + * The height of this component in pixels (defaults to auto). + * Note to express this dimension as a percentage or offset see {@link Ext.Component#anchor}. + */ + /** + * @cfg {Number} width + * The width of this component in pixels (defaults to auto). + * Note to express this dimension as a percentage or offset see {@link Ext.Component#anchor}. + */ + container: undefined, + height: 352, + width: 512, + x: 0, + y: 0, + + constructor: function(config) { + var me = this; + config = config || {}; + Ext.apply(me, config); + + me.domRef = Ext.getDoc().dom; + + me.customAttributes = {}; + + me.addEvents( + 'mousedown', + 'mouseup', + 'mouseover', + 'mouseout', + 'mousemove', + 'mouseenter', + 'mouseleave', + 'click' + ); + + me.mixins.observable.constructor.call(me); + + me.getId(); + me.initGradients(); + me.initItems(); + if (me.renderTo) { + me.render(me.renderTo); + delete me.renderTo; + } + me.initBackground(config.background); + }, + + // @private called to initialize components in the surface + // this is dependent on the underlying implementation. + initSurface: Ext.emptyFn, + + // @private called to setup the surface to render an item + //this is dependent on the underlying implementation. + renderItem: Ext.emptyFn, + + // @private + renderItems: Ext.emptyFn, + + // @private + setViewBox: Ext.emptyFn, + + /** + * Adds one or more CSS classes to the element. Duplicate classes are automatically filtered out. + * + * For example: + * + * drawComponent.surface.addCls(sprite, 'x-visible'); + * + * @param {Object} sprite The sprite to add the class to. + * @param {String/Array} className The CSS class to add, or an array of classes + */ + addCls: Ext.emptyFn, + + /** + * Removes one or more CSS classes from the element. + * + * For example: + * + * drawComponent.surface.removeCls(sprite, 'x-visible'); + * + * @param {Object} sprite The sprite to remove the class from. + * @param {String/Array} className The CSS class to remove, or an array of classes + */ + removeCls: Ext.emptyFn, + + /** + * Sets CSS style attributes to an element. + * + * For example: + * + * drawComponent.surface.setStyle(sprite, { + * 'cursor': 'pointer' + * }); + * + * @param {Object} sprite The sprite to add, or an array of classes to + * @param {Object} styles An Object with CSS styles. + */ + setStyle: Ext.emptyFn, + + // @private + initGradients: function() { + var gradients = this.gradients; + if (gradients) { + Ext.each(gradients, this.addGradient, this); + } + }, + + // @private + initItems: function() { + var items = this.items; + this.items = Ext.create('Ext.draw.CompositeSprite'); + this.groups = Ext.create('Ext.draw.CompositeSprite'); + if (items) { + this.add(items); + } + }, + + // @private + initBackground: function(config) { + var gradientId, + gradient, + backgroundSprite, + width = this.width, + height = this.height; + if (config) { + if (config.gradient) { + gradient = config.gradient; + gradientId = gradient.id; + this.addGradient(gradient); + this.background = this.add({ + type: 'rect', + x: 0, + y: 0, + width: width, + height: height, + fill: 'url(#' + gradientId + ')' + }); + } else if (config.fill) { + this.background = this.add({ + type: 'rect', + x: 0, + y: 0, + width: width, + height: height, + fill: config.fill + }); + } else if (config.image) { + this.background = this.add({ + type: 'image', + x: 0, + y: 0, + width: width, + height: height, + src: config.image + }); + } + } + }, + + /** + * Sets the size of the surface. Accomodates the background (if any) to fit the new size too. + * + * For example: + * + * drawComponent.surface.setSize(500, 500); + * + * This method is generally called when also setting the size of the draw Component. + * + * @param {Number} w The new width of the canvas. + * @param {Number} h The new height of the canvas. + */ + setSize: function(w, h) { + if (this.background) { + this.background.setAttributes({ + width: w, + height: h, + hidden: false + }, true); + } + }, + + // @private + scrubAttrs: function(sprite) { + var i, + attrs = {}, + exclude = {}, + sattr = sprite.attr; + for (i in sattr) { + // Narrow down attributes to the main set + if (this.translateAttrs.hasOwnProperty(i)) { + // Translated attr + attrs[this.translateAttrs[i]] = sattr[i]; + exclude[this.translateAttrs[i]] = true; + } + else if (this.availableAttrs.hasOwnProperty(i) && !exclude[i]) { + // Passtrhough attr + attrs[i] = sattr[i]; + } + } + return attrs; + }, + + // @private + onClick: function(e) { + this.processEvent('click', e); + }, + + // @private + onMouseUp: function(e) { + this.processEvent('mouseup', e); + }, + + // @private + onMouseDown: function(e) { + this.processEvent('mousedown', e); + }, + + // @private + onMouseOver: function(e) { + this.processEvent('mouseover', e); + }, + + // @private + onMouseOut: function(e) { + this.processEvent('mouseout', e); + }, + + // @private + onMouseMove: function(e) { + this.fireEvent('mousemove', e); + }, + + // @private + onMouseEnter: Ext.emptyFn, + + // @private + onMouseLeave: Ext.emptyFn, + + /** + * Add a gradient definition to the Surface. Note that in some surface engines, adding + * a gradient via this method will not take effect if the surface has already been rendered. + * Therefore, it is preferred to pass the gradients as an item to the surface config, rather + * than calling this method, especially if the surface is rendered immediately (e.g. due to + * 'renderTo' in its config). For more information on how to create gradients in the Chart + * configuration object please refer to {@link Ext.chart.Chart}. + * + * The gradient object to be passed into this method is composed by: + * + * + * - **id** - string - The unique name of the gradient. + * - **angle** - number, optional - The angle of the gradient in degrees. + * - **stops** - object - An object with numbers as keys (from 0 to 100) and style objects as values. + * + * + For example: + drawComponent.surface.addGradient({ + id: 'gradientId', + angle: 45, + stops: { + 0: { + color: '#555' + }, + 100: { + color: '#ddd' + } + } + }); + */ + addGradient: Ext.emptyFn, + + /** + * Add a Sprite to the surface. See {@link Ext.draw.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 args = Array.prototype.slice.call(arguments), + sprite, + index; + + var hasMultipleArgs = args.length > 1; + if (hasMultipleArgs || Ext.isArray(args[0])) { + var items = hasMultipleArgs ? args : args[0], + results = [], + i, ln, item; + + for (i = 0, ln = items.length; i < ln; i++) { + item = items[i]; + item = this.add(item); + results.push(item); + } + + return results; + } + sprite = this.prepareItems(args[0], true)[0]; + this.normalizeSpriteCollection(sprite); + this.onAdd(sprite); + return sprite; + }, + + /** + * @private + * Insert or move a given sprite into the correct position in the items + * MixedCollection, according to its zIndex. Will be inserted at the end of + * an existing series of sprites with the same or lower zIndex. If the sprite + * is already positioned within an appropriate zIndex group, it will not be moved. + * This ordering can be used by subclasses to assist in rendering the sprites in + * the correct order for proper z-index stacking. + * @param {Ext.draw.Sprite} sprite + * @return {Number} the sprite's new index in the list + */ + normalizeSpriteCollection: function(sprite) { + var items = this.items, + zIndex = sprite.attr.zIndex, + idx = items.indexOf(sprite); + + if (idx < 0 || (idx > 0 && items.getAt(idx - 1).attr.zIndex > zIndex) || + (idx < items.length - 1 && items.getAt(idx + 1).attr.zIndex < zIndex)) { + items.removeAt(idx); + idx = items.findIndexBy(function(otherSprite) { + return otherSprite.attr.zIndex > zIndex; + }); + if (idx < 0) { + idx = items.length; + } + items.insert(idx, sprite); + } + return idx; + }, + + onAdd: function(sprite) { + var group = sprite.group, + draggable = sprite.draggable, + groups, ln, i; + if (group) { + groups = [].concat(group); + ln = groups.length; + for (i = 0; i < ln; i++) { + group = groups[i]; + this.getGroup(group).add(sprite); + } + delete sprite.group; + } + if (draggable) { + sprite.initDraggable(); + } + }, + + /** + * 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 + * @param {Boolean} destroySprite + * @return {Number} the sprite's new index in the list + */ + remove: function(sprite, destroySprite) { + if (sprite) { + this.items.remove(sprite); + this.groups.each(function(item) { + item.remove(sprite); + }); + sprite.onRemove(); + if (destroySprite === true) { + sprite.destroy(); + } + } + }, + + /** + * Remove all sprites from the surface, optionally destroying the sprites in the process. + * + * For example: + * + * drawComponent.surface.removeAll(); + * + * @param {Boolean} destroySprites Whether to destroy all sprites when removing them. + * @return {Number} The sprite's new index in the list. + */ + removeAll: function(destroySprites) { + var items = this.items.items, + ln = items.length, + i; + for (i = ln - 1; i > -1; i--) { + this.remove(items[i], destroySprites); + } + }, + + onRemove: Ext.emptyFn, + + onDestroy: Ext.emptyFn, + + // @private + applyTransformations: function(sprite) { + sprite.bbox.transform = 0; + this.transform(sprite); + + var me = this, + dirty = false, + attr = sprite.attr; + + if (attr.translation.x != null || attr.translation.y != null) { + me.translate(sprite); + dirty = true; + } + if (attr.scaling.x != null || attr.scaling.y != null) { + me.scale(sprite); + dirty = true; + } + if (attr.rotation.degrees != null) { + me.rotate(sprite); + dirty = true; + } + if (dirty) { + sprite.bbox.transform = 0; + this.transform(sprite); + sprite.transformations = []; + } + }, + + // @private + rotate: function (sprite) { + var bbox, + deg = sprite.attr.rotation.degrees, + centerX = sprite.attr.rotation.x, + centerY = sprite.attr.rotation.y; + if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) { + bbox = this.getBBox(sprite); + centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX; + centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY; + } + sprite.transformations.push({ + type: "rotate", + degrees: deg, + x: centerX, + y: centerY + }); + }, + + // @private + translate: function(sprite) { + var x = sprite.attr.translation.x || 0, + y = sprite.attr.translation.y || 0; + sprite.transformations.push({ + type: "translate", + x: x, + y: y + }); + }, + + // @private + scale: function(sprite) { + var bbox, + x = sprite.attr.scaling.x || 1, + y = sprite.attr.scaling.y || 1, + centerX = sprite.attr.scaling.centerX, + centerY = sprite.attr.scaling.centerY; + + if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) { + bbox = this.getBBox(sprite); + centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX; + centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY; + } + sprite.transformations.push({ + type: "scale", + x: x, + y: y, + centerX: centerX, + centerY: centerY + }); + }, + + // @private + rectPath: function (x, y, w, h, r) { + if (r) { + return [["M", x + r, y], ["l", w - r * 2, 0], ["a", r, r, 0, 0, 1, r, r], ["l", 0, h - r * 2], ["a", r, r, 0, 0, 1, -r, r], ["l", r * 2 - w, 0], ["a", r, r, 0, 0, 1, -r, -r], ["l", 0, r * 2 - h], ["a", r, r, 0, 0, 1, r, -r], ["z"]]; + } + return [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]]; + }, + + // @private + ellipsePath: function (x, y, rx, ry) { + if (ry == null) { + ry = rx; + } + return [["M", x, y], ["m", 0, -ry], ["a", rx, ry, 0, 1, 1, 0, 2 * ry], ["a", rx, ry, 0, 1, 1, 0, -2 * ry], ["z"]]; + }, + + // @private + getPathpath: function (el) { + return el.attr.path; + }, + + // @private + getPathcircle: function (el) { + var a = el.attr; + return this.ellipsePath(a.x, a.y, a.radius, a.radius); + }, + + // @private + getPathellipse: function (el) { + var a = el.attr; + return this.ellipsePath(a.x, a.y, a.radiusX, a.radiusY); + }, + + // @private + getPathrect: function (el) { + var a = el.attr; + return this.rectPath(a.x, a.y, a.width, a.height, a.r); + }, + + // @private + getPathimage: function (el) { + var a = el.attr; + return this.rectPath(a.x || 0, a.y || 0, a.width, a.height); + }, + + // @private + getPathtext: function (el) { + var bbox = this.getBBoxText(el); + return this.rectPath(bbox.x, bbox.y, bbox.width, bbox.height); + }, + + createGroup: function(id) { + var group = this.groups.get(id); + if (!group) { + group = Ext.create('Ext.draw.CompositeSprite', { + surface: this + }); + group.id = id || Ext.id(null, 'ext-surface-group-'); + this.groups.add(group); + } + return group; + }, + + /** + * Returns a new group or an existent group associated with the current surface. + * The group returned is a {@link Ext.draw.CompositeSprite} group. + * + * For example: + * + * var spriteGroup = drawComponent.surface.getGroup('someGroupId'); + * + * @param {String} id The unique identifier of the group. + * @return {Object} The {@link Ext.draw.CompositeSprite}. + */ + getGroup: function(id) { + if (typeof id == "string") { + var group = this.groups.get(id); + if (!group) { + group = this.createGroup(id); + } + } else { + group = id; + } + return group; + }, + + // @private + prepareItems: function(items, applyDefaults) { + items = [].concat(items); + // Make sure defaults are applied and item is initialized + var item, i, ln; + for (i = 0, ln = items.length; i < ln; i++) { + item = items[i]; + if (!(item instanceof Ext.draw.Sprite)) { + // Temporary, just take in configs... + item.surface = this; + items[i] = this.createItem(item); + } else { + item.surface = this; + } + } + return items; + }, + + /** + * Changes the text in the sprite element. The sprite must be a `text` sprite. + * This method can also be called from {@link Ext.draw.Sprite}. + * + * For example: + * + * var spriteGroup = drawComponent.surface.setText(sprite, 'my new text'); + * + * @param {Object} sprite The Sprite to change the text. + * @param {String} text The new text to be set. + */ + setText: Ext.emptyFn, + + //@private Creates an item and appends it to the surface. Called + //as an internal method when calling `add`. + createItem: Ext.emptyFn, + + /** + * Retrieves the id of this component. + * Will autogenerate an id if one has not already been set. + */ + getId: function() { + return this.id || (this.id = Ext.id(null, 'ext-surface-')); + }, + + /** + * 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() { + delete this.domRef; + this.removeAll(); + } +}); \ No newline at end of file