Render a single SVG element to canvas or image - javascript

How can I render a single SVG element (as opposed to the whole SVG document or parts of it) to an image or a canvas?
I have an SVG document with a lot of nodes. If I render the whole SVG or a part of it, the resulting image will contain other pieces of graphics I'm not interested in. I am interested in one specific SVG element and would like to render all of it without anything else.
Example:
<!DOCTYPE html>
<html>
<body>
<svg height="150" width="150">
<circle cx="50" cy="50" r="25" stroke="green" stroke-width="3" fill="gray" />
<circle id="IWantToRenderThis" cx="75" cy="75" r="25" stroke="red" stroke-width="3" fill="white" />
<circle cx="100" cy="100" r="25" stroke="blue" stroke-width="3" fill="black" />
</svg>
<script>
const target = document.getElementById("IWantToRenderThis");
// how can I render *target* into a texture?
</script>
</body>
</html>
How can I render target alone? I would like to obtain an image which has no traces of the other two circles, a transparent background, and the right size to fit target perfectly.

You could remove all elements but the one you want to render.
To fit the SVG to the remaining element, you need to calculate a new position and size. For your example the code could look like this:
<!DOCTYPE html>
<html>
<body>
<svg height="150" width="150" id="svg">
<circle cx="50" cy="50" r="25" stroke="green" stroke-width="3" fill="gray" />
<circle id="IWantToRenderThis" cx="75" cy="75" r="25" stroke="red" stroke-width="3" fill="white" />
<circle cx="100" cy="100" r="25" stroke="blue" stroke-width="3" fill="black" />
</svg>
<script>
const svg = document.getElementById("svg");
const target = document.getElementById("IWantToRenderThis");
const children = svg.children;
// Remove all child elements but the target
for(let index = 0; index < children.length; index++) {
const child = children[index];
if(child.id !== 'IWantToRenderThis') {
child.remove()
}
}
// Recalculate element position and svg size
const targetSize = parseInt(target.getAttribute('r'))
const targetStroke = parseInt(target.getAttribute('stroke-width'))
target.setAttribute('cx', targetSize + (targetStroke/2))
target.setAttribute('cy', targetSize + (targetStroke/2))
svg.setAttribute('width', targetSize*2 + targetStroke)
svg.setAttribute('height', targetSize*2 + targetStroke)
</script>
</body>
</html>
Note that you have to include the stroke width of your element to properly calculate its new position and the SVG's new size.

Here the target element is just copied using outerHTML into a new data URL representing the new SVG, loaded into an image object, drawn in a canvas and exported as a PNG image.
let img = document.getElementById('img');
let target = document.getElementById("IWantToRenderThis");
let svg = target.closest('svg');
let width = svg.attributes['width'].value;
let height = svg.attributes['height'].value;
let image = new Image();
let canvas = document.getElementById('canvas');
canvas.height = height;
canvas.width = width;
let ctx = canvas.getContext('2d');
image.addEventListener('load', e => {
ctx.drawImage(e.target, 0, 0, e.target.width, e.target.height);
img.src = canvas.toDataURL("image/png");
});
image.src = `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"
width="${width}" height="${height}">${target.outerHTML}</svg>`;
<p>Original SVG:</p>
<svg id="svg" height="150" width="150">
<circle cx="50" cy="50" r="25" stroke="green" stroke-width="3" fill="gray" />
<circle id="IWantToRenderThis" cx="75" cy="75" r="25" stroke="red" stroke-width="3" fill="white" />
<circle cx="100" cy="100" r="25" stroke="blue" stroke-width="3" fill="black" />
</svg>
<p>SVG rendered in canvas:</p>
<canvas id="canvas"></canvas>
<p>PNG image based on canvas:</p>
<img id="img" />

Related

Using SVG with IDs for subcomponents

I am trying (in javascript) to access parts of a list of SVG components in a little test page, but I am not sure I can achieve what I want this way. The main question is:
Can I have sub-components having the same id in the two SVG top components?
In the code hereafter I want to change the color inside the first disk and the first rectangle. Here is what I tried, but it is not working.
Any tip would be appreciated.
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name=viewport content='width=device-width, initial-scale=1'>
<title>SVG-ID-Test</title>
</head>
<body>
<svg id="theSVGOne" width="200" height="300" fill="#d55">
<circle id="theCircle" cx="100" cy="75" r="50"
stroke="firebrick" fill="#ddd" stroke-width="3" />
<rect id="theRectangle" x="30" y="140" width="110" height="30"
stroke="black" fill="#ddd" stroke-width="3" />
</svg>
<svg id="theSVGTwo" width="200" height="200">
<circle id="theCircle" cx="100" cy="75" r="50"
stroke="firebrick" fill="#ddd" stroke-width="3" />
<rect id="theRectangle" x="30" y="140" width="110" height="30"
stroke="black" fill="#ddd" stroke-width="3" />
</svg>
<div id="status"></div>
<script type='text/javascript'>
let svgOne = document.getElementById('theSVGOne')
let svgTwo = document.getElementById('theSVGTwo')
let statusPane = document.getElementById('status')
statusPane.innerHTML = 'svgOne => '+svgOne.childElementCount.toString()
let circOne = svgOne.firstChild
let rctOne = svgOne.lastChild
circOne.setAttribute(('fill', '#ec3'))
rctOne.setAttribute(('fill', '#e3c'))
</script>
</body>
You can use a class instead of id on the child elements (you can have as many duplicate class attributes as you want in the document) and then use querySelector on each svg to target specific children.
let svgOne = document.getElementById('theSVGOne')
let svgTwo = document.getElementById('theSVGTwo')
let statusPane = document.getElementById('status')
statusPane.innerHTML = 'svgOne => '+svgOne.childElementCount.toString()
let circOne = svgOne.querySelector(".theCircle")
let rctOne = svgOne.querySelector(".theRectangle")
circOne.setAttribute('fill', '#ec3')
rctOne.setAttribute('fill', '#e3c')
let circTwo = svgTwo.querySelector(".theCircle")
let rctTwo = svgTwo.querySelector(".theRectangle")
circTwo.setAttribute('fill', 'blue')
rctTwo.setAttribute('fill', 'green')
<svg id="theSVGOne" width="200" height="300" fill="#d55">
<circle class="theCircle" cx="100" cy="75" r="50" stroke="firebrick" fill="#ddd" stroke-width="3" />
<rect class="theRectangle" x="30" y="140" width="110" height="30" stroke="black" fill="#ddd" stroke-width="3" />
</svg>
<svg id="theSVGTwo" width="200" height="200">
<circle class="theCircle" cx="100" cy="75" r="50" stroke="firebrick" fill="#ddd" stroke-width="3" />
<rect class="theRectangle" x="30" y="140" width="110" height="30" stroke="black" fill="#ddd" stroke-width="3" />
</svg>
<div id="status"></div>
Don't use IDs
While technically you can have duplicate ids in the DOM (and make it work), this is very bad practise. Id stands for identifier and should be unique to be able to "tell them apart". You can't do that with identical ids (which theCircle are we talking about?).
Problem is, html won't complain about duplicate ids (no errors are thrown).
To actually get feedback on right or wrong markup you can go to w3's validator and check your html there.
Eg. Error: Duplicate ID theCircle.
Working (but wrong) example using id:
let svgOne = document.getElementById('theSVGOne')
let svgTwo = document.getElementById('theSVGTwo')
let statusPane = document.getElementById('status')
statusPane.innerHTML = 'svgOne => '+svgOne.childElementCount.toString()
let circOne = svgOne.getElementById("theCircle")
let rctOne = svgOne.getElementById("theRectangle")
circOne.setAttribute('fill', '#ec3')
rctOne.setAttribute('fill', '#e3c')
let circTwo = svgTwo.getElementById("theCircle")
let rctTwo = svgTwo.getElementById("theRectangle")
circTwo.setAttribute('fill', 'pink')
rctTwo.setAttribute('fill', 'teal')
<svg id="theSVGOne" width="200" height="300" fill="#d55">
<circle id="theCircle" cx="100" cy="75" r="50" stroke="firebrick" fill="#ddd" stroke-width="3" />
<rect id="theRectangle" x="30" y="140" width="110" height="30" stroke="black" fill="#ddd" stroke-width="3" />
</svg>
<svg id="theSVGTwo" width="200" height="200">
<circle id="theCircle" cx="100" cy="75" r="50" stroke="firebrick" fill="#ddd" stroke-width="3" />
<rect id="theRectangle" x="30" y="140" width="110" height="30" stroke="black" fill="#ddd" stroke-width="3" />
</svg>
<div id="status"></div>

SVG connect two points with a line, and automatically update the line if a point is moved

I'd like to connect two points (circles) with a line:
window.onclick = () => {
document.getElementById('c2').setAttribute("cx", 150);
}
<svg>
<circle cx="10" cy="10" r="2" id="c1" />
<circle cx="90" cy="50" r="2" id="c2" />
<line x1="10" y1="10" x2="90" y2="50" stroke="black" />
</svg><br>
Click here to move a circle.
such that if I modify the center of any <circle> with setAttribute("cx", 150) then the line automatically follows the new circle position.
Is there a way to do this with <use>? Something like (pseudo-code):
<svg>
<circle cx="10" cy="10" r="2" id="c1" />
<circle cx="90" cy="50" r="2" id="c2" />
<use x1=xlink:c1:cx y1=xlink:c1:cy x2=xlink:c2:cx y2=xlink:c2:cy stroke="black" type="line" />
</svg>
Goal: I don't want to have to set the coordinates two times, in both the circle and line. Instead I would like to set the coordinates once, and that the line uses a reference to the circle elements.
Note: I have read SVG connect two points with a line but it did not help.
You can use a <marker> that can be placed on start, middle and end of an element.
window.onclick = () => {
document.getElementById('l1').setAttribute("x2", 150);
}
<svg viewBox="0 0 200 100" width="200">
<defs>
<marker id="circle" viewBox="0 0 4 4" refX="2"
refY="2" markerWidth="4" markerHeight="4">
<circle cx="2" cy="2" r="2" />
</marker>
</defs>
<line id="l1" x1="10" y1="10" x2="90" y2="50" stroke="black"
marker-start="url(#circle)" marker-end="url(#circle)"/>
</svg><br>
Click here to move a circle.

Captain America with svg

How I can do this star right with svg? I must use svg and I try with points but not work. I mustn't use element path. Thanks.
<!DOCTYPE html>
<html>
<body>
<svg width="100" height="100">
<circle cx="50" cy="50" r="45" fill="white" stroke="red" stroke-width="10" />
<circle cx="50" cy="50" r="30" stroke="red" stroke-width="10" fill="blue" />
<polygon points="50,25 30,80 75,40 25,40 70,70" style="fill:white;"/>
</svg>
</body>
</html>
If you want to get the most accurate points for the pentastar, you can get them easily from the underlying pentagon.
A simple js function to obtain these points is something like this (REPL, which by the way you can use for polygons with any n edges):
var n = 5;
var points = [];
for (var i=0; i < n; i++) {
var x = 50;
var y = -50;
var r = 25;
points.push([x + r * Math.sin(2 * Math.PI * i / n),
y + r * Math.cos(2 * Math.PI * i / n)]);
}
Result is the pentagon points clock-wise, starting at the top (use all values as positive ones):
[ [ 50, -25 ],
[ 73.77641290737884, -42.27457514062631 ],
[ 64.69463130731182, -70.22542485937369 ],
[ 35.30536869268818, -70.22542485937369 ],
[ 26.22358709262116, -42.27457514062632 ] ]
Your order would be points[x], where x = 0, 3, 1, 4, 2.
And using them for your example rounded to the nearest pixel:
<!DOCTYPE html>
<html>
<body>
<svg width="100" height="100">
<circle cx="50" cy="50" r="45" fill="white" stroke="red" stroke-width="10" />
<circle cx="50" cy="50" r="30" stroke="red" stroke-width="10" fill="blue" />
<polygon points="50,25 35,70 73,42 26,42 65,70" style="fill:white;"/>
</svg>
</body>
</html>
This seems a bit closer.
<!DOCTYPE html>
<html>
<body>
<svg width="100" height="100">
<circle cx="50" cy="50" r="45" fill="white" stroke="red" stroke-width="10" />
<circle cx="50" cy="50" r="30" stroke="red" stroke-width="10" fill="blue" />
<polygon points="50,25 35,70 73,42 26,42 65,70" style="fill:white;"/>
</svg>
</body>
</html>
updated to use #frhd's number, make community wiki,
please see his answer for calculations
Here with path
<svg width="100" height="100">
<circle cx="50" cy="50" r="45" fill="white" stroke="red" stroke-width="10" />
<circle cx="50" cy="50" r="30" stroke="red" stroke-width="10" fill="blue" />
<!-- <polygon points="50,25 30,80 75,40 25,40 70,70" style="fill:white;"/> -->
<path fill="#fff" d="m50,25 5,17h18l-14,11 5,17-15-10-15,10 5-17-14-11h18z" />
</svg>

How to apply canvg() function for particular div?

I am using canvg() function to convert svg into canvas.
If we use canvg() directly on onload it will convert all svg to canvas.
I wanted to convert svg related to particular div.
Html
<div id="notapply">
<svg><text x="50" y="50">Not to Apply!</text></svg>
</div>
<div id="apply">
<svg width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>
</div>
Script
canvg();
Here it should convert svg related to div which is having id=apply.
Fiddle demo here
I have found an answer on your question in the source code of canvg itself: canvg
You need to change query selector to select SVG from your div:
// Your selector here
var svgTags = document.querySelectorAll('#apply svg');
// Process SVG tags
for (var i=0; i<svgTags.length; i++) {
var svgTag = svgTags[i];
var c = document.createElement('canvas');
c.width = svgTag.clientWidth;
c.height = svgTag.clientHeight;
svgTag.parentNode.insertBefore(c, svgTag);
svgTag.parentNode.removeChild(svgTag);
var div = document.createElement('div');
div.appendChild(svgTag);
canvg(c, div.innerHTML);
}
Here is your example modified: http://jsfiddle.net/ischenkodv/L3hondLn/135/
You can use XMLSerializer to serialize the SVG you want to send to canvg.
Something like this perhaps...
canvg("canvas", (new XMLSerializer).serializeToString(document.getElementById("apply").firstElementChild));
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.5/canvg.min.js"></script>
<div id="notapply">
<svg><text x="50" y="50">Not to Apply!</text></svg>
</div>
<div id="apply">
<svg width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>
</div>
<canvas id="canvas" width="500" height="500"></canvas>

"ellipse" element created through JS doesn't appear in html

I'm new to svg. What i'm trying to do is to create an ellipse element with JS and append it to the SVG tag. The HTML code is
<svg width="640" height="480">
<ellipse cx="200" cy="100" rx="90" ry="60" stroke-width="10" stroke="orange" fill="none" opacity="0.6"/>
<ellipse cx="200" cy="100" rx="70" ry="40" stroke-width="10" stroke="green" fill="none" opacity="0.6"/>
</svg>
Below is the JS code
<script type="text/javascript">
var el=document.createElement('ellipse');
$(el).attr("cx",300);
$(el).attr("cy",200);
$(el).attr("stroke","red");
$(el).attr("stroke-width","10");
$(el).attr("fill","green");
$(el).attr("rx",120);
$(el).attr("ry",80);
$("svg").append(el);
</script>
But the ellipse didn't appear on the viewport ,but when i inspect HTML i found the ellipse element that i created is appended to SVG. what makes the difference when created this way & what is the correct approach to dynamically add elements to SVG
FYI see the image below
What you have is a custom HTML element instead of an SVG element. Use createElementNS to create the SVG element:
$(function(){
var el = document.createElementNS("http://www.w3.org/2000/svg", 'ellipse');
el.setAttribute('cx', 300);
el.setAttribute('cy', 200);
el.setAttribute('stroke', "red");
el.setAttribute('stroke-width', 10);
el.setAttribute('fill', "green");
el.setAttribute('rx', 120);
el.setAttribute('ry', 80);
$("svg").append(el);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<svg width="640" height="480">
<ellipse cx="200" cy="100" rx="90" ry="60" stroke-width="10" stroke="orange" fill="none" opacity="0.6"/>
<ellipse cx="200" cy="100" rx="70" ry="40" stroke-width="10" stroke="green" fill="none" opacity="0.6"/>
</svg>

Categories

Resources