/*
* svgcanvas.js
*
* Licensed under the Apache License, Version 2
*
* Copyright(c) 2010 Alexis Deveria
* Copyright(c) 2010 Pavol Rusnak
* Copyright(c) 2010 Jeff Schiller
*
*/
if(!window.console) {
window.console = {};
window.console.log = function(str) {};
window.console.dir = function(str) {};
}
if(window.opera) {
window.console.log = function(str) {opera.postError(str);};
window.console.dir = function(str) {};
}
(function() {
// This fixes $(...).attr() to work as expected with SVG elements.
// Does not currently use *AttributeNS() since we rarely need that.
// See http://api.jquery.com/attr/ for basic documentation of .attr()
// Additional functionality:
// - When getting attributes, a string that's a number is return as type number.
// - If an array is supplied as first parameter, multiple values are returned
// as an object with values for each given attributes
var proxied = jQuery.fn.attr, svgns = "http://www.w3.org/2000/svg";
jQuery.fn.attr = function(key, value) {
var len = this.length;
if(!len) return this;
for(var i=0; i').text(str).html();
},
fromXml = function(str) {
return $('
').html(str).text();
};
var unit_types = {'em':0,'ex':0,'px':1,'cm':35.43307,'mm':3.543307,'in':90,'pt':1.25,'pc':15,'%':0};
// These command objects are used for the Undo/Redo stack
// attrs contains the values that the attributes had before the change
function ChangeElementCommand(elem, attrs, text) {
this.elem = elem;
this.text = text ? ("Change " + elem.tagName + " " + text) : ("Change " + elem.tagName);
this.newValues = {};
this.oldValues = attrs;
for (var attr in attrs) {
if (attr == "#text") this.newValues[attr] = elem.textContent;
else this.newValues[attr] = elem.getAttribute(attr);
}
this.apply = function() {
var bChangedTransform = false;
for(var attr in this.newValues ) {
if (this.newValues[attr]) {
if (attr == "#text") this.elem.textContent = this.newValues[attr];
else this.elem.setAttribute(attr, this.newValues[attr]);
}
else {
if (attr == "#text") this.elem.textContent = "";
else {
this.elem.setAttribute(attr, "");
this.elem.removeAttribute(attr);
}
}
if (attr == "transform") { bChangedTransform = true; }
}
// relocate rotational transform, if necessary
if(!bChangedTransform) {
var angle = canvas.getRotationAngle(elem);
if (angle) {
var bbox = elem.getBBox();
var cx = bbox.x + bbox.width/2,
cy = bbox.y + bbox.height/2;
var rotate = ["rotate(", angle, " ", cx, ",", cy, ")"].join('');
if (rotate != elem.getAttribute("transform")) {
elem.setAttribute("transform", rotate);
}
}
}
// if we are changing layer names, re-identify all layers
if (this.elem.tagName == "title" && this.elem.parentNode.parentNode == svgcontent) {
identifyLayers();
}
return true;
};
this.unapply = function() {
var bChangedTransform = false;
for(var attr in this.oldValues ) {
if (this.oldValues[attr]) {
if (attr == "#text") this.elem.textContent = this.oldValues[attr];
else this.elem.setAttribute(attr, this.oldValues[attr]);
}
else {
if (attr == "#text") this.elem.textContent = "";
else this.elem.removeAttribute(attr);
}
if (attr == "transform") { bChangedTransform = true; }
}
// relocate rotational transform, if necessary
if(!bChangedTransform) {
var angle = canvas.getRotationAngle(elem);
if (angle) {
var bbox = elem.getBBox();
var cx = bbox.x + bbox.width/2,
cy = bbox.y + bbox.height/2;
var rotate = ["rotate(", angle, " ", cx, ",", cy, ")"].join('');
if (rotate != elem.getAttribute("transform")) {
elem.setAttribute("transform", rotate);
}
}
}
// if we are changing layer names, re-identify all layers
if (this.elem.tagName == "title" && this.elem.parentNode.parentNode == svgcontent) {
identifyLayers();
}
return true;
};
this.elements = function() { return [this.elem]; }
}
function InsertElementCommand(elem, text) {
this.elem = elem;
this.text = text || ("Create " + elem.tagName);
this.parent = elem.parentNode;
this.apply = function() {
this.elem = this.parent.insertBefore(this.elem, this.elem.nextSibling);
if (this.parent == svgcontent) {
identifyLayers();
}
};
this.unapply = function() {
this.parent = this.elem.parentNode;
this.elem = this.elem.parentNode.removeChild(this.elem);
if (this.parent == svgcontent) {
identifyLayers();
}
};
this.elements = function() { return [this.elem]; };
}
// this is created for an element that has or will be removed from the DOM
// (creating this object does not remove the element from the DOM itself)
function RemoveElementCommand(elem, parent, text) {
this.elem = elem;
this.text = text || ("Delete " + elem.tagName);
this.parent = parent;
this.apply = function() {
this.parent = this.elem.parentNode;
this.elem = this.parent.removeChild(this.elem);
if (this.parent == svgcontent) {
identifyLayers();
}
};
this.unapply = function() {
this.elem = this.parent.insertBefore(this.elem, this.elem.nextSibling);
if (this.parent == svgcontent) {
identifyLayers();
}
};
this.elements = function() { return [this.elem]; };
// special hack for webkit: remove this element's entry in the svgTransformLists map
if (svgTransformLists[elem.id]) {
delete svgTransformLists[elem.id];
}
}
function MoveElementCommand(elem, oldNextSibling, oldParent, text) {
this.elem = elem;
this.text = text ? ("Move " + elem.tagName + " to " + text) : ("Move " + elem.tagName);
this.oldNextSibling = oldNextSibling;
this.oldParent = oldParent;
this.newNextSibling = elem.nextSibling;
this.newParent = elem.parentNode;
this.apply = function() {
this.elem = this.newParent.insertBefore(this.elem, this.newNextSibling);
if (this.newParent == svgcontent) {
identifyLayers();
}
};
this.unapply = function() {
this.elem = this.oldParent.insertBefore(this.elem, this.oldNextSibling);
if (this.oldParent == svgcontent) {
identifyLayers();
}
};
this.elements = function() { return [this.elem]; };
}
// TODO: create a 'typing' command object that tracks changes in text
// if a new Typing command is created and the top command on the stack is also a Typing
// and they both affect the same element, then collapse the two commands into one
// this command object acts an arbitrary number of subcommands
function BatchCommand(text) {
this.text = text || "Batch Command";
this.stack = [];
this.apply = function() {
var len = this.stack.length;
for (var i = 0; i < len; ++i) {
this.stack[i].apply();
}
};
this.unapply = function() {
for (var i = this.stack.length-1; i >= 0; i--) {
this.stack[i].unapply();
}
};
this.elements = function() {
// iterate through all our subcommands and find all the elements we are changing
var elems = [];
var cmd = this.stack.length;
while (cmd--) {
var thisElems = this.stack[cmd].elements();
var elem = thisElems.length;
while (elem--) {
if (elems.indexOf(thisElems[elem]) == -1) elems.push(thisElems[elem]);
}
}
return elems;
};
this.addSubCommand = function(cmd) { this.stack.push(cmd); };
this.isEmpty = function() { return this.stack.length == 0; };
}
// private members
// **************************************************************************************
function Selector(id, elem) {
// this is the selector's unique number
this.id = id;
// this holds a reference to the element for which this selector is being used
this.selectedElement = elem;
// this is a flag used internally to track whether the selector is being used or not
this.locked = true;
// this function is used to reset the id and element that the selector is attached to
this.reset = function(e, update) {
this.locked = true;
this.selectedElement = e;
this.resize();
this.selectorGroup.setAttribute("display", "inline");
};
// this holds a reference to the element that holds all visual elements of the selector
this.selectorGroup = addSvgElementFromJson({ "element": "g",
"attr": {"id": ("selectorGroup"+this.id)}
});
// this holds a reference to the path rect
this.selectorRect = this.selectorGroup.appendChild( addSvgElementFromJson({
"element": "path",
"attr": {
"id": ("selectedBox"+this.id),
"fill": "none",
"stroke": "#22C",
"stroke-width": "1",
"stroke-dasharray": "5,5",
// need to specify this so that the rect is not selectable
"style": "pointer-events:none"
}
}) );
// this holds a reference to the grip elements for this selector
this.selectorGrips = { "nw":null,
"n":null,
"ne":null,
"e":null,
"se":null,
"s":null,
"sw":null,
"w":null
};
this.rotateGripConnector = this.selectorGroup.appendChild( addSvgElementFromJson({
"element": "line",
"attr": {
"id": ("selectorGrip_rotateconnector_" + this.id),
"stroke": "#22C",
"stroke-width": "1"
}
}) );
this.rotateGrip = this.selectorGroup.appendChild( addSvgElementFromJson({
"element": "circle",
"attr": {
"id": ("selectorGrip_rotate_" + this.id),
"fill": "lime",
"r": 4,
"stroke": "#22C",
"stroke-width": 2,
"style": "cursor:url(images/rotate.png) 12 12, auto;"
}
}) );
// add the corner grips
for (var dir in this.selectorGrips) {
this.selectorGrips[dir] = this.selectorGroup.appendChild(
addSvgElementFromJson({
"element": "circle",
"attr": {
"id": ("selectorGrip_resize_" + dir + "_" + this.id),
"fill": "#22C",
"r": 4,
"style": ("cursor:" + dir + "-resize"),
// This expands the mouse-able area of the grips making them
// easier to grab with the mouse.
// This works in Opera and WebKit, but does not work in Firefox
// see https://bugzilla.mozilla.org/show_bug.cgi?id=500174
"stroke-width": 2,
"pointer-events":"all",
"display":"none"
}
}) );
}
this.showGrips = function(show) {
// TODO: use suspendRedraw() here
var bShow = show ? "inline" : "none";
this.rotateGrip.setAttribute("display", bShow);
this.rotateGripConnector.setAttribute("display", bShow);
var elem = this.selectedElement;
for (var dir in this.selectorGrips) {
this.selectorGrips[dir].setAttribute("display", bShow);
}
if(elem) this.updateGripCursors(canvas.getRotationAngle(elem));
};
// Updates cursors for corner grips on rotation so arrows point the right way
this.updateGripCursors = function(angle) {
var dir_arr = [];
var steps = Math.round(angle / 45);
if(steps < 0) steps += 8;
for (var dir in this.selectorGrips) {
dir_arr.push(dir);
}
while(steps > 0) {
dir_arr.push(dir_arr.shift());
steps--;
}
var i = 0;
for (var dir in this.selectorGrips) {
this.selectorGrips[dir].setAttribute('style', ("cursor:" + dir_arr[i] + "-resize"));
i++;
};
};
this.resize = function() {
var selectedBox = this.selectorRect,
selectedGrips = this.selectorGrips,
selected = this.selectedElement,
sw = round(selected.getAttribute("stroke-width"));
var offset = 1/canvas.getZoom();
if (selected.getAttribute("stroke") != "none" && !isNaN(sw)) {
offset += sw/2;
}
if (selected.tagName == "text") {
offset += 2/canvas.getZoom();
}
var bbox = canvas.getBBox(selected);
if(selected.tagName == 'g') {
// The bbox for a group does not include stroke vals, so we
// get the bbox based on its children.
var stroked_bbox = canvas.getStrokedBBox(selected.childNodes);
$.each(bbox, function(key, val) {
bbox[key] = stroked_bbox[key];
});
}
// loop and transform our bounding box until we reach our first rotation
var tlist = canvas.getTransformList(selected),
m = transformListToTransform(tlist).matrix;
// This should probably be handled somewhere else, but for now
// it keeps the selection box correctly positioned when zoomed
m.e *= current_zoom;
m.f *= current_zoom;
// apply the transforms
var l=bbox.x-offset, t=bbox.y-offset, w=bbox.width+(offset<<1), h=bbox.height+(offset<<1),
bbox = {x:l, y:t, width:w, height:h};
// we need to handle temporary transforms too
// if skewed, get its transformed box, then find its axis-aligned bbox
//*
var nbox = transformBox(l*current_zoom, t*current_zoom, w*current_zoom, h*current_zoom, m),
nbax = nbox.aabox.x,
nbay = nbox.aabox.y,
nbaw = nbox.aabox.width,
nbah = nbox.aabox.height;
// now if the shape is rotated, un-rotate it
var cx = nbax + nbaw/2,
cy = nbay + nbah/2;
var angle = canvas.getRotationAngle(selected);
if (angle) {
var rot = svgroot.createSVGTransform();
rot.setRotate(-angle,cx,cy);
var rotm = rot.matrix;
nbox.tl = transformPoint(nbox.tl.x,nbox.tl.y,rotm);
nbox.tr = transformPoint(nbox.tr.x,nbox.tr.y,rotm);
nbox.bl = transformPoint(nbox.bl.x,nbox.bl.y,rotm);
nbox.br = transformPoint(nbox.br.x,nbox.br.y,rotm);
// calculate the axis-aligned bbox
var minx = nbox.tl.x,
miny = nbox.tl.y,
maxx = nbox.tl.x,
maxy = nbox.tl.y;
minx = Math.min(minx, Math.min(nbox.tr.x, Math.min(nbox.bl.x, nbox.br.x) ) );
miny = Math.min(miny, Math.min(nbox.tr.y, Math.min(nbox.bl.y, nbox.br.y) ) );
maxx = Math.max(maxx, Math.max(nbox.tr.x, Math.max(nbox.bl.x, nbox.br.x) ) );
maxy = Math.max(maxy, Math.max(nbox.tr.y, Math.max(nbox.bl.y, nbox.br.y) ) );
nbax = minx;
nbay = miny;
nbaw = (maxx-minx);
nbah = (maxy-miny);
}
var sr_handle = svgroot.suspendRedraw(100);
var dstr = "M" + nbax + "," + nbay
+ " L" + (nbax+nbaw) + "," + nbay
+ " " + (nbax+nbaw) + "," + (nbay+nbah)
+ " " + nbax + "," + (nbay+nbah) + "z";
assignAttributes(selectedBox, {'d': dstr});
var gripCoords = {
nw: [nbax, nbay],
ne: [nbax+nbaw, nbay],
sw: [nbax, nbay+nbah],
se: [nbax+nbaw, nbay+nbah],
n: [nbax + (nbaw)/2, nbay],
w: [nbax, nbay + (nbah)/2],
e: [nbax + nbaw, nbay + (nbah)/2],
s: [nbax + (nbaw)/2, nbay + nbah]
};
if(selected == selectedElements[0]) {
for(var dir in gripCoords) {
var coords = gripCoords[dir];
assignAttributes(selectedGrips[dir], {
cx: coords[0], cy: coords[1]
});
};
}
if (angle) {
this.selectorGroup.setAttribute("transform", "rotate(" + [angle,cx,cy].join(",") + ")");
}
else {
this.selectorGroup.setAttribute("transform", "");
}
// we want to go 20 pixels in the negative transformed y direction, ignoring scale
assignAttributes(this.rotateGripConnector, { x1: nbax + (nbaw)/2,
y1: nbay,
x2: nbax + (nbaw)/2,
y2: nbay- 20});
assignAttributes(this.rotateGrip, { cx: nbax + (nbaw)/2,
cy: nbay - 20 });
svgroot.unsuspendRedraw(sr_handle);
};
// now initialize the selector
this.reset(elem);
};
function SelectorManager() {
// this will hold the element that contains all selector rects/grips
this.selectorParentGroup = null;
// this is a special rect that is used for multi-select
this.rubberBandBox = null;
// this will hold objects of type Selector (see above)
this.selectors = [];
// this holds a map of SVG elements to their Selector object
this.selectorMap = {};
// local reference to this object
var mgr = this;
this.initGroup = function() {
// remove old selector parent group if it existed
if (mgr.selectorParentGroup && mgr.selectorParentGroup.parentNode) {
mgr.selectorParentGroup.parentNode.removeChild(mgr.selectorParentGroup);
}
// create parent selector group and add it to svgroot
mgr.selectorParentGroup = svgdoc.createElementNS(svgns, "g");
mgr.selectorParentGroup.setAttribute("id", "selectorParentGroup");
svgroot.appendChild(mgr.selectorParentGroup);
mgr.selectorMap = {};
mgr.selectors = [];
mgr.rubberBandBox = null;
if($("#canvasBackground").length) return;
var canvasbg = svgdoc.createElementNS(svgns, "svg");
assignAttributes(canvasbg, {
'id':'canvasBackground',
'width': 640,
'height': 480,
'x': 0,
'y': 0,
'style': 'pointer-events:none'
});
// Opera has a rendering bug
if (!window.opera) canvasbg.setAttribute("filter", "url(#canvashadow)");
var rect = svgdoc.createElementNS(svgns, "rect");
assignAttributes(rect, {
'width': '100%',
'height': '100%',
'x': 0,
'y': 0,
'stroke-width': 1,
'stroke': '#000',
'fill': '#FFF',
'style': 'pointer-events:none'
});
canvasbg.appendChild(rect);
svgroot.insertBefore(canvasbg, svgcontent);
};
this.requestSelector = function(elem) {
if (elem == null) return null;
var N = this.selectors.length;
// if we've already acquired one for this element, return it
if (typeof(this.selectorMap[elem.id]) == "object") {
this.selectorMap[elem.id].locked = true;
return this.selectorMap[elem.id];
}
for (var i = 0; i < N; ++i) {
if (this.selectors[i] && !this.selectors[i].locked) {
this.selectors[i].locked = true;
this.selectors[i].reset(elem);
this.selectorMap[elem.id] = this.selectors[i];
return this.selectors[i];
}
}
// if we reached here, no available selectors were found, we create one
this.selectors[N] = new Selector(N, elem);
this.selectorParentGroup.appendChild(this.selectors[N].selectorGroup);
this.selectorMap[elem.id] = this.selectors[N];
return this.selectors[N];
};
this.releaseSelector = function(elem) {
if (elem == null) return;
var N = this.selectors.length,
sel = this.selectorMap[elem.id];
for (var i = 0; i < N; ++i) {
if (this.selectors[i] && this.selectors[i] == sel) {
if (sel.locked == false) {
console.log("WARNING! selector was released but was already unlocked");
}
delete this.selectorMap[elem.id];
sel.locked = false;
sel.selectedElement = null;
sel.showGrips(false);
// remove from DOM and store reference in JS but only if it exists in the DOM
try {
sel.selectorGroup.setAttribute("display", "none");
} catch(e) { }
break;
}
}
};
this.getRubberBandBox = function() {
if (this.rubberBandBox == null) {
this.rubberBandBox = this.selectorParentGroup.appendChild(
addSvgElementFromJson({ "element": "rect",
"attr": {
"id": "selectorRubberBand",
"fill": "#22C",
"fill-opacity": 0.15,
"stroke": "#22C",
"stroke-width": 0.5,
"display": "none",
"style": "pointer-events:none"
}
}));
}
return this.rubberBandBox;
};
this.initGroup();
}
// **************************************************************************************
// **************************************************************************************
// SVGTransformList implementation for Webkit
// These methods do not currently raise any exceptions.
// These methods also do not check that transforms are being inserted or handle if
// a transform is already in the list, etc. This is basically implementing as much
// of SVGTransformList that we need to get the job done.
//
// interface SVGEditTransformList {
// attribute unsigned long numberOfItems;
// void clear ( )
// SVGTransform initialize ( in SVGTransform newItem )
// SVGTransform getItem ( in unsigned long index )
// SVGTransform insertItemBefore ( in SVGTransform newItem, in unsigned long index )
// SVGTransform replaceItem ( in SVGTransform newItem, in unsigned long index )
// SVGTransform removeItem ( in unsigned long index )
// SVGTransform appendItem ( in SVGTransform newItem )
// NOT IMPLEMENTED: SVGTransform createSVGTransformFromMatrix ( in SVGMatrix matrix );
// NOT IMPLEMENTED: SVGTransform consolidate ( );
// }
// **************************************************************************************
var svgTransformLists = {};
var SVGEditTransformList = function(elem) {
this._elem = elem || null;
this._xforms = [];
// TODO: how do we capture the undo-ability in the changed transform list?
this._update = function() {
var tstr = "";
var concatMatrix = svgroot.createSVGMatrix();
for (var i = 0; i < this.numberOfItems; ++i) {
var xform = this._list.getItem(i);
tstr += transformToObj(xform).text + " ";
}
this._elem.setAttribute("transform", tstr);
};
this._list = this;
this._init = function() {
// Transform attribute parser
var str = this._elem.getAttribute("transform");
if(!str) return;
// TODO: Add skew support in future
var re = /\s*((scale|matrix|rotate|translate)\s*\(.*?\))\s*,?\s*/;
var arr = [];
var m = true;
while(m) {
m = str.match(re);
str = str.replace(re,'');
if(m && m[1]) {
var x = m[1];
var bits = x.split(/\s*\(/);
var name = bits[0];
var val_bits = bits[1].match(/\s*(.*?)\s*\)/);
var val_arr = val_bits[1].split(/[, ]+/);
var letters = 'abcdef'.split('');
var mtx = svgroot.createSVGMatrix();
$.each(val_arr, function(i, item) {
val_arr[i] = parseFloat(item);
if(name == 'matrix') {
mtx[letters[i]] = val_arr[i];
}
});
var xform = svgroot.createSVGTransform();
var fname = 'set' + name.charAt(0).toUpperCase() + name.slice(1);
var values = name=='matrix'?[mtx]:val_arr;
xform[fname].apply(xform, values);
this._list.appendItem(xform);
}
}
}
this.numberOfItems = 0;
this.clear = function() {
this.numberOfItems = 0;
this._xforms = [];
};
this.initialize = function(newItem) {
this.numberOfItems = 1;
this._xforms = [newItem];
};
this.getItem = function(index) {
if (index < this.numberOfItems && index >= 0) {
return this._xforms[index];
}
return null;
};
this.insertItemBefore = function(newItem, index) {
var retValue = null;
if (index >= 0) {
if (index < this.numberOfItems) {
var newxforms = new Array(this.numberOfItems + 1);
// TODO: use array copying and slicing
for ( var i = 0; i < index; ++i) {
newxforms[i] = this._xforms[i];
}
newxforms[i] = newItem;
for ( var j = i+1; i < this.numberOfItems; ++j, ++i) {
newxforms[j] = this._xforms[i];
}
this.numberOfItems++;
this._xforms = newxforms;
retValue = newItem;
this._list._update();
}
else {
retValue = this._list.appendItem(newItem);
}
}
return retValue;
};
this.replaceItem = function(newItem, index) {
var retValue = null;
if (index < this.numberOfItems && index >= 0) {
this._xforms[index] = newItem;
retValue = newItem;
this._list._update();
}
return retValue;
};
this.removeItem = function(index) {
var retValue = null;
if (index < this.numberOfItems && index >= 0) {
var retValue = this._xforms[index];
var newxforms = new Array(this.numberOfItems - 1);
for (var i = 0; i < index; ++i) {
newxforms[i] = this._xforms[i];
}
for (var j = i; j < this.numberOfItems-1; ++j, ++i) {
newxforms[j] = this._xforms[i+1];
}
this.numberOfItems--;
this._xforms = newxforms;
this._list._update();
}
return retValue;
};
this.appendItem = function(newItem) {
this._xforms.push(newItem);
this.numberOfItems++;
this._list._update();
return newItem;
};
};
// **************************************************************************************
var addSvgElementFromJson = function(data) {
return canvas.updateElementFromJson(data)
};
// TODO: declare the variables and set them as null, then move this setup stuff to
// an initialization function - probably just use clear()
var canvas = this,
svgns = "http://www.w3.org/2000/svg",
xlinkns = "http://www.w3.org/1999/xlink",
xmlns = "http://www.w3.org/XML/1998/namespace",
se_ns = "http://svg-edit.googlecode.com",
htmlns = "http://www.w3.org/1999/xhtml",
mathns = "http://www.w3.org/1998/Math/MathML",
idprefix = "svg_",
svgdoc = container.ownerDocument,
svgroot = svgdoc.importNode(Utils.text2xml('').documentElement, true);
$(svgroot).appendTo(container);
var svgcontent = svgdoc.createElementNS(svgns, "svg");
$(svgcontent).attr({
id: 'svgcontent',
width: 640,
height: 480,
x: 640,
y: 480,
overflow: 'visible',
xmlns: svgns,
"xmlns:xlink": xlinkns
}).appendTo(svgroot);
var convertToNum, convertToUnit, setUnitAttr;
(function() {
var w_attrs = ['x', 'x1', 'cx', 'rx', 'width'];
var h_attrs = ['y', 'y1', 'cy', 'ry', 'height'];
var unit_attrs = $.merge(['r','radius'], w_attrs);
$.merge(unit_attrs, h_attrs);
// Converts given values to numbers. Attributes must be supplied in
// case a percentage is given
convertToNum = function(attr, val) {
// Return a number if that's what it already is
if(!isNaN(val)) return val-0;
if(val.substr(-1) === '%') {
// Deal with percentage, depends on attribute
var num = val.substr(0, val.length-1)/100;
var res = canvas.getResolution();
if($.inArray(attr, w_attrs) !== -1) {
return num * res.w;
} else if($.inArray(attr, w_attrs) !== -1) {
return num * res.h;
} else {
return num * Math.sqrt((res.w*res.w) + (res.h*res.h))/Math.sqrt(2);
}
} else {
var unit = val.substr(-2);
var num = val.substr(0, val.length-2);
// Note that this multiplication turns the string into a number
return num * unit_types[unit];
}
};
setUnitAttr = function(elem, attr, val) {
if(!isNaN(val)) {
// New value is a number, so check currently used unit
var old_val = elem.getAttribute(attr);
if(old_val !== null && isNaN(old_val)) {
// Old value was a number, so get unit, then convert
var unit;
if(old_val.substr(-1) === '%') {
var res = canvas.getResolution();
unit = '%';
val *= 100;
if($.inArray(attr, w_attrs) !== -1) {
val = val / res.w;
} else if($.inArray(attr, w_attrs) !== -1) {
val = val / res.h;
} else {
return val / Math.sqrt((res.w*res.w) + (res.h*res.h))/Math.sqrt(2);
}
} else {
unit = old_val.substr(-2);
val = val / unit_types[unit];
}
val += unit;
}
}
elem.setAttribute(attr, val);
}
canvas.isValidUnit = function(attr, val) {
var valid = false;
if($.inArray(attr, unit_attrs) != -1) {
// True if it's just a number
if(!isNaN(val)) {
valid = true;
} else {
// Not a number, check if it has a valid unit
val = val.toLowerCase();
$.each(unit_types, function(unit) {
if(valid) return;
var re = new RegExp('^-?[\\d\\.]+' + unit + '$');
if(re.test(val)) valid = true;
});
}
} else valid = true;
return valid;
}
})();
var assignAttributes = function(node, attrs, suspendLength, unitCheck) {
if(!suspendLength) suspendLength = 0;
// Opera has a problem with suspendRedraw() apparently
var handle = null;
if (!window.opera) svgroot.suspendRedraw(suspendLength);
for (var i in attrs) {
var ns = (i.substr(0,4) == "xml:" ? xmlns :
i.substr(0,6) == "xlink:" ? xlinkns : null);
if(ns || !unitCheck) {
node.setAttributeNS(ns, i, attrs[i]);
} else {
setUnitAttr(node, i, attrs[i]);
}
}
if (!window.opera) svgroot.unsuspendRedraw(handle);
};
// remove unneeded attributes
// makes resulting SVG smaller
var cleanupElement = function(element) {
var handle = svgroot.suspendRedraw(60);
var defaults = {
'fill-opacity':1,
'opacity':1,
'stroke':'none',
'stroke-dasharray':'none',
'stroke-opacity':1,
'stroke-width':1,
'rx':0,
'ry':0
}
for(var attr in defaults) {
var val = defaults[attr];
if(element.getAttribute(attr) == val) {
element.removeAttribute(attr);
}
}
svgroot.unsuspendRedraw(handle);
};
this.updateElementFromJson = function(data) {
var shape = getElem(data.attr.id);
// if shape is a path but we need to create a rect/ellipse, then remove the path
if (shape && data.element != shape.tagName) {
current_layer.removeChild(shape);
shape = null;
}
if (!shape) {
shape = svgdoc.createElementNS(svgns, data.element);
if (current_layer) {
current_layer.appendChild(shape);
}
}
assignAttributes(shape, data.attr, 100);
cleanupElement(shape);
return shape;
};
(function() {
// TODO: make this string optional and set by the client
var comment = svgdoc.createComment(" Created with SVG-edit - http://svg-edit.googlecode.com/ ");
svgcontent.appendChild(comment);
// TODO For Issue 208: this is a start on a thumbnail
// var svgthumb = svgdoc.createElementNS(svgns, "use");
// svgthumb.setAttribute('width', '100');
// svgthumb.setAttribute('height', '100');
// svgthumb.setAttributeNS(xlinkns, 'href', '#svgcontent');
// svgroot.appendChild(svgthumb);
})();
// z-ordered array of tuples containing layer names and elements
// the first layer is the one at the bottom of the rendering
var all_layers = [],
encodableImages = {},
last_good_img_url = 'images/logo.png',
// pointer to the current layer
current_layer = null,
save_options = {round_digits: 5},
started = false,
obj_num = 1,
start_transform = null,
current_mode = "select",
current_resize_mode = "none",
all_properties = {
shape: {
fill: "#FF0000",
fill_paint: null,
fill_opacity: 1,
stroke: "#000000",
stroke_paint: null,
stroke_opacity: 1,
stroke_width: 5,
stroke_style: 'none',
opacity: 1
}
};
all_properties.text = $.extend(true, {}, all_properties.shape);
$.extend(all_properties.text, {
fill: "#000000",
stroke_width: 0,
font_size: 24,
font_family: 'serif'
});
var cur_shape = all_properties.shape,
cur_text = all_properties.text,
cur_properties = cur_shape,
current_zoom = 1,
// this will hold all the currently selected elements
// default size of 1 until it needs to grow bigger
selectedElements = new Array(1),
// this holds the selected's bbox
selectedBBoxes = new Array(1),
justSelected = null,
// this object manages selectors for us
selectorManager = new SelectorManager(),
rubberBox = null,
events = {},
undoStackPointer = 0,
undoStack = [],
curBBoxes = [],
extensions = {};
// Should this return an array by default, so extension results aren't overwritten?
var runExtensions = this.runExtensions = function(action, vars, returnArray) {
var result = false;
if(returnArray) result = [];
$.each(extensions, function(name, opts) {
if(action in opts) {
if(returnArray) {
result.push(opts[action](vars))
} else {
result = opts[action](vars);
}
}
});
return result;
}
// This method rounds the incoming value to the nearest value based on the current_zoom
var round = function(val){
return parseInt(val*current_zoom)/current_zoom;
};
// This method sends back an array or a NodeList full of elements that
// intersect the multi-select rubber-band-box on the current_layer only.
//
// Since the only browser that supports the SVG DOM getIntersectionList is Opera,
// we need to provide an implementation here. We brute-force it for now.
//
// Reference:
// Firefox does not implement getIntersectionList(), see https://bugzilla.mozilla.org/show_bug.cgi?id=501421
// Webkit does not implement getIntersectionList(), see https://bugs.webkit.org/show_bug.cgi?id=11274
var getIntersectionList = function(rect) {
if (rubberBox == null) { return null; }
if(!curBBoxes.length) {
// Cache all bboxes
curBBoxes = canvas.getVisibleElements(current_layer, true);
}
var resultList = null;
try {
resultList = current_layer.getIntersectionList(rect, null);
} catch(e) { }
if (resultList == null || typeof(resultList.item) != "function") {
resultList = [];
var rubberBBox = rubberBox.getBBox();
$.each(rubberBBox, function(key, val) {
rubberBBox[key] = val / current_zoom;
});
var i = curBBoxes.length;
while (i--) {
if(!rubberBBox.width || !rubberBBox.width) continue;
if (Utils.rectsIntersect(rubberBBox, curBBoxes[i].bbox)) {
resultList.push(curBBoxes[i].elem);
}
}
}
// addToSelection expects an array, but it's ok to pass a NodeList
// because using square-bracket notation is allowed:
// http://www.w3.org/TR/DOM-Level-2-Core/ecma-script-binding.html
return resultList;
};
// FIXME: we MUST compress consecutive text changes to the same element
// (right now each keystroke is saved as a separate command that includes the
// entire text contents of the text element)
// TODO: consider limiting the history that we store here (need to do some slicing)
var addCommandToHistory = function(cmd) {
// if our stack pointer is not at the end, then we have to remove
// all commands after the pointer and insert the new command
if (undoStackPointer < undoStack.length && undoStack.length > 0) {
undoStack = undoStack.splice(0, undoStackPointer);
}
undoStack.push(cmd);
undoStackPointer = undoStack.length;
};
// private functions
var getId = function() {
if (events["getid"]) return call("getid", obj_num);
return idprefix + obj_num;
};
var getNextId = function() {
// ensure the ID does not exist
var id = getId();
while (getElem(id)) {
obj_num++;
id = getId();
}
return id;
};
var call = function(event, arg) {
if (events[event]) {
return events[event](this,arg);
}
};
// this function sanitizes the input node and its children
// this function only keeps what is allowed from our whitelist defined above
var sanitizeSvg = function(node) {
// we only care about element nodes
// automatically return for all comment, etc nodes
// for text, we do a whitespace trim
if (node.nodeType == 3) {
node.nodeValue = node.nodeValue.replace(/^\s+|\s+$/g, "");
// Remove empty text nodes
if(!node.nodeValue.length) node.parentNode.removeChild(node);
}
if (node.nodeType != 1) return;
var doc = node.ownerDocument;
var parent = node.parentNode;
// can parent ever be null here? I think the root node's parent is the document...
if (!doc || !parent) return;
var allowedAttrs = svgWhiteList[node.nodeName];
// if this element is allowed
if (allowedAttrs != undefined) {
var se_attrs = [];
var i = node.attributes.length;
while (i--) {
// if the attribute is not in our whitelist, then remove it
// could use jQuery's inArray(), but I don't know if that's any better
var attr = node.attributes.item(i);
// TODO: use localName here and grab the namespace URI. Then, make sure that
// anything in our whitelist with a prefix is parsed out properly.
// i.e. "xlink:href" in our whitelist would mean we check that localName matches
// "href" and that namespaceURI matches the XLINK namespace
var attrName = attr.nodeName;
if (allowedAttrs.indexOf(attrName) == -1) {
// Bypassing the whitelist to allow se: prefixes. Is there
// a more appropriate way to do this?
if(attrName.indexOf('se:') == 0) {
se_attrs.push([attrName, attr.nodeValue]);
}
node.removeAttribute(attrName);
}
// special handling for path d attribute
if (node.nodeName == 'path' && attrName == 'd') {
// Convert to absolute
node.setAttribute('d',pathActions.convertPath(node));
}
// for the style attribute, rewrite it in terms of XML presentational attributes
if (attrName == "style") {
var props = attr.nodeValue.split(";"),
p = props.length;
while(p--) {
var nv = props[p].split(":");
// now check that this attribute is supported
if (allowedAttrs.indexOf(nv[0]) != -1) {
node.setAttribute(nv[0],nv[1]);
}
}
}
}
$.each(se_attrs, function(i, attr) {
node.setAttributeNS(se_ns, attr[0], attr[1]);
});
// for some elements that have a xlink:href, ensure the URI refers to a local element
// (but not for links)
var href = node.getAttributeNS(xlinkns,"href");
if(href &&
$.inArray(node.nodeName, ["filter", "linearGradient", "pattern",
"radialGradient", "textPath", "use"]) != -1)
{
// TODO: we simply check if the first character is a #, is this bullet-proof?
if (href[0] != "#") {
// remove the attribute (but keep the element)
node.setAttributeNS(xlinkns, "href", "");
node.removeAttributeNS(xlinkns, "href");
}
}
// if the element has attributes pointing to a non-local reference,
// need to remove the attribute
$.each(["clip-path", "fill", "marker-end", "marker-mid", "marker-start", "mask", "stroke"],function(i,attr) {
var val = node.getAttribute(attr);
if (val) {
val = getUrlFromAttr(val);
// simply check for first character being a '#'
if (val && val[0] != "#") {
node.setAttribute(attr, "");
node.removeAttribute(attr);
}
}
});
// recurse to children
i = node.childNodes.length;
while (i--) { sanitizeSvg(node.childNodes.item(i)); }
}
// else, remove this element
else {
// remove all children from this node and insert them before this node
// FIXME: in the case of animation elements this will hardly ever be correct
var children = [];
while (node.hasChildNodes()) {
children.push(parent.insertBefore(node.firstChild, node));
}
// remove this node from the document altogether
parent.removeChild(node);
// call sanitizeSvg on each of those children
var i = children.length;
while (i--) { sanitizeSvg(children[i]); }
}
};
// extracts the URL from the url(...) syntax of some attributes. Three variants:
// i.e. or
// or
//
this.getUrlFromAttr = function(attrVal) {
if (attrVal) {
// url("#somegrad")
if (attrVal.indexOf('url("') == 0) {
return attrVal.substring(5,attrVal.indexOf('"',6));
}
// url('#somegrad')
else if (attrVal.indexOf("url('") == 0) {
return attrVal.substring(5,attrVal.indexOf("'",6));
}
else if (attrVal.indexOf("url(") == 0) {
return attrVal.substring(4,attrVal.indexOf(')'));
}
}
return null;
};
var getUrlFromAttr = this.getUrlFromAttr;
var removeUnusedGrads = function() {
var defs = svgcontent.getElementsByTagNameNS(svgns, "defs");
if(!defs || !defs.length) return 0;
var all_els = svgcontent.getElementsByTagNameNS(svgns, '*'),
grad_uses = [],
numRemoved = 0;
$.each(all_els, function(i, el) {
var fill = getUrlFromAttr(el.getAttribute('fill'));
if(fill) {
grad_uses.push(fill.substr(1));
}
var stroke = getUrlFromAttr(el.getAttribute('stroke'));
if (stroke) {
grad_uses.push(stroke.substr(1));
}
// gradients can refer to other gradients
var href = el.getAttributeNS(xlinkns, "href");
if (href && href.indexOf('#') == 0) {
grad_uses.push(href.substr(1));
}
});
var lgrads = svgcontent.getElementsByTagNameNS(svgns, "linearGradient"),
grad_ids = [],
i = lgrads.length;
while (i--) {
var grad = lgrads[i];
var id = grad.id;
if($.inArray(id, grad_uses) == -1) {
// Not found, so remove
grad.parentNode.removeChild(grad);
numRemoved++;
}
}
// Remove defs if empty
var i = defs.length;
while (i--) {
var def = defs[i];
if(!def.getElementsByTagNameNS(svgns,'*').length) {
def.parentNode.removeChild(def);
}
}
return numRemoved;
}
var svgCanvasToString = function() {
// keep calling it until there are none to remove
while (removeUnusedGrads() > 0) {};
pathActions.clear(true);
// Keep SVG-Edit comment on top
$.each(svgcontent.childNodes, function(i, node) {
if(i && node.nodeType == 8 && node.data.indexOf('Created with') != -1) {
svgcontent.insertBefore(node, svgcontent.firstChild);
}
});
var output = svgToString(svgcontent, 0);
return output;
}
var svgToString = function(elem, indent) {
var out = new Array();
if (elem) {
cleanupElement(elem);
var attrs = elem.attributes,
attr,
i,
childs = elem.childNodes;
for (var i=0; i=0; i--) {
attr = attrs.item(i);
var attrVal = attr.nodeValue;
if (attr.localName == '-moz-math-font-style') continue;
if (attrVal != "") {
if(attrVal.indexOf('pointer-events') == 0) continue;
if(attr.localName == "class" && attrVal.indexOf('se_') == 0) continue;
out.push(" ");
if(attr.localName == 'd') attrVal = pathActions.convertPath(elem, true);
if(!isNaN(attrVal)) {
attrVal = shortFloat(attrVal);
}
// Embed images when saving
if(save_options.apply
&& elem.nodeName == 'image'
&& attr.localName == 'href'
&& save_options.images
&& save_options.images == 'embed') {
var img = encodableImages[attrVal];
if(img) attrVal = img;
}
// map various namespaces to our fixed namespace prefixes
// TODO: put this into a map and do a look-up instead of if-else
if (attr.namespaceURI == xlinkns) {
out.push('xlink:');
}
else if(attr.namespaceURI == 'http://www.w3.org/2000/xmlns/' && attr.localName != 'xmlns') {
out.push('xmlns:');
}
else if(attr.namespaceURI == xmlns) {
out.push('xml:');
}
else if(attr.namespaceURI == se_ns) {
out.push('se:');
}
out.push(attr.localName); out.push("=\"");
out.push(attrVal); out.push("\"");
}
}
}
if (elem.hasChildNodes()) {
out.push(">");
indent++;
var bOneLine = false;
for (var i=0; i");
break;
} // switch on node type
}
indent--;
if (!bOneLine) {
out.push("\n");
for (var i=0; i");
} else {
out.push("/>");
}
}
return out.join('');
}; // end svgToString()
this.embedImage = function(val, callback) {
// load in the image and once it's loaded, get the dimensions
$(new Image()).load(function() {
// create a canvas the same size as the raster image
var canvas = document.createElement("canvas");
canvas.width = this.width;
canvas.height = this.height;
// load the raster image into the canvas
canvas.getContext("2d").drawImage(this,0,0);
// retrieve the data: URL
try {
var urldata = ';svgedit_url=' + encodeURIComponent(val);
urldata = canvas.toDataURL().replace(';base64',urldata+';base64');
encodableImages[val] = urldata;
} catch(e) {
encodableImages[val] = false;
}
last_good_img_url = val;
if(callback) callback(encodableImages[val]);
}).attr('src',val);
}
// importNode, like cloneNode, causes the comma-to-period
// issue in Opera/Win/non-en. Thankfully we can compare to the original XML
// and simply use the original value when necessary
this.fixOperaXML = function(elem, orig_el) {
var x_attrs = elem.attributes;
$.each(x_attrs, function(i, attr) {
if(attr.nodeValue.indexOf(',') == -1) return;
// attr val has comma, so let's get the good value
var ns = attr.prefix == 'xlink' ? xlinkns :
attr.prefix == "xml" ? xmlns : null;
var good_attrval = orig_el.getAttribute(attr.localName);
if(ns) {
elem.setAttributeNS(ns, attr.nodeName, good_attrval);
} else {
elem.setAttribute(attr.nodeName, good_attrval);
}
});
var childs = elem.childNodes;
var o_childs = orig_el.childNodes;
$.each(childs, function(i, child) {
if(child.nodeType == 1) {
canvas.fixOperaXML(child, o_childs[i]);
}
});
}
var recalculateAllSelectedDimensions = function() {
var text = (current_resize_mode == "none" ? "position" : "size");
var batchCmd = new BatchCommand(text);
var i = selectedElements.length;
while(i--) {
var cmd = recalculateDimensions(selectedElements[i]);
if (cmd) {
batchCmd.addSubCommand(cmd);
}
}
if (!batchCmd.isEmpty()) {
addCommandToHistory(batchCmd);
call("changed", selectedElements);
}
};
// this is how we map paths to our preferred relative segment types
var pathMap = [0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a',
'H', 'h', 'V', 'v', 'S', 's', 'T', 't'];
var logMatrix = function(m) {
console.log([m.a,m.b,m.c,m.d,m.e,m.f]);
};
var remapElement = function(selected,changes,m) {
var remap = function(x,y) { return transformPoint(x,y,m); },
scalew = function(w) { return m.a*w; },
scaleh = function(h) { return m.d*h; },
box = canvas.getBBox(selected);
switch (selected.tagName)
{
case "line":
var pt1 = remap(changes["x1"],changes["y1"]),
pt2 = remap(changes["x2"],changes["y2"]);
changes["x1"] = pt1.x;
changes["y1"] = pt1.y;
changes["x2"] = pt2.x;
changes["y2"] = pt2.y;
break;
case "circle":
var c = remap(changes["cx"],changes["cy"]);
changes["cx"] = c.x;
changes["cy"] = c.y;
// take the minimum of the new selected box's dimensions for the new circle radius
var tbox = transformBox(box.x, box.y, box.width, box.height, m);
var w = tbox.tr.x - tbox.tl.x, h = tbox.bl.y - tbox.tl.y;
changes["r"] = Math.min(w/2, h/2);
break;
case "ellipse":
var c = remap(changes["cx"],changes["cy"]);
changes["cx"] = c.x;
changes["cy"] = c.y;
changes["rx"] = scalew(changes["rx"]);
changes["ry"] = scaleh(changes["ry"]);
break;
case "foreignObject":
case "rect":
case "image":
var pt1 = remap(changes["x"],changes["y"]);
changes["x"] = pt1.x;
changes["y"] = pt1.y;
changes["width"] = scalew(changes["width"]);
changes["height"] = scaleh(changes["height"]);
break;
case "use":
var pt1 = remap(changes["x"],changes["y"]);
changes["x"] = pt1.x;
changes["y"] = pt1.y;
break;
case "text":
// if it was a translate, then just update x,y
if (m.a == 1 && m.b == 0 && m.c == 0 && m.d == 1 &&
(m.e != 0 || m.f != 0) )
{
// [T][M] = [M][T']
// therefore [T'] = [M_inv][T][M]
var existing = transformListToTransform(selected).matrix,
t_new = matrixMultiply(existing.inverse(), m, existing);
changes["x"] = parseFloat(changes["x"]) + t_new.e;
changes["y"] = parseFloat(changes["y"]) + t_new.f;
}
else {
// we just absorb all matrices into the element and don't do any remapping
var chlist = canvas.getTransformList(selected);
var mt = svgroot.createSVGTransform();
mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix,m));
chlist.clear();
chlist.appendItem(mt);
}
break;
case "polygon":
case "polyline":
var len = changes["points"].length;
for (var i = 0; i < len; ++i) {
var pt = changes["points"][i];
pt = remap(pt.x,pt.y);
changes["points"][i].x = pt.x;
changes["points"][i].y = pt.y;
}
break;
case "path":
var segList = selected.pathSegList;
var len = segList.numberOfItems;
changes.d = new Array(len);
for (var i = 0; i < len; ++i) {
var seg = segList.getItem(i);
changes.d[i] = {
type: seg.pathSegType,
x: seg.x,
y: seg.y,
x1: seg.x1,
y1: seg.y1,
x2: seg.x2,
y2: seg.y2,
r1: seg.r1,
r2: seg.r2,
angle: seg.angle,
largeArcFlag: seg.largeArcFlag,
sweepFlag: seg.sweepFlag
};
}
var len = changes["d"].length,
firstseg = changes["d"][0],
currentpt = remap(firstseg.x,firstseg.y);
changes["d"][0].x = currentpt.x;
changes["d"][0].y = currentpt.y;
for (var i = 1; i < len; ++i) {
var seg = changes["d"][i];
var type = seg.type;
// if absolute or first segment, we want to remap x, y, x1, y1, x2, y2
// if relative, we want to scalew, scaleh
if (type % 2 == 0) { // absolute
var thisx = seg.x ? seg.x : currentpt.x, // for V commands
thisy = seg.y ? seg.y : currentpt.y, // for H commands
pt = remap(thisx,thisy),
pt1 = remap(seg.x1,seg.y1),
pt2 = remap(seg.x2,seg.y2);
seg.x = pt.x;
seg.y = pt.y;
seg.x1 = pt1.x;
seg.y1 = pt1.y;
seg.x2 = pt2.x;
seg.y2 = pt2.y;
seg.r1 = scalew(seg.r1),
seg.r2 = scaleh(seg.r2);
}
else { // relative
seg.x = scalew(seg.x);
seg.y = scaleh(seg.y);
seg.x1 = scalew(seg.x1);
seg.y1 = scaleh(seg.y1);
seg.x2 = scalew(seg.x2);
seg.y2 = scaleh(seg.y2);
seg.r1 = scalew(seg.r1),
seg.r2 = scaleh(seg.r2);
}
// tracks the current position (for H,V commands)
if (seg.x) currentpt.x = seg.x;
if (seg.y) currentpt.y = seg.y;
} // for each segment
break;
} // switch on element type to get initial values
// now we have a set of changes and an applied reduced transform list
// we apply the changes directly to the DOM
// TODO: merge this switch with the above one and optimize
switch (selected.tagName)
{
case "foreignObject":
case "rect":
case "image":
changes.x = changes.x-0 + Math.min(0,changes.width);
changes.y = changes.y-0 + Math.min(0,changes.height);
changes.width = Math.abs(changes.width);
changes.height = Math.abs(changes.height);
assignAttributes(selected, changes, 1000, true);
break;
case "use":
assignAttributes(selected, changes, 1000, true);
break;
case "ellipse":
changes.rx = Math.abs(changes.rx);
changes.ry = Math.abs(changes.ry);
case "circle":
if(changes.r) changes.r = Math.abs(changes.r);
case "line":
case "text":
assignAttributes(selected, changes, 1000, true);
break;
case "polyline":
case "polygon":
var len = changes["points"].length;
var pstr = "";
for (var i = 0; i < len; ++i) {
var pt = changes["points"][i];
pstr += pt.x + "," + pt.y + " ";
}
selected.setAttribute("points", pstr);
break;
case "path":
var dstr = "";
var len = changes["d"].length;
for (var i = 0; i < len; ++i) {
var seg = changes["d"][i];
var type = seg.type;
dstr += pathMap[type];
switch(type) {
case 13: // relative horizontal line (h)
case 12: // absolute horizontal line (H)
dstr += seg.x + " ";
break;
case 15: // relative vertical line (v)
case 14: // absolute vertical line (V)
dstr += seg.y + " ";
break;
case 3: // relative move (m)
case 5: // relative line (l)
case 19: // relative smooth quad (t)
case 2: // absolute move (M)
case 4: // absolute line (L)
case 18: // absolute smooth quad (T)
dstr += seg.x + "," + seg.y + " ";
break;
case 7: // relative cubic (c)
case 6: // absolute cubic (C)
dstr += seg.x1 + "," + seg.y1 + " " + seg.x2 + "," + seg.y2 + " " +
seg.x + "," + seg.y + " ";
break;
case 9: // relative quad (q)
case 8: // absolute quad (Q)
dstr += seg.x + "," + seg.y + " " + seg.x1 + "," + seg.y1 + " ";
break;
case 11: // relative elliptical arc (a)
case 10: // absolute elliptical arc (A)
dstr += seg.r1 + "," + seg.r2 + " " + seg.angle + " " + Number(seg.largeArcFlag) +
" " + Number(seg.sweepFlag) + " " + seg.x + "," + seg.y + " ";
break;
case 17: // relative smooth cubic (s)
case 16: // absolute smooth cubic (S)
dstr += seg.x + "," + seg.y + " " + seg.x2 + "," + seg.y2 + " ";
break;
}
}
selected.setAttribute("d", dstr);
break;
}
};
// this function returns the command which resulted from the selected change
// TODO: use suspendRedraw() and unsuspendRedraw() around this function
var recalculateDimensions = function(selected) {
if (selected == null) return null;
var tlist = canvas.getTransformList(selected);
// remove any unnecessary transforms
if (tlist && tlist.numberOfItems > 0) {
var k = tlist.numberOfItems;
while (k--) {
var xform = tlist.getItem(k);
if (xform.type == 0) {
tlist.removeItem(k);
}
// remove identity matrices
else if (xform.type == 1) {
if (isIdentity(xform.matrix)) {
tlist.removeItem(k);
}
}
// remove zero-degree rotations
else if (xform.type == 4) {
if (xform.angle == 0) {
tlist.removeItem(k);
}
}
}
}
// if this element had no transforms, we are done
if (tlist.numberOfItems == 0) {
selected.removeAttribute("transform");
return null;
}
// we know we have some transforms, so set up return variable
var batchCmd = new BatchCommand("Transform");
// store initial values that will be affected by reducing the transform list
var changes = {}, initial = null, attrs = [];
switch (selected.tagName)
{
case "line":
attrs = ["x1", "y1", "x2", "y2"];
break;
case "circle":
attrs = ["cx", "cy", "r"];
break;
case "ellipse":
attrs = ["cx", "cy", "rx", "ry"];
break;
case "foreignObject":
case "rect":
case "image":
attrs = ["width", "height", "x", "y"];
break;
case "use":
attrs = ["x", "y"];
break;
case "text":
attrs = ["x", "y"];
break;
case "polygon":
case "polyline":
initial = {};
initial["points"] = selected.getAttribute("points");
var list = selected.points;
var len = list.numberOfItems;
changes["points"] = new Array(len);
for (var i = 0; i < len; ++i) {
var pt = list.getItem(i);
changes["points"][i] = {x:pt.x,y:pt.y};
}
break;
case "path":
initial = {};
initial["d"] = selected.getAttribute("d");
changes["d"] = selected.getAttribute("d");
break;
} // switch on element type to get initial values
if(attrs.length) {
changes = $(selected).attr(attrs);
$.each(changes, function(attr, val) {
changes[attr] = convertToNum(attr, val);
});
}
// if we haven't created an initial array in polygon/polyline/path, then
// make a copy of initial values and include the transform
if (initial == null) {
initial = $.extend(true, {}, changes);
$.each(initial, function(attr, val) {
initial[attr] = convertToNum(attr, val);
});
}
// save the start transform value too
initial["transform"] = start_transform ? start_transform : "";
// if it's a group, we have special processing to flatten transforms
if (selected.tagName == "g" || selected.tagName == "a") {
var box = canvas.getBBox(selected),
oldcenter = {x: box.x+box.width/2, y: box.y+box.height/2},
newcenter = transformPoint(box.x+box.width/2, box.y+box.height/2,
transformListToTransform(tlist).matrix),
m = svgroot.createSVGMatrix();
// temporarily strip off the rotate and save the old center
var gangle = canvas.getRotationAngle(selected);
if (gangle) {
for (var i = 0; i < tlist.numberOfItems; ++i) {
var xform = tlist.getItem(i);
if (xform.type == 4) {
// extract old center through mystical arts
var rm = xform.matrix;
var a = gangle * Math.PI / 180;
// FIXME: This blows up if the angle is exactly 0 or 180 degrees!
oldcenter.y = 0.5 * (Math.sin(a)*rm.e + (1-Math.cos(a))*rm.f) / (1 - Math.cos(a));
oldcenter.x = ((1 - Math.cos(a)) * oldcenter.y - rm.f) / Math.sin(a);
tlist.removeItem(i);
break;
}
}
}
var tx = 0, ty = 0,
operation = 0,
N = tlist.numberOfItems;
// first, if it was a scale then the second-last transform will be it
if (N >= 3 && tlist.getItem(N-2).type == 3 &&
tlist.getItem(N-3).type == 2 && tlist.getItem(N-1).type == 2)
{
operation = 3; // scale
// if the children are unrotated, pass the scale down directly
// otherwise pass the equivalent matrix() down directly
var tm = tlist.getItem(N-3).matrix,
sm = tlist.getItem(N-2).matrix,
tmn = tlist.getItem(N-1).matrix;
var children = selected.childNodes;
var c = children.length;
while (c--) {
var child = children.item(c);
tx = 0;
ty = 0;
if (child.nodeType == 1) {
var childTlist = canvas.getTransformList(child);
var m = transformListToTransform(childTlist).matrix;
var angle = canvas.getRotationAngle(child);
var old_start_transform = start_transform;
start_transform = child.getAttribute("transform");
if(angle || hasMatrixTransform(childTlist)) {
var e2t = svgroot.createSVGTransform();
e2t.setMatrix(matrixMultiply(tm, sm, tmn, m));
childTlist.clear();
childTlist.appendItem(e2t,0);
}
// if not rotated or skewed, push the [T][S][-T] down to the child
else {
// update the transform list with translate,scale,translate
// slide the [T][S][-T] from the front to the back
// [T][S][-T][M] = [M][T2][S2][-T2]
// [T][S][-T][M] = [T][S][M][-T2]
// [-T2] = [M_inv][-T][M]
var t2n = matrixMultiply(m.inverse(), tmn, m);
// [T2] is always negative translation of [-T2]
var t2 = svgroot.createSVGMatrix();
t2.e = -t2n.e;
t2.f = -t2n.f;
// [T][S][-T][M] = [M][T2][S2][-T2]
// [S2] = [T2_inv][M_inv][T][S][-T][M][-T2_inv]
var s2 = matrixMultiply(t2.inverse(), m.inverse(), tm, sm, tmn, m, t2n.inverse());
var translateOrigin = svgroot.createSVGTransform(),
scale = svgroot.createSVGTransform(),
translateBack = svgroot.createSVGTransform();
translateOrigin.setTranslate(t2n.e, t2n.f);
scale.setScale(s2.a, s2.d);
translateBack.setTranslate(t2.e, t2.f);
childTlist.appendItem(translateBack);
childTlist.appendItem(scale);
childTlist.appendItem(translateOrigin);
} // not rotated
batchCmd.addSubCommand( recalculateDimensions(child) );
start_transform = old_start_transform;
} // element
} // for each child
// Remove these transforms from group
tlist.removeItem(N-1);
tlist.removeItem(N-2);
tlist.removeItem(N-3);
}
else if (N >= 3 && tlist.getItem(N-1).type == 1)
{
operation = 3; // scale
m = transformListToTransform(tlist).matrix;
var e2t = svgroot.createSVGTransform();
e2t.setMatrix(m);
tlist.clear();
tlist.appendItem(e2t);
}
// next, check if the first transform was a translate
// if we had [ T1 ] [ M ] we want to transform this into [ M ] [ T2 ]
// therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ]
else if ( (N == 1 || (N > 1 && tlist.getItem(1).type != 3)) &&
tlist.getItem(0).type == 2)
{
operation = 2; // translate
var T_M = transformListToTransform(tlist).matrix;
tlist.removeItem(0);
var M_inv = transformListToTransform(tlist).matrix.inverse();
var M2 = matrixMultiply( M_inv, T_M );
tx = M2.e;
ty = M2.f;
if (tx != 0 || ty != 0) {
// we pass the translates down to the individual children
var children = selected.childNodes;
var c = children.length;
while (c--) {
var child = children.item(c);
if (child.nodeType == 1) {
var old_start_transform = start_transform;
start_transform = child.getAttribute("transform");
var childTlist = canvas.getTransformList(child);
var newxlate = svgroot.createSVGTransform();
newxlate.setTranslate(tx,ty);
childTlist.insertItemBefore(newxlate, 0);
batchCmd.addSubCommand( recalculateDimensions(child) );
start_transform = old_start_transform;
}
}
start_transform = old_start_transform;
}
}
// else, a matrix imposition from a parent group
// keep pushing it down to the children
else if (N == 1 && tlist.getItem(0).type == 1 && !gangle) {
operation = 1;
var m = tlist.getItem(0).matrix,
children = selected.childNodes,
c = children.length;
while (c--) {
var child = children.item(c);
if (child.nodeType == 1) {
var old_start_transform = start_transform;
start_transform = child.getAttribute("transform");
var childTlist = canvas.getTransformList(child);
var em = matrixMultiply(m, transformListToTransform(childTlist).matrix);
var e2m = svgroot.createSVGTransform();
e2m.setMatrix(em);
childTlist.clear();
childTlist.appendItem(e2m,0);
batchCmd.addSubCommand( recalculateDimensions(child) );
start_transform = old_start_transform;
}
}
tlist.clear();
}
// else it was just a rotate
else {
if (gangle) {
var newRot = svgroot.createSVGTransform();
newRot.setRotate(gangle,newcenter.x,newcenter.y);
tlist.insertItemBefore(newRot, 0);
}
if (tlist.numberOfItems == 0) {
selected.removeAttribute("transform");
}
return null;
}
// if it was a translate, put back the rotate at the new center
if (operation == 2) {
if (gangle) {
var newRot = svgroot.createSVGTransform();
newRot.setRotate(gangle,newcenter.x,newcenter.y);
tlist.insertItemBefore(newRot, 0);
}
}
// if it was a resize
else if (operation == 3) {
var m = transformListToTransform(tlist).matrix;
var roldt = svgroot.createSVGTransform();
roldt.setRotate(gangle, oldcenter.x, oldcenter.y);
var rold = roldt.matrix;
var rnew = svgroot.createSVGTransform();
rnew.setRotate(gangle, newcenter.x, newcenter.y);
var rnew_inv = rnew.matrix.inverse(),
m_inv = m.inverse(),
extrat = matrixMultiply(m_inv, rnew_inv, rold, m);
tx = extrat.e;
ty = extrat.f;
if (tx != 0 || ty != 0) {
// now push this transform down to the children
// we pass the translates down to the individual children
var children = selected.childNodes;
var c = children.length;
while (c--) {
var child = children.item(c);
if (child.nodeType == 1) {
var old_start_transform = start_transform;
start_transform = child.getAttribute("transform");
var childTlist = canvas.getTransformList(child);
var newxlate = svgroot.createSVGTransform();
newxlate.setTranslate(tx,ty);
childTlist.insertItemBefore(newxlate, 0);
batchCmd.addSubCommand( recalculateDimensions(child) );
start_transform = old_start_transform;
}
}
}
if (gangle) {
tlist.insertItemBefore(rnew, 0);
}
}
}
// else, it's a non-group
else {
var box = canvas.getBBox(selected),
oldcenter = {x: box.x+box.width/2, y: box.y+box.height/2},
newcenter = transformPoint(box.x+box.width/2, box.y+box.height/2,
transformListToTransform(tlist).matrix),
m = svgroot.createSVGMatrix(),
// temporarily strip off the rotate and save the old center
angle = canvas.getRotationAngle(selected);
if (angle) {
for (var i = 0; i < tlist.numberOfItems; ++i) {
var xform = tlist.getItem(i);
if (xform.type == 4) {
// extract old center through mystical arts
var rm = xform.matrix;
var a = angle * Math.PI / 180;
// FIXME: This blows up if the angle is exactly 0 or 180 degrees!
oldcenter.y = 0.5 * (Math.sin(a)*rm.e + (1-Math.cos(a))*rm.f) / (1 - Math.cos(a));
oldcenter.x = ((1 - Math.cos(a)) * oldcenter.y - rm.f) / Math.sin(a);
tlist.removeItem(i);
break;
}
}
}
// 2 = translate, 3 = scale, 4 = rotate, 1 = matrix imposition
var operation = 0;
var N = tlist.numberOfItems;
// first, if it was a scale of a non-skewed element, then the second-last
// transform will be the [S]
// if we had [M][T][S][T] we want to extract the matrix equivalent of
// [T][S][T] and push it down to the element
if (N >= 3 && tlist.getItem(N-2).type == 3 &&
tlist.getItem(N-3).type == 2 && tlist.getItem(N-1).type == 2 &&
selected.nodeName != "use")
{
operation = 3; // scale
m = transformListToTransform(tlist,N-3,N-1).matrix;
tlist.removeItem(N-1);
tlist.removeItem(N-2);
tlist.removeItem(N-3);
} // if we had [T][S][-T][M], then this was a skewed element being resized
// Thus, we simply combine it all into one matrix
else if(N == 4 && tlist.getItem(N-1).type == 1) {
operation = 3; // scale
m = transformListToTransform(tlist).matrix;
var e2t = svgroot.createSVGTransform();
e2t.setMatrix(m);
tlist.clear();
tlist.appendItem(e2t);
// reset the matrix so that the element is not re-mapped
m = svgroot.createSVGMatrix();
} // if we had [R][T][S][-T][M], then this was a rotated matrix-element
// if we had [T1][M] we want to transform this into [M][T2]
// therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ] and we can push [T2]
// down to the element
else if ( (N == 1 || (N > 1 && tlist.getItem(1).type != 3)) &&
tlist.getItem(0).type == 2)
{
operation = 2; // translate
var oldxlate = tlist.getItem(0).matrix,
meq = transformListToTransform(tlist,1).matrix,
meq_inv = meq.inverse();
m = matrixMultiply( meq_inv, oldxlate, meq );
tlist.removeItem(0);
}
// else if this child now has a matrix imposition (from a parent group)
// we might be able to simplify
else if (N == 1 && tlist.getItem(0).type == 1 && !angle) {
// Remap all point-based elements
m = transformListToTransform(tlist).matrix;
switch (selected.tagName) {
case 'line':
changes = $(selected).attr(["x1","y1","x2","y2"]);
case 'polyline':
case 'polygon':
changes.points = selected.getAttribute("points");
if(changes.points) {
var list = selected.points;
var len = list.numberOfItems;
changes.points = new Array(len);
for (var i = 0; i < len; ++i) {
var pt = list.getItem(i);
changes.points[i] = {x:pt.x,y:pt.y};
}
}
case 'path':
changes.d = selected.getAttribute("d");
operation = 1;
tlist.clear();
break;
default:
break;
}
}
// if it was a rotation, put the rotate back and return without a command
// (this function has zero work to do for a rotate())
else {
operation = 4; // rotation
if (angle) {
var newRot = svgroot.createSVGTransform();
newRot.setRotate(angle,newcenter.x,newcenter.y);
tlist.insertItemBefore(newRot, 0);
}
if (tlist.numberOfItems == 0) {
selected.removeAttribute("transform");
}
return null;
}
// if it was a translate or resize, we need to remap the element and absorb the xform
if (operation == 1 || operation == 2 || operation == 3) {
remapElement(selected,changes,m);
} // if we are remapping
// if it was a translate, put back the rotate at the new center
if (operation == 2) {
if (angle) {
var newRot = svgroot.createSVGTransform();
newRot.setRotate(angle,newcenter.x,newcenter.y);
tlist.insertItemBefore(newRot, 0);
}
}
// [Rold][M][T][S][-T] became [Rold][M]
// we want it to be [Rnew][M][Tr] where Tr is the
// translation required to re-center it
// Therefore, [Tr] = [M_inv][Rnew_inv][Rold][M]
else if (operation == 3) {
var m = transformListToTransform(tlist).matrix;
var roldt = svgroot.createSVGTransform();
roldt.setRotate(angle, oldcenter.x, oldcenter.y);
var rold = roldt.matrix;
var rnew = svgroot.createSVGTransform();
rnew.setRotate(angle, newcenter.x, newcenter.y);
var rnew_inv = rnew.matrix.inverse();
var m_inv = m.inverse();
var extrat = matrixMultiply(m_inv, rnew_inv, rold, m);
remapElement(selected,changes,extrat);
if (angle) {
tlist.insertItemBefore(rnew,0);
}
}
} // a non-group
// if the transform list has been emptied, remove it
if (tlist.numberOfItems == 0) {
selected.removeAttribute("transform");
}
batchCmd.addSubCommand(new ChangeElementCommand(selected, initial));
return batchCmd;
};
// public events
// Group: Selection
// Function: clearSelection
// Clears the selection. The 'selected' handler is then called.
this.clearSelection = function() {
if (selectedElements[0] != null) {
var len = selectedElements.length;
for (var i = 0; i < len; ++i) {
var elem = selectedElements[i];
if (elem == null) break;
selectorManager.releaseSelector(elem);
selectedElements[i] = null;
}
selectedBBoxes[0] = null;
}
call("selected", selectedElements);
};
// TODO: do we need to worry about selectedBBoxes here?
// Function: addToSelection
// Adds a list of elements to the selection. The 'selected' handler is then called.
//
// Parameters:
// elemsToAdd - an array of DOM elements to add to the selection
// showGrips - a boolean flag indicating whether the resize grips should be shown
this.addToSelection = function(elemsToAdd, showGrips) {
if (elemsToAdd.length == 0) { return; }
// find the first null in our selectedElements array
var j = 0;
while (j < selectedElements.length) {
if (selectedElements[j] == null) {
break;
}
++j;
}
// now add each element consecutively
var i = elemsToAdd.length;
while (i--) {
var elem = elemsToAdd[i];
// we ignore any selectors
if (!elem || elem.id.substr(0,13) == "selectorGrip_" || !this.getBBox(elem)) continue;
// if it's not already there, add it
if (selectedElements.indexOf(elem) == -1) {
selectedElements[j] = elem;
// only the first selectedBBoxes element is ever used in the codebase these days
if (j == 0) selectedBBoxes[j] = this.getBBox(elem);
j++;
var sel = selectorManager.requestSelector(elem);
if (selectedElements.length > 1) {
sel.showGrips(false);
}
}
}
call("selected", selectedElements);
if (showGrips || selectedElements.length == 1) {
selectorManager.requestSelector(selectedElements[0]).showGrips(true);
}
else {
selectorManager.requestSelector(selectedElements[0]).showGrips(false);
}
// make sure the elements are in the correct order
// See: http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition
selectedElements.sort(function(a,b) {
if(a && b && a.compareDocumentPosition) {
return 3 - (b.compareDocumentPosition(a) & 6);
} else if(a == null) {
return 1;
}
});
// Make sure first elements are not null
while(selectedElements[0] == null) selectedElements.shift(0);
};
// TODO: could use slice here to make this faster?
// TODO: should the 'selected' handler
// Function: removeFromSelection
// Removes elements from the selection.
//
// Parameters:
// elemsToRemove - an array of elements to remove from selection
this.removeFromSelection = function(elemsToRemove) {
if (selectedElements[0] == null) { return; }
if (elemsToRemove.length == 0) { return; }
// find every element and remove it from our array copy
var newSelectedItems = new Array(selectedElements.length),
newSelectedBBoxes = new Array(selectedBBoxes.length),
j = 0,
len = selectedElements.length;
for (var i = 0; i < len; ++i) {
var elem = selectedElements[i];
if (elem) {
// keep the item
if (elemsToRemove.indexOf(elem) == -1) {
newSelectedItems[j] = elem;
if (j==0) newSelectedBBoxes[j] = selectedBBoxes[i];
j++;
}
else { // remove the item and its selector
selectorManager.releaseSelector(elem);
}
}
}
// the copy becomes the master now
selectedElements = newSelectedItems;
selectedBBoxes = newSelectedBBoxes;
};
// Some global variables that we may need to refactor
var root_sctm = null;
// A (hopefully) quicker function to transform a point by a matrix
// (this function avoids any DOM calls and just does the math)
// Returns a x,y object representing the transformed point
var transformPoint = function(x, y, m) {
return { x: m.a * x + m.c * y + m.e, y: m.b * x + m.d * y + m.f};
};
var isIdentity = function(m) {
return (m.a == 1 && m.b == 0 && m.c == 0 && m.d == 1 && m.e == 0 && m.f == 0);
}
// expects three points to be sent, each point must have an x,y field
// returns an array of two points that are the smoothed
this.smoothControlPoints = function(ct1, ct2, pt) {
// each point must not be the origin
var x1 = ct1.x - pt.x,
y1 = ct1.y - pt.y,
x2 = ct2.x - pt.x,
y2 = ct2.y - pt.y;
if ( (x1 != 0 || y1 != 0) && (x2 != 0 || y2 != 0) ) {
var anglea = Math.atan2(y1,x1),
angleb = Math.atan2(y2,x2),
r1 = Math.sqrt(x1*x1+y1*y1),
r2 = Math.sqrt(x2*x2+y2*y2),
nct1 = svgroot.createSVGPoint(),
nct2 = svgroot.createSVGPoint();
if (anglea < 0) { anglea += 2*Math.PI; }
if (angleb < 0) { angleb += 2*Math.PI; }
var angleBetween = Math.abs(anglea - angleb),
angleDiff = Math.abs(Math.PI - angleBetween)/2;
var new_anglea, new_angleb;
if (anglea - angleb > 0) {
new_anglea = angleBetween < Math.PI ? (anglea + angleDiff) : (anglea - angleDiff);
new_angleb = angleBetween < Math.PI ? (angleb - angleDiff) : (angleb + angleDiff);
}
else {
new_anglea = angleBetween < Math.PI ? (anglea - angleDiff) : (anglea + angleDiff);
new_angleb = angleBetween < Math.PI ? (angleb + angleDiff) : (angleb - angleDiff);
}
// rotate the points
nct1.x = r1 * Math.cos(new_anglea) + pt.x;
nct1.y = r1 * Math.sin(new_anglea) + pt.y;
nct2.x = r2 * Math.cos(new_angleb) + pt.x;
nct2.y = r2 * Math.sin(new_angleb) + pt.y;
return [nct1, nct2];
}
return undefined;
};
var smoothControlPoints = this.smoothControlPoints;
// matrixMultiply() is provided because WebKit didn't implement multiply() correctly
// on the SVGMatrix interface. See https://bugs.webkit.org/show_bug.cgi?id=16062
// This function tries to return a SVGMatrix that is the multiplication m1*m2.
// We also round to zero when it's near zero
this.matrixMultiply = function() {
var NEAR_ZERO = 1e-14,
multi2 = function(m1, m2) {
var m = svgroot.createSVGMatrix();
m.a = m1.a*m2.a + m1.c*m2.b;
m.b = m1.b*m2.a + m1.d*m2.b,
m.c = m1.a*m2.c + m1.c*m2.d,
m.d = m1.b*m2.c + m1.d*m2.d,
m.e = m1.a*m2.e + m1.c*m2.f + m1.e,
m.f = m1.b*m2.e + m1.d*m2.f + m1.f;
return m;
},
args = arguments, i = args.length, m = args[i-1];
while(i-- > 1) {
var m1 = args[i-1];
m = multi2(m1, m);
}
if (Math.abs(m.a) < NEAR_ZERO) m.a = 0;
if (Math.abs(m.b) < NEAR_ZERO) m.b = 0;
if (Math.abs(m.c) < NEAR_ZERO) m.c = 0;
if (Math.abs(m.d) < NEAR_ZERO) m.d = 0;
if (Math.abs(m.e) < NEAR_ZERO) m.e = 0;
if (Math.abs(m.f) < NEAR_ZERO) m.f = 0;
return m;
}
var matrixMultiply = this.matrixMultiply;
// This returns a single matrix Transform for a given Transform List
// (this is the equivalent of SVGTransformList.consolidate() but unlike
// that method, this one does not modify the actual SVGTransformList)
// This function is very liberal with its min,max arguments
var transformListToTransform = function(tlist, min, max) {
var min = min == undefined ? 0 : min;
var max = max == undefined ? (tlist.numberOfItems-1) : max;
min = parseInt(min);
max = parseInt(max);
if (min > max) { var temp = max; max = min; min = temp; }
var m = svgroot.createSVGMatrix();
for (var i = min; i <= max; ++i) {
// if our indices are out of range, just use a harmless identity matrix
var mtom = (i >= 0 && i < tlist.numberOfItems ?
tlist.getItem(i).matrix :
svgroot.createSVGMatrix());
m = matrixMultiply(m, mtom);
}
return svgroot.createSVGTransformFromMatrix(m);
};
var hasMatrixTransform = function(tlist) {
var num = tlist.numberOfItems;
while (num--) {
var xform = tlist.getItem(num);
if (xform.type == 1 && !isIdentity(xform.matrix)) return true;
}
return false;
}
// // Easy way to loop through transform list, but may not be worthwhile
// var eachXform = function(elem, callback) {
// var tlist = canvas.getTransformList(elem);
// var num = tlist.numberOfItems;
// if(num == 0) return;
// while(num--) {
// var xform = tlist.getItem(num);
// callback(xform, tlist);
// }
// }
// FIXME: this should not have anything to do with zoom here - update the one place it is used this way
// converts a tiny object equivalent of a SVGTransform
// has the following properties:
// - tx, ty, sx, sy, angle, cx, cy, string
var transformToObj = function(xform, mZoom) {
var m = xform.matrix,
tobj = {tx:0,ty:0,sx:1,sy:1,angle:0,cx:0,cy:0,text:""},
z = mZoom?current_zoom:1;
switch(xform.type) {
case 1: // MATRIX
tobj.text = "matrix(" + [m.a,m.b,m.c,m.d,m.e,m.f].join(",") + ")";
break;
case 2: // TRANSLATE
tobj.tx = m.e;
tobj.ty = m.f;
tobj.text = "translate(" + m.e*z + "," + m.f*z + ")";
break;
case 3: // SCALE
tobj.sx = m.a;
tobj.sy = m.d;
if (m.a == m.d) tobj.text = "scale(" + m.a + ")";
else tobj.text = "scale(" + m.a + "," + m.d + ")";
break;
case 4: // ROTATE
tobj.angle = xform.angle;
// this prevents divide by zero
if (xform.angle != 0) {
var K = 1 - m.a;
tobj.cy = ( K * m.f + m.b*m.e ) / ( K*K + m.b*m.b );
tobj.cx = ( m.e - m.b * tobj.cy ) / K;
}
tobj.text = "rotate(" + xform.angle + " " + tobj.cx*z + "," + tobj.cy*z + ")";
break;
// TODO: matrix, skewX, skewY
}
return tobj;
};
var transformBox = function(l, t, w, h, m) {
var topleft = {x:l,y:t},
topright = {x:(l+w),y:t},
botright = {x:(l+w),y:(t+h)},
botleft = {x:l,y:(t+h)};
topleft = transformPoint( topleft.x, topleft.y, m );
var minx = topleft.x,
maxx = topleft.x,
miny = topleft.y,
maxy = topleft.y;
topright = transformPoint( topright.x, topright.y, m );
minx = Math.min(minx, topright.x);
maxx = Math.max(maxx, topright.x);
miny = Math.min(miny, topright.y);
maxy = Math.max(maxy, topright.y);
botleft = transformPoint( botleft.x, botleft.y, m);
minx = Math.min(minx, botleft.x);
maxx = Math.max(maxx, botleft.x);
miny = Math.min(miny, botleft.y);
maxy = Math.max(maxy, botleft.y);
botright = transformPoint( botright.x, botright.y, m );
minx = Math.min(minx, botright.x);
maxx = Math.max(maxx, botright.x);
miny = Math.min(miny, botright.y);
maxy = Math.max(maxy, botright.y);
return {tl:topleft, tr:topright, bl:botleft, br:botright,
aabox: {x:minx, y:miny, width:(maxx-minx), height:(maxy-miny)} };
};
// Mouse events
(function() {
var d_attr = null,
start_x = null,
start_y = null,
init_bbox = {},
freehand = {
minx: null,
miny: null,
maxx: null,
maxy: null
};
// - when we are in a create mode, the element is added to the canvas
// but the action is not recorded until mousing up
// - when we are in select mode, select the element, remember the position
// and do nothing else
var mouseDown = function(evt)
{
root_sctm = svgcontent.getScreenCTM().inverse();
var pt = transformPoint( evt.pageX, evt.pageY, root_sctm ),
mouse_x = pt.x * current_zoom,
mouse_y = pt.y * current_zoom;
evt.preventDefault();
if($.inArray(current_mode, ['select', 'resize']) == -1) {
addGradient();
}
var x = mouse_x / current_zoom,
y = mouse_y / current_zoom,
mouse_target = evt.target;
start_x = x;
start_y = y;
// if it was a