2 * @class Ext.draw.Surface
5 * A Surface is an interface to render methods inside a draw {@link Ext.draw.Component}.
6 * A Surface contains methods to render sprites, get bounding boxes of sprites, add
7 * sprites to the canvas, initialize other graphic components, etc. One of the most used
8 * methods for this class is the `add` method, to add Sprites to the surface.
10 * Most of the Surface methods are abstract and they have a concrete implementation
11 * in VML or SVG engines.
13 * A Surface instance can be accessed as a property of a draw component. For example:
15 * drawComponent.surface.add({
23 * The configuration object passed in the `add` method is the same as described in the {@link Ext.draw.Sprite}
24 * class documentation.
28 * You can also add event listeners to the surface using the `Observable` listener syntax. Supported events are:
41 * drawComponent.surface.on({
42 * 'mousemove': function() {
43 * console.log('moving the mouse over the surface');
49 * drawComponent.surface.add([
97 * // Get references to my groups
98 * my circles = surface.getGroup('circles');
99 * my rectangles = surface.getGroup('rectangles');
101 * // Animate the circles down
109 * // Animate the rectangles across
110 * rectangles.animate({
117 Ext.define('Ext.draw.Surface', {
119 /* Begin Definitions */
122 observable: 'Ext.util.Observable'
125 requires: ['Ext.draw.CompositeSprite'],
126 uses: ['Ext.draw.engine.Svg', 'Ext.draw.engine.Vml'],
128 separatorRe: /[, ]+/,
132 * Create and return a new concrete Surface instance appropriate for the current environment.
133 * @param {Object} config Initial configuration for the Surface instance
134 * @param {Array} enginePriority Optional order of implementations to use; the first one that is
135 * available in the current environment will be used. Defaults to
136 * <code>['Svg', 'Vml']</code>.
138 create: function(config, enginePriority) {
139 enginePriority = enginePriority || ['Svg', 'Vml'];
142 len = enginePriority.length,
145 for (; i < len; i++) {
146 if (Ext.supports[enginePriority[i]]) {
147 return Ext.create('Ext.draw.engine.' + enginePriority[i], config);
154 /* End Definitions */
159 "clip-rect": "0 0 1e9 1e9",
163 'dominant-baseline': 'auto',
166 font: '10px "Arial"',
167 "font-family": '"Arial"',
169 "font-style": "normal",
174 href: "http://sencha.com/",
183 "stroke-dasharray": "",
184 "stroke-linecap": "butt",
185 "stroke-linejoin": "butt",
186 "stroke-miterlimit": 0,
191 "text-anchor": "middle",
200 * @cfg {Number} height
201 * The height of this component in pixels (defaults to auto).
202 * <b>Note</b> to express this dimension as a percentage or offset see {@link Ext.Component#anchor}.
205 * @cfg {Number} width
206 * The width of this component in pixels (defaults to auto).
207 * <b>Note</b> to express this dimension as a percentage or offset see {@link Ext.Component#anchor}.
209 container: undefined,
215 constructor: function(config) {
217 config = config || {};
218 Ext.apply(me, config);
220 me.domRef = Ext.getDoc().dom;
222 me.customAttributes = {};
235 me.mixins.observable.constructor.call(me);
241 me.render(me.renderTo);
244 me.initBackground(config.background);
247 // @private called to initialize components in the surface
248 // this is dependent on the underlying implementation.
249 initSurface: Ext.emptyFn,
251 // @private called to setup the surface to render an item
252 //this is dependent on the underlying implementation.
253 renderItem: Ext.emptyFn,
256 renderItems: Ext.emptyFn,
259 setViewBox: Ext.emptyFn,
262 * Adds one or more CSS classes to the element. Duplicate classes are automatically filtered out.
266 * drawComponent.surface.addCls(sprite, 'x-visible');
268 * @param {Object} sprite The sprite to add the class to.
269 * @param {String/Array} className The CSS class to add, or an array of classes
275 * Removes one or more CSS classes from the element.
279 * drawComponent.surface.removeCls(sprite, 'x-visible');
281 * @param {Object} sprite The sprite to remove the class from.
282 * @param {String/Array} className The CSS class to remove, or an array of classes
285 removeCls: Ext.emptyFn,
288 * Sets CSS style attributes to an element.
292 * drawComponent.surface.setStyle(sprite, {
293 * 'cursor': 'pointer'
296 * @param {Object} sprite The sprite to add, or an array of classes to
297 * @param {Object} styles An Object with CSS styles.
300 setStyle: Ext.emptyFn,
303 initGradients: function() {
304 var gradients = this.gradients;
306 Ext.each(gradients, this.addGradient, this);
311 initItems: function() {
312 var items = this.items;
313 this.items = Ext.create('Ext.draw.CompositeSprite');
314 this.groups = Ext.create('Ext.draw.CompositeSprite');
321 initBackground: function(config) {
325 gradientId, gradient, backgroundSprite;
327 if (config.gradient) {
328 gradient = config.gradient;
329 gradientId = gradient.id;
330 me.addGradient(gradient);
331 me.background = me.add({
337 fill: 'url(#' + gradientId + ')'
339 } else if (config.fill) {
340 me.background = me.add({
348 } else if (config.image) {
349 me.background = me.add({
362 * Sets the size of the surface. Accomodates the background (if any) to fit the new size too.
366 * drawComponent.surface.setSize(500, 500);
368 * This method is generally called when also setting the size of the draw Component.
370 * @param {Number} w The new width of the canvas.
371 * @param {Number} h The new height of the canvas.
373 setSize: function(w, h) {
374 if (this.background) {
375 this.background.setAttributes({
384 scrubAttrs: function(sprite) {
390 // Narrow down attributes to the main set
391 if (this.translateAttrs.hasOwnProperty(i)) {
393 attrs[this.translateAttrs[i]] = sattr[i];
394 exclude[this.translateAttrs[i]] = true;
396 else if (this.availableAttrs.hasOwnProperty(i) && !exclude[i]) {
405 onClick: function(e) {
406 this.processEvent('click', e);
410 onMouseUp: function(e) {
411 this.processEvent('mouseup', e);
415 onMouseDown: function(e) {
416 this.processEvent('mousedown', e);
420 onMouseOver: function(e) {
421 this.processEvent('mouseover', e);
425 onMouseOut: function(e) {
426 this.processEvent('mouseout', e);
430 onMouseMove: function(e) {
431 this.fireEvent('mousemove', e);
435 onMouseEnter: Ext.emptyFn,
438 onMouseLeave: Ext.emptyFn,
441 * Add a gradient definition to the Surface. Note that in some surface engines, adding
442 * a gradient via this method will not take effect if the surface has already been rendered.
443 * Therefore, it is preferred to pass the gradients as an item to the surface config, rather
444 * than calling this method, especially if the surface is rendered immediately (e.g. due to
445 * 'renderTo' in its config). For more information on how to create gradients in the Chart
446 * configuration object please refer to {@link Ext.chart.Chart}.
448 * The gradient object to be passed into this method is composed by:
451 * - **id** - string - The unique name of the gradient.
452 * - **angle** - number, optional - The angle of the gradient in degrees.
453 * - **stops** - object - An object with numbers as keys (from 0 to 100) and style objects as values.
457 drawComponent.surface.addGradient({
472 addGradient: Ext.emptyFn,
475 * Add a Sprite to the surface. See {@link Ext.draw.Sprite} for the configuration object to be passed into this method.
479 * drawComponent.surface.add({
489 var args = Array.prototype.slice.call(arguments),
493 var hasMultipleArgs = args.length > 1;
494 if (hasMultipleArgs || Ext.isArray(args[0])) {
495 var items = hasMultipleArgs ? args : args[0],
499 for (i = 0, ln = items.length; i < ln; i++) {
501 item = this.add(item);
507 sprite = this.prepareItems(args[0], true)[0];
508 this.normalizeSpriteCollection(sprite);
515 * Insert or move a given sprite into the correct position in the items
516 * MixedCollection, according to its zIndex. Will be inserted at the end of
517 * an existing series of sprites with the same or lower zIndex. If the sprite
518 * is already positioned within an appropriate zIndex group, it will not be moved.
519 * This ordering can be used by subclasses to assist in rendering the sprites in
520 * the correct order for proper z-index stacking.
521 * @param {Ext.draw.Sprite} sprite
522 * @return {Number} the sprite's new index in the list
524 normalizeSpriteCollection: function(sprite) {
525 var items = this.items,
526 zIndex = sprite.attr.zIndex,
527 idx = items.indexOf(sprite);
529 if (idx < 0 || (idx > 0 && items.getAt(idx - 1).attr.zIndex > zIndex) ||
530 (idx < items.length - 1 && items.getAt(idx + 1).attr.zIndex < zIndex)) {
532 idx = items.findIndexBy(function(otherSprite) {
533 return otherSprite.attr.zIndex > zIndex;
538 items.insert(idx, sprite);
543 onAdd: function(sprite) {
544 var group = sprite.group,
545 draggable = sprite.draggable,
548 groups = [].concat(group);
550 for (i = 0; i < ln; i++) {
552 this.getGroup(group).add(sprite);
557 sprite.initDraggable();
562 * Remove a given sprite from the surface, optionally destroying the sprite in the process.
563 * You can also call the sprite own `remove` method.
567 * drawComponent.surface.remove(sprite);
571 * @param {Ext.draw.Sprite} sprite
572 * @param {Boolean} destroySprite
573 * @return {Number} the sprite's new index in the list
575 remove: function(sprite, destroySprite) {
577 this.items.remove(sprite);
578 this.groups.each(function(item) {
582 if (destroySprite === true) {
589 * Remove all sprites from the surface, optionally destroying the sprites in the process.
593 * drawComponent.surface.removeAll();
595 * @param {Boolean} destroySprites Whether to destroy all sprites when removing them.
596 * @return {Number} The sprite's new index in the list.
598 removeAll: function(destroySprites) {
599 var items = this.items.items,
602 for (i = ln - 1; i > -1; i--) {
603 this.remove(items[i], destroySprites);
607 onRemove: Ext.emptyFn,
609 onDestroy: Ext.emptyFn,
612 applyTransformations: function(sprite) {
613 sprite.bbox.transform = 0;
614 this.transform(sprite);
620 if (attr.translation.x != null || attr.translation.y != null) {
621 me.translate(sprite);
624 if (attr.scaling.x != null || attr.scaling.y != null) {
628 if (attr.rotation.degrees != null) {
633 sprite.bbox.transform = 0;
634 this.transform(sprite);
635 sprite.transformations = [];
640 rotate: function (sprite) {
642 deg = sprite.attr.rotation.degrees,
643 centerX = sprite.attr.rotation.x,
644 centerY = sprite.attr.rotation.y;
645 if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) {
646 bbox = this.getBBox(sprite);
647 centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX;
648 centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY;
650 sprite.transformations.push({
659 translate: function(sprite) {
660 var x = sprite.attr.translation.x || 0,
661 y = sprite.attr.translation.y || 0;
662 sprite.transformations.push({
670 scale: function(sprite) {
672 x = sprite.attr.scaling.x || 1,
673 y = sprite.attr.scaling.y || 1,
674 centerX = sprite.attr.scaling.centerX,
675 centerY = sprite.attr.scaling.centerY;
677 if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) {
678 bbox = this.getBBox(sprite);
679 centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX;
680 centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY;
682 sprite.transformations.push({
692 rectPath: function (x, y, w, h, r) {
694 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"]];
696 return [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]];
700 ellipsePath: function (x, y, rx, ry) {
704 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"]];
708 getPathpath: function (el) {
713 getPathcircle: function (el) {
715 return this.ellipsePath(a.x, a.y, a.radius, a.radius);
719 getPathellipse: function (el) {
721 return this.ellipsePath(a.x, a.y,
722 a.radiusX || (a.width / 2) || 0,
723 a.radiusY || (a.height / 2) || 0);
727 getPathrect: function (el) {
729 return this.rectPath(a.x, a.y, a.width, a.height, a.r);
733 getPathimage: function (el) {
735 return this.rectPath(a.x || 0, a.y || 0, a.width, a.height);
739 getPathtext: function (el) {
740 var bbox = this.getBBoxText(el);
741 return this.rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
744 createGroup: function(id) {
745 var group = this.groups.get(id);
747 group = Ext.create('Ext.draw.CompositeSprite', {
750 group.id = id || Ext.id(null, 'ext-surface-group-');
751 this.groups.add(group);
757 * Returns a new group or an existent group associated with the current surface.
758 * The group returned is a {@link Ext.draw.CompositeSprite} group.
762 * var spriteGroup = drawComponent.surface.getGroup('someGroupId');
764 * @param {String} id The unique identifier of the group.
765 * @return {Object} The {@link Ext.draw.CompositeSprite}.
767 getGroup: function(id) {
768 if (typeof id == "string") {
769 var group = this.groups.get(id);
771 group = this.createGroup(id);
780 prepareItems: function(items, applyDefaults) {
781 items = [].concat(items);
782 // Make sure defaults are applied and item is initialized
784 for (i = 0, ln = items.length; i < ln; i++) {
786 if (!(item instanceof Ext.draw.Sprite)) {
787 // Temporary, just take in configs...
789 items[i] = this.createItem(item);
798 * Changes the text in the sprite element. The sprite must be a `text` sprite.
799 * This method can also be called from {@link Ext.draw.Sprite}.
803 * var spriteGroup = drawComponent.surface.setText(sprite, 'my new text');
805 * @param {Object} sprite The Sprite to change the text.
806 * @param {String} text The new text to be set.
809 setText: Ext.emptyFn,
811 //@private Creates an item and appends it to the surface. Called
812 //as an internal method when calling `add`.
813 createItem: Ext.emptyFn,
816 * Retrieves the id of this component.
817 * Will autogenerate an id if one has not already been set.
820 return this.id || (this.id = Ext.id(null, 'ext-surface-'));
824 * Destroys the surface. This is done by removing all components from it and
825 * also removing its reference to a DOM element.
829 * drawComponent.surface.destroy();
831 destroy: function() {