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