/* * 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 SvgCanvas(container) { var isOpera = !!window.opera, isWebkit = navigator.userAgent.indexOf("AppleWebKit") != -1, support = {}, // this defines which elements and attributes that we support // TODO: add to this and marker attributes // TODO: add to this // TODO: add to this // TODO: add to this svgWhiteList = { "a": ["clip-path", "clip-rule", "fill", "fill-opacity", "fill-rule", "filter", "id", "mask", "opacity", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "systemLanguage", "transform", "xlink:href", "xlink:title"], "circle": ["clip-path", "clip-rule", "cx", "cy", "fill", "fill-opacity", "fill-rule", "filter", "id", "mask", "opacity", "r", "requiredFeatures", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "systemLanguage", "transform"], "clipPath": ["clipPathUnits", "id"], "defs": [], "desc": [], "ellipse": ["clip-path", "clip-rule", "cx", "cy", "fill", "fill-opacity", "fill-rule", "filter", "id", "mask", "opacity", "requiredFeatures", "rx", "ry", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "systemLanguage", "transform"], "feGaussianBlur": ["id", "requiredFeatures", "stdDeviation"], "filter": ["filterRes", "filterUnits", "height", "id", "primitiveUnits", "requiredFeatures", "width", "x", "xlink:href", "y"], "g": ["clip-path", "clip-rule", "id", "display", "fill", "fill-opacity", "fill-rule", "filter", "mask", "opacity", "requiredFeatures", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "systemLanguage", "transform"], "image": ["clip-path", "clip-rule", "filter", "height", "id", "mask", "opacity", "requiredFeatures", "style", "systemLanguage", "transform", "width", "x", "xlink:href", "xlink:title", "y"], "line": ["clip-path", "clip-rule", "fill", "fill-opacity", "fill-rule", "filter", "id", "mask", "opacity", "requiredFeatures", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "systemLanguage", "transform", "x1", "x2", "y1", "y2"], "linearGradient": ["id", "gradientTransform", "gradientUnits", "requiredFeatures", "spreadMethod", "systemLanguage", "x1", "x2", "xlink:href", "y1", "y2"], "mask": ["height", "id", "maskContentUnits", "maskUnits", "width", "x", "y"], "metadata": ["id"], "path": ["clip-path", "clip-rule", "d", "fill", "fill-opacity", "fill-rule", "filter", "id", "mask", "opacity", "requiredFeatures", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "systemLanguage", "transform"], "polygon": ["clip-path", "clip-rule", "id", "fill", "fill-opacity", "fill-rule", "filter", "id", "mask", "opacity", "points", "requiredFeatures", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "systemLanguage", "transform"], "polyline": ["clip-path", "clip-rule", "id", "fill", "fill-opacity", "fill-rule", "filter", "mask", "opacity", "points", "requiredFeatures", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "systemLanguage", "transform"], "radialGradient": ["id", "cx", "cy", "fx", "fy", "gradientTransform", "gradientUnits", "r", "requiredFeatures", "spreadMethod", "systemLanguage", "xlink:href"], "rect": ["clip-path", "clip-rule", "fill", "fill-opacity", "fill-rule", "filter", "height", "id", "mask", "opacity", "requiredFeatures", "rx", "ry", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "systemLanguage", "transform", "width", "x", "y"], "stop": ["id", "offset", "requiredFeatures", "stop-color", "stop-opacity", "style", "systemLanguage"], "switch": ["id", "requiredFeatures", "systemLanguage"], "svg": ["clip-path", "clip-rule", "filter", "id", "height", "mask", "requiredFeatures", "style", "systemLanguage", "transform", "viewBox", "width", "xmlns", "xmlns:xlink"], "text": ["clip-path", "clip-rule", "fill", "fill-opacity", "fill-rule", "filter", "font-family", "font-size", "font-style", "font-weight", "id", "mask", "opacity", "requiredFeatures", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "systemLanguage", "transform", "text-anchor", "x", "xml:space", "y"], "title": [], "use": ["clip-path", "clip-rule", "filter", "height", "id", "mask", "style", "transform", "width", "x", "xlink:href", "y"] }, // console.log('Start profiling') // setTimeout(function() { // canvas.addToSelection(canvas.getVisibleElements()); // console.log('Stop profiling') // },3000); uiStrings = { "pathNodeTooltip":"Drag node to move it. Double-click node to change segment type", "pathCtrlPtTooltip":"Drag control point to adjust curve properties" }, toXml = function(str) { return $('

').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 // ************************************************************************************** // FIXME: what's the right way to make this 'class' private to the SelectorManager? 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' }); 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) }; var assignAttributes = function(node, attrs, suspendLength) { 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); node.setAttributeNS(ns, 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, 'display':'inline' } 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; }; // 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", idprefix = "svg_", svgdoc = container.ownerDocument, svgroot = svgdoc.createElementNS(svgns, "svg"); svgroot.setAttribute("width", 640); svgroot.setAttribute("height", 480); svgroot.setAttribute("id", "svgroot"); svgroot.setAttribute("xmlns", svgns); svgroot.setAttribute("xmlns:xlink", xlinkns); container.appendChild(svgroot); var svgcontent = svgdoc.createElementNS(svgns, "svg"); svgcontent.setAttribute('id', 'svgcontent'); // svgcontent.setAttribute('viewBox', '0 0 640 480'); svgcontent.setAttribute('width', '640'); svgcontent.setAttribute('height', '480'); svgcontent.setAttribute('x', '640'); svgcontent.setAttribute('y', '480'); svgcontent.setAttribute('overflow', 'visible'); svgcontent.setAttribute("xmlns", svgns); svgcontent.setAttribute("xmlns:xlink", xlinkns); svgroot.appendChild(svgcontent); (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.addExtension = function(name, ext_func) { if(!(name in extensions)) { // Provide constants here (or should these be accessed through getSomething()? var ext = ext_func({ content: svgcontent, root: svgroot, getNextId: getNextId, getElem: getElem, addSvgElementFromJson: addSvgElementFromJson, selectorManager: selectorManager, findDefs: findDefs, recalculateDimensions: recalculateDimensions, // Probably only needed for extensions, so no need to make public? // Also, should we prevent extensions from allowing