/*
* @class Ext.draw.Surface
* @extends Object
*/
Ext.define('Ext.draw.Surface', {
/* Begin Definitions */
mixins: {
observable: 'Ext.util.Observable'
},
requires: ['Ext.draw.SpriteGroup'],
uses: ['Ext.draw.engine.SVG', 'Ext.draw.engine.VML', 'Ext.draw.engine.Canvas'],
statics: {
/**
* Create and return a new concrete Surface instance appropriate for the current environment.
* @param {Object} config Initial configuration for the Surface instance
* @param {Array} implOrder Optional order of implementations to use; the first one that is
* available in the current environment will be used. Defaults to
* ['SVG', 'Canvas', 'VML]
.
*/
newInstance: function(config, implOrder) {
implOrder = implOrder || ['SVG', 'Canvas', 'VML'];
var i = 0,
len = implOrder.length,
surfaceClass;
for (; i < len; i++) {
if (Ext.supports[implOrder[i]]) {
surfaceClass = Ext.draw.engine[implOrder[i]];
if (surfaceClass) {
return new surfaceClass(config);
}
}
}
return false;
}
},
/* End Definitions */
availableAttrs: {
blur: 0,
"clip-rect": "0 0 1e9 1e9",
cursor: "default",
cx: 0,
cy: 0,
'dominant-baseline': 'auto',
fill: "none",
"fill-opacity": 1,
font: '10px "Arial"',
"font-family": '"Arial"',
"font-size": "10",
"font-style": "normal",
"font-weight": 400,
gradient: "",
height: 0,
hidden: false,
href: "http://sencha.com/",
opacity: 1,
path: "M0,0",
radius: 0,
rx: 0,
ry: 0,
scale: "1 1",
src: "",
stroke: "#000",
"stroke-dasharray": "",
"stroke-linecap": "butt",
"stroke-linejoin": "butt",
"stroke-miterlimit": 0,
"stroke-opacity": 1,
"stroke-width": 1,
target: "_blank",
text: "",
"text-anchor": "middle",
title: "Ext Draw",
width: 0,
x: 0,
y: 0,
zIndex: 0
},
/**
* @cfg {Number} height
* The height of this component in pixels (defaults to auto).
* Note to express this dimension as a percentage or offset see {@link Ext.Component#anchor}.
*/
/**
* @cfg {Number} width
* The width of this component in pixels (defaults to auto).
* Note to express this dimension as a percentage or offset see {@link Ext.Component#anchor}.
*/
container: undefined,
height: 352,
width: 512,
x: 0,
y: 0,
constructor: function(config) {
config = config || {};
Ext.apply(this, config);
this.mixins.observable.constructor.call(this);
this.domRef = Ext.getDoc().dom;
this.customAttributes = {};
this.addEvents(
'mousedown',
'mouseup',
'mouseover',
'mouseout',
'mousemove',
'mouseenter',
'mouseleave',
'click'
);
this.getId();
this.initGradients();
this.initItems();
if (this.renderTo) {
this.render(this.renderTo);
delete this.renderTo;
}
this.initBackground(config.background);
return this;
},
initSurface: Ext.emptyFn,
renderItem: Ext.emptyFn,
renderItems: Ext.emptyFn,
setViewBox: Ext.emptyFn,
/**
* Adds one or more CSS classes to the element. Duplicate classes are automatically filtered out.
* @param {String} sprite The sprite to add, or an array of classes to
* @param {String/Array} className The CSS class to add, or an array of classes
* @return {Ext.draw.Sprite} this
*/
addCls: Ext.emptyFn,
/**
* Removes one or more CSS classes from the element.
* @param {String} sprite The sprite to add, or an array of classes to
* @param {String/Array} className The CSS class to remove, or an array of classes
* @return {Ext.draw.Sprite} this
*/
removeCls: Ext.emptyFn,
setStyle: Ext.emptyFn,
initGradients: function() {
var gradients = this.gradients;
if (gradients) {
Ext.each(gradients, this.addGradient, this);
}
},
initItems: function() {
var items = this.items;
this.items = new Ext.draw.SpriteGroup();
this.groups = new Ext.draw.SpriteGroup();
if (items) {
this.add(items);
}
},
initBackground: function(config) {
var gradientId,
gradient,
backgroundSprite,
width = this.width,
height = this.height;
if (config) {
if (config.gradient) {
gradient = config.gradient;
gradientId = gradient.id;
this.addGradient(gradient);
this.background = this.add({
type: 'rect',
x: 0,
y: 0,
width: width,
height: height,
fill: 'url(#' + gradientId + ')'
});
} else if (config.fill) {
this.background = this.add({
type: 'rect',
x: 0,
y: 0,
width: width,
height: height,
fill: config.fill
});
} else if (config.image) {
this.background = this.add({
type: 'image',
x: 0,
y: 0,
width: width,
height: height,
src: config.image
});
}
}
},
setSize: function(w, h) {
if (this.background) {
this.background.setAttributes({
width: w,
height: h,
hidden: false
}, true);
}
},
scrubAttrs: function(sprite) {
var i,
attrs = {},
exclude = {},
sattr = sprite.attr;
for (i in sattr) {
// Narrow down attributes to the main set
if (this.translateAttrs.hasOwnProperty(i)) {
// Translated attr
attrs[this.translateAttrs[i]] = sattr[i];
exclude[this.translateAttrs[i]] = true;
}
else if (this.availableAttrs.hasOwnProperty(i) && !exclude[i]) {
// Passtrhough attr
attrs[i] = sattr[i];
}
}
return attrs;
},
// private
onClick: function(e) {
this.processEvent('click', e);
},
// private
onMouseUp: function(e) {
this.processEvent('mouseup', e);
},
// private
onMouseDown: function(e) {
this.processEvent('mousedown', e);
},
// private
onMouseOver: function(e) {
this.processEvent('mouseover', e);
},
// private
onMouseOut: function(e) {
this.processEvent('mouseout', e);
},
// private
onMouseMove: function(e) {
this.fireEvent('mousemove', e);
},
// private
onMouseEnter: Ext.emptyFn,
// private
onMouseLeave: Ext.emptyFn,
/**
* Add a gradient definition to the Surface. Note that in some surface engines, adding
* a gradient via this method will not take effect if the surface has already been rendered.
* Therefore, it is preferred to pass the gradients as an item to the surface config, rather
* than calling this method, especially if the surface is rendered immediately (e.g. due to
* 'renderTo' in its config).
*/
addGradient: Ext.emptyFn,
add: function() {
var args = Array.prototype.slice.call(arguments),
sprite,
index;
var hasMultipleArgs = args.length > 1;
if (hasMultipleArgs || Ext.isArray(args[0])) {
var items = hasMultipleArgs ? args : args[0],
results = [],
i, ln, item;
for (i = 0, ln = items.length; i < ln; i++) {
item = items[i];
item = this.add(item);
results.push(item);
}
return results;
}
sprite = this.prepareItems(args[0], true)[0];
this.positionSpriteInList(sprite);
this.onAdd(sprite);
return sprite;
},
/**
* Insert or move a given sprite into the correct position in the items
* MixedCollection, according to its zIndex. Will be inserted at the end of
* an existing series of sprites with the same or lower zIndex. If the sprite
* is already positioned within an appropriate zIndex group, it will not be moved.
* This ordering can be used by subclasses to assist in rendering the sprites in
* the correct order for proper z-index stacking.
* @param {Ext.draw.Sprite} sprite
* @return {Number} the sprite's new index in the list
*/
positionSpriteInList: function(sprite) {
var items = this.items,
zIndex = sprite.attr.zIndex,
idx = items.indexOf(sprite);
if (idx < 0 || (idx > 0 && items.getAt(idx - 1).attr.zIndex > zIndex) ||
(idx < items.length - 1 && items.getAt(idx + 1).attr.zIndex < zIndex)) {
items.removeAt(idx);
idx = items.findIndexBy(function(otherSprite) {
return otherSprite.attr.zIndex > zIndex;
});
if (idx < 0) {
idx = items.length;
}
items.insert(idx, sprite);
}
return idx;
},
onAdd: function(sprite) {
var group = sprite.group,
groups, ln, i;
if (group) {
groups = [].concat(group);
ln = groups.length;
for (i = 0; i < ln; i++) {
group = groups[i];
this.getGroup(group).add(sprite);
}
delete sprite.group;
}
},
remove: function(sprite) {
if (sprite) {
this.items.remove(sprite);
this.onRemove(sprite);
this.groups.each(function(item) {
item.remove(sprite);
});
}
},
removeAll : function() {
var items = this.items.items,
ln = items.length,
i;
for (i = ln; i > -1; i--) {
this.remove(items[i]);
}
},
onRemove: Ext.emptyFn,
applyTransformations: function(sprite) {
sprite.bbox.transform = 0;
this.transform(sprite);
var me = this,
dirty = false,
attr = sprite.attr;
if (attr.translation.x != null || attr.translation.y != null) {
me.translate(sprite);
dirty = true;
}
if (attr.scaling.x != null || attr.scaling.y != null) {
me.scale(sprite);
dirty = true;
}
if (attr.rotation.degrees != null) {
me.rotate(sprite);
dirty = true;
}
if (dirty) {
sprite.bbox.transform = 0;
this.transform(sprite);
sprite.transformations = [];
}
},
rotate: function (sprite) {
var bbox,
deg = sprite.attr.rotation.degrees,
centerX = sprite.attr.rotation.x,
centerY = sprite.attr.rotation.y;
if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) {
bbox = this.getBBox(sprite);
centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX;
centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY;
}
sprite.transformations.push({
type: "rotate",
degrees: deg,
x: centerX,
y: centerY
});
},
translate: function(sprite) {
var x = sprite.attr.translation.x || 0,
y = sprite.attr.translation.y || 0;
sprite.transformations.push({
type: "translate",
x: x,
y: y
});
},
scale: function(sprite) {
var bbox,
x = sprite.attr.scaling.x || 1,
y = sprite.attr.scaling.y || 1,
centerX = sprite.attr.scaling.centerX,
centerY = sprite.attr.scaling.centerY;
if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) {
bbox = this.getBBox(sprite);
centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX;
centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY;
}
sprite.transformations.push({
type: "scale",
x: x,
y: y,
centerX: centerX,
centerY: centerY
});
},
rectPath: function (x, y, w, h, r) {
if (r) {
return [["M", x + r, y], ["l", w - r * 2, 0], ["a", r, r, 0, 0, 1, r, r], ["l", 0, h - r * 2], ["a", r, r, 0, 0, 1, -r, r], ["l", r * 2 - w, 0], ["a", r, r, 0, 0, 1, -r, -r], ["l", 0, r * 2 - h], ["a", r, r, 0, 0, 1, r, -r], ["z"]];
}
return [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]];
},
ellipsePath: function (x, y, rx, ry) {
if (ry == null) {
ry = rx;
}
return [["M", x, y], ["m", 0, -ry], ["a", rx, ry, 0, 1, 1, 0, 2 * ry], ["a", rx, ry, 0, 1, 1, 0, -2 * ry], ["z"]];
},
getPathpath: function (el) {
return el.attr.path;
},
getPathcircle: function (el) {
var a = el.attr;
return this.ellipsePath(a.x, a.y, a.radius, a.radius);
},
getPathellipse: function (el) {
var a = el.attr;
return this.ellipsePath(a.x, a.y, a.radiusX, a.radiusY);
},
getPathrect: function (el) {
var a = el.attr;
return this.rectPath(a.x, a.y, a.width, a.height, a.r);
},
getPathimage: function (el) {
var a = el.attr;
return this.rectPath(a.x, a.y, a.width, a.height);
},
getPathtext: function (el) {
var bbox = this.getBBoxText(el);
return this.rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
},
createGroup: function(id) {
var group = this.groups.get(id);
if (!group) {
group = new Ext.draw.SpriteGroup({
surface: this
});
group.id = id || Ext.id(null, 'ext-surface-group-');
this.groups.add(group);
}
return group;
},
// Returns the group mixed collection with the given id
getGroup: function(id) {
if (typeof id == "string") {
var group = this.groups.get(id);
if (!group) {
group = this.createGroup(id);
}
} else {
group = id;
}
return group;
},
prepareItems: function(items, applyDefaults) {
items = [].concat(items);
// Make sure defaults are applied and item is initialized
var item, i, ln;
for (i = 0, ln = items.length; i < ln; i++) {
item = items[i];
// Temporary, just take in configs...
item.surface = this;
items[i] = this.createItem(item);
}
return items;
},
createItem: Ext.emptyFn,
/**
* Retrieves the id of this component.
* Will autogenerate an id if one has not already been set.
*/
getId: function() {
return this.id || (this.id = Ext.id(null, 'ext-surface-'));
},
destroy: function() {
delete this.domRef;
this.removeAll();
}
});