/*
* @class Ext.draw.engine.VML
* @extends Ext.draw.Surface
* Provides specific methods to draw with VML.
*/
Ext.define('Ext.draw.engine.VML', {
/* Begin Definitions */
extend: 'Ext.draw.Surface',
requires: ['Ext.draw.Draw', 'Ext.draw.Color', 'Ext.draw.Sprite', 'Ext.draw.Matrix', 'Ext.core.Element'],
/* End Definitions */
engine: 'VML',
map: {M: "m", L: "l", C: "c", Z: "x", m: "t", l: "r", c: "v", z: "x"},
bitesRe: /([clmz]),?([^clmz]*)/gi,
valRe: /-?[^,\s-]+/g,
fillUrlRe: /^url\(\s*['"]?([^\)]+?)['"]?\s*\)$/i,
pathlike: /^(path|rect)$/,
NonVMLPathRe: /[ahqstv]/ig, // Non-VML Pathing ops
partialPathRe: /[clmz]/g,
fontFamilyRe: /^['"]+|['"]+$/g,
separatorRe: /[, ]+/,
baseVMLCls: Ext.baseCSSPrefix + 'vml-base',
vmlGroupCls: Ext.baseCSSPrefix + 'vml-group',
spriteCls: Ext.baseCSSPrefix + 'vml-sprite',
measureSpanCls: Ext.baseCSSPrefix + 'vml-measure-span',
zoom: 21600,
coordsize: 1000,
coordorigin: '0 0',
// @private
// Convert an SVG standard path into a VML path
path2vml: function (path) {
var me = this,
nonVML = me.NonVMLPathRe,
map = me.map,
val = me.valRe,
zoom = me.zoom,
bites = me.bitesRe,
command = Ext.Function.bind(Ext.draw.Draw.pathToAbsolute, Ext.draw.Draw),
res, pa, p, r, i, ii, j, jj;
if (String(path).match(nonVML)) {
command = Ext.Function.bind(Ext.draw.Draw.path2curve, Ext.draw.Draw);
} else if (!String(path).match(me.partialPathRe)) {
res = String(path).replace(bites, function (all, command, args) {
var vals = [],
isMove = command.toLowerCase() == "m",
res = map[command];
args.replace(val, function (value) {
if (isMove && vals[length] == 2) {
res += vals + map[command == "m" ? "l" : "L"];
vals = [];
}
vals.push(Math.round(value * zoom));
});
return res + vals;
});
return res;
}
pa = command(path);
res = [];
for (i = 0, ii = pa.length; i < ii; i++) {
p = pa[i];
r = pa[i][0].toLowerCase();
if (r == "z") {
r = "x";
}
for (j = 1, jj = p.length; j < jj; j++) {
r += Math.round(p[j] * me.zoom) + (j != jj - 1 ? "," : "");
}
res.push(r);
}
return res.join(" ");
},
// @private - set of attributes which need to be translated from the sprite API to the native browser API
translateAttrs: {
radius: "r",
radiusX: "rx",
radiusY: "ry",
lineWidth: "stroke-width",
fillOpacity: "fill-opacity",
strokeOpacity: "stroke-opacity",
strokeLinejoin: "stroke-linejoin"
},
// @private - Minimun set of defaults for different types of sprites.
minDefaults: {
circle: {
fill: "none",
stroke: null,
"stroke-width": null,
opacity: null,
"fill-opacity": null,
"stroke-opacity": null
},
ellipse: {
cx: 0,
cy: 0,
rx: 0,
ry: 0,
fill: "none",
stroke: null,
"stroke-width": null,
opacity: null,
"fill-opacity": null,
"stroke-opacity": null
},
rect: {
x: 0,
y: 0,
width: 0,
height: 0,
rx: 0,
ry: 0,
fill: "none",
stroke: null,
"stroke-width": null,
opacity: null,
"fill-opacity": null,
"stroke-opacity": null
},
text: {
x: 0,
y: 0,
"text-anchor": "start",
font: "10px Helvetica, Arial, sans-serif",
fill: "#000",
stroke: null,
"stroke-width": null,
opacity: null,
"fill-opacity": null,
"stroke-opacity": null
},
path: {
d: "M0,0",
fill: "none",
stroke: null,
"stroke-width": null,
opacity: null,
"fill-opacity": null,
"stroke-opacity": null
},
image: {
x: 0,
y: 0,
width: 0,
height: 0,
preserveAspectRatio: "none",
opacity: null
}
},
// private
onMouseEnter: function(e) {
this.fireEvent("mouseenter", e);
},
// private
onMouseLeave: function(e) {
this.fireEvent("mouseleave", e);
},
// @private - Normalize a delegated single event from the main container to each sprite and sprite group
processEvent: function(name, e) {
var target = e.getTarget(),
surface = this.surface,
sprite;
this.fireEvent(name, e);
sprite = this.items.get(target.id);
if (sprite) {
sprite.fireEvent(name, sprite, e);
}
},
// Create the VML element/elements and append them to the DOM
createElement: function(sprite) {
var me = this,
attr = sprite.attr,
type = sprite.type,
zoom = me.zoom,
vml = sprite.vml || (sprite.vml = {}),
round = Math.round,
el = me.createNode("shape"),
skew = me.createNode("skew"),
path,
textPath;
el.coordsize = zoom + ' ' + zoom;
el.coordorigin = attr.coordorigin || "0 0";
Ext.get(el).addCls(me.spriteCls);
if (type == "text") {
vml.path = path = me.createNode("path");
path.textpathok = true;
vml.textpath = textPath = me.createNode("textpath");
textPath.on = true;
el.appendChild(textPath);
el.appendChild(path);
}
el.id = sprite.id;
sprite.el = Ext.get(el);
me.el.appendChild(el);
skew.on = true;
el.appendChild(skew);
sprite.skew = skew;
sprite.matrix = new Ext.draw.Matrix;
sprite.bbox = {
plain: 0,
transform: 0
};
sprite.fireEvent("render", sprite);
return sprite.el;
},
// @private - Get bounding box for the sprite. The Sprite itself has the public method.
getBBox: function (sprite, isWithoutTransform) {
var realPath = this["getPath" + sprite.type](sprite);
if (isWithoutTransform) {
sprite.bbox.plain = sprite.bbox.plain || Ext.draw.Draw.pathDimensions(realPath);
return sprite.bbox.plain;
}
sprite.bbox.transform = sprite.bbox.transform || Ext.draw.Draw.pathDimensions(Ext.draw.Draw.mapPath(realPath, sprite.matrix));
return sprite.bbox.transform;
},
getBBoxText: function (sprite) {
var vml = sprite.vml;
return {
x: vml.X + (vml.bbx || 0) - vml.W / 2,
y: vml.Y - vml.H / 2,
width: vml.W,
height: vml.H
};
},
applyAttrs: function (sprite) {
var me = this,
vml = sprite.vml,
group = sprite.group,
spriteAttr = sprite.attr,
el = sprite.el,
dom = el.dom,
style, name, groups, i, ln, scrubbedAttrs, font, key;
if (group) {
groups = [].concat(group);
ln = groups.length;
for (i = 0; i < ln; i++) {
group = groups[i];
me.getGroup(group).add(sprite);
}
delete sprite.group;
}
scrubbedAttrs = me.scrubAttrs(sprite) || {};
if (sprite.zIndexDirty) {
me.setZIndex(sprite);
}
if (sprite.type == 'image') {
dom.src = scrubbedAttrs.src;
}
// Apply minimum default attributes
Ext.applyIf(scrubbedAttrs, me.minDefaults[sprite.type]);
if (dom.href) {
dom.href = scrubbedAttrs.href;
}
if (dom.title) {
dom.title = scrubbedAttrs.title;
}
if (dom.target) {
dom.target = scrubbedAttrs.target;
}
if (dom.cursor) {
dom.cursor = scrubbedAttrs.cursor;
}
// Change visibility
if (sprite.dirtyHidden) {
(scrubbedAttrs.hidden) ? me.hidePrim(sprite) : me.showPrim(sprite);
sprite.dirtyHidden = false;
}
// Update path
if (sprite.dirtyPath) {
if (sprite.type == "circle" || sprite.type == "ellipse") {
var cx = scrubbedAttrs.x,
cy = scrubbedAttrs.y,
rx = scrubbedAttrs.rx || scrubbedAttrs.r || 0,
ry = scrubbedAttrs.ry || scrubbedAttrs.r || 0;
dom.path = Ext.String.format("ar{0},{1},{2},{3},{4},{1},{4},{1}",
Math.round((cx - rx) * me.zoom),
Math.round((cy - ry) * me.zoom),
Math.round((cx + rx) * me.zoom),
Math.round((cy + ry) * me.zoom),
Math.round(cx * me.zoom));
sprite.dirtyPath = false;
} else if (sprite.type != "text") {
sprite.attr.path = scrubbedAttrs.path = me.setPaths(sprite, scrubbedAttrs) || scrubbedAttrs.path;
dom.path = me.path2vml(scrubbedAttrs.path);
sprite.dirtyPath = false;
}
}
// Apply clipping
if ("clip-rect" in scrubbedAttrs) {
me.setClip(sprite, scrubbedAttrs);
}
// Handle text (special handling required)
if (sprite.type == "text") {
me.setText(sprite, scrubbedAttrs);
}
// Handle fill and opacity
if (scrubbedAttrs.opacity || scrubbedAttrs.fill) {
me.setFill(sprite, scrubbedAttrs);
}
// Handle stroke (all fills require a stroke element)
if (scrubbedAttrs.stroke || scrubbedAttrs.fill) {
me.setStroke(sprite, scrubbedAttrs);
}
//set styles
style = spriteAttr.style;
if (style) {
el.setStyle(style);
}
sprite.dirty = false;
},
setZIndex: function(sprite) {
if (sprite.el) {
if (sprite.attr.zIndex != undefined) {
sprite.el.setStyle('zIndex', sprite.attr.zIndex);
}
sprite.zIndexDirty = false;
}
},
// Normalize all virtualized types into paths.
setPaths: function(sprite, params) {
var spriteAttr = sprite.attr;
if (sprite.type == 'circle') {
spriteAttr.rx = spriteAttr.ry = params.r;
return Ext.draw.Draw.ellipsePath(sprite);
}
else if (sprite.type == 'ellipse') {
spriteAttr.rx = params.rx;
spriteAttr.ry = params.ry;
return Ext.draw.Draw.ellipsePath(sprite);
}
else if (sprite.type == 'rect') {
spriteAttr.rx = spriteAttr.ry = params.r;
return Ext.draw.Draw.rectPath(sprite);
}
else if (sprite.type == 'path' && spriteAttr.path) {
return Ext.draw.Draw.pathToAbsolute(spriteAttr.path);
}
else if (sprite.type == 'image') {
return Ext.draw.Draw.rectPath(sprite);
}
return false;
},
setFill: function(sprite, params) {
var me = this,
el = sprite.el.dom,
fillEl = el.fill,
newfill = false,
opacity,
gradient,
fillUrl;
if (!fillEl) {
// NOT an expando (but it sure looks like one)...
fillEl = el.fill = me.createNode("fill");
newFill = true;
}
if (Ext.isArray(params.fill)) {
params.fill = params.fill[0];
}
if (typeof params["fill-opacity"] == "number" || typeof params.opacity == "number") {
fillEl.opacity = params["fill-opacity"] || params.opacity;
}
if (params.fill == "none") {
fillEl.on = false;
}
else {
fillEl.on = true;
if (fillEl.on && typeof params.fill == "string") {
fillUrl = params.fill.match(me.fillUrlRe);
if (fillUrl) {
fillUrl = fillUrl[1];
// If the URL matches one of the registered gradients, render that gradient
if (fillUrl.charAt(0) == "#") {
gradient = me.gradientsColl.getByKey(fillUrl.substring(1));
}
if (gradient) {
// VML angle is offset and inverted from standard, and must be adjusted to match rotation transform
rotation = params.rotation;
fillEl.angle = -(gradient.angle + 270 + (rotation ? rotation.degrees : 0)) % 360;
fillEl.type = "gradient";
fillEl.method = "sigma";
fillEl.colors.value = gradient.colors;
}
// Otherwise treat it as an image
else {
fillEl.src = fillUrl;
fillEl.type = "tile";
}
}
else {
fillEl.color = Ext.draw.Color.toHex(params.fill);
fillEl.src = "";
fillEl.type = "solid";
}
}
}
if (newfill) {
el.appendChild(fillEl);
}
},
setStroke: function(sprite, params) {
var me = this,
el = sprite.el.dom,
strokeEl = sprite.strokeEl,
newStroke = false,
width, opacity;
if (!strokeEl) {
strokeEl = sprite.strokeEl = me.createNode("stroke");
newStroke = true;
}
if (Ext.isArray(params.stroke)) {
params.stroke = params.stroke[0];
}
if (!params.stroke || params.stroke == "none" || params.stroke == 0 || params["stroke-width"] == 0) {
strokeEl.on = false;
}
else {
strokeEl.on = true;
if (params.stroke && !params.stroke.match(me.fillUrlRe)) {
// VML does NOT support a gradient stroke :(
strokeEl.color = Ext.draw.Color.toHex(params.stroke);
}
strokeEl.joinstyle = params["stroke-linejoin"];
strokeEl.endcap = params["stroke-linecap"] || "round";
strokeEl.miterlimit = params["stroke-miterlimit"] || 8;
width = parseFloat(params["stroke-width"] || 1) * 0.75;
opacity = params["stroke-opacity"] || 1;
// VML Does not support stroke widths under 1, so we're going to fiddle with stroke-opacity instead.
if (Ext.isNumber(width) && width < 1) {
strokeEl.weight = 1;
strokeEl.opacity = opacity * width;
}
else {
strokeEl.weight = 1;
strokeEl.opacity = opacity;
}
}
if (newStroke) {
el.appendChild(strokeEl);
}
},
setClip: function(sprite, params) {
var me = this,
el = sprite.el,
clipEl = sprite.clipEl,
rect = String(params["clip-rect"]).split(me.separatorRe);
if (!clipEl) {
clipEl = sprite.clipEl = Ext.core.DomHelper.insertBefore(el, "div");
clipEl.addCls(Ext.baseCSSPrefix + 'vml-sprite');
}
if (rect.length == 4) {
rect[2] = +rect[2] + (+rect[0]);
rect[3] = +rect[3] + (+rect[1]);
clipEl.setStyle("clip", Ext.String.format("rect({1}px {2}px {3}px {0}px)", rect));
clipEl.setSize(me.el.width, me.el.height);
}
else {
clipEl.setStyle("clip", "");
}
},
setText: function(sprite, params) {
var me = this,
vml = sprite.vml,
textStyle = vml.textpath.style,
spanCacheStyle = me.span.style,
zoom = me.zoom,
round = Math.round,
fontObj = {
font: "font",
fontSize: "font-size",
fontWeight: "font-weight",
fontStyle: "font-style"
},
fontProp,
paramProp;
if (sprite.dirtyFont) {
if (params["font-family"]) {
textStyle.fontFamily = '"' + params["font-family"].split(",")[0].replace(me.fontFamilyRe, "") + '"';
spanCacheStyle.fontFamily = params["font-family"];
}
for (fontProp in fontObj) {
paramProp = params[fontObj[fontProp]];
if (paramProp) {
textStyle[fontProp] = spanCacheStyle[fontProp] = paramProp;
}
}
vml.textpath.string = params.text;
if (vml.textpath.string) {
me.span.innerHTML = String(vml.textpath.string).replace(/");
}
vml.W = params.w = me.span.offsetWidth;
vml.H = params.h = me.span.offsetHeight;
// text-anchor emulation
if (params["text-anchor"] == "middle") {
vml.textpath.style["v-text-align"] = "center";
}
else if (params["text-anchor"] == "end") {
vml.textpath.style["v-text-align"] = "right";
vml.bbx = -Math.round(vml.W / 2);
}
else {
vml.textpath.style["v-text-align"] = "left";
vml.bbx = Math.round(vml.W / 2);
}
}
vml.X = params.x;
vml.Y = params.y;
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);
sprite.bbox.plain = 0;
sprite.bbox.transform = 0;
sprite.dirtyFont = false;
},
hide: function() {
this.el.hide();
},
show: function() {
this.el.show();
},
hidePrim: function(sprite) {
sprite.el.addCls(Ext.baseCSSPrefix + 'hide-visibility');
},
showPrim: function(sprite) {
sprite.el.removeCls(Ext.baseCSSPrefix + 'hide-visibility');
},
setSize: function(width, height) {
var me = this,
viewBox = me.viewBox,
scaleX, scaleY;
width = width || me.width;
height = height || me.height;
me.width = width;
me.height = height;
if (!me.el) {
return;
}
// Size outer div
if (width != undefined) {
me.el.setWidth(width);
}
if (height != undefined) {
me.el.setHeight(height);
}
// Handle viewBox sizing
if (viewBox && (width || height)) {
var viewBoxX = viewBox.x,
viewBoxY = viewBox.y,
viewBoxWidth = viewBox.width,
viewBoxHeight = viewBox.height,
relativeHeight = height / viewBoxHeight,
relativeWidth = width / viewBoxWidth,
size;
if (viewBoxWidth * relativeHeight < width) {
viewBoxX -= (width - viewBoxWidth * relativeHeight) / 2 / relativeHeight;
}
if (viewBoxHeight * relativeWidth < height) {
viewBoxY -= (height - viewBoxHeight * relativeWidth) / 2 / relativeWidth;
}
size = 1 / Math.max(viewBoxWidth / width, viewBoxHeight / height);
// Scale and translate group
me.viewBoxShift = {
dx: -viewBoxX,
dy: -viewBoxY,
scale: size
};
var items = me.items.items;
for (var i = 0, len = items.length; i < len; i++) {
me.transform(items[i]);
}
}
this.callParent(arguments);
},
setViewBox: function(x, y, width, height) {
this.callParent(arguments);
this.viewBox = {
x: x,
y: y,
width: width,
height: height
};
},
onAdd: function(item) {
this.callParent(arguments);
if (this.el) {
this.renderItem(item);
}
},
onRemove: function(item) {
if (item.el) {
Ext.removeNode(item.el);
delete item.el;
}
this.callParent(arguments);
},
render: function (container) {
var me = this,
doc = Ext.getDoc().dom;
// VML Node factory method (createNode)
if (!me.createNode) {
doc.createStyleSheet().addRule(".rvml", "behavior:url(#default#VML)");
try {
if (!doc.namespaces.rvml) {
doc.namespaces.add("rvml", "urn:schemas-microsoft-com:vml");
}
me.createNode = function (tagName) {
return doc.createElement("');
};
} catch (e) {
me.createNode = function (tagName) {
return doc.createElement("<" + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">');
};
}
}
if (!me.el) {
var el = doc.createElement("div");
me.el = Ext.get(el);
me.el.addCls(me.baseVMLCls);
// Measuring span (offscrren)
me.span = doc.createElement("span");
Ext.get(me.span).addCls(me.measureSpanCls);
el.appendChild(me.span);
me.el.setSize(me.width || 10, me.height || 10);
container.appendChild(el);
me.el.on({
scope: me,
mouseup: me.onMouseUp,
mousedown: me.onMouseDown,
mouseover: me.onMouseOver,
mouseout: me.onMouseOut,
mousemove: me.onMouseMove,
mouseenter: me.onMouseEnter,
mouseleave: me.onMouseLeave,
click: me.onClick
});
}
me.renderAll();
},
renderAll: function() {
this.items.each(this.renderItem, this);
},
redraw: function(sprite) {
sprite.dirty = true;
this.renderItem(sprite);
},
renderItem: function (sprite) {
// Does the surface element exist?
if (!this.el) {
return;
}
// Create sprite element if necessary
if (!sprite.el) {
this.createElement(sprite);
}
if (sprite.dirty) {
this.applyAttrs(sprite);
if (sprite.dirtyTransform) {
this.applyTransformations(sprite);
}
}
},
rotationCompensation: function (deg, dx, dy) {
var matrix = new Ext.draw.Matrix;
matrix.rotate(-deg, 0.5, 0.5);
return {
x: matrix.x(dx, dy),
y: matrix.y(dx, dy)
};
},
transform: function(sprite) {
var me = this,
matrix = new Ext.draw.Matrix,
transforms = sprite.transformations,
transformsLength = transforms.length,
i = 0,
deltaDegrees = 0,
deltaScaleX = 1,
deltaScaleY = 1,
flip = "",
el = sprite.el,
dom = el.dom,
domStyle = dom.style,
zoom = me.zoom,
skew = sprite.skew,
vml = sprite.vml,
deltaX, deltaY, transform, type, compensate, y, fill, newAngle,zoomScaleX, zoomScaleY;
for (; i < transformsLength; i++) {
transform = transforms[i];
type = transform.type;
if (type == "translate") {
matrix.translate(transform.x, transform.y);
}
else if (type == "rotate") {
matrix.rotate(transform.degrees, transform.x, transform.y);
deltaDegrees += transform.degrees;
}
else if (type == "scale") {
matrix.scale(transform.x, transform.y, transform.centerX, transform.centerY);
deltaScaleX *= transform.x;
deltaScaleY *= transform.y;
}
}
if (me.viewBoxShift) {
matrix.scale(me.viewBoxShift.scale, me.viewBoxShift.scale, 0, 0);
matrix.add(1, 0, 0, 1, me.viewBoxShift.dx, me.viewBoxShift.dy);
}
sprite.matrix = matrix;
// Hide element while we transform
if (me.type != "image" && skew) {
// matrix transform via VML skew
skew.matrix = matrix.toString();
skew.offset = matrix.offset();
} else {
deltaX = matrix.m[0][2];
deltaY = matrix.m[1][2];
// Scale via coordsize property
zoomScaleX = zoom / deltaScaleX;
zoomScaleY = zoom / deltaScaleY;
dom.coordsize = Math.abs(zoomScaleX) + " " + Math.abs(zoomScaleY);
// Rotate via rotation property
newAngle = deltaDegrees * (deltaScaleX * ((deltaScaleY < 0) ? -1 : 1));
if (newAngle != domStyle.rotation && !(newAngle === 0 && !domStyle.rotation)) {
domStyle.rotation = newAngle;
}
if (deltaDegrees) {
// Compensate x/y position due to rotation
compensate = me.rotationCompensation(deltaDegrees, deltaX, deltaY);
deltaX = compensate.x;
deltaY = compensate.y;
}
// Handle negative scaling via flipping
if (deltaScaleX < 0) {
flip += "x";
}
if (deltaScaleY < 0) {
flip += " y";
y = -1;
}
if (flip != "" && !dom.style.flip) {
domStyle.flip = flip;
}
// Translate via coordorigin property
newOrigin = (deltaX * -zoomScaleX) + " " + (deltaY * -zoomScaleY);
if (newOrigin != dom.coordorigin) {
dom.coordorigin = (deltaX * -zoomScaleX) + " " + (deltaY * -zoomScaleY);
}
}
},
createItem: function (config) {
return new Ext.draw.Sprite(config);
},
getRegion: function() {
return this.el.getRegion();
},
addCls: function(sprite, className) {
if (sprite && sprite.el) {
sprite.el.addCls(className);
}
},
removeCls: function(sprite, className) {
if (sprite && sprite.el) {
sprite.el.removeCls(className);
}
},
/**
* Adds a definition to this Surface for a linear gradient. We convert the gradient definition
* to its corresponding VML attributes and store it for later use by individual sprites.
* @param {Object} gradient
*/
addGradient: function(gradient) {
var gradients = this.gradientsColl || (this.gradientsColl = new Ext.util.MixedCollection()),
colors = [],
stops = new Ext.util.MixedCollection();
// Build colors string
stops.addAll(gradient.stops);
stops.sortByKey("ASC", function(a, b) {
a = parseInt(a, 10);
b = parseInt(b, 10);
return a > b ? 1 : (a < b ? -1 : 0);
});
stops.eachKey(function(k, v) {
colors.push(k + "% " + v.color);
});
gradients.add(gradient.id, {
colors: colors.join(","),
angle: gradient.angle
});
},
destroy: function() {
this.callParent(arguments);
Ext.removeNode(this.el);
delete this.el;
}
});