SVG: Dragging objects between two different SVG elements - svg

I'm developing a prototype to mock this tool and provide minimal functionality like
Drag-n-Drop simple objects
Connect related objects via paths
Generate JSON from this structure
I opted SVG and Snap.svg framework (had hard time in deciding between D3 and Snap.svg but ended up with latter just because it is latest and successor of Raphael) to implement it. I stuck with below during implementation
Question 1: How to drag objects between different SVG elements? Try dragging elements in this Fiddle, objects were hidden when dragged outside of its parent dimension. Fiddle snippet below for your perusal.
HTML/SVG:
<div class="stencil">
<svg id="stencil" height="300" version="1.1" width="120" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
</svg>
</div>
<div class="paper">
<div class="paper__scroller">
<svg id="paper" height="1000" version="1.1" width="1000" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
</svg>
</div>
</div>
Javascript:
(function(){
var pap = Snap("#paper"),
stencil = Snap("#stencil"),
cir = stencil.circle(30, 50, 20),
rec = stencil.rect(70, 33, 35, 35);
stencilElements = stencil.group(rec, cir);
stencilElements.attr({
fill: "#f00",
stroke: "#000",
strokeWidth: 2,
"fill-opacity": 0.5
});
cir.clone().drag();
rec.clone().drag();
}());
Question 2: How to connect related objects via paths that adjust/moves accordingly when the object is dragged?
Question 3: How to convert the generated graph/diagram to JSON structure?
Really appreciate any reference or hints in implementing this.

Related

Snap.svg inner shadow on animated arc

I just starting working with svgs and Snap.svg library. Following a tutorial I created an arc progress indicator. What I want now is to add an inner shadow to the animated arc. I read the documentation but the filter.shadow command only gives the option for an outer shadow. I did some research on adding filters to regular svgs and I added one to the svg that I'm binding to my Snap object. It does give me the inner shadow, however it ruins the arc animation; only about half of the arc appears even though the inner shadow is present. I changed some of the filter settings trying to fix it but nothing works. I'm thinking that filter only works for static svgs.
Any help would be appreciated. Thanx
UPDATE
Here is the code that I'm using.
<svg class="gauge" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" viewBox="0 0 400 200" xml:space="preserve">
<path fill="none" stroke="#D1D2D1" stroke-miterlimit="10" d="M199.7,12.7V18h0.4v-5.3H199.7z M382,200c0-50-20.3-95.4-53.1-128.4
l8.4-8.4l-0.4-0.4l-8.4,8.4C295.6,38.4,250.1,18,200,18S104.5,38.3,71.5,71.2l-8.3-8.3l-0.4,0.4l8.3,8.3C38.3,104.5,18,149.9,18,200
h44c0-38,15.4-72.4,40.3-97.4l7.5,7.5l0.4-0.4l-7.5-7.5c24.9-24.8,59.2-40.2,97.1-40.3v10.5h0.5V62c38.1,0.1,72.6,15.6,97.5,40.7
c-0.1-0.1-0.2-0.2-0.3-0.4l-7.4,7.4l0.4,0.4l7.4-7.4c24.8,25,40.2,59.4,40.2,97.3H382z M102.3,102.6l0.3-0.3L102.3,102.6z"/>
</svg>
<script type="text/javascript">
var canvasSize = 400,
centre = canvasSize/2,
radius = canvasSize*0.8/2,
s = Snap('.gauge'),
path = "",
startY = centre-radius,
arc = s.path(path);
function fill(percent) {
var endpoint = percent*180;
Snap.animate(0, endpoint, function (val) {
arc.remove();
var d = val,
dr = d-180;
radians = Math.PI*(dr)/180,
endx = centre + radius*Math.cos(radians),
endy = centre + radius * Math.sin(radians),
largeArc = d>180 ? 1 : 0;
path = "M"+startY+","+centre+" A"+radius+","+radius+" 0 "+largeArc+",1 "+endx+","+endy;
arc = s.path(path);
arc.attr({
stroke: '#fff',
fill: 'none',
strokeWidth: 44,
});
}, 1500, mina.backout);
}
fill(50/100);
</script>
I added this code to the attribute to get an outer shadow.
arc.attr({
filter : s.filter(Snap.filter.shadow(1, 1, 2, 'black', 0.5))
})
I was wondering if there's something similar for an inner shadow.
I also tried adding this to the the embedded svg and then adding the attribute to the animated arc thinking it might work like a regular, static svg. It does give me an inner shadow, however only part of the animated arc gets displayed; it gets displayed within a rectangle.
<defs>
<filter id="inner-shadow">
<feGaussianBlur result="offset-blur" stdDeviation="5"></feGaussianBlur>
<feOffset dx="1" dy="1"></feOffset>
<feFlood flood-color="black" flood-opacity="1" result="color"></feFlood>
<feComposite operator="in" in2="offset-blur"></feComposite>
<feComponentTransfer>
<feFuncA slope="0.5"></feFuncA>
</feComponentTransfer>
<feMerge>
<feMergeNode></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
</defs>
arc.attr({
stroke: '#fff',
fill: 'none',
strokeWidth: 44,
filter: 'url(#inner-shadow)',
});
You can adjust the filter area that its applied over so that it doesn't get cut off. The Snap code seemed to give an error for me, so I just tried it using the svg filter markup, as the problem seems to be SVG related, rather than Snap.
So the key bit to change would be..
<filter id="inner-shadow" y="-30" x="-30" height="400" width="500">
This gives an offset wider range. You can read more on that at filter docs
jsfiddle

Get attributes of use element that are defined by defs element with snap.svg

I'm new to svgs and brand new to snap.svg. I'm working on generating elements within an SVG and have the following to work with:
<svg width="600" height="400" style="shape-rendering: geometricPrecision; position: absolute;
left: 0; top: 0;">
<defs>
...
<circle id="dot" r="10" stroke-width="2"></circle>
</defs>
</svg>
I want to use javascript to create mutliple instances of the circle #dot at different positions. So I have some javascript using snap.svg like this:
var dot = svg.use("dot");
var r = dot.attr("r");
dot.attr({ x: shapeData.X-r, y: shapeData.Y-r, class: "dot" });
but the value I'm getting for the radius, r, is null. How can I access values describing my circle like, r, width, height, fill color, etc?
The way use elements work in SVG is they are basically just pointers to the original object. If you place a clone of that dot on the canvas with use, the clone doesn't have a defined radius. It points back to dot, which has a defined radius.
It's not entirely clear to me what all you need to do but I think the right way to approach this is to get a reference to dot that you can then use for this purpose. You can clone dot and add other attributes to it later.
Aside from that you're just missing a lot of Snap stuff you need but maybe that's because you're just giving us a snippet.
Here's some code:
<svg id="svg" width="600" height="400" style="shape-rendering: geometricPrecision; position:absolute;left: 0; top: 0;">
<defs>
<circle id="dot" r="10" stroke-width="2"></circle>
</defs>
</svg>
JS:
// reference to svg
var svg=Snap('#svg')
// reference to dot, stored in a
var a=svg.select('[id="dot"]')
// what's the radius of a?
var r=a.attr('r')
alert('The radius is '+r)
// clone a and add it to the svg
b=a.use()
svg.append(b)
// give b some attributes
b.attr({x:100,y:50})
console.log(b.attr())
Fiddle here: https://jsfiddle.net/ksy7mLsx/1/

SVG Won't show until I edit the element in Chrome developer tools

I'm creating a dom structure which includes an SVG element:
<div data-bind="with: searchable_select.f0000001" style="verticalAlign: top">
<input data-bind="value: select_filter_value, valueUpdate: 'afterkeydown', event: { change: change_filter_, blur: blur_filter_ }" style="display: none">
<div>
<select data-bind="options: select_list, value: working_value, event: { change: change_selector_ }, optionsText: 'label', optionsValue: 'value'" style="display: inline-block; maxWidth: 150px">
<option value="person_full_name_asc">Member Full Name (A-Z)</option>
<option value="person_full_name_desc">Member Full Name (Z-A)</option>
</select>
<svg style="display: inline-block; verticalAlign: middle" width="18" height="18" xmlns="http://www.w3.org/2000/svg" version="1.1">
<g>
<circle cx="6" cy="6" r="5" fill="#AAAAAA" stroke="#000000" stroke-width="2"></circle>
<path fill="#AAAAAA" stroke="#000000" stroke-width="2" d="M10,10 L17,17"></path>
</g>
</svg>
</div>
</div>
The svg element does not actually display. When I find the svg element in Chrome developer tools, both width and height show as zero.
I've tried removing the style attribute. I've tried setting the width and height in the style attribute.
I've copied the svg to a separate HTML file:
<html>
<head><title>maggen</title></head>
<body>
<svg style="display: inline-block; verticalAlign: middle" width="18" height="18" xmlns="http://www.w3.org/2000/svg" version="1.1">
<g>
<circle cx="6" cy="6" r="5" fill="#AAAAAA" stroke="#000000" stroke-width="2"></circle>
<path fill="#AAAAAA" stroke="#000000" stroke-width="2" d="M10,10 L17,17"></path>
</g>
</svg>
</body>
</html>
Where it displays fine.
If I wrap the svg element in a div, the svg element shows up. In fact, if I edit the HTML in Chrome developer tools it will show up.
So, yeah, why? I've done a search on Google for this, but either it isn't there or (more likely) my Google Fu is not up to the task. I mean, sure, I can wrap it in a div - and maybe that's the right thing to do - but I'd rather not because then I'd need to wrap other stuff in divs and the dom will start to get cluttered.
EDIT: On a hunch I tried editing viewport attribute into the SVG element in Chrome tools. Voila! The element is visible! Expect when I included the viewport attribute when creating the document, it's not visible. So I tried just adding a random attribute in Chrome tools to the SVG element. Voila! The Element is visible! So, I thought, the problem is specific to Chrome and tried running it in Firefox...
...where the element doesn't show up.
EDIT: Great, so wrapping it in a div is not guaranteed to make it show up. But doing an "edit as HTML" in Chrome developer tools does make it show up.
EDIT: Well, I've gotten it to work correctly and, yes, it turns out to be a function of the Javascript creating the DOM elements. There's a lot of stuff in the code, but I can boil it down to this:
This code works (createElement creates a tag and sets attributes based on passed in parameters):
var div = this.createElement(
'div',
element_name + '_div',
null, style_options, null, null
);
div.innerHTML = [
'<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width=' + width + ' height=' + height + '>',
svg_xml.join(''),
'</svg>'
].join('');
if (parent) parent.appendChild(div);
return div;
This code doesn't:
var svg = this.createElement('svg', element_name, null, style_options, classlist, {
viewport: '0 0 ' + width + ' ' + height,
version: "1.1",
xmlns: "http://www.w3.org/2000/svg",
width: width,
height: height
});
svg.innerHTML = svg_xml.join('');
if (parent) parent.appendChild(svg);
return svg;
So, sure, I've got something working now, but I don't understand why. Or more to the point, I don't understand why one way works and the other doesn't. I have a couple of guesses, but they are just wild guesses, really.
"I've got something working now, but I don't understand why."
The issue is that the methods you were using do not create an SVG element in the SVG namespace. The xmlns attribute only affects the behaviour of an XML parser, not of DOM methods.
I'm not sure which library you're using for the this.createElement() method with multiple parameters. However, I suspect it probably starts by calling the basic document.createElement(tagName) method. This is what MDN says about the standard DOM createElement method:
In an HTML document creates the specified HTML element or HTMLUnknownElement if the element is not known. ... In other documents creates an element with a null namespaceURI.
In other words, because you're (presumably, indirectly) calling createElement on an HTML document, it always creates an HTML element. An HTML element with tag name "svg" is just treated as an unknown span-type element.
In contrast, using div.innerHTML to pass a markup string creates the SVG element correctly because it invokes the HTML5 parser to figure out what type of element to create. The namespace is determined using the same rules as when parsing markup from a file. Editing the HTML in the Chrome developer tools has the same effect.
Sidenote: Avoid calling .innerHTML on an SVG element. Some browsers support it, but it's not part of the specs. You're not getting an error because your svg variable is actually an instance of HTMLUnknownElement. Passing SVG code to the innerHTML method of a parent <div> usually works, although there are some bugs with SMIL animation. As #Robert Longson says in the comments, you can use the DOMParser object to parse either HTML or XML code into a document.
The other way to dynamically create an SVG element is to use document.createElementNS(namespaceURI, tagName). You'll also have to use this method to create all the child elements of the SVG. Once they are created, you may be able to set attributes, styles, and classes using your library methods. (But you haven't specified what library you're using, so I'm not sure.)

How to append a text element with inline tspan children?

Starting with a DOM that already contains something like
<svg id="svg0" width="600" height="300" xmlns="http://www.w3.org/2000/svg" version="1.1">
</svg>
...I want to programmatically modify the element in d3.select("#svg0") so that I end up with
<svg id="svg0" width="600" height="300" xmlns="http://www.w3.org/2000/svg" version="1.1">
<text x="20" y="20">
Lorem ipsum
<tspan style="alignment-baseline:text-before-edge">dolor</tspan>
sit amet</text>
</svg>
This is as far as I can get:
var $svg = d3.select("#svg0");
$svg.append("text").text("Lorem ipsum ")
.attr({x:"20", y:"20"});
It looks as though the rest should be easy, but I've spent the last two hours trying all the "obvious" things to finish this without success.1
What does one have to do to finish the task described above?
1I've tried far too many things to describe them all. Suffice it to say that the text method, when used as a setter, wipes out whatever textContent the text object had before. This means that, effectively, this method can be called only once, which precludes solutions relying on calling .text(...) a second time to add the " sit amet" fragment.)
Normally you would think to use the html function for this, but from the docs:
Note: as its name suggests, selection.html is only supported on HTML
elements. SVG elements and other non-HTML elements do not support the
innerHTML property, and thus are incompatible with selection.html.
Consider using XMLSerializer to convert a DOM subtree to text. See
also the innersvg polyfill, which provides a shim to support the
innerHTML property on SVG elements.
Here's with the polyfill: http://jsfiddle.net/GNGF5/
And if you don't want to do that, you can hack it up using multiple tspan elements w/ a transform, as seen here: http://jsfiddle.net/cAuCM/
var $svg = d3.select("#svg0");
var $text = $svg.append("text");
var $tspan1 = $text.append('tspan');
var $tspan2 = $text.append('tspan');
var $tspan3 = $text.append('tspan');
$text.attr('transform', 'translate(0, 18)');
$tspan1.text('Lorem ipsum');
$tspan2.text('dolor').style('alignment-baseline', 'text-before-edge');
$tspan3.text('sit amet');
Here's how to do it with Snap.svg:
var paper = Snap("#svg0");
var t1 = paper.text(50, 50, "Snap");
var t2 = paper.text(50, 70, ["S","n","a","p"]);
<script src="https://cdn.jsdelivr.net/snap.svg/0.1.0/snap.svg-min.js"></script>
<svg id="svg0" width="600" height="300" xmlns="http://www.w3.org/2000/svg" version="1.1">
</svg>

Pass SVG string from cookie to RaphaelJS

I have a cookie, called 'plan' which contains a string value representing a RaphaelJS canvas and some objects within it as SVG. The exact paths etc may vary, but here's typlically what's being saved to the cookie:
<svg height="100%" version="1.1" width="100%" xmlns="http://www.w3.org/2000/svg" style="overflow: hidden; position: relative;"><desc>Created with Raphaƫl 2.0.1</desc><defs/><path style="" fill="none" stroke="#d9e026" d="M153,74L384,74L384,304L0,304L0,150L153,150L153,74"/><path style="" fill="#333333" stroke="#d9e026" d="M160,160L160,220L220,220A60,60,0,0,0,160,160" transform="matrix(1, 0, 0, 1, -103, 84)"/><rect x="121.53846153846155" y="158.5" width="76.92307692307692" height="3" r="0" rx="0" ry="0" fill="#333333" stroke="#d9e026" style="" transform="matrix(1, 0, 0, 1, -89, -9)"/><rect x="83.0769230769231" y="156" width="76.92307692307692" height="5" r="0" rx="0" ry="0" fill="#333333" stroke="#d9e026" style="" transform="matrix(0, -1, 1, 0, 223.038, 364.039)"/></svg>
Now I need to be able to pass this data into a new raphaeljs object on a new page and add some more paths etc into it.
Can anyone suggest how to do this as I'm struggling to know where to start?
Thanks!
Theres a spiffing new function in Raphael 2 which also simplifies these tasks
Paper.add
http://www.irunmywebsite.com/raphael/additionalhelp.php?v=2&q=paper.add
Looking into this in a lot more detail, it turns out not to be quite so simple. The SVG code is only available on newer browsers (IE8+) so this approach won't work in IE7 or less. Handily though, the Raphael object does store the required data to be able to use before it hits the DOM. There's a very handy plug-in I found to serialize the Raphael data then de-serialize on output. Although I'm having a few issues with the deserialization part, in theory this should be the solution: https://github.com/jspies/raphael.serialize.
So that plug-in essentially allows me to do this:
Raphael canvas > serialized JSON object > String > Cookie > String > serialized JSON object > Raphael canvas.

Resources