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