Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / src / draw / Surface.js
1 /**
2  * @class Ext.draw.Surface
3  * @extends Object
4  *
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.
9  *
10  * Most of the Surface methods are abstract and they have a concrete implementation
11  * in VML or SVG engines.
12  *
13  * A Surface instance can be accessed as a property of a draw component. For example:
14  *
15  *     drawComponent.surface.add({
16  *         type: 'circle',
17  *         fill: '#ffc',
18  *         radius: 100,
19  *         x: 100,
20  *         y: 100
21  *     });
22  *
23  * The configuration object passed in the `add` method is the same as described in the {@link Ext.draw.Sprite}
24  * class documentation.
25  *
26  * ### Listeners
27  *
28  * You can also add event listeners to the surface using the `Observable` listener syntax. Supported events are:
29  *
30  * - mousedown
31  * - mouseup
32  * - mouseover
33  * - mouseout
34  * - mousemove
35  * - mouseenter
36  * - mouseleave
37  * - click
38  *
39  * For example:
40  *
41  *     drawComponent.surface.on({
42  *        'mousemove': function() {
43  *             console.log('moving the mouse over the surface');   
44  *         }
45  *     });
46  *
47  * ## Example
48  *
49  *     drawComponent.surface.add([
50  *         {
51  *             type: 'circle',
52  *             radius: 10,
53  *             fill: '#f00',
54  *             x: 10,
55  *             y: 10,
56  *             group: 'circles'
57  *         },
58  *         {
59  *             type: 'circle',
60  *             radius: 10,
61  *             fill: '#0f0',
62  *             x: 50,
63  *             y: 50,
64  *             group: 'circles'
65  *         },
66  *         {
67  *             type: 'circle',
68  *             radius: 10,
69  *             fill: '#00f',
70  *             x: 100,
71  *             y: 100,
72  *             group: 'circles'
73  *         },
74  *         {
75  *             type: 'rect',
76  *             radius: 10,
77  *             x: 10,
78  *             y: 10,
79  *             group: 'rectangles'
80  *         },
81  *         {
82  *             type: 'rect',
83  *             radius: 10,
84  *             x: 50,
85  *             y: 50,
86  *             group: 'rectangles'
87  *         },
88  *         {
89  *             type: 'rect',
90  *             radius: 10,
91  *             x: 100,
92  *             y: 100,
93  *             group: 'rectangles'
94  *         }
95  *     ]);
96  *     
97  *     // Get references to my groups
98  *     my circles = surface.getGroup('circles');
99  *     my rectangles = surface.getGroup('rectangles');
100  *     
101  *     // Animate the circles down
102  *     circles.animate({
103  *         duration: 1000,
104  *         translate: {
105  *             y: 200
106  *         }
107  *     });
108  *     
109  *     // Animate the rectangles across
110  *     rectangles.animate({
111  *         duration: 1000,
112  *         translate: {
113  *             x: 200
114  *         }
115  *     });
116  */
117 Ext.define('Ext.draw.Surface', {
118
119     /* Begin Definitions */
120
121     mixins: {
122         observable: 'Ext.util.Observable'
123     },
124
125     requires: ['Ext.draw.CompositeSprite'],
126     uses: ['Ext.draw.engine.Svg', 'Ext.draw.engine.Vml'],
127
128     separatorRe: /[, ]+/,
129
130     statics: {
131         /**
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>.
137          */
138         create: function(config, enginePriority) {
139             enginePriority = enginePriority || ['Svg', 'Vml'];
140
141             var i = 0,
142                 len = enginePriority.length,
143                 surfaceClass;
144
145             for (; i < len; i++) {
146                 if (Ext.supports[enginePriority[i]]) {
147                     return Ext.create('Ext.draw.engine.' + enginePriority[i], config);
148                 }
149             }
150             return false;
151         }
152     },
153
154     /* End Definitions */
155
156     // @private
157     availableAttrs: {
158         blur: 0,
159         "clip-rect": "0 0 1e9 1e9",
160         cursor: "default",
161         cx: 0,
162         cy: 0,
163         'dominant-baseline': 'auto',
164         fill: "none",
165         "fill-opacity": 1,
166         font: '10px "Arial"',
167         "font-family": '"Arial"',
168         "font-size": "10",
169         "font-style": "normal",
170         "font-weight": 400,
171         gradient: "",
172         height: 0,
173         hidden: false,
174         href: "http://sencha.com/",
175         opacity: 1,
176         path: "M0,0",
177         radius: 0,
178         rx: 0,
179         ry: 0,
180         scale: "1 1",
181         src: "",
182         stroke: "#000",
183         "stroke-dasharray": "",
184         "stroke-linecap": "butt",
185         "stroke-linejoin": "butt",
186         "stroke-miterlimit": 0,
187         "stroke-opacity": 1,
188         "stroke-width": 1,
189         target: "_blank",
190         text: "",
191         "text-anchor": "middle",
192         title: "Ext Draw",
193         width: 0,
194         x: 0,
195         y: 0,
196         zIndex: 0
197     },
198
199  /**
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}.
203   */
204  /**
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}.
208   */
209     container: undefined,
210     height: 352,
211     width: 512,
212     x: 0,
213     y: 0,
214
215     constructor: function(config) {
216         var me = this;
217         config = config || {};
218         Ext.apply(me, config);
219
220         me.domRef = Ext.getDoc().dom;
221         
222         me.customAttributes = {};
223
224         me.addEvents(
225             'mousedown',
226             'mouseup',
227             'mouseover',
228             'mouseout',
229             'mousemove',
230             'mouseenter',
231             'mouseleave',
232             'click'
233         );
234
235         me.mixins.observable.constructor.call(me);
236
237         me.getId();
238         me.initGradients();
239         me.initItems();
240         if (me.renderTo) {
241             me.render(me.renderTo);
242             delete me.renderTo;
243         }
244         me.initBackground(config.background);
245     },
246
247     // @private called to initialize components in the surface
248     // this is dependent on the underlying implementation.
249     initSurface: Ext.emptyFn,
250
251     // @private called to setup the surface to render an item
252     //this is dependent on the underlying implementation.
253     renderItem: Ext.emptyFn,
254
255     // @private
256     renderItems: Ext.emptyFn,
257
258     // @private
259     setViewBox: Ext.emptyFn,
260
261     /**
262      * Adds one or more CSS classes to the element. Duplicate classes are automatically filtered out.
263      *
264      * For example:
265      *
266      *          drawComponent.surface.addCls(sprite, 'x-visible');
267      *      
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
270      * @method
271      */
272     addCls: Ext.emptyFn,
273
274     /**
275      * Removes one or more CSS classes from the element.
276      *
277      * For example:
278      *
279      *      drawComponent.surface.removeCls(sprite, 'x-visible');
280      *      
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
283      * @method
284      */
285     removeCls: Ext.emptyFn,
286
287     /**
288      * Sets CSS style attributes to an element.
289      *
290      * For example:
291      *
292      *      drawComponent.surface.setStyle(sprite, {
293      *          'cursor': 'pointer'
294      *      });
295      *      
296      * @param {Object} sprite The sprite to add, or an array of classes to
297      * @param {Object} styles An Object with CSS styles.
298      * @method
299      */
300     setStyle: Ext.emptyFn,
301
302     // @private
303     initGradients: function() {
304         var gradients = this.gradients;
305         if (gradients) {
306             Ext.each(gradients, this.addGradient, this);
307         }
308     },
309
310     // @private
311     initItems: function() {
312         var items = this.items;
313         this.items = Ext.create('Ext.draw.CompositeSprite');
314         this.groups = Ext.create('Ext.draw.CompositeSprite');
315         if (items) {
316             this.add(items);
317         }
318     },
319     
320     // @private
321     initBackground: function(config) {
322         var me = this,
323             width = me.width,
324             height = me.height,
325             gradientId, gradient, backgroundSprite;
326         if (config) {
327             if (config.gradient) {
328                 gradient = config.gradient;
329                 gradientId = gradient.id;
330                 me.addGradient(gradient);
331                 me.background = me.add({
332                     type: 'rect',
333                     x: 0,
334                     y: 0,
335                     width: width,
336                     height: height,
337                     fill: 'url(#' + gradientId + ')'
338                 });
339             } else if (config.fill) {
340                 me.background = me.add({
341                     type: 'rect',
342                     x: 0,
343                     y: 0,
344                     width: width,
345                     height: height,
346                     fill: config.fill
347                 });
348             } else if (config.image) {
349                 me.background = me.add({
350                     type: 'image',
351                     x: 0,
352                     y: 0,
353                     width: width,
354                     height: height,
355                     src: config.image
356                 });
357             }
358         }
359     },
360     
361     /**
362      * Sets the size of the surface. Accomodates the background (if any) to fit the new size too.
363      *
364      * For example:
365      *
366      *      drawComponent.surface.setSize(500, 500);
367      *
368      * This method is generally called when also setting the size of the draw Component.
369      * 
370      * @param {Number} w The new width of the canvas.
371      * @param {Number} h The new height of the canvas.
372      */
373     setSize: function(w, h) {
374         if (this.background) {
375             this.background.setAttributes({
376                 width: w,
377                 height: h,
378                 hidden: false
379             }, true);
380         }
381     },
382
383     // @private
384     scrubAttrs: function(sprite) {
385         var i,
386             attrs = {},
387             exclude = {},
388             sattr = sprite.attr;
389         for (i in sattr) {    
390             // Narrow down attributes to the main set
391             if (this.translateAttrs.hasOwnProperty(i)) {
392                 // Translated attr
393                 attrs[this.translateAttrs[i]] = sattr[i];
394                 exclude[this.translateAttrs[i]] = true;
395             }
396             else if (this.availableAttrs.hasOwnProperty(i) && !exclude[i]) {
397                 // Passtrhough attr
398                 attrs[i] = sattr[i];
399             }
400         }
401         return attrs;
402     },
403
404     // @private
405     onClick: function(e) {
406         this.processEvent('click', e);
407     },
408
409     // @private
410     onMouseUp: function(e) {
411         this.processEvent('mouseup', e);
412     },
413
414     // @private
415     onMouseDown: function(e) {
416         this.processEvent('mousedown', e);
417     },
418
419     // @private
420     onMouseOver: function(e) {
421         this.processEvent('mouseover', e);
422     },
423
424     // @private
425     onMouseOut: function(e) {
426         this.processEvent('mouseout', e);
427     },
428
429     // @private
430     onMouseMove: function(e) {
431         this.fireEvent('mousemove', e);
432     },
433
434     // @private
435     onMouseEnter: Ext.emptyFn,
436
437     // @private
438     onMouseLeave: Ext.emptyFn,
439
440     /**
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}.
447      *
448      * The gradient object to be passed into this method is composed by:
449      * 
450      * 
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.
454      * 
455      *
456      For example:
457                 drawComponent.surface.addGradient({
458                     id: 'gradientId',
459                     angle: 45,
460                     stops: {
461                         0: {
462                             color: '#555'
463                         },
464                         100: {
465                             color: '#ddd'
466                         }
467                     }
468                 });
469      *
470      * @method
471      */
472     addGradient: Ext.emptyFn,
473
474     /**
475      * Add a Sprite to the surface. See {@link Ext.draw.Sprite} for the configuration object to be passed into this method.
476      *
477      * For example:
478      *
479      *     drawComponent.surface.add({
480      *         type: 'circle',
481      *         fill: '#ffc',
482      *         radius: 100,
483      *         x: 100,
484      *         y: 100
485      *     });
486      *
487     */
488     add: function() {
489         var args = Array.prototype.slice.call(arguments),
490             sprite,
491             index;
492
493         var hasMultipleArgs = args.length > 1;
494         if (hasMultipleArgs || Ext.isArray(args[0])) {
495             var items = hasMultipleArgs ? args : args[0],
496                 results = [],
497                 i, ln, item;
498
499             for (i = 0, ln = items.length; i < ln; i++) {
500                 item = items[i];
501                 item = this.add(item);
502                 results.push(item);
503             }
504
505             return results;
506         }
507         sprite = this.prepareItems(args[0], true)[0];
508         this.normalizeSpriteCollection(sprite);
509         this.onAdd(sprite);
510         return sprite;
511     },
512
513     /**
514      * @private
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
523      */
524     normalizeSpriteCollection: function(sprite) {
525         var items = this.items,
526             zIndex = sprite.attr.zIndex,
527             idx = items.indexOf(sprite);
528
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)) {
531             items.removeAt(idx);
532             idx = items.findIndexBy(function(otherSprite) {
533                 return otherSprite.attr.zIndex > zIndex;
534             });
535             if (idx < 0) {
536                 idx = items.length;
537             }
538             items.insert(idx, sprite);
539         }
540         return idx;
541     },
542
543     onAdd: function(sprite) {
544         var group = sprite.group,
545             draggable = sprite.draggable,
546             groups, ln, i;
547         if (group) {
548             groups = [].concat(group);
549             ln = groups.length;
550             for (i = 0; i < ln; i++) {
551                 group = groups[i];
552                 this.getGroup(group).add(sprite);
553             }
554             delete sprite.group;
555         }
556         if (draggable) {
557             sprite.initDraggable();
558         }
559     },
560
561     /**
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.
564      *
565      * For example:
566      *
567      *      drawComponent.surface.remove(sprite);
568      *      //or...
569      *      sprite.remove();
570      *      
571      * @param {Ext.draw.Sprite} sprite
572      * @param {Boolean} destroySprite
573      * @return {Number} the sprite's new index in the list
574      */
575     remove: function(sprite, destroySprite) {
576         if (sprite) {
577             this.items.remove(sprite);
578             this.groups.each(function(item) {
579                 item.remove(sprite);
580             });
581             sprite.onRemove();
582             if (destroySprite === true) {
583                 sprite.destroy();
584             }
585         }
586     },
587
588     /**
589      * Remove all sprites from the surface, optionally destroying the sprites in the process.
590      *
591      * For example:
592      *
593      *      drawComponent.surface.removeAll();
594      *      
595      * @param {Boolean} destroySprites Whether to destroy all sprites when removing them.
596      * @return {Number} The sprite's new index in the list.
597      */
598     removeAll: function(destroySprites) {
599         var items = this.items.items,
600             ln = items.length,
601             i;
602         for (i = ln - 1; i > -1; i--) {
603             this.remove(items[i], destroySprites);
604         }
605     },
606
607     onRemove: Ext.emptyFn,
608
609     onDestroy: Ext.emptyFn,
610
611     // @private
612     applyTransformations: function(sprite) {
613             sprite.bbox.transform = 0;
614             this.transform(sprite);
615
616         var me = this,
617             dirty = false,
618             attr = sprite.attr;
619
620         if (attr.translation.x != null || attr.translation.y != null) {
621             me.translate(sprite);
622             dirty = true;
623         }
624         if (attr.scaling.x != null || attr.scaling.y != null) {
625             me.scale(sprite);
626             dirty = true;
627         }
628         if (attr.rotation.degrees != null) {
629             me.rotate(sprite);
630             dirty = true;
631         }
632         if (dirty) {
633             sprite.bbox.transform = 0;
634             this.transform(sprite);
635             sprite.transformations = [];
636         }
637     },
638
639     // @private
640     rotate: function (sprite) {
641         var bbox,
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;
649         }
650         sprite.transformations.push({
651             type: "rotate",
652             degrees: deg,
653             x: centerX,
654             y: centerY
655         });
656     },
657
658     // @private
659     translate: function(sprite) {
660         var x = sprite.attr.translation.x || 0,
661             y = sprite.attr.translation.y || 0;
662         sprite.transformations.push({
663             type: "translate",
664             x: x,
665             y: y
666         });
667     },
668
669     // @private
670     scale: function(sprite) {
671         var bbox,
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;
676
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;
681         }
682         sprite.transformations.push({
683             type: "scale",
684             x: x,
685             y: y,
686             centerX: centerX,
687             centerY: centerY
688         });
689     },
690
691     // @private
692     rectPath: function (x, y, w, h, r) {
693         if (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"]];
695         }
696         return [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]];
697     },
698
699     // @private
700     ellipsePath: function (x, y, rx, ry) {
701         if (ry == null) {
702             ry = rx;
703         }
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"]];
705     },
706
707     // @private
708     getPathpath: function (el) {
709         return el.attr.path;
710     },
711
712     // @private
713     getPathcircle: function (el) {
714         var a = el.attr;
715         return this.ellipsePath(a.x, a.y, a.radius, a.radius);
716     },
717
718     // @private
719     getPathellipse: function (el) {
720         var a = el.attr;
721         return this.ellipsePath(a.x, a.y,
722                                 a.radiusX || (a.width / 2) || 0,
723                                 a.radiusY || (a.height / 2) || 0);
724     },
725
726     // @private
727     getPathrect: function (el) {
728         var a = el.attr;
729         return this.rectPath(a.x, a.y, a.width, a.height, a.r);
730     },
731
732     // @private
733     getPathimage: function (el) {
734         var a = el.attr;
735         return this.rectPath(a.x || 0, a.y || 0, a.width, a.height);
736     },
737
738     // @private
739     getPathtext: function (el) {
740         var bbox = this.getBBoxText(el);
741         return this.rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
742     },
743
744     createGroup: function(id) {
745         var group = this.groups.get(id);
746         if (!group) {
747             group = Ext.create('Ext.draw.CompositeSprite', {
748                 surface: this
749             });
750             group.id = id || Ext.id(null, 'ext-surface-group-');
751             this.groups.add(group);
752         }
753         return group;
754     },
755
756     /**
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.
759      *
760      * For example:
761      *
762      *      var spriteGroup = drawComponent.surface.getGroup('someGroupId');
763      *      
764      * @param {String} id The unique identifier of the group.
765      * @return {Object} The {@link Ext.draw.CompositeSprite}.
766      */
767     getGroup: function(id) {
768         if (typeof id == "string") {
769             var group = this.groups.get(id);
770             if (!group) {
771                 group = this.createGroup(id);
772             }
773         } else {
774             group = id;
775         }
776         return group;
777     },
778
779     // @private
780     prepareItems: function(items, applyDefaults) {
781         items = [].concat(items);
782         // Make sure defaults are applied and item is initialized
783         var item, i, ln;
784         for (i = 0, ln = items.length; i < ln; i++) {
785             item = items[i];
786             if (!(item instanceof Ext.draw.Sprite)) {
787                 // Temporary, just take in configs...
788                 item.surface = this;
789                 items[i] = this.createItem(item);
790             } else {
791                 item.surface = this;
792             }
793         }
794         return items;
795     },
796     
797     /**
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}.
800      *
801      * For example:
802      *
803      *      var spriteGroup = drawComponent.surface.setText(sprite, 'my new text');
804      *      
805      * @param {Object} sprite The Sprite to change the text.
806      * @param {String} text The new text to be set.
807      * @method
808      */
809     setText: Ext.emptyFn,
810     
811     //@private Creates an item and appends it to the surface. Called
812     //as an internal method when calling `add`.
813     createItem: Ext.emptyFn,
814
815     /**
816      * Retrieves the id of this component.
817      * Will autogenerate an id if one has not already been set.
818      */
819     getId: function() {
820         return this.id || (this.id = Ext.id(null, 'ext-surface-'));
821     },
822
823     /**
824      * Destroys the surface. This is done by removing all components from it and
825      * also removing its reference to a DOM element.
826      *
827      * For example:
828      *
829      *      drawComponent.surface.destroy();
830      */
831     destroy: function() {
832         delete this.domRef;
833         this.removeAll();
834     }
835 });