3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.draw.engine.Vml
17 * @extends Ext.draw.Surface
18 * Provides specific methods to draw with VML.
21 Ext.define('Ext.draw.engine.Vml', {
23 /* Begin Definitions */
25 extend: 'Ext.draw.Surface',
27 requires: ['Ext.draw.Draw', 'Ext.draw.Color', 'Ext.draw.Sprite', 'Ext.draw.Matrix', 'Ext.core.Element'],
33 map: {M: "m", L: "l", C: "c", Z: "x", m: "t", l: "r", c: "v", z: "x"},
34 bitesRe: /([clmz]),?([^clmz]*)/gi,
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',
50 // Convert an SVG standard path into a VML path
51 path2vml: function (path) {
53 nonVML = me.NonVmlPathRe,
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) {
65 isMove = command.toLowerCase() == "m",
67 args.replace(val, function (value) {
68 if (isMove && vals[length] == 2) {
69 res += vals + map[command == "m" ? "l" : "L"];
72 vals.push(Math.round(value * zoom));
80 for (i = 0, ii = pa.length; i < ii; i++) {
82 r = pa[i][0].toLowerCase();
86 for (j = 1, jj = p.length; j < jj; j++) {
87 r += Math.round(p[j] * me.zoom) + (j != jj - 1 ? "," : "");
94 // @private - set of attributes which need to be translated from the sprite API to the native browser API
99 lineWidth: "stroke-width",
100 fillOpacity: "fill-opacity",
101 strokeOpacity: "stroke-opacity",
102 strokeLinejoin: "stroke-linejoin"
105 // @private - Minimun set of defaults for different types of sprites.
110 "stroke-width": null,
112 "fill-opacity": null,
113 "stroke-opacity": null
122 "stroke-width": null,
124 "fill-opacity": null,
125 "stroke-opacity": null
136 "stroke-width": null,
138 "fill-opacity": null,
139 "stroke-opacity": null
144 "text-anchor": "start",
145 font: '10px "Arial"',
148 "stroke-width": null,
150 "fill-opacity": null,
151 "stroke-opacity": null
157 "stroke-width": null,
159 "fill-opacity": null,
160 "stroke-opacity": null
167 preserveAspectRatio: "none",
173 onMouseEnter: function(e) {
174 this.fireEvent("mouseenter", e);
178 onMouseLeave: function(e) {
179 this.fireEvent("mouseleave", e);
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,
187 this.fireEvent(name, e);
188 sprite = this.items.get(target.id);
190 sprite.fireEvent(name, sprite, e);
194 // Create the VML element/elements and append them to the DOM
195 createSpriteElement: function(sprite) {
200 vml = sprite.vml || (sprite.vml = {}),
202 el = (type === 'image') ? me.createNode('image') : me.createNode('shape'),
203 path, skew, textPath;
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");
213 el.appendChild(textPath);
214 el.appendChild(path);
217 sprite.el = Ext.get(el);
218 me.el.appendChild(el);
219 if (type !== 'image') {
220 skew = me.createNode("skew");
222 el.appendChild(skew);
225 sprite.matrix = Ext.create('Ext.draw.Matrix');
230 sprite.fireEvent("render", sprite);
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;
241 sprite.bbox.transform = sprite.bbox.transform || Ext.draw.Draw.pathDimensions(Ext.draw.Draw.mapPath(realPath, sprite.matrix));
242 return sprite.bbox.transform;
245 getBBoxText: function (sprite) {
246 var vml = sprite.vml;
248 x: vml.X + (vml.bbx || 0) - vml.W / 2,
249 y: vml.Y - vml.H / 2,
255 applyAttrs: function (sprite) {
258 group = sprite.group,
259 spriteAttr = sprite.attr,
262 style, name, groups, i, ln, scrubbedAttrs, font, key, bbox;
265 groups = [].concat(group);
267 for (i = 0; i < ln; i++) {
269 me.getGroup(group).add(sprite);
273 scrubbedAttrs = me.scrubAttrs(sprite) || {};
275 if (sprite.zIndexDirty) {
276 me.setZIndex(sprite);
279 // Apply minimum default attributes
280 Ext.applyIf(scrubbedAttrs, me.minDefaults[sprite.type]);
282 if (sprite.type == 'image') {
283 Ext.apply(sprite.attr, {
286 width: scrubbedAttrs.width,
287 height: scrubbedAttrs.height
289 bbox = sprite.getBBox();
291 width: bbox.width + 'px',
292 height: bbox.height + 'px'
294 dom.src = scrubbedAttrs.src;
298 dom.href = scrubbedAttrs.href;
301 dom.title = scrubbedAttrs.title;
304 dom.target = scrubbedAttrs.target;
307 dom.cursor = scrubbedAttrs.cursor;
311 if (sprite.dirtyHidden) {
312 (scrubbedAttrs.hidden) ? me.hidePrim(sprite) : me.showPrim(sprite);
313 sprite.dirtyHidden = false;
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;
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;
339 if ("clip-rect" in scrubbedAttrs) {
340 me.setClip(sprite, scrubbedAttrs);
343 // Handle text (special handling required)
344 if (sprite.type == "text") {
345 me.setTextAttributes(sprite, scrubbedAttrs);
348 // Handle fill and opacity
349 if (scrubbedAttrs.opacity || scrubbedAttrs['stroke-opacity'] || scrubbedAttrs.fill) {
350 me.setFill(sprite, scrubbedAttrs);
353 // Handle stroke (all fills require a stroke element)
354 if (scrubbedAttrs.stroke || scrubbedAttrs['stroke-opacity'] || scrubbedAttrs.fill) {
355 me.setStroke(sprite, scrubbedAttrs);
359 style = spriteAttr.style;
364 sprite.dirty = false;
367 setZIndex: function(sprite) {
369 if (sprite.attr.zIndex != undefined) {
370 sprite.el.setStyle('zIndex', sprite.attr.zIndex);
372 sprite.zIndexDirty = false;
376 // Normalize all virtualized types into paths.
377 setPaths: function(sprite, params) {
378 var spriteAttr = sprite.attr;
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);
386 else if (sprite.type == 'ellipse') {
387 spriteAttr.rx = params.rx;
388 spriteAttr.ry = params.ry;
389 return Ext.draw.Draw.ellipsePath(sprite);
391 else if (sprite.type == 'rect') {
392 spriteAttr.rx = spriteAttr.ry = params.r;
393 return Ext.draw.Draw.rectPath(sprite);
395 else if (sprite.type == 'path' && spriteAttr.path) {
396 return Ext.draw.Draw.pathToAbsolute(spriteAttr.path);
401 setFill: function(sprite, params) {
406 opacity, gradient, fillUrl, rotation, angle;
409 // NOT an expando (but it sure looks like one)...
410 fillEl = el.fill = me.createNode("fill");
413 if (Ext.isArray(params.fill)) {
414 params.fill = params.fill[0];
416 if (params.fill == "none") {
420 if (typeof params.opacity == "number") {
421 fillEl.opacity = params.opacity;
423 if (typeof params["fill-opacity"] == "number") {
424 fillEl.opacity = params["fill-opacity"];
427 if (typeof params.fill == "string") {
428 fillUrl = params.fill.match(me.fillUrlRe);
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));
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...
443 fillEl.angle = angle;
444 fillEl.type = "gradient";
445 fillEl.method = "sigma";
446 fillEl.colors.value = gradient.colors;
448 // Otherwise treat it as an image
450 fillEl.src = fillUrl;
451 fillEl.type = "tile";
455 fillEl.color = Ext.draw.Color.toHex(params.fill);
457 fillEl.type = "solid";
462 el.appendChild(fillEl);
466 setStroke: function(sprite, params) {
469 strokeEl = sprite.strokeEl,
474 strokeEl = sprite.strokeEl = me.createNode("stroke");
477 if (Ext.isArray(params.stroke)) {
478 params.stroke = params.stroke[0];
480 if (!params.stroke || params.stroke == "none" || params.stroke == 0 || params["stroke-width"] == 0) {
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);
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) {
497 strokeEl.opacity = opacity * width;
500 strokeEl.weight = width;
501 strokeEl.opacity = opacity;
505 el.appendChild(strokeEl);
509 setClip: function(sprite, params) {
512 clipEl = sprite.clipEl,
513 rect = String(params["clip-rect"]).split(me.separatorRe);
515 clipEl = sprite.clipEl = me.el.insertFirst(Ext.getDoc().dom.createElement("div"));
516 clipEl.addCls(Ext.baseCSSPrefix + 'vml-sprite');
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);
525 clipEl.setStyle("clip", "");
529 setTextAttributes: function(sprite, params) {
532 textStyle = vml.textpath.style,
533 spanCacheStyle = me.span.style,
537 fontSize: "font-size",
538 fontWeight: "font-weight",
539 fontStyle: "font-style"
543 if (sprite.dirtyFont) {
545 textStyle.font = spanCacheStyle.font = params.font;
547 if (params["font-family"]) {
548 textStyle.fontFamily = '"' + params["font-family"].split(",")[0].replace(me.fontFamilyRe, "") + '"';
549 spanCacheStyle.fontFamily = params["font-family"];
552 for (fontProp in fontObj) {
553 paramProp = params[fontObj[fontProp]];
555 textStyle[fontProp] = spanCacheStyle[fontProp] = paramProp;
559 me.setText(sprite, params.text);
561 if (vml.textpath.string) {
562 me.span.innerHTML = String(vml.textpath.string).replace(/</g, "<").replace(/&/g, "&").replace(/\n/g, "<br>");
564 vml.W = me.span.offsetWidth;
565 vml.H = me.span.offsetHeight + 2; // TODO handle baseline differences and offset in VML Textpath
567 // text-anchor emulation
568 if (params["text-anchor"] == "middle") {
569 textStyle["v-text-align"] = "center";
571 else if (params["text-anchor"] == "end") {
572 textStyle["v-text-align"] = "right";
573 vml.bbx = -Math.round(vml.W / 2);
576 textStyle["v-text-align"] = "left";
577 vml.bbx = Math.round(vml.W / 2);
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);
584 sprite.bbox.plain = null;
585 sprite.bbox.transform = null;
586 sprite.dirtyFont = false;
589 setText: function(sprite, text) {
590 sprite.vml.textpath.string = Ext.htmlDecode(text);
601 hidePrim: function(sprite) {
602 sprite.el.addCls(Ext.baseCSSPrefix + 'hide-visibility');
605 showPrim: function(sprite) {
606 sprite.el.removeCls(Ext.baseCSSPrefix + 'hide-visibility');
609 setSize: function(width, height) {
611 width = width || me.width;
612 height = height || me.height;
618 if (width != undefined) {
619 me.el.setWidth(width);
621 if (height != undefined) {
622 me.el.setHeight(height);
625 // Handle viewBox sizing
628 me.callParent(arguments);
632 setViewBox: function(x, y, width, height) {
633 this.callParent(arguments);
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.
647 applyViewBox: function() {
649 viewBox = me.viewBox,
652 viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight,
653 relativeHeight, relativeWidth, size;
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;
663 if (viewBoxWidth * relativeHeight < width) {
664 viewBoxX -= (width - viewBoxWidth * relativeHeight) / 2 / relativeHeight;
666 if (viewBoxHeight * relativeWidth < height) {
667 viewBoxY -= (height - viewBoxHeight * relativeWidth) / 2 / relativeWidth;
670 size = 1 / Math.max(viewBoxWidth / width, viewBoxHeight / height);
677 me.items.each(function(item) {
683 onAdd: function(item) {
684 this.callParent(arguments);
686 this.renderItem(item);
690 onRemove: function(sprite) {
695 this.callParent(arguments);
698 render: function (container) {
700 doc = Ext.getDoc().dom;
701 // VML Node factory method (createNode)
702 if (!me.createNode) {
704 if (!doc.namespaces.rvml) {
705 doc.namespaces.add("rvml", "urn:schemas-microsoft-com:vml");
707 me.createNode = function (tagName) {
708 return doc.createElement("<rvml:" + tagName + ' class="rvml">');
711 me.createNode = function (tagName) {
712 return doc.createElement("<" + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">');
718 var el = doc.createElement("div");
720 me.el.addCls(me.baseVmlCls);
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);
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,
743 renderAll: function() {
744 this.items.each(this.renderItem, this);
747 redraw: function(sprite) {
749 this.renderItem(sprite);
752 renderItem: function (sprite) {
753 // Does the surface element exist?
758 // Create sprite element if necessary
760 this.createSpriteElement(sprite);
764 this.applyAttrs(sprite);
765 if (sprite.dirtyTransform) {
766 this.applyTransformations(sprite);
771 rotationCompensation: function (deg, dx, dy) {
772 var matrix = Ext.create('Ext.draw.Matrix');
773 matrix.rotate(-deg, 0.5, 0.5);
780 transform: function(sprite) {
782 matrix = Ext.create('Ext.draw.Matrix'),
783 transforms = sprite.transformations,
784 transformsLength = transforms.length,
792 domStyle = dom.style,
795 deltaX, deltaY, transform, type, compensate, y, fill, newAngle,zoomScaleX, zoomScaleY, newOrigin;
797 for (; i < transformsLength; i++) {
798 transform = transforms[i];
799 type = transform.type;
800 if (type == "translate") {
801 matrix.translate(transform.x, transform.y);
803 else if (type == "rotate") {
804 matrix.rotate(transform.degrees, transform.x, transform.y);
805 deltaDegrees += transform.degrees;
807 else if (type == "scale") {
808 matrix.scale(transform.x, transform.y, transform.centerX, transform.centerY);
809 deltaScaleX *= transform.x;
810 deltaScaleY *= transform.y;
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);
819 sprite.matrix = matrix;
822 // Hide element while we transform
824 if (sprite.type != "image" && skew) {
825 // matrix transform via VML skew
826 skew.matrix = matrix.toString();
827 skew.offset = matrix.offset();
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;
836 dom.coordsize = Math.abs(zoomScaleX) + " " + Math.abs(zoomScaleY);
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;
844 // Compensate x/y position due to rotation
845 compensate = me.rotationCompensation(deltaDegrees, deltaX, deltaY);
846 deltaX = compensate.x;
847 deltaY = compensate.y;
850 // Handle negative scaling via flipping
851 if (deltaScaleX < 0) {
854 if (deltaScaleY < 0) {
858 if (flip != "" && !dom.style.flip) {
859 domStyle.flip = flip;
862 // Translate via coordorigin property
863 newOrigin = (deltaX * -zoomScaleX) + " " + (deltaY * -zoomScaleY);
864 if (newOrigin != dom.coordorigin) {
865 dom.coordorigin = (deltaX * -zoomScaleX) + " " + (deltaY * -zoomScaleY);
870 createItem: function (config) {
871 return Ext.create('Ext.draw.Sprite', config);
874 getRegion: function() {
875 return this.el.getRegion();
878 addCls: function(sprite, className) {
879 if (sprite && sprite.el) {
880 sprite.el.addCls(className);
884 removeCls: function(sprite, className) {
885 if (sprite && sprite.el) {
886 sprite.el.removeCls(className);
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
895 addGradient: function(gradient) {
896 var gradients = this.gradientsColl || (this.gradientsColl = Ext.create('Ext.util.MixedCollection')),
898 stops = Ext.create('Ext.util.MixedCollection');
900 // Build colors string
901 stops.addAll(gradient.stops);
902 stops.sortByKey("ASC", function(a, b) {
905 return a > b ? 1 : (a < b ? -1 : 0);
907 stops.eachKey(function(k, v) {
908 colors.push(k + "% " + v.color);
911 gradients.add(gradient.id, {
912 colors: colors.join(","),
913 angle: gradient.angle
917 destroy: function() {
920 me.callParent(arguments);