Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / draw / engine / Vml.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.draw.engine.Vml
17  * @extends Ext.draw.Surface
18  * Provides specific methods to draw with VML.
19  */
20
21 Ext.define('Ext.draw.engine.Vml', {
22
23     /* Begin Definitions */
24
25     extend: 'Ext.draw.Surface',
26
27     requires: ['Ext.draw.Draw', 'Ext.draw.Color', 'Ext.draw.Sprite', 'Ext.draw.Matrix', 'Ext.core.Element'],
28
29     /* End Definitions */
30
31     engine: 'Vml',
32
33     map: {M: "m", L: "l", C: "c", Z: "x", m: "t", l: "r", c: "v", z: "x"},
34     bitesRe: /([clmz]),?([^clmz]*)/gi,
35     valRe: /-?[^,\s-]+/g,
36     fillUrlRe: /^url\(\s*['"]?([^\)]+?)['"]?\s*\)$/i,
37     pathlike: /^(path|rect)$/,
38     NonVmlPathRe: /[ahqstv]/ig, // Non-VML Pathing ops
39     partialPathRe: /[clmz]/g,
40     fontFamilyRe: /^['"]+|['"]+$/g,
41     baseVmlCls: Ext.baseCSSPrefix + 'vml-base',
42     vmlGroupCls: Ext.baseCSSPrefix + 'vml-group',
43     spriteCls: Ext.baseCSSPrefix + 'vml-sprite',
44     measureSpanCls: Ext.baseCSSPrefix + 'vml-measure-span',
45     zoom: 21600,
46     coordsize: 1000,
47     coordorigin: '0 0',
48
49     // @private
50     // Convert an SVG standard path into a VML path
51     path2vml: function (path) {
52         var me = this,
53             nonVML =  me.NonVmlPathRe,
54             map = me.map,
55             val = me.valRe,
56             zoom = me.zoom,
57             bites = me.bitesRe,
58             command = Ext.Function.bind(Ext.draw.Draw.pathToAbsolute, Ext.draw.Draw),
59             res, pa, p, r, i, ii, j, jj;
60         if (String(path).match(nonVML)) {
61             command = Ext.Function.bind(Ext.draw.Draw.path2curve, Ext.draw.Draw);
62         } else if (!String(path).match(me.partialPathRe)) {
63             res = String(path).replace(bites, function (all, command, args) {
64                 var vals = [],
65                     isMove = command.toLowerCase() == "m",
66                     res = map[command];
67                 args.replace(val, function (value) {
68                     if (isMove && vals[length] == 2) {
69                         res += vals + map[command == "m" ? "l" : "L"];
70                         vals = [];
71                     }
72                     vals.push(Math.round(value * zoom));
73                 });
74                 return res + vals;
75             });
76             return res;
77         }
78         pa = command(path);
79         res = [];
80         for (i = 0, ii = pa.length; i < ii; i++) {
81             p = pa[i];
82             r = pa[i][0].toLowerCase();
83             if (r == "z") {
84                 r = "x";
85             }
86             for (j = 1, jj = p.length; j < jj; j++) {
87                 r += Math.round(p[j] * me.zoom) + (j != jj - 1 ? "," : "");
88             }
89             res.push(r);
90         }
91         return res.join(" ");
92     },
93
94     // @private - set of attributes which need to be translated from the sprite API to the native browser API
95     translateAttrs: {
96         radius: "r",
97         radiusX: "rx",
98         radiusY: "ry",
99         lineWidth: "stroke-width",
100         fillOpacity: "fill-opacity",
101         strokeOpacity: "stroke-opacity",
102         strokeLinejoin: "stroke-linejoin"
103     },
104
105     // @private - Minimun set of defaults for different types of sprites.
106     minDefaults: {
107         circle: {
108             fill: "none",
109             stroke: null,
110             "stroke-width": null,
111             opacity: null,
112             "fill-opacity": null,
113             "stroke-opacity": null
114         },
115         ellipse: {
116             cx: 0,
117             cy: 0,
118             rx: 0,
119             ry: 0,
120             fill: "none",
121             stroke: null,
122             "stroke-width": null,
123             opacity: null,
124             "fill-opacity": null,
125             "stroke-opacity": null
126         },
127         rect: {
128             x: 0,
129             y: 0,
130             width: 0,
131             height: 0,
132             rx: 0,
133             ry: 0,
134             fill: "none",
135             stroke: null,
136             "stroke-width": null,
137             opacity: null,
138             "fill-opacity": null,
139             "stroke-opacity": null
140         },
141         text: {
142             x: 0,
143             y: 0,
144             "text-anchor": "start",
145             font: '10px "Arial"',
146             fill: "#000",
147             stroke: null,
148             "stroke-width": null,
149             opacity: null,
150             "fill-opacity": null,
151             "stroke-opacity": null
152         },
153         path: {
154             d: "M0,0",
155             fill: "none",
156             stroke: null,
157             "stroke-width": null,
158             opacity: null,
159             "fill-opacity": null,
160             "stroke-opacity": null
161         },
162         image: {
163             x: 0,
164             y: 0,
165             width: 0,
166             height: 0,
167             preserveAspectRatio: "none",
168             opacity: null
169         }
170     },
171
172     // private
173     onMouseEnter: function(e) {
174         this.fireEvent("mouseenter", e);
175     },
176
177     // private
178     onMouseLeave: function(e) {
179         this.fireEvent("mouseleave", e);
180     },
181
182     // @private - Normalize a delegated single event from the main container to each sprite and sprite group
183     processEvent: function(name, e) {
184         var target = e.getTarget(),
185             surface = this.surface,
186             sprite;
187         this.fireEvent(name, e);
188         sprite = this.items.get(target.id);
189         if (sprite) {
190             sprite.fireEvent(name, sprite, e);
191         }
192     },
193
194     // Create the VML element/elements and append them to the DOM
195     createSpriteElement: function(sprite) {
196         var me = this,
197             attr = sprite.attr,
198             type = sprite.type,
199             zoom = me.zoom,
200             vml = sprite.vml || (sprite.vml = {}),
201             round = Math.round,
202             el = (type === 'image') ? me.createNode('image') : me.createNode('shape'),
203             path, skew, textPath;
204
205         el.coordsize = zoom + ' ' + zoom;
206         el.coordorigin = attr.coordorigin || "0 0";
207         Ext.get(el).addCls(me.spriteCls);
208         if (type == "text") {
209             vml.path = path = me.createNode("path");
210             path.textpathok = true;
211             vml.textpath = textPath = me.createNode("textpath");
212             textPath.on = true;
213             el.appendChild(textPath);
214             el.appendChild(path);
215         }
216         el.id = sprite.id;
217         sprite.el = Ext.get(el);
218         me.el.appendChild(el);
219         if (type !== 'image') {
220             skew = me.createNode("skew");
221             skew.on = true;
222             el.appendChild(skew);
223             sprite.skew = skew;
224         }
225         sprite.matrix = Ext.create('Ext.draw.Matrix');
226         sprite.bbox = {
227             plain: null,
228             transform: null
229         };
230         sprite.fireEvent("render", sprite);
231         return sprite.el;
232     },
233
234     // @private - Get bounding box for the sprite.  The Sprite itself has the public method.
235     getBBox: function (sprite, isWithoutTransform) {
236         var realPath = this["getPath" + sprite.type](sprite);
237         if (isWithoutTransform) {
238             sprite.bbox.plain = sprite.bbox.plain || Ext.draw.Draw.pathDimensions(realPath);
239             return sprite.bbox.plain;
240         }
241         sprite.bbox.transform = sprite.bbox.transform || Ext.draw.Draw.pathDimensions(Ext.draw.Draw.mapPath(realPath, sprite.matrix));
242         return sprite.bbox.transform;
243     },
244
245     getBBoxText: function (sprite) {
246         var vml = sprite.vml;
247         return {
248             x: vml.X + (vml.bbx || 0) - vml.W / 2,
249             y: vml.Y - vml.H / 2,
250             width: vml.W,
251             height: vml.H
252         };
253     },
254
255     applyAttrs: function (sprite) {
256         var me = this,
257             vml = sprite.vml,
258             group = sprite.group,
259             spriteAttr = sprite.attr,
260             el = sprite.el,
261             dom = el.dom,
262             style, name, groups, i, ln, scrubbedAttrs, font, key, bbox;
263
264         if (group) {
265             groups = [].concat(group);
266             ln = groups.length;
267             for (i = 0; i < ln; i++) {
268                 group = groups[i];
269                 me.getGroup(group).add(sprite);
270             }
271             delete sprite.group;
272         }
273         scrubbedAttrs = me.scrubAttrs(sprite) || {};
274
275         if (sprite.zIndexDirty) {
276             me.setZIndex(sprite);
277         }
278
279         // Apply minimum default attributes
280         Ext.applyIf(scrubbedAttrs, me.minDefaults[sprite.type]);
281
282         if (sprite.type == 'image') {
283             Ext.apply(sprite.attr, {
284                 x: scrubbedAttrs.x,
285                 y: scrubbedAttrs.y,
286                 width: scrubbedAttrs.width,
287                 height: scrubbedAttrs.height
288             });
289             bbox = sprite.getBBox();
290             el.setStyle({
291                 width: bbox.width + 'px',
292                 height: bbox.height + 'px'
293             });
294             dom.src = scrubbedAttrs.src;
295         }
296
297         if (dom.href) {
298             dom.href = scrubbedAttrs.href;
299         }
300         if (dom.title) {
301             dom.title = scrubbedAttrs.title;
302         }
303         if (dom.target) {
304             dom.target = scrubbedAttrs.target;
305         }
306         if (dom.cursor) {
307             dom.cursor = scrubbedAttrs.cursor;
308         }
309
310         // Change visibility
311         if (sprite.dirtyHidden) {
312             (scrubbedAttrs.hidden) ? me.hidePrim(sprite) : me.showPrim(sprite);
313             sprite.dirtyHidden = false;
314         }
315
316         // Update path
317         if (sprite.dirtyPath) {
318             if (sprite.type == "circle" || sprite.type == "ellipse") {
319                 var cx = scrubbedAttrs.x,
320                     cy = scrubbedAttrs.y,
321                     rx = scrubbedAttrs.rx || scrubbedAttrs.r || 0,
322                     ry = scrubbedAttrs.ry || scrubbedAttrs.r || 0;
323                 dom.path = Ext.String.format("ar{0},{1},{2},{3},{4},{1},{4},{1}",
324                             Math.round((cx - rx) * me.zoom),
325                             Math.round((cy - ry) * me.zoom),
326                             Math.round((cx + rx) * me.zoom),
327                             Math.round((cy + ry) * me.zoom),
328                             Math.round(cx * me.zoom));
329                 sprite.dirtyPath = false;
330             }
331             else if (sprite.type !== "text" && sprite.type !== 'image') {
332                 sprite.attr.path = scrubbedAttrs.path = me.setPaths(sprite, scrubbedAttrs) || scrubbedAttrs.path;
333                 dom.path = me.path2vml(scrubbedAttrs.path);
334                 sprite.dirtyPath = false;
335             }
336         }
337
338         // Apply clipping
339         if ("clip-rect" in scrubbedAttrs) {
340             me.setClip(sprite, scrubbedAttrs);
341         }
342
343         // Handle text (special handling required)
344         if (sprite.type == "text") {
345             me.setTextAttributes(sprite, scrubbedAttrs);
346         }
347
348         // Handle fill and opacity
349         if (scrubbedAttrs.opacity  || scrubbedAttrs['stroke-opacity'] || scrubbedAttrs.fill) {
350             me.setFill(sprite, scrubbedAttrs);
351         }
352
353         // Handle stroke (all fills require a stroke element)
354         if (scrubbedAttrs.stroke || scrubbedAttrs['stroke-opacity'] || scrubbedAttrs.fill) {
355             me.setStroke(sprite, scrubbedAttrs);
356         }
357         
358         //set styles
359         style = spriteAttr.style;
360         if (style) {
361             el.setStyle(style);
362         }
363
364         sprite.dirty = false;
365     },
366
367     setZIndex: function(sprite) {
368         if (sprite.el) {
369             if (sprite.attr.zIndex != undefined) {
370                 sprite.el.setStyle('zIndex', sprite.attr.zIndex);
371             }
372             sprite.zIndexDirty = false;
373         }
374     },
375
376     // Normalize all virtualized types into paths.
377     setPaths: function(sprite, params) {
378         var spriteAttr = sprite.attr;
379         // Clear bbox cache
380         sprite.bbox.plain = null;
381         sprite.bbox.transform = null;
382         if (sprite.type == 'circle') {
383             spriteAttr.rx = spriteAttr.ry = params.r;
384             return Ext.draw.Draw.ellipsePath(sprite);
385         }
386         else if (sprite.type == 'ellipse') {
387             spriteAttr.rx = params.rx;
388             spriteAttr.ry = params.ry;
389             return Ext.draw.Draw.ellipsePath(sprite);
390         }
391         else if (sprite.type == 'rect') {
392             spriteAttr.rx = spriteAttr.ry = params.r;
393             return Ext.draw.Draw.rectPath(sprite);
394         }
395         else if (sprite.type == 'path' && spriteAttr.path) {
396             return Ext.draw.Draw.pathToAbsolute(spriteAttr.path);
397         }
398         return false;
399     },
400
401     setFill: function(sprite, params) {
402         var me = this,
403             el = sprite.el.dom,
404             fillEl = el.fill,
405             newfill = false,
406             opacity, gradient, fillUrl, rotation, angle;
407
408         if (!fillEl) {
409             // NOT an expando (but it sure looks like one)...
410             fillEl = el.fill = me.createNode("fill");
411             newfill = true;
412         }
413         if (Ext.isArray(params.fill)) {
414             params.fill = params.fill[0];
415         }
416         if (params.fill == "none") {
417             fillEl.on = false;
418         }
419         else {
420             if (typeof params.opacity == "number") {
421                 fillEl.opacity = params.opacity;
422             }
423             if (typeof params["fill-opacity"] == "number") {
424                 fillEl.opacity = params["fill-opacity"];
425             }
426             fillEl.on = true;
427             if (typeof params.fill == "string") {
428                 fillUrl = params.fill.match(me.fillUrlRe);
429                 if (fillUrl) {
430                     fillUrl = fillUrl[1];
431                     // If the URL matches one of the registered gradients, render that gradient
432                     if (fillUrl.charAt(0) == "#") {
433                         gradient = me.gradientsColl.getByKey(fillUrl.substring(1));
434                     }
435                     if (gradient) {
436                         // VML angle is offset and inverted from standard, and must be adjusted to match rotation transform
437                         rotation = params.rotation;
438                         angle = -(gradient.angle + 270 + (rotation ? rotation.degrees : 0)) % 360;
439                         // IE will flip the angle at 0 degrees...
440                         if (angle === 0) {
441                             angle = 180;
442                         }
443                         fillEl.angle = angle;
444                         fillEl.type = "gradient";
445                         fillEl.method = "sigma";
446                         fillEl.colors.value = gradient.colors;
447                     }
448                     // Otherwise treat it as an image
449                     else {
450                         fillEl.src = fillUrl;
451                         fillEl.type = "tile";
452                     }
453                 }
454                 else {
455                     fillEl.color = Ext.draw.Color.toHex(params.fill);
456                     fillEl.src = "";
457                     fillEl.type = "solid";
458                 }
459             }
460         }
461         if (newfill) {
462             el.appendChild(fillEl);
463         }
464     },
465
466     setStroke: function(sprite, params) {
467         var me = this,
468             el = sprite.el.dom,
469             strokeEl = sprite.strokeEl,
470             newStroke = false,
471             width, opacity;
472
473         if (!strokeEl) {
474             strokeEl = sprite.strokeEl = me.createNode("stroke");
475             newStroke = true;
476         }
477         if (Ext.isArray(params.stroke)) {
478             params.stroke = params.stroke[0];
479         }
480         if (!params.stroke || params.stroke == "none" || params.stroke == 0 || params["stroke-width"] == 0) {
481             strokeEl.on = false;
482         }
483         else {
484             strokeEl.on = true;
485             if (params.stroke && !params.stroke.match(me.fillUrlRe)) {
486                 // VML does NOT support a gradient stroke :(
487                 strokeEl.color = Ext.draw.Color.toHex(params.stroke);
488             }
489             strokeEl.joinstyle = params["stroke-linejoin"];
490             strokeEl.endcap = params["stroke-linecap"] || "round";
491             strokeEl.miterlimit = params["stroke-miterlimit"] || 8;
492             width = parseFloat(params["stroke-width"] || 1) * 0.75;
493             opacity = params["stroke-opacity"] || 1;
494             // VML Does not support stroke widths under 1, so we're going to fiddle with stroke-opacity instead.
495             if (Ext.isNumber(width) && width < 1) {
496                 strokeEl.weight = 1;
497                 strokeEl.opacity = opacity * width;
498             }
499             else {
500                 strokeEl.weight = width;
501                 strokeEl.opacity = opacity;
502             }
503         }
504         if (newStroke) {
505             el.appendChild(strokeEl);
506         }
507     },
508
509     setClip: function(sprite, params) {
510         var me = this,
511             el = sprite.el,
512             clipEl = sprite.clipEl,
513             rect = String(params["clip-rect"]).split(me.separatorRe);
514         if (!clipEl) {
515             clipEl = sprite.clipEl = me.el.insertFirst(Ext.getDoc().dom.createElement("div"));
516             clipEl.addCls(Ext.baseCSSPrefix + 'vml-sprite');
517         }
518         if (rect.length == 4) {
519             rect[2] = +rect[2] + (+rect[0]);
520             rect[3] = +rect[3] + (+rect[1]);
521             clipEl.setStyle("clip", Ext.String.format("rect({1}px {2}px {3}px {0}px)", rect[0], rect[1], rect[2], rect[3]));
522             clipEl.setSize(me.el.width, me.el.height);
523         }
524         else {
525             clipEl.setStyle("clip", "");
526         }
527     },
528
529     setTextAttributes: function(sprite, params) {
530         var me = this,
531             vml = sprite.vml,
532             textStyle = vml.textpath.style,
533             spanCacheStyle = me.span.style,
534             zoom = me.zoom,
535             round = Math.round,
536             fontObj = {
537                 fontSize: "font-size",
538                 fontWeight: "font-weight",
539                 fontStyle: "font-style"
540             },
541             fontProp,
542             paramProp;
543         if (sprite.dirtyFont) {
544             if (params.font) {
545                 textStyle.font = spanCacheStyle.font = params.font;
546             }
547             if (params["font-family"]) {
548                 textStyle.fontFamily = '"' + params["font-family"].split(",")[0].replace(me.fontFamilyRe, "") + '"';
549                 spanCacheStyle.fontFamily = params["font-family"];
550             }
551
552             for (fontProp in fontObj) {
553                 paramProp = params[fontObj[fontProp]];
554                 if (paramProp) {
555                     textStyle[fontProp] = spanCacheStyle[fontProp] = paramProp;
556                 }
557             }
558
559             me.setText(sprite, params.text);
560             
561             if (vml.textpath.string) {
562                 me.span.innerHTML = String(vml.textpath.string).replace(/</g, "&#60;").replace(/&/g, "&#38;").replace(/\n/g, "<br>");
563             }
564             vml.W = me.span.offsetWidth;
565             vml.H = me.span.offsetHeight + 2; // TODO handle baseline differences and offset in VML Textpath
566
567             // text-anchor emulation
568             if (params["text-anchor"] == "middle") {
569                 textStyle["v-text-align"] = "center";
570             }
571             else if (params["text-anchor"] == "end") {
572                 textStyle["v-text-align"] = "right";
573                 vml.bbx = -Math.round(vml.W / 2);
574             }
575             else {
576                 textStyle["v-text-align"] = "left";
577                 vml.bbx = Math.round(vml.W / 2);
578             }
579         }
580         vml.X = params.x;
581         vml.Y = params.y;
582         vml.path.v = Ext.String.format("m{0},{1}l{2},{1}", Math.round(vml.X * zoom), Math.round(vml.Y * zoom), Math.round(vml.X * zoom) + 1);
583         // Clear bbox cache
584         sprite.bbox.plain = null;
585         sprite.bbox.transform = null;
586         sprite.dirtyFont = false;
587     },
588     
589     setText: function(sprite, text) {
590         sprite.vml.textpath.string = Ext.htmlDecode(text);
591     },
592
593     hide: function() {
594         this.el.hide();
595     },
596
597     show: function() {
598         this.el.show();
599     },
600
601     hidePrim: function(sprite) {
602         sprite.el.addCls(Ext.baseCSSPrefix + 'hide-visibility');
603     },
604
605     showPrim: function(sprite) {
606         sprite.el.removeCls(Ext.baseCSSPrefix + 'hide-visibility');
607     },
608
609     setSize: function(width, height) {
610         var me = this;
611         width = width || me.width;
612         height = height || me.height;
613         me.width = width;
614         me.height = height;
615
616         if (me.el) {
617             // Size outer div
618             if (width != undefined) {
619                 me.el.setWidth(width);
620             }
621             if (height != undefined) {
622                 me.el.setHeight(height);
623             }
624
625             // Handle viewBox sizing
626             me.applyViewBox();
627
628             me.callParent(arguments);
629         }
630     },
631
632     setViewBox: function(x, y, width, height) {
633         this.callParent(arguments);
634         this.viewBox = {
635             x: x,
636             y: y,
637             width: width,
638             height: height
639         };
640         this.applyViewBox();
641     },
642
643     /**
644      * @private Using the current viewBox property and the surface's width and height, calculate the
645      * appropriate viewBoxShift that will be applied as a persistent transform to all sprites.
646      */
647     applyViewBox: function() {
648         var me = this,
649             viewBox = me.viewBox,
650             width = me.width,
651             height = me.height,
652             viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight,
653             relativeHeight, relativeWidth, size;
654
655         if (viewBox && (width || height)) {
656             viewBoxX = viewBox.x;
657             viewBoxY = viewBox.y;
658             viewBoxWidth = viewBox.width;
659             viewBoxHeight = viewBox.height;
660             relativeHeight = height / viewBoxHeight;
661             relativeWidth = width / viewBoxWidth;
662
663             if (viewBoxWidth * relativeHeight < width) {
664                 viewBoxX -= (width - viewBoxWidth * relativeHeight) / 2 / relativeHeight;
665             }
666             if (viewBoxHeight * relativeWidth < height) {
667                 viewBoxY -= (height - viewBoxHeight * relativeWidth) / 2 / relativeWidth;
668             }
669
670             size = 1 / Math.max(viewBoxWidth / width, viewBoxHeight / height);
671
672             me.viewBoxShift = {
673                 dx: -viewBoxX,
674                 dy: -viewBoxY,
675                 scale: size
676             };
677             me.items.each(function(item) {
678                 me.transform(item);
679             });
680         }
681     },
682
683     onAdd: function(item) {
684         this.callParent(arguments);
685         if (this.el) {
686             this.renderItem(item);
687         }
688     },
689
690     onRemove: function(sprite) {
691         if (sprite.el) {
692             sprite.el.remove();
693             delete sprite.el;
694         }
695         this.callParent(arguments);
696     },
697
698     render: function (container) {
699         var me = this,
700             doc = Ext.getDoc().dom;
701         // VML Node factory method (createNode)
702         if (!me.createNode) {
703             try {
704                 if (!doc.namespaces.rvml) {
705                     doc.namespaces.add("rvml", "urn:schemas-microsoft-com:vml");
706                 }
707                 me.createNode = function (tagName) {
708                     return doc.createElement("<rvml:" + tagName + ' class="rvml">');
709                 };
710             } catch (e) {
711                 me.createNode = function (tagName) {
712                     return doc.createElement("<" + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">');
713                 };
714             }
715         }
716
717         if (!me.el) {
718             var el = doc.createElement("div");
719             me.el = Ext.get(el);
720             me.el.addCls(me.baseVmlCls);
721
722             // Measuring span (offscrren)
723             me.span = doc.createElement("span");
724             Ext.get(me.span).addCls(me.measureSpanCls);
725             el.appendChild(me.span);
726             me.el.setSize(me.width || 10, me.height || 10);
727             container.appendChild(el);
728             me.el.on({
729                 scope: me,
730                 mouseup: me.onMouseUp,
731                 mousedown: me.onMouseDown,
732                 mouseover: me.onMouseOver,
733                 mouseout: me.onMouseOut,
734                 mousemove: me.onMouseMove,
735                 mouseenter: me.onMouseEnter,
736                 mouseleave: me.onMouseLeave,
737                 click: me.onClick
738             });
739         }
740         me.renderAll();
741     },
742
743     renderAll: function() {
744         this.items.each(this.renderItem, this);
745     },
746
747     redraw: function(sprite) {
748         sprite.dirty = true;
749         this.renderItem(sprite);
750     },
751
752     renderItem: function (sprite) {
753         // Does the surface element exist?
754         if (!this.el) {
755             return;
756         }
757
758         // Create sprite element if necessary
759         if (!sprite.el) {
760             this.createSpriteElement(sprite);
761         }
762
763         if (sprite.dirty) {
764             this.applyAttrs(sprite);
765             if (sprite.dirtyTransform) {
766                 this.applyTransformations(sprite);
767             }
768         }
769     },
770
771     rotationCompensation: function (deg, dx, dy) {
772         var matrix = Ext.create('Ext.draw.Matrix');
773         matrix.rotate(-deg, 0.5, 0.5);
774         return {
775             x: matrix.x(dx, dy),
776             y: matrix.y(dx, dy)
777         };
778     },
779
780     transform: function(sprite) {
781         var me = this,
782             matrix = Ext.create('Ext.draw.Matrix'),
783             transforms = sprite.transformations,
784             transformsLength = transforms.length,
785             i = 0,
786             deltaDegrees = 0,
787             deltaScaleX = 1,
788             deltaScaleY = 1,
789             flip = "",
790             el = sprite.el,
791             dom = el.dom,
792             domStyle = dom.style,
793             zoom = me.zoom,
794             skew = sprite.skew,
795             deltaX, deltaY, transform, type, compensate, y, fill, newAngle,zoomScaleX, zoomScaleY, newOrigin;
796
797         for (; i < transformsLength; i++) {
798             transform = transforms[i];
799             type = transform.type;
800             if (type == "translate") {
801                 matrix.translate(transform.x, transform.y);
802             }
803             else if (type == "rotate") {
804                 matrix.rotate(transform.degrees, transform.x, transform.y);
805                 deltaDegrees += transform.degrees;
806             }
807             else if (type == "scale") {
808                 matrix.scale(transform.x, transform.y, transform.centerX, transform.centerY);
809                 deltaScaleX *= transform.x;
810                 deltaScaleY *= transform.y;
811             }
812         }
813
814         if (me.viewBoxShift) {
815             matrix.scale(me.viewBoxShift.scale, me.viewBoxShift.scale, -1, -1);
816             matrix.add(1, 0, 0, 1, me.viewBoxShift.dx, me.viewBoxShift.dy);
817         }
818
819         sprite.matrix = matrix;
820
821
822         // Hide element while we transform
823
824         if (sprite.type != "image" && skew) {
825             // matrix transform via VML skew
826             skew.matrix = matrix.toString();
827             skew.offset = matrix.offset();
828         }
829         else {
830             deltaX = matrix.matrix[0][2];
831             deltaY = matrix.matrix[1][2];
832             // Scale via coordsize property
833             zoomScaleX = zoom / deltaScaleX;
834             zoomScaleY = zoom / deltaScaleY;
835
836             dom.coordsize = Math.abs(zoomScaleX) + " " + Math.abs(zoomScaleY);
837
838             // Rotate via rotation property
839             newAngle = deltaDegrees * (deltaScaleX * ((deltaScaleY < 0) ? -1 : 1));
840             if (newAngle != domStyle.rotation && !(newAngle === 0 && !domStyle.rotation)) {
841                 domStyle.rotation = newAngle;
842             }
843             if (deltaDegrees) {
844                 // Compensate x/y position due to rotation
845                 compensate = me.rotationCompensation(deltaDegrees, deltaX, deltaY);
846                 deltaX = compensate.x;
847                 deltaY = compensate.y;
848             }
849
850             // Handle negative scaling via flipping
851             if (deltaScaleX < 0) {
852                 flip += "x";
853             }
854             if (deltaScaleY < 0) {
855                 flip += " y";
856                 y = -1;
857             }
858             if (flip != "" && !dom.style.flip) {
859                 domStyle.flip = flip;
860             }
861
862             // Translate via coordorigin property
863             newOrigin = (deltaX * -zoomScaleX) + " " + (deltaY * -zoomScaleY);
864             if (newOrigin != dom.coordorigin) {
865                 dom.coordorigin = (deltaX * -zoomScaleX) + " " + (deltaY * -zoomScaleY);
866             }
867         }
868     },
869
870     createItem: function (config) {
871         return Ext.create('Ext.draw.Sprite', config);
872     },
873
874     getRegion: function() {
875         return this.el.getRegion();
876     },
877
878     addCls: function(sprite, className) {
879         if (sprite && sprite.el) {
880             sprite.el.addCls(className);
881         }
882     },
883
884     removeCls: function(sprite, className) {
885         if (sprite && sprite.el) {
886             sprite.el.removeCls(className);
887         }
888     },
889
890     /**
891      * Adds a definition to this Surface for a linear gradient. We convert the gradient definition
892      * to its corresponding VML attributes and store it for later use by individual sprites.
893      * @param {Object} gradient
894      */
895     addGradient: function(gradient) {
896         var gradients = this.gradientsColl || (this.gradientsColl = Ext.create('Ext.util.MixedCollection')),
897             colors = [],
898             stops = Ext.create('Ext.util.MixedCollection');
899
900         // Build colors string
901         stops.addAll(gradient.stops);
902         stops.sortByKey("ASC", function(a, b) {
903             a = parseInt(a, 10);
904             b = parseInt(b, 10);
905             return a > b ? 1 : (a < b ? -1 : 0);
906         });
907         stops.eachKey(function(k, v) {
908             colors.push(k + "% " + v.color);
909         });
910
911         gradients.add(gradient.id, {
912             colors: colors.join(","),
913             angle: gradient.angle
914         });
915     },
916
917     destroy: function() {
918         var me = this;
919         
920         me.callParent(arguments);
921         if (me.el) {
922             me.el.remove();
923         }
924         delete me.el;
925     }
926 });
927