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.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',
49 // VML uses CSS z-index and therefore doesn't need sprites to be kept in zIndex order
50 orderSpritesByZIndex: false,
53 // Convert an SVG standard path into a VML path
54 path2vml: function (path) {
56 nonVML = me.NonVmlPathRe,
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) {
68 isMove = command.toLowerCase() == "m",
70 args.replace(val, function (value) {
71 if (isMove && vals[length] == 2) {
72 res += vals + map[command == "m" ? "l" : "L"];
75 vals.push(Math.round(value * zoom));
83 for (i = 0, ii = pa.length; i < ii; i++) {
85 r = pa[i][0].toLowerCase();
89 for (j = 1, jj = p.length; j < jj; j++) {
90 r += Math.round(p[j] * me.zoom) + (j != jj - 1 ? "," : "");
97 // @private - set of attributes which need to be translated from the sprite API to the native browser API
102 lineWidth: "stroke-width",
103 fillOpacity: "fill-opacity",
104 strokeOpacity: "stroke-opacity",
105 strokeLinejoin: "stroke-linejoin"
108 // @private - Minimun set of defaults for different types of sprites.
113 "stroke-width": null,
115 "fill-opacity": null,
116 "stroke-opacity": null
125 "stroke-width": null,
127 "fill-opacity": null,
128 "stroke-opacity": null
139 "stroke-width": null,
141 "fill-opacity": null,
142 "stroke-opacity": null
147 "text-anchor": "start",
148 font: '10px "Arial"',
151 "stroke-width": null,
153 "fill-opacity": null,
154 "stroke-opacity": null
160 "stroke-width": null,
162 "fill-opacity": null,
163 "stroke-opacity": null
170 preserveAspectRatio: "none",
176 onMouseEnter: function(e) {
177 this.fireEvent("mouseenter", e);
181 onMouseLeave: function(e) {
182 this.fireEvent("mouseleave", e);
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,
190 this.fireEvent(name, e);
191 sprite = this.items.get(target.id);
193 sprite.fireEvent(name, sprite, e);
197 // Create the VML element/elements and append them to the DOM
198 createSpriteElement: function(sprite) {
203 vml = sprite.vml || (sprite.vml = {}),
205 el = me.createNode('shape'),
206 path, skew, textPath;
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");
216 el.appendChild(textPath);
217 el.appendChild(path);
220 sprite.el = Ext.get(el);
221 me.el.appendChild(el);
222 if (type !== 'image') {
223 skew = me.createNode("skew");
225 el.appendChild(skew);
228 sprite.matrix = Ext.create('Ext.draw.Matrix');
233 sprite.fireEvent("render", sprite);
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;
244 sprite.bbox.transform = sprite.bbox.transform || Ext.draw.Draw.pathDimensions(Ext.draw.Draw.mapPath(realPath, sprite.matrix));
245 return sprite.bbox.transform;
248 getBBoxText: function (sprite) {
249 var vml = sprite.vml;
251 x: vml.X + (vml.bbx || 0) - vml.W / 2,
252 y: vml.Y - vml.H / 2,
258 applyAttrs: function (sprite) {
261 group = sprite.group,
262 spriteAttr = sprite.attr,
265 style, name, groups, i, ln, scrubbedAttrs, font, key, bbox;
268 groups = [].concat(group);
270 for (i = 0; i < ln; i++) {
272 me.getGroup(group).add(sprite);
276 scrubbedAttrs = me.scrubAttrs(sprite) || {};
278 if (sprite.zIndexDirty) {
279 me.setZIndex(sprite);
282 // Apply minimum default attributes
283 Ext.applyIf(scrubbedAttrs, me.minDefaults[sprite.type]);
286 dom.href = scrubbedAttrs.href;
289 dom.title = scrubbedAttrs.title;
292 dom.target = scrubbedAttrs.target;
295 dom.cursor = scrubbedAttrs.cursor;
299 if (sprite.dirtyHidden) {
300 (scrubbedAttrs.hidden) ? me.hidePrim(sprite) : me.showPrim(sprite);
301 sprite.dirtyHidden = false;
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;
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;
327 if ("clip-rect" in scrubbedAttrs) {
328 me.setClip(sprite, scrubbedAttrs);
331 // Handle text (special handling required)
332 if (sprite.type == "text") {
333 me.setTextAttributes(sprite, scrubbedAttrs);
336 // Handle fill and opacity
337 if (sprite.type == 'image' || scrubbedAttrs.opacity || scrubbedAttrs['fill-opacity'] || scrubbedAttrs.fill) {
338 me.setFill(sprite, scrubbedAttrs);
341 // Handle stroke (all fills require a stroke element)
342 if (scrubbedAttrs.stroke || scrubbedAttrs['stroke-opacity'] || scrubbedAttrs.fill) {
343 me.setStroke(sprite, scrubbedAttrs);
347 style = spriteAttr.style;
352 sprite.dirty = false;
355 setZIndex: function(sprite) {
357 if (sprite.attr.zIndex != undefined) {
358 sprite.el.setStyle('zIndex', sprite.attr.zIndex);
360 sprite.zIndexDirty = false;
364 // Normalize all virtualized types into paths.
365 setPaths: function(sprite, params) {
366 var spriteAttr = sprite.attr;
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);
374 else if (sprite.type == 'ellipse') {
375 spriteAttr.rx = params.rx;
376 spriteAttr.ry = params.ry;
377 return Ext.draw.Draw.ellipsePath(sprite);
379 else if (sprite.type == 'rect' || sprite.type == 'image') {
380 spriteAttr.rx = spriteAttr.ry = params.r;
381 return Ext.draw.Draw.rectPath(sprite);
383 else if (sprite.type == 'path' && spriteAttr.path) {
384 return Ext.draw.Draw.pathToAbsolute(spriteAttr.path);
389 setFill: function(sprite, params) {
393 fillEl = dom.getElementsByTagName('fill')[0],
394 opacity, gradient, fillUrl, rotation, angle;
397 dom.removeChild(fillEl);
399 fillEl = me.createNode('fill');
401 if (Ext.isArray(params.fill)) {
402 params.fill = params.fill[0];
404 if (sprite.type == 'image') {
406 fillEl.src = params.src;
407 fillEl.type = "tile";
408 fillEl.rotate = true;
409 } else if (params.fill == "none") {
412 if (typeof params.opacity == "number") {
413 fillEl.opacity = params.opacity;
415 if (typeof params["fill-opacity"] == "number") {
416 fillEl.opacity = params["fill-opacity"];
419 if (typeof params.fill == "string") {
420 fillUrl = params.fill.match(me.fillUrlRe);
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));
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...
435 fillEl.angle = angle;
436 fillEl.type = "gradient";
437 fillEl.method = "sigma";
438 fillEl.colors = gradient.colors;
440 // Otherwise treat it as an image
442 fillEl.src = fillUrl;
443 fillEl.type = "tile";
444 fillEl.rotate = true;
448 fillEl.color = Ext.draw.Color.toHex(params.fill) || params.fill;
450 fillEl.type = "solid";
454 dom.appendChild(fillEl);
457 setStroke: function(sprite, params) {
460 strokeEl = sprite.strokeEl,
465 strokeEl = sprite.strokeEl = me.createNode("stroke");
468 if (Ext.isArray(params.stroke)) {
469 params.stroke = params.stroke[0];
471 if (!params.stroke || params.stroke == "none" || params.stroke == 0 || params["stroke-width"] == 0) {
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);
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) {
488 strokeEl.opacity = opacity * width;
491 strokeEl.weight = width;
492 strokeEl.opacity = opacity;
496 el.appendChild(strokeEl);
500 setClip: function(sprite, params) {
503 clipEl = sprite.clipEl,
504 rect = String(params["clip-rect"]).split(me.separatorRe);
506 clipEl = sprite.clipEl = me.el.insertFirst(Ext.getDoc().dom.createElement("div"));
507 clipEl.addCls(Ext.baseCSSPrefix + 'vml-sprite');
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);
516 clipEl.setStyle("clip", "");
520 setTextAttributes: function(sprite, params) {
523 textStyle = vml.textpath.style,
524 spanCacheStyle = me.span.style,
528 fontSize: "font-size",
529 fontWeight: "font-weight",
530 fontStyle: "font-style"
534 if (sprite.dirtyFont) {
536 textStyle.font = spanCacheStyle.font = params.font;
538 if (params["font-family"]) {
539 textStyle.fontFamily = '"' + params["font-family"].split(",")[0].replace(me.fontFamilyRe, "") + '"';
540 spanCacheStyle.fontFamily = params["font-family"];
543 for (fontProp in fontObj) {
544 paramProp = params[fontObj[fontProp]];
546 textStyle[fontProp] = spanCacheStyle[fontProp] = paramProp;
550 me.setText(sprite, params.text);
552 if (vml.textpath.string) {
553 me.span.innerHTML = String(vml.textpath.string).replace(/</g, "<").replace(/&/g, "&").replace(/\n/g, "<br>");
555 vml.W = me.span.offsetWidth;
556 vml.H = me.span.offsetHeight + 2; // TODO handle baseline differences and offset in VML Textpath
558 // text-anchor emulation
559 if (params["text-anchor"] == "middle") {
560 textStyle["v-text-align"] = "center";
562 else if (params["text-anchor"] == "end") {
563 textStyle["v-text-align"] = "right";
564 vml.bbx = -Math.round(vml.W / 2);
567 textStyle["v-text-align"] = "left";
568 vml.bbx = Math.round(vml.W / 2);
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);
575 sprite.bbox.plain = null;
576 sprite.bbox.transform = null;
577 sprite.dirtyFont = false;
580 setText: function(sprite, text) {
581 sprite.vml.textpath.string = Ext.htmlDecode(text);
592 hidePrim: function(sprite) {
593 sprite.el.addCls(Ext.baseCSSPrefix + 'hide-visibility');
596 showPrim: function(sprite) {
597 sprite.el.removeCls(Ext.baseCSSPrefix + 'hide-visibility');
600 setSize: function(width, height) {
602 width = width || me.width;
603 height = height || me.height;
609 if (width != undefined) {
610 me.el.setWidth(width);
612 if (height != undefined) {
613 me.el.setHeight(height);
616 // Handle viewBox sizing
619 me.callParent(arguments);
623 setViewBox: function(x, y, width, height) {
624 this.callParent(arguments);
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.
638 applyViewBox: function() {
640 viewBox = me.viewBox,
643 viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight,
644 relativeHeight, relativeWidth, size;
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;
654 if (viewBoxWidth * relativeHeight < width) {
655 viewBoxX -= (width - viewBoxWidth * relativeHeight) / 2 / relativeHeight;
657 if (viewBoxHeight * relativeWidth < height) {
658 viewBoxY -= (height - viewBoxHeight * relativeWidth) / 2 / relativeWidth;
661 size = 1 / Math.max(viewBoxWidth / width, viewBoxHeight / height);
668 me.items.each(function(item) {
674 onAdd: function(item) {
675 this.callParent(arguments);
677 this.renderItem(item);
681 onRemove: function(sprite) {
686 this.callParent(arguments);
689 // VML Node factory method (createNode)
690 createNode : (function () {
692 var doc = Ext.getDoc().dom;
693 if (!doc.namespaces.rvml) {
694 doc.namespaces.add("rvml", "urn:schemas-microsoft-com:vml");
696 return function (tagName) {
697 return doc.createElement("<rvml:" + tagName + ' class="rvml">');
700 return function (tagName) {
701 return doc.createElement("<" + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">');
706 render: function (container) {
708 doc = Ext.getDoc().dom;
711 var el = doc.createElement("div");
713 me.el.addCls(me.baseVmlCls);
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);
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,
736 renderAll: function() {
737 this.items.each(this.renderItem, this);
740 redraw: function(sprite) {
742 this.renderItem(sprite);
745 renderItem: function (sprite) {
746 // Does the surface element exist?
751 // Create sprite element if necessary
753 this.createSpriteElement(sprite);
757 this.applyAttrs(sprite);
758 if (sprite.dirtyTransform) {
759 this.applyTransformations(sprite);
764 rotationCompensation: function (deg, dx, dy) {
765 var matrix = Ext.create('Ext.draw.Matrix');
766 matrix.rotate(-deg, 0.5, 0.5);
773 extractTransform: function (sprite) {
775 matrix = Ext.create('Ext.draw.Matrix'), scale,
776 transformstions, tranformationsLength,
778 shift = me.viewBoxShift;
780 for(transformstions = sprite.transformations, tranformationsLength = transformstions.length;
781 i < tranformationsLength; i ++) {
782 transform = transformstions[i];
783 switch (transform.type) {
785 matrix.translate(transform.x, transform.y);
788 matrix.rotate(transform.degrees, transform.x, transform.y);
791 matrix.scale(transform.x || transform.scale, transform.y || transform.scale, transform.centerX, transform.centerY);
797 matrix.add(1, 0, 0, 1, shift.dx, shift.dy);
798 matrix.prepend(shift.scale, 0, 0, shift.scale, 0, 0);
801 return sprite.matrix = matrix;
804 setSimpleCoords: function(sprite, sx, sy, dx, dy, rotate) {
806 matrix = sprite.matrix,
811 fill = dom.getElementsByTagName('fill')[0],
814 rotationCompensation;
818 dom.coordsize = Math.abs(kx) + ' ' + Math.abs(ky);
819 style.rotation = rotate * (sx * sy < 0 ? -1 : 1);
821 rotationCompensation = me.rotationCompensation(rotate, dx, dy);
822 dx = rotationCompensation.x;
823 dy = rotationCompensation.y;
833 dom.coordorigin = (dx * -kx) + ' ' + (dy * -ky);
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);
843 transform : function (sprite) {
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("),
856 // Hide element while we transform
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
864 offset = matrix.offset();
865 if (offset[0] > 32767) {
867 } else if (offset[0] < -32768) {
870 if (offset[1] > 32767) {
872 } else if (offset[1] < -32768) {
875 skew.offset = offset;
878 skew.matrix = "1 0 0 1";
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);
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);
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);
901 createItem: function (config) {
902 return Ext.create('Ext.draw.Sprite', config);
905 getRegion: function() {
906 return this.el.getRegion();
909 addCls: function(sprite, className) {
910 if (sprite && sprite.el) {
911 sprite.el.addCls(className);
915 removeCls: function(sprite, className) {
916 if (sprite && sprite.el) {
917 sprite.el.removeCls(className);
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
926 addGradient: function(gradient) {
927 var gradients = this.gradientsColl || (this.gradientsColl = Ext.create('Ext.util.MixedCollection')),
929 stops = Ext.create('Ext.util.MixedCollection');
931 // Build colors string
932 stops.addAll(gradient.stops);
933 stops.sortByKey("ASC", function(a, b) {
936 return a > b ? 1 : (a < b ? -1 : 0);
938 stops.eachKey(function(k, v) {
939 colors.push(k + "% " + v.color);
942 gradients.add(gradient.id, {
943 colors: colors.join(","),
944 angle: gradient.angle
948 destroy: function() {
951 me.callParent(arguments);