I have some code that retrieves a scripted svg image from a server via Ajax. I can get the image text back into the browser, but I can't find a way to insert it into the DOM that will actually display it. Can anyone help with this?
The svg looks likes this:
<svg id="chart" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" onload="init(evt)">
<script type="application/ecmascript">
<![CDATA[
...lots of code, changes on each Ajax request
//]]>
</script>
<script type="application/ecmascript" xlink:href="js-on-server-1.js"/>
<script type="application/ecmascript" xlink:href="js-on-server-2.js"/>
</svg>
I've tried various things. If I do this:
// xmlhttp.onreadystatechange:
addImage(xmlhttp.responseXML, "somewhere");
...
function addImage(txt, dst_id) {
var scr = document.createElement("div");
if("textContent" in scr)
scr.textContent = txt; // everybody else
else
scr.text = txt; // IE
document.getElementById(dst_id).appendChild(scr);
}
Then Opera and Chrome do nothing, and F/F complains "[object XMLDocument]". If I change 'responseXML' to 'responseText', then Opera/Chrome correctly display the entire svg text (not image) in the right place, and F/F still gives the same warning.
I've also tried assigning the response to an innerHTML, but that does nothing.
Any ideas? Thanks.
EDIT
In response to Phrogz'z answer below - I've added two simple svg files. The first is a 'standard' simple svg, displaying a circle. The second is a scripted svg, displaying a rectangle. You should be able to view both directly in any browser, except IE8-.
If I edit Phrogz'z code to use the circle file (replace 'stirling4.svg' with the name of this file), then it works, but if I want the scripted rectangle instead, it doesn't. Tested on F/F, Opera, Chromium, but doesn't work anyway on (my) Chromium.
File 1, circle:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red" />
</svg>
File 2, rectangle:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" onload="init(evt)">
<script type="application/ecmascript">
<![CDATA[
var svgDocument;
var svgns = "http://www.w3.org/2000/svg";
function init(evt) {
if(window.svgDocument == null)
svgDocument = evt.target.ownerDocument;
var lbox = svgDocument.createElementNS(svgns, "rect");
lbox.setAttributeNS(null, "x", 10);
lbox.setAttributeNS(null, "y", 10);
lbox.setAttributeNS(null, "width", 30);
lbox.setAttributeNS(null, "height", 30);
lbox.setAttributeNS(null, "stroke", "#8080ff");
lbox.setAttributeNS(null, "stroke-width", 2);
lbox.setAttributeNS(null, "fill-opacity", 0);
lbox.setAttributeNS(null, "stroke-opacity", 1);
lbox.setAttributeNS(null, "stroke-dasharray", 0);
svgDocument.documentElement.appendChild(lbox);
}
//]]>
</script>
</svg>
Presumably the answer is to get the script into the header??
In general, the problem is twofold threefold:
HTML is not XHTML, and support for SVG in HTML is shoddy and poorly-defined as of this writing. The solution is to use a real XHTML document where SVG-namespaced elements are actually treated as SVG.
The responseXML is in another DOM document, and you can't normally just move nodes from one document to another. You are supposed to use document.importNode to import a node from one document to another.
Loading an SVG file with onload event handlers will not have those handlers invoked by either creating the node or appending it to the document. Code inside the script block will be run, however, so you need to rewrite your scripts in a manner that works standalone and also with the dynamic loading.
Here's a simple example that works in Chrome, Safari, and Firefox...but not IE9:
var xhr = new XMLHttpRequest;
xhr.open('get','stirling4.svg',true);
xhr.onreadystatechange = function(){
if (xhr.readyState != 4) return;
var svg = xhr.responseXML.documentElement;
svg = document.importNode(svg,true); // surprisingly optional in these browsers
document.body.appendChild(svg);
};
xhr.send();
See it in action here: http://phrogz.net/SVG/import_svg.xhtml
Unfortunately IE9 does not properly support document.importNode. To work around this, we write our own cloneToDoc function that creates an equivalent structure for any given node by recursively crawling the hierarchy. Here's a full working example:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><head>
<meta http-equiv="content-type" content="application/xhtml+xml;charset=utf-8"/>
<title>Fetch and Include SVG in XHTML</title>
<script type="text/ecmascript"><![CDATA[
setTimeout(function(){
var xhr = new XMLHttpRequest;
xhr.open('get','stirling4.svg',true);
xhr.onreadystatechange = function(){
if (xhr.readyState != 4) return;
var svg = cloneToDoc(xhr.responseXML.documentElement);
document.body.appendChild(svg);
};
xhr.send();
},1000);
function cloneToDoc(node,doc){
if (!doc) doc=document;
var clone = doc.createElementNS(node.namespaceURI,node.nodeName);
for (var i=0,len=node.attributes.length;i<len;++i){
var a = node.attributes[i];
if (/^xmlns\b/.test(a.nodeName)) continue; // IE can't create these
clone.setAttributeNS(a.namespaceURI,a.nodeName,a.nodeValue);
}
for (var i=0,len=node.childNodes.length;i<len;++i){
var c = node.childNodes[i];
clone.insertBefore(
c.nodeType==1 ? cloneToDoc(c,doc) : doc.createTextNode(c.nodeValue),
null
); }
return clone;
}
]]></script>
</head><body></body></html>
See it in action here: http://phrogz.net/SVG/import_svg_ie9.xhtml
Edit 2: As suspected, the problem is that the onload event does not fire when dynamically adding script. Here's a paired solution that works:
Rewrite your script to remove the onload event handler. Instead, trust that document exists.
Rewrite your script to ask for a global svgRoot; if it doesn't exist, use document.documentElement.
When fetching the SVG set a global svgRoot to the new svg element after you import it into the document.
Here's the code in action:
Standalone scripted SVG: http://phrogz.net/SVG/script-created.svg
IE9-friendly page that imports it: http://phrogz.net/SVG/import_svg_with_script.xhtml
And, in case my site is down, here is the code for posterity:
script-created.svg
<svg xmlns="http://www.w3.org/2000/svg">
<script type="text/javascript"><![CDATA[
function createOn( root, name, a ){
var el = document.createElementNS(svgNS,name);
for (var n in a) if (a.hasOwnProperty(n)) el.setAttribute(n,a[n]);
return root.appendChild(el);
}
// Trust someone else for the root, in case we're being
// imported into another document
if (!window.svgRoot) svgRoot=document.documentElement;
var svgNS = svgRoot.namespaceURI;
createOn(svgRoot,'rect',{
x:10, y:10, width:30, height:30,
stroke:'#8080ff', "stroke-width":5,
fill:"none"
});
]]></script>
</svg>
import_svg_with_script.xhtml
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><head>
<meta http-equiv="content-type"
content="application/xhtml+xml;charset=utf-8" />
<title>Fetch and Include Scripted SVG in XHTML</title>
<script type="text/ecmascript"><![CDATA[
setTimeout(function(){
var xhr = new XMLHttpRequest;
xhr.open('get','script-created.svg',true);
xhr.onreadystatechange = function(){
if (xhr.readyState != 4) return;
var svg = xhr.responseXML.documentElement;
svg = cloneToDoc(svg);
window.svgRoot = svg; // For reference by scripts
document.body.appendChild(svg);
delete window.svgRoot;
};
xhr.send();
},1000);
function cloneToDoc(node,doc){
if (!doc) doc=document;
var clone = doc.createElementNS(node.namespaceURI,node.nodeName);
for (var i=0,len=node.attributes.length;i<len;++i){
var a = node.attributes[i];
if (/^xmlns\b/.test(a.nodeName)) continue; // IE can't create these
clone.setAttributeNS(a.namespaceURI,a.nodeName,a.nodeValue);
}
for (var i=0,len=node.childNodes.length;i<len;++i){
var c = node.childNodes[i];
clone.insertBefore(
c.nodeType==1 ? cloneToDoc(c,doc) : doc.createTextNode(c.nodeValue),
null
)
}
return clone;
}
]]></script>
</head><body></body></html>
Related
After loading an svg file from InkScape I want to add new elements to the drawing, created under program control. This seems to work fine if we just change the attributes of already present elements, but new created ones, even if they appear when inspecting the DOM, do not show!
A hand-simplified SVG test file:
<svg id="svg8" width="1e3" height="750" version="1.1"
viewBox="0 0 264.58333 198.43751" xmlns="http://www.w3.org/2000/svg">
<g id="layer"><circle id="cc0" cx="20" cy="20" r="10"/></g>
</svg>
The javascript/html file:
<!doctype html><html><head><meta charset="UTF-8"/>
<script>
function addCircle() {
var svgDoc = document.getElementById("test");
var svg = svgDoc.contentDocument;
var svgns = svgDoc.namespaceURI;
// Ok, this changes the circle to red
var c0 = svg.getElementById("cc0");
c0.setAttribute("fill", "#ff0000");
var layer = svg.getElementById("layer");
// Create a circle inside layer "g"
var cc = document.createElementNS(svgns, "circle");
cc.setAttribute("cx", "50");
cc.setAttribute("cy", "50");
cc.setAttribute("r", "20");
layer.appendChild(cc);
// However it's not updating the screen
// even if DOM shows the circle is there, inside the "g"
}
</script></head>
<body>
<object id="test" data="test.svg" type="image/svg+xml"></object>
<script>
document.onreadystatechange = function(){
if(document.readyState === 'complete') addCircle(); }
</script>
</body>
</html>
What am I doing wrong? Or what am I missing? Thank you!
Its the issue with your svg namespace that you are passing to create the circle. Try this
var cc = document.createElementNS("http://www.w3.org/2000/svg", "circle");
You are getting the namespaceURI of the <object> element.
What you need is the one of the inner document's documentElement :
function addCircle() {
// this is the <object>
var svgDoc = document.getElementById("test");
var svg = svgDoc.contentDocument;
// here get the real svg document
var svgns = svg.documentElement.namespaceURI;
//...
As a plunker since stacksnippets® won't allow the modification of inner frames...
Svg looks like:
<svg width=xxx height=xxx>
<g width=xxx height=xxx>
</g>
</svg>
My original resize script:
var desiredWidth1=$('svg').attr("width");
var scaleVal1=$(window).width()/desiredWidth1;
var desiredWidth2=$('svg').attr("height");
var scaleVal2=$(window).height()/desiredWidth2;
var originalTrans = $('svg').attr('transform');
if(scaleVal1>scaleVal2){
$('svg').css("-webkit-transform","scale("+scaleVal2+")");
$('svg').attr("transform", 'translate('+80*scaleVal2+',0)');
}
else{
$('svg').css("-webkit-transform","scale("+scaleVal1+")");
$('svg').attr("transform", 'translate('+80*scaleVal1+',0)');
}
It only resizes the svg once the page loaded, and it is not dynamically resizing.
Therefore my new jquery on window resize here:
$(window).on("resize","g",function(){
var desiredWidth1=$("svg").attr("width");
var scaleVal1=$(window).width()/desiredWidth1;
var desiredWidth2=$("svg").attr("height");
var scaleVal2=$(window).height()/desiredWidth2;
if(scaleVal1>scaleVal2){
$("g").css("-webkit-transform","scale("+scaleVal2+")");
$("g").attr("transform", 'translate('+80*scaleVal2+',0)');
}
else{
$("g").css("-webkit-transform","scale("+scaleVal1+")");
$("g").attr("transform", 'translate('+80*scaleVal1+',0)');
}
});
This is my resize jquery. I want to resize the element 'g' based on client window size.
However this jquery is not working properly. There is no warning or error in console, and it seems to be some problems in DOM and cannot change the element g.
Any information on my code or better scenarios would be helpful. Thanks.
If you set the viewBox, width and height attributes of the <svg> to the right values, the browser will scale everything for you.
var svg = $("#mysvg").get(0);
var w = svg.width.baseVal.value;
var h = svg.height.baseVal.value;
svg.setAttribute('viewBox', '0 0 '+w+' '+h);
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
Demo here
If you need the width and height to be something specific (rather than "100%"), just modify those last two lines. No need to go in and modify the <g> element.
PS. Note that you can't trust jQuery to modify the attributes of the SVG correctly. It is designed for HTML, not SVG and doesn't always do the right thing. It is usually better to use vanilla Javascript as I have done here.
window.onload = function(){//First open resize
var desiredWidth1=$('svg').attr("width");
var scaleVal1=$(window).width()/desiredWidth1;
var desiredWidth2=$('svg').attr("height");
var scaleVal2=$(window).height()/desiredWidth2;
var originalTrans = $('svg').attr('transform');
$('svg').css("transform-origin","left");
if(scaleVal1>scaleVal2){
$('svg').css("-webkit-transform","scale("+scaleVal2+")");
$('svg').css("-ms-transform","scale("+scaleVal1+")");
$('svg').attr("transform", 'translate('+80*scaleVal2+',0)');
}
else{
$('svg').css("-webkit-transform","scale("+scaleVal1+")");
$('svg').css("-ms-transform","scale("+scaleVal1+")");
$('svg').attr("transform", 'translate('+80*scaleVal1+',0)');
}
}
window.onresize = function() {//Dynamically resize the svg to fit the window
var desiredWidth1=$("svg").attr("width");
var scaleVal1=$(window).width()/desiredWidth1;
var desiredWidth2=$("svg").attr("height");
var scaleVal2=$(window).height()/desiredWidth2;
if(scaleVal1>scaleVal2){
$("svg").css("-webkit-transform","scale("+scaleVal2+")")
$("svg").css("-ms-transform","scale("+scaleVal2+")")
$("svg").attr("transform", 'translate('+80*scaleVal2+',0)');
}
else{
$("svg").css("-webkit-transform","scale("+scaleVal1+")");
$("svg").css("-ms-transform","scale("+scaleVal1+")");
$("svg").attr("transform", 'translate('+80*scaleVal1+',0)');
}
}
Finally work this out based on my original codes. As BigBadaboom mentioned and thank you very much, jQuery is usually not working for elements inside the SVG, such as g, path, node etc. However, it just works fine for the whole SVG element.
I have a script that adds elements to an inline SVG image via jQuery, but the new elements don't seem to be showing up. The output is perfectly valid; I can copy it into the original file, reload it, and it will render just fine. But when the script generates it, the new elements aren't visible.
Here is a snippet that replicates the problem: http://tinkerbin.com/7OmDWlsz
Thanks in advance!
You will not see any svg output if the elements are not in the svg namespace.
Try replacing your script snippet with this:
var slices = 10;
for(i = 0; i < 360; i += 360 / slices) {
var element = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
element.setAttribute("points", "0,0 -10,100 10,100");
element.setAttribute("transform", "rotate(" + i + ")");
$('#rotate').append(element);
}
Thank you to much #Erik Dahlström, I may add a contribution to this. Here is the function I define in order to build svg trees :
Node.prototype.svg_grow = function(node_name, node_attr) {
n = document.createElementNS("http://www.w3.org/2000/svg", node_name);
this.appendChild(n);
if (typeof node_attr !== 'undefined') {
for (key in node_attr) {
n.setAttribute(key, node_attr[key]);
}
}
return n;
}
So you can just use svg_grow() on any node like this :
s_g = document.getElementById("parent");
s_rect = s_g.svg_grow('rect', {x:0, y:0, width:480, height:640});
which just do :
<g id="parent">
<rect x="0" y="0" width="480" height="640" />
</g>
I'm currently working with SVG. I need to know the string length in pixels in order to do some alignment. How can I do to get the length of a string in pixel ?
Update: Thanks to nrabinowitz. Based on his help, I can now get the length of dynamic-added text. Here is an example:
<svg id="main"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
width="1020"
height="620"
viewBox="0 0 1020 620"
onload="startup(evt)">
<script>
<![CDATA[
var startup = function (evt) {
var width;
var svgNS = "http://www.w3.org/2000/svg";
var txtNode = document.createTextNode("Hello");
text = document.createElementNS(svgNS,"text");
text.setAttributeNS(null,"x",100);
text.setAttributeNS(null,"y",100);
text.setAttributeNS(null,"fill","black");
text.appendChild(txtNode);
width = text.getComputedTextLength();
alert(" Width before appendChild: "+ width);
document.getElementById("main").appendChild(text);
width = text.getComputedTextLength();
alert(" Width after appendChild: "+ width)
document.getElementById("main").removeChild(text);
}
//]]>
</script>
</svg>
I've been wondering this too, and I was pleasantly surprised to find that, according to the SVG spec, there is a specific function to return this info: getComputedTextLength()
// access the text element you want to measure
var el = document.getElementsByTagName('text')[3];
el.getComputedTextLength(); // returns a pixel integer
Working fiddle (only tested in Chrome): http://jsfiddle.net/jyams/
Having read various similar threads with interest and benefitted from some of the ideas, I've created a page which compares three of the Javascript methods side-by-side. I've noted results in
IE9
Firefox 29.0.1 and
Chrome 34.0.1847.131 m
You can load it in your browser and see what works for you:
http://bl.ocks.org/MSCAU/58bba77cdcae42fc2f44.
It would be incredibly useful to be able to temporarily convert a regular element into a canvas. For example, say I have a styled div that I want to flip. I want to dynamically create a canvas, "render" the HTMLElement into the canvas, hide the original element and animate the canvas.
Can it be done?
There is a library that try to do what you say.
See this examples and get the code
http://hertzen.com/experiments/jsfeedback/
http://html2canvas.hertzen.com/
Reads the DOM, from the html and render it to a canvas, fail on some, but in general works.
Take a look at this tutorial on MDN: https://developer.mozilla.org/en/HTML/Canvas/Drawing_DOM_objects_into_a_canvas (archived)
Its key trick was:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var data = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">' +
'<foreignObject width="100%" height="100%">' +
'<div xmlns="http://www.w3.org/1999/xhtml" style="font-size:40px">' +
'<em>I</em> like ' +
'<span style="color:white; text-shadow:0 0 2px blue;">' +
'cheese</span>' +
'</div>' +
'</foreignObject>' +
'</svg>';
var DOMURL = window.URL || window.webkitURL || window;
var img = new Image();
var svg = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
var url = DOMURL.createObjectURL(svg);
img.onload = function () {
ctx.drawImage(img, 0, 0);
DOMURL.revokeObjectURL(url);
}
img.src = url;
That is, it used a temporary SVG image to include the HTML content as a "foreign element", then renders said SVG image into a canvas element. There are significant restrictions on what you can include in an SVG image in this way, however. (See the "Security" section for details — basically it's a lot more limited than an iframe or AJAX due to privacy and cross-domain concerns.)
Sorry, the browser won't render HTML into a canvas.
It would be a potential security risk if you could, as HTML can include content (in particular images and iframes) from third-party sites. If canvas could turn HTML content into an image and then you read the image data, you could potentially extract privileged content from other sites.
To get a canvas from HTML, you'd have to basically write your own HTML renderer from scratch using drawImage and fillText, which is a potentially huge task. There's one such attempt here but it's a bit dodgy and a long way from complete. (It even attempts to parse the HTML/CSS from scratch, which I think is crazy! It'd be easier to start from a real DOM node with styles applied, and read the styling using getComputedStyle and relative positions of parts of it using offsetTop et al.)
You can use dom-to-image library (I'm the maintainer).
Here's how you could approach your problem:
var parent = document.getElementById('my-node-parent');
var node = document.getElementById('my-node');
var canvas = document.createElement('canvas');
canvas.width = node.scrollWidth;
canvas.height = node.scrollHeight;
domtoimage.toPng(node).then(function (pngDataUrl) {
var img = new Image();
img.onload = function () {
var context = canvas.getContext('2d');
context.translate(canvas.width, 0);
context.scale(-1, 1);
context.drawImage(img, 0, 0);
parent.removeChild(node);
parent.appendChild(canvas);
};
img.src = pngDataUrl;
});
And here is jsfiddle
Building on top of the Mozdev post that natevw references I've started a small project to render HTML to canvas in Firefox, Chrome & Safari. So for example you can simply do:
rasterizeHTML.drawHTML('<span class="color: green">This is HTML</span>'
+ '<img src="local_img.png"/>', canvas);
Source code and a more extensive example is here.
No such thing, sorry.
Though the spec states:
A future version of the 2D context API may provide a way to render fragments of documents, rendered using CSS, straight to the canvas.
Which may be as close as you'll get.
A lot of people want a ctx.drawArbitraryHTML/Element kind of deal but there's nothing built in like that.
The only exception is Mozilla's exclusive drawWindow, which draws a snapshot of the contents of a DOM window into the canvas. This feature is only available for code running with Chrome ("local only") privileges. It is not allowed in normal HTML pages. So you can use it for writing FireFox extensions like this one does but that's it.
You could spare yourself the transformations, you could use CSS3 Transitions to flip <div>'s and <ol>'s and any HTML tag you want. Here are some demos with source code explain to see and learn: http://www.webdesignerwall.com/trends/47-amazing-css3-animation-demos/
the next code can be used in 2 modes, mode 1 save the html code to a image, mode 2 save the html code to a canvas.
this code work with the library: https://github.com/tsayen/dom-to-image
*the "id_div" is the id of the element html that you want to transform.
**the "canvas_out" is the id of the div that will contain the canvas
so try this code.
:
function Guardardiv(id_div){
var mode = 2 // default 1 (save to image), mode 2 = save to canvas
console.log("Process start");
var node = document.getElementById(id_div);
// get the div that will contain the canvas
var canvas_out = document.getElementById('canvas_out');
var canvas = document.createElement('canvas');
canvas.width = node.scrollWidth;
canvas.height = node.scrollHeight;
domtoimage.toPng(node).then(function (pngDataUrl) {
var img = new Image();
img.onload = function () {
var context = canvas.getContext('2d');
context.drawImage(img, 0, 0);
};
if (mode == 1){ // save to image
downloadURI(pngDataUrl, "salida.png");
}else if (mode == 2){ // save to canvas
img.src = pngDataUrl;
canvas_out.appendChild(img);
}
console.log("Process finish");
});
}
so, if you want to save to image just add this function:
function downloadURI(uri, name) {
var link = document.createElement("a");
link.download = name;
link.href = uri;
document.body.appendChild(link);
link.click();
}
Example of use:
<html>
<head>
</script src="/dom-to-image.js"></script>
</head>
<body>
<div id="container">
All content that want to transform
</div>
<button onclick="Guardardiv('container');">Convert<button>
<!-- if use mode 2 -->
<div id="canvas_out"></div>
</html>
Comment if that work.
Comenten si les sirvio :)
The easiest solution to animate the DOM elements is using CSS transitions/animations but I think you already know that and you try to use canvas to do stuff CSS doesn't let you to do. What about CSS custom filters? you can transform your elements in any imaginable way if you know how to write shaders. Some other link and don't forget to check the CSS filter lab.
Note: As you can probably imagine browser support is bad.
function convert() {
dom = document.getElementById('divname');
var script,
$this = this,
options = this.options,
runH2c = function(){
try {
var canvas = window.html2canvas([ document.getElementById('divname') ], {
onrendered: function( canvas ) {
window.open(canvas.toDataURL());
}
});
} catch( e ) {
$this.h2cDone = true;
log("Error in html2canvas: " + e.message);
}
};
if ( window.html2canvas === undefined && script === undefined ) {
} else {.
// html2canvas already loaded, just run it then
runH2c();
}
}