regex: extract SVG path d attribute - javascript

I have a source containing one SVG path:
<svg xmlns="http://www.w3.org/2000/svg" stroke="red" fill="grey"><path d="M 10 10 L 100 100 Q 200 50 300 100 A 80 50 20 1 0 400 600 Z" fill="none" stroke="red" stroke-width="5" /></svg>
it also can be single quoted
<svg xmlns="http://www.w3.org/2000/svg" stroke="red" fill="grey"><path d='M 10 10 L 100 100 Q 200 50 300 100 A 80 50 20 1 0 400 600 Z' fill="none" stroke="red" stroke-width="5" />
I want the part between <path=start quote and finish quote.
M 10 10 L 100 100 Q 200 50 300 100 A 80 50 20 1 0 400 600 Z
I have tried (JS)
var result = svg_tag.match(/<path(.*?)>/g).map(function(val){
return val.replace(/<path d="/g,'');
});
But it returns
[
'M', '10',
'10', 'L',
'100', '100',
'Q', '200',
'50', '300',
'100', 'A',
'80', '50',
'20', '1',
'0', '400',
'600', 'Z"',
'fill="none"', 'stroke="red"',
'stroke-width="5"', '/>'
]

[ \w]{10,}
This RegExp matches any substring at least 10 characters long containing only letters, numbers, spaces, and underscores.
const regex = /[ \w]{10,}/;
const tag1 = '<svg xmlns="http://www.w3.org/2000/svg" stroke="red" fill="grey"><path d="M 10 10 L 100 100 Q 200 50 300 100 A 80 50 20 1 0 400 600 Z" fill="none" stroke="red" stroke-width="5" /></svg>';
const tag2 = `<svg xmlns="http://www.w3.org/2000/svg" stroke="red" fill="grey"><path d='M 10 10 L 100 100 Q 200 50 300 100 A 80 50 20 1 0 400 600 Z' fill="none" stroke="red" stroke-width="5" />`;
console.log(tag1.match(regex)[0]);
console.log(tag2.match(regex)[0]);

Related

SVG: how to draw multiple semicircles (arcs) path

Using the answer from this thread I was able to draw a semicircle (arc):
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
function describeArc(x, y, radius, startAngle, endAngle) {
var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);
var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
].join(" ");
console.log(d)
return d;
}
window.onload = function() {
document.getElementById("arc1").setAttribute("d", describeArc(100, 100, 50, -90, 90));
};
<svg width="1000" height="1000">
<path id="arc1" fill="red" stroke="#446688" stroke-width="2" />
</svg>
What I'm trying to achieve is to be able to draw an SVG as a path consistent with many arcs (semicircles) and be able to set fill on them.
Something like this:
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M 50 100 A 10 10 0 0 1 100 100 M 100 100 A 10 10 0 0 1 150 100 M 150 100 A 10 10 0 0 1 200 100 M 200 100 A 10 10 0 0 1 250 100" fill="red" stroke="blue" stroke-width="3" />
</svg>
Is there a better way to achieve a simpler path? For now, it looks like this:
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M 50 100 A 10 10 0 0 1 100 100 M 100 100 A 10 10 0 0 1 150 100 M 150 100 A 10 10 0 0 1 200 100 M 200 100 A 10 10 0 0 1 250 100" fill="red" stroke="blue" stroke-width="3" />
</svg>
Or do I have to generate a longer and longer path when there are, let's say, 30 semicircles?
Edit: the IE9+ support is required. Also, those elements will be clickable, draggable and controllable. By controllable I mean that their number and size will change when mouse clicking/moving.
I choose my first approach with a dynamic very long path.
Thanks!
For this I would use lower case commands. For example this is drawing the arc you need: an arc with a radius of 25 and an ending point 50 units ( 2 * 25 ) away from the starting point of the arc.
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M 50 100 a 25 25 0 0 1 50 0" fill="red" stroke="blue" stroke-width="3" />
</svg>
In order to get a path of 4 arcs you need to repeat the arc (a 25 25 0 0 1 50 0) 4 times something like this:
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M 50 100 a 25 25 0 0 1 50 0
a 25 25 0 0 1 50 0
a 25 25 0 0 1 50 0
a 25 25 0 0 1 50 0 " fill="red" stroke="blue" stroke-width="3" />
</svg>
It's easy to see how you can use javascript to generate the d attribute you need:
let d ="M 50 100";
for(let i=0; i<4;i++){d +="a 25 25 0 0 1 50 0 "}
document.querySelector("path").setAttribute("d",d);
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M 50 100" fill="red" stroke="blue" stroke-width="3" />
</svg>
You can use a vanilla JavaScript Web Component (supported in all modern Browsers) to create the SVG
Your Custom Element <svg-arcs repeat="7"></svg-arcs> then creates:
<style>
svg { background: pink }
svg path { stroke-width: 3 }
</style>
<svg-arcs repeat="30"></svg-arcs>
<script>
customElements.define("svg-arcs", class extends HTMLElement {
connectedCallback() {
let repeat = this.getAttribute("repeat") || 5;
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
for (let x = 0; x < repeat; x++) {
let path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", `M${3 + 50*x} 100 A 10 10 0 0 1 ${50+50*x} 100`);
path.setAttribute("fill", "red");
path.setAttribute("stroke", "blue");
svg.append(path);
}
svg.setAttribute("viewBox", `0 0 ${50*repeat + 3} 150`);
this.append(svg);
}
})
</script>
For more dynamic control over individual arcs see the Web Component in SO post:
Firefox: shadow-DOM compatibility
You could use a pattern and size your patterned object appropriately. Here is one that accomodates 4 iterations.
Edit & Update:
If you want those arcs to be independently clickable/draggable, then they need to be separately addressable in the DOM. The "use" element might be what you're looking for.
svg {
background: grey;
}
<svg width="800px" height="600px">
<defs>
<path id="arc-template" d="M1.5 50 a 10 10 0 0 1 97 0" fill="red" stroke="blue" stroke-width="3" />
</defs>
<use id="arc1" href="#arc-template" x="50" y="100"/>
<use id="arc2" href="#arc-template" x="150" y="100"/>
<use id="arc3" href="#arc-template" x="250" y="100"/>
<use id="arc4" href="#arc-template" x="350" y="100"/>
</svg>

Add a class to specific elements with pure Javascript

I am evaluating an Svg coloring image. A function evaluates the fill colors set by the user, against a correct answers list, by looping through them with querySelectorAll method and converted (to compare their values) to arrays. Only the paths with the class setColor can be colored.
Question: How can I add the class wrongColor ONLY to the path elements that have the wrong fill color?
<!--Hypothetical SVG that must be colored correctly.-->
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<path id="aSVG" class="setColor" d="M10 10 C 20 20, 40 20, 50 10" stroke="black" fill="green" />
<path id="bSVG" class="setColor" d="M70 10 C 70 20, 120 20, 120 10" stroke="black" fill="red" />
<path d="M130 10 C 120 20, 180 20, 170 10" stroke="black" fill="white" />
<path id="cSVG" class="setColor" d="M10 60 C 20 80, 40 80, 50 60" stroke="black" fill="lightblue" />
<path id="dSVG" class="setColor" d="M70 60 C 70 80, 110 80, 110 60" stroke="black" fill="lightblue" />
<path d="M130 60 C 120 80, 180 80, 170 60" stroke="black" fill="white" />
<path id="eSVG" class="setColor" d="M10 110 C 20 140, 40 140, 50 110" stroke="black" fill="lightblue" />
<path id="fSVG" class="setColor" d="M70 110 C 70 140, 110 140, 110 110" stroke="black" fill="lightblue" />
<path d="M130 110 C 120 140, 180 140, 170 110" stroke="black" fill="white" />
</svg>
<!--Correct answers list. Not visible in user-->
<ol class="answersList">
<li class="t1">red</li>
<li class="t1">green</li>
<li class="t1">lightblue</li>
<li class="t1">lightblue</li>
<li class="t1">lightblue</li>
<li class="t1">lightblue</li>
</ol>
Below is a functional example in order to clarify the functionality of the game.
#Amethystx87 Your code adds class wrongColor to all the paths with the class setColor. You can see it below in the code Snippet. The map property will create error "document.querySelectorAll(...).map is not a function" because querySelectorAll returns a Node List (array like) so you have to convert the Node List to array and then array methods can be used.
I had the same problem too and solved it as in the JavaScript code below with the comments "check users answers","check correct answers list". In the comment "Amethystx87 solution" is the way I made your code to work but not completely there yet.
#DacreDenny I couldn't utilize your solution maybe my fault. I hope the code snippet below can give you a clear view of the problem. You will see in the snippet that the wrong answers (I made the mistake on purpose) are the first two (green,red) but the correct (red, green) are as appeared in the answer list.
/*~~~~~~~~check button~~~~~~~~~~~*/
document.querySelector(".check").addEventListener('click', checkSvgAnswers);
function checkSvgAnswers() {
/*~~~check users answers~~~*/
var selectedFillColor = document.querySelectorAll(".setColor");
var selectedFillColorArray = [];
for (var i = 0; i < selectedFillColor.length; i++) {
var selectedColors = selectedFillColor[i].getAttribute('fill').toUpperCase();
selectedFillColorArray.push(selectedColors);
}
console.log(selectedFillColorArray);
/*~~~check correct answers list~~~~~~~~~~~*/
var correctAnswers = document.querySelectorAll("li.t1");
var correctAnswersArray = [];
for (var i = 0; i < correctAnswers.length; i++) {
var answerList = correctAnswers[i].innerText.toUpperCase();
correctAnswersArray.push(answerList);
}
console.log(correctAnswersArray);
/*~~~~compare answers helper function~~~~~~~~~~*/
var isEqual = function(value, other) {
// Get the value type
var type = Object.prototype.toString.call(value);
// If the two objects are not the same type, return false
if (type !== Object.prototype.toString.call(other)) {
return false;
}
// If items are not an object or array, return false
if (['[object Array]', '[object Object]'].indexOf(type) < 0) {
return false;
}
// Compare the length of the length of the two items
var valueLen = type === '[object Array]' ? value.length : Object.keys(value).length;
var otherLen = type === '[object Array]' ? other.length : Object.keys(other).length;
if (valueLen !== otherLen) {
return false;
}
// Compare two items
var compare = function(item1, item2) {
// Get the object type
var itemType = Object.prototype.toString.call(item1);
// If an object or array, compare recursively
if (['[object Array]', '[object Object]'].indexOf(itemType) >= 0) {
if (!isEqual(item1, item2)) {
return false;
}
}
// Otherwise, do a simple comparison
else {
// If the two items are not the same type, return false
if (itemType !== Object.prototype.toString.call(item2)) {
return false;
}
// Else if it's a function, convert to a string and compare
// Otherwise, just compare
if (itemType === '[object Function]') {
if (item1.toString() !== item2.toString()) {
return false;
}
} else if (item1 !== item2) {
return false;
}
}
};
// Compare properties
if (type === '[object Array]') {
for (var i = 0; i < valueLen; i++) {
if (compare(value[i], other[i]) === false) {
return false;
}
}
} else {
for (var key in value) {
if (value.hasOwnProperty(key)) {
if (compare(value[key], other[key]) === false) {
return false;
}
}
}
}
// If nothing failed, return true
return true;
};
/*~~~~~~~~compare colors~~~~~~~~~~~*/
if (isEqual(selectedFillColorArray, correctAnswersArray)) {
document.querySelector(".coloringResult").innerHTML = "<span style='color:#00B119;'>✔</span>";
} else if (!isEqual(selectedFillColorArray, correctAnswersArray)) {
document.querySelector(".coloringResult").innerHTML = "<span style='color:#D40D0D;'>✗</span>";
}
console.log(isEqual(selectedFillColorArray, correctAnswersArray));
/*~~~~~~~~Amethystx87 solution*/
document.querySelectorAll('.setColor').forEach((element) => {
if (!correctAnswersArray.includes(element.fill)) {
element.classList.add('wrongColor');
}
});
};
<!--Hypothetical color palette. User can drag and drop colors ONLY to svg paths with the class="setColor" to change the default "fill" color values-->
<div class="colorPalette">
<p>green</p>
<p>red</p>
<p>lightblue</p>
</div>
<!--Hypothetical SVG that must be colored correctly.-->
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<path id="aSVG" class="setColor" d="M10 10 C 20 20, 40 20, 50 10" stroke="black" fill="green" />
<path id="bSVG" class="setColor" d="M70 10 C 70 20, 120 20, 120 10" stroke="black" fill="red" />
<path d="M130 10 C 120 20, 180 20, 170 10" stroke="black" fill="white" />
<path id="cSVG" class="setColor" d="M10 60 C 20 80, 40 80, 50 60" stroke="black" fill="lightblue" />
<path id="dSVG" class="setColor" d="M70 60 C 70 80, 110 80, 110 60" stroke="black" fill="lightblue" />
<path d="M130 60 C 120 80, 180 80, 170 60" stroke="black" fill="white" />
<path id="eSVG" class="setColor" d="M10 110 C 20 140, 40 140, 50 110" stroke="black" fill="lightblue" />
<path id="fSVG" class="setColor" d="M70 110 C 70 140, 110 140, 110 110" stroke="black" fill="lightblue" />
<path d="M130 110 C 120 140, 180 140, 170 110" stroke="black" fill="white" />
</svg>
<!--Appears on check button click showing tick for correct, x for wrong.-->
<p class="coloringResult"> </p>
<!--control buttons-->
<!--Check button: compares the users colors placed on the svg, with the correct colors from the answers list. Score container appears and shows user score-->
<button class='check' type='button'>Check</button>
<!--Try again button: keep the correct answers, the wrong answers return to the original "fill" color and the score is changed keeping only the correct colors score-->
<button class='try-again' type='button'>Try again</button>
<!--Reset button: reset exersice to original state. All svg paths to default "fill" color. Score resets to 0 and disappears from screen -->
<button class='clear-answers' type='button'>Reset</button>
<!--Correct answers list. Not visible in user-->
<ol class="answersList">
<li class="t1">red</li>
<li class="t1">green</li>
<li class="t1">lightblue</li>
<li class="t1">lightblue</li>
<li class="t1">lightblue</li>
<li class="t1">lightblue</li>
</ol>
Something like this
// Get all the correct colors and map them to an array
const correctColors = document.querySelectorAll('.t1')
.map(element => return element.textContent);
// Loop through all with class setColor and add wrongColor to the element's classList
document.querySelectorAll('.setColor').forEach((element) => {
if (!correctColors.includes(element.fill)) {
element.classList.add('wrongColor');
}
});
You can query SVG elements, and update the classlist of SVG element in the same way that you would any other element in the DOM.
So for instance, you can query all SVG elements with a fill attribute value of a "wrong fill color" of white by doing this:
const wrongFillColor = "white";
document.querySelectorAll(`svg [fill="${wrongFillColor}"]`)
You can then iterate the resulting node set, and add classes to matching elements like so:
const wrongFillColor = "white";
document.querySelectorAll(`svg [fill="${wrongFillColor}"]`).forEach(function(element) {
element.classList.add("wrongColor")
})
.wrongColor {
fill: yellow;
}
<!--Hypothetical SVG that must be colored correctly.-->
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<path id="aSVG" class="setColor" d="M10 10 C 20 20, 40 20, 50 10" stroke="black" fill="green" />
<path id="bSVG" class="setColor" d="M70 10 C 70 20, 120 20, 120 10" stroke="black" fill="red" />
<path d="M130 10 C 120 20, 180 20, 170 10" stroke="black" fill="white" />
<path id="cSVG" class="setColor" d="M10 60 C 20 80, 40 80, 50 60" stroke="black" fill="lightblue" />
<path id="dSVG" class="setColor" d="M70 60 C 70 80, 110 80, 110 60" stroke="black" fill="lightblue" />
<path d="M130 60 C 120 80, 180 80, 170 60" stroke="black" fill="white" />
<path id="eSVG" class="setColor" d="M10 110 C 20 140, 40 140, 50 110" stroke="black" fill="lightblue" />
<path id="fSVG" class="setColor" d="M70 110 C 70 140, 110 140, 110 110" stroke="black" fill="lightblue" />
<path d="M130 110 C 120 140, 180 140, 170 110" stroke="black" fill="white" />
</svg>

Filling in a Bezier Curve SVG

acknowledgement: reference
[SVG] How to close the path (between the 1st and the last point) and fill the area accordingly:
It's not a single path, so i can't close it with 'Z' at the end
It's not a single path, so I can't close it with 'Z' at the end
Join the paths first.
E.g. the sample from your link is originally
<path xmlns="http://www.w3.org/2000/svg" fill="none" stroke="blue" stroke-width="8" d="M 60 60 C 111.55555555555557 160 163.11111111111114 260 220 300"/>
<path xmlns="http://www.w3.org/2000/svg" fill="none" stroke="red" stroke-width="8" d="M 220 300 C 276.88888888888886 340 339.11111111111114 320 420 300"/>
But if you append the dstring of the second to the first and replace the second M (Move) with L (LineTo), you get this:
<path xmlns="http://www.w3.org/2000/svg" fill="cyan" stroke="red" stroke-width="8" d="M 60 60 C 111.55555555555557 160 163.11111111111114 260 220 300 L 220 300 C 276.88888888888886 340 339.11111111111114 320 420 300 Z"/>
I have also set fill=cyan.
Some of these drawn things are from the linked site and not in the code in my answer.

change of SVG cannot reflected onto PNG

This page displays a SVG having three strokes. Clicking on Save as PNG button will convert the SVG to PNG. This works well.
http://www.holisticedu.us/wechat/three.htm
Clicking on the strokes of the SVG will change the stroke color from back to red. Then clicking on Save as PNG button , the stoke color of the PNG does not change. I have no idea about this. I want that the stroke color of the PNG can also be changed when the stroke color of the SGV was changed.
The following is my code, thank you for any suggestions.
<!DOCTYPE html><html xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><head><meta charset="utf-8"/>
<title>three</title>
<script type="text/javascript">
function changeRectColor(evt) {
evt.target.setAttributeNS(null,"fill","red");
}
</script>
</head><body>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="250" height="250" viewBox="0 0 2052 2052">
<def>
<g id="g1" onclick="changeRectColor(evt)" name="三" transform="translate(514 718) matrix(1 0 0 -1 0 616)">
<path name="1.1" onclick="changeRectColor(evt)" d="M677 619Q699 619 733 602Q756 592 756 577Q756 566 726 559Q691 548 572 530Q390 505 335 505Q284 505 264 517Q244 531 244 541Q244 550 288 552Q390 561 513 583Q599 597 652 613Q671 619 677 619Z"/>
<path name="1.2" onclick="changeRectColor(evt)" d="M645 343Q667 343 701 326Q724 316 724 301Q724 293 700 288Q672 280 575 267Q428 249 383 249Q332 249 312 261Q292 275 292 285Q292 292 328 293Q411 300 512 316Q582 327 625 338Q640 343 645 343Z"/>
<path name="1.3" onclick="changeRectColor(evt)" d="M852 104Q860 104 914 84Q960 59 960 39Q960 21 897 21Q816 23 722 23Q623 23 465 12Q351 7 222 -19Q185 -27 177 -27Q156 -27 115 -9Q72 10 72 21Q72 36 100 38Q244 39 311 51Q438 65 591 73Q735 85 811 99Q844 104 852 104Z"/>
</g>
</def>
<g id="bg" transform="translate(514 718)">
<rect id="sq" style="fill:none;stroke:lightgray;stroke-width:4" x="-512" y="-716" width="0" height="0"/>
<g id="box"><rect style="fill:none;stroke:black;stroke-width:4" x="0" y="-204" width="1024" height="1024"/>
<line style="stroke:black;stroke-width:2;stroke-dasharray:60 60;" x1="0" y1="308" x2="1024" y2="308"/>
<line style="stroke:black;stroke-width:2;stroke-dasharray:60 60;" x1="512" y1="-204" x2="512" y2="820"/>
</g></g>
<use id="u1" xlink:href="#g1" x="0" y="0"/>
</svg>
<button><font size = 4>Save as PNG</font></button>
<canvas id="he_canvas" width="250" height="250"></canvas>
<script type="text/javascript">
var btn = document.querySelector('button');
var svg = document.querySelector('svg');
var canvas = document.querySelector('he_canvas');
function triggerDownload (imgURI) {
var evt = new MouseEvent('click', {
view: window,
bubbles: false,
cancelable: true
});
var a = document.createElement('a');
a.setAttribute('download', 'mypng.png');
a.setAttribute('href', imgURI);
a.setAttribute('target', '_blank');
a.dispatchEvent(evt);
}
btn.addEventListener('click', function () {
var canvas_browser = document.getElementById('he_canvas');
var ctx = canvas_browser.getContext('2d');
var data = (new XMLSerializer()).serializeToString(svg);
var DOMURL = window.URL || window.webkitURL || window;
var img = new Image();
var svgBlob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
var url = DOMURL.createObjectURL(svgBlob);
img.onload = function () {
ctx.drawImage(img, 0, 0);
DOMURL.revokeObjectURL(url);
var imgURI = canvas_browser
.toDataURL('image/png')
.replace('image/png', 'image/octet-stream');
triggerDownload(imgURI);
};
img.src = url;
});
</script>
</body></html>
Important note :
This question is raised by a behavior which is only implemented in Firefox, so the first part of this answer will assume to be seen from Firefox.
This one is an interesting case...
So what you are doing is to handle click events on cloned nodes through an <use> element.
evt.target.setAttributeNS(null,"fill","red");
Here, evt.target will be the cloned node, not the original one, you can check it by cloning twice your elements :
<script>
function changeRectColor(evt) {
evt.target.setAttributeNS(null,"fill","red");
}
</script>
<svg width="500" height="250" viewBox="0 0 4104 2052">
<def>
<g id="g1" onclick="changeRectColor(evt)" name="三" transform="translate(514 718) matrix(1 0 0 -1 0 616)">
<path name="1.1" onclick="changeRectColor(evt)" d="M677 619Q699 619 733 602Q756 592 756 577Q756 566 726 559Q691 548 572 530Q390 505 335 505Q284 505 264 517Q244 531 244 541Q244 550 288 552Q390 561 513 583Q599 597 652 613Q671 619 677 619Z"></path>
<path name="1.2" onclick="changeRectColor(evt)" d="M645 343Q667 343 701 326Q724 316 724 301Q724 293 700 288Q672 280 575 267Q428 249 383 249Q332 249 312 261Q292 275 292 285Q292 292 328 293Q411 300 512 316Q582 327 625 338Q640 343 645 343Z"></path>
<path name="1.3" onclick="changeRectColor(evt)" d="M852 104Q860 104 914 84Q960 59 960 39Q960 21 897 21Q816 23 722 23Q623 23 465 12Q351 7 222 -19Q185 -27 177 -27Q156 -27 115 -9Q72 10 72 21Q72 36 100 38Q244 39 311 51Q438 65 591 73Q735 85 811 99Q844 104 852 104Z"></path>
</g>
</def>
<g id="bg" transform="translate(514 718)">
<rect id="sq" style="fill:none;stroke:lightgray;stroke-width:4" x="-512" y="-716" width="0" height="0"></rect>
<g id="box"><rect style="fill:none;stroke:black;stroke-width:4" x="0" y="-204" width="1024" height="1024"></rect>
<line style="stroke:black;stroke-width:2;stroke-dasharray:60 60;" x1="0" y1="308" x2="1024" y2="308"></line>
<line style="stroke:black;stroke-width:2;stroke-dasharray:60 60;" x1="512" y1="-204" x2="512" y2="820"></line>
</g></g>
<use id="u1" xlink:href="#g1" x="0" y="0"></use>
<use id="u2" xlink:href="#g1" x="1026" y="0"></use>
</svg>
As you can see, only the cloned one gets modified, but this cloned one is not accessible from the DOM (it is only in some shadow-DOM), hence, when you parse your SVG element to create a new image from it, you won't be able to parse the changes made to these cloned nodes.
Solution
Don't use a <use> element... Instead, display directly the original nodes.
Anyway, as said previously, only Firefox does handle click events on cloned nodes inside a <use> element.
var btn = document.querySelector('button');
var svg = document.querySelector('svg');
btn.addEventListener('click', function () {
var canvas_browser = document.createElement('canvas');
var ctx = canvas_browser.getContext('2d');
var data = (new XMLSerializer()).serializeToString(svg);
var DOMURL = window.URL || window.webkitURL || window;
var img = new Image();
var svgBlob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
var url = DOMURL.createObjectURL(svgBlob);
img.onload = function () {
canvas_browser.width = img.width;
canvas_browser.height = img.height;
ctx.drawImage(img, 0, 0);
DOMURL.revokeObjectURL(url);
document.body.appendChild(canvas_browser)
};
img.src = url;
});
<button>download</button><br>
<script>
function changeRectColor(evt) {
evt.target.setAttributeNS(null,"fill","red");
}
</script>
<svg width="250" height="250" viewBox="0 0 2052 2052">
<g id="bg" transform="translate(514 718)">
<rect id="sq" style="fill:none;stroke:lightgray;stroke-width:4" x="-512" y="-716" width="0" height="0"></rect>
<g id="box"><rect style="fill:none;stroke:black;stroke-width:4" x="0" y="-204" width="1024" height="1024"></rect>
<line style="stroke:black;stroke-width:2;stroke-dasharray:60 60;" x1="0" y1="308" x2="1024" y2="308"></line>
<line style="stroke:black;stroke-width:2;stroke-dasharray:60 60;" x1="512" y1="-204" x2="512" y2="820"></line>
</g></g>
<g id="g1" onclick="changeRectColor(evt)" name="三" transform="translate(514 718) matrix(1 0 0 -1 0 616)">
<path name="1.1" onclick="changeRectColor(evt)" d="M677 619Q699 619 733 602Q756 592 756 577Q756 566 726 559Q691 548 572 530Q390 505 335 505Q284 505 264 517Q244 531 244 541Q244 550 288 552Q390 561 513 583Q599 597 652 613Q671 619 677 619Z"></path>
<path name="1.2" onclick="changeRectColor(evt)" d="M645 343Q667 343 701 326Q724 316 724 301Q724 293 700 288Q672 280 575 267Q428 249 383 249Q332 249 312 261Q292 275 292 285Q292 292 328 293Q411 300 512 316Q582 327 625 338Q640 343 645 343Z"></path>
<path name="1.3" onclick="changeRectColor(evt)" d="M852 104Q860 104 914 84Q960 59 960 39Q960 21 897 21Q816 23 722 23Q623 23 465 12Q351 7 222 -19Q185 -27 177 -27Q156 -27 115 -9Q72 10 72 21Q72 36 100 38Q244 39 311 51Q438 65 591 73Q735 85 811 99Q844 104 852 104Z"></path>
</g>
</svg>

Is there a way to merge two path elements (svg) using Javascript?

I have drawn two path lines using SVG and I've saved these elements into two variables in my javascript code: 'Line1', and 'Line2', and I need to merge the two lines into one single path element. Is there a way to do so?
Are your paths defined relatively (small letters) or absolutely (capitals)? If absolute, joining two paths is trivial, just append the values of the d attribute. If you have two paths like this:
<path id="Line1" d="M50,50
A30,30 0 0,1 35,20
L100,100"
style="stroke:#660000; fill:none;"/>
<path id="Line2" d="M110,110
L100,0"
style="stroke:#660000; fill:none;"/>
Then this JavaScript code:
var Line1 = document.getElementById("Line1");
var Line2 = document.getElementById("Line2");
//Add paths together
Line1.setAttribute('d', Line1.getAttribute('d') + ' ' + Line2.getAttribute('d'));
//Remove unnecessary second path
Line2.parentNode.removeChild(Line2);
Will lead to you having a single path like this:
<path id="Line1" d="M50,50
A30,30 0 0,1 35,20
L100,100 M110,110
L100,0"
style="stroke:#660000; fill:none;"/>
Here's a jsFiddle, it works in Firefox 4 (needs an HTML5 parser so you can have inline SVG).
If your paths are relative then you're going to have to add something between the appended paths so that the second one starts in the correct place.
Concatenate d attributes
Usually, you can just concatenate the pathdata d attribute of several <path> elements to get a combined path.
Unfortunately, you might encounter some »bad practices« using M or m as interchangeable commands.
Common misconceptions about M or m:
M (moveto) can be absolute or relative.
Unlike the z (closepath) command (lowercase/uppercase – doesn't matter).
Relative m commands can be used used for compound paths like e.g the inner "hole" of the letter "o" referring to the previous command's end coordinate.
In fact, every first m or M command uses absolute coordinates – since there are no preceding points.
However, the first M command can be uppercase or lowercase – doesn't matter
(blame the specs)
Exception: the lowercase m command introduces a row of implicit relative l lineto commands. (But you can/should also avoid this)
Example 1: paths starting with (unnecessary) relative m command
svg{
border:1px solid #ccc;
width:25%;
}
path{
fill:#555;
}
<p>Seperate paths</p>
<svg viewBox="0 0 50 10">
<path id="path1" d="m 20 0 l 10 0 l 0 10 l -10 0z" />
<path id="path2" d="m 40 0 l 10 0 l 0 10 l -10 0z" />
<path id="path3" d="m 0 0 l 10 0 l 0 10 l -10 0z" />
</svg>
<p>Merged paths</p>
<svg viewBox="0 0 50 10">
<path id="pathConcat"
d="
m 20 0 l 10 0 l 0 10 l -10 0z
m 40 0 l 10 0 l 0 10 l -10 0z
m 0 0 l 10 0 l 0 10 l -10 0z
" />
</svg>
<p>Merged paths - fixed</p>
<svg viewBox="0 0 50 10">
<path id="pathConcat"
d="
M 20 0 l 10 0 l 0 10 l -10 0z
M 40 0 l 10 0 l 0 10 l -10 0z
M 0 0 l 10 0 l 0 10 l -10 0z
" />
</svg>
Fix: just replace each starting m with an absolute M
Example 2: m command for adjacent linetos
The exception are m commands followed by coordinates – used as a shorthand for succeeding l (relative linetos). (See also w3c specs.)
svg{
border:1px solid #ccc;
width:25%;
}
path{
fill:#555;
}
<p>Seperate paths</p>
<svg viewBox="0 0 50 10">
<path id="path1" d="m 20 0 10 0 0 10 -10 0z" />
<path id="path2" d="m 40 0 10 0 0 10 -10 0z" />
<path id="path3" d="m 0 0 10 0 0 10 -10 0z" />
</svg>
<p>Merged paths</p>
<svg viewBox="0 0 50 10">
<path id="pathConcat"
d="
m 20 0 10 0 0 10 -10 0z
m 40 0 10 0 0 10 -10 0z
m 0 0 10 0 0 10 -10 0z
" />
</svg>
<p>Merged paths - fixed</p>
<svg viewBox="0 0 50 10">
<path id="pathConcat"
d="
m 20 0 10 0 0 10 -10 0z
M 40 0 l 10 0 0 10 -10 0z
M 0 0 l 10 0 0 10 -10 0z
" />
</svg>
Fix: insert l commands
<path d="m 20 0 10 0 0 10 -10 0z" />
equals
<path d="M 20 0 l 10 0 l 0 10 l -10 0z" />
or
<path d="M 20 0 l 10 0 0 10 -10 0z" />
Example 3: fix pseudo relative m commands via getPathData()
Currently still a draft and not natively supported by major browser.
However you can use Jarek Foksa's polyfill..
getPathData() will return an array of command objects and normalize
repeated commands like this:
[
{type: 'm', values:[20, 0] },
{type: 'l', values:[10, 0]},
{type: 'l', values:[0, 10]},
{type: 'l', values:[-10, 0]}
]
function concatSimple(){
let d1= path1.getAttribute('d')
let d2= path2.getAttribute('d')
let d3= path3.getAttribute('d')
pathConcat.setAttribute('d', d1+d2)
}
function concatPathData(){
let pathData1= fixFirstM(path1.getPathData());
let pathData2= fixFirstM(path2.getPathData());
let pathData3= fixFirstM(path3.getPathData());
let pathDataConcat = pathData1.concat(pathData2).concat(pathData3);
pathConcat.setPathData(pathDataConcat);
}
// change first m to absolute M
function fixFirstM(pathData){
pathData[0].type='M';
return pathData;
}
svg{
border:1px solid #ccc;
width:25%;
}
path{
fill:#555;
}
<p><button onclick="concatSimple()">concat d simple</button>
<button onclick="concatPathData()">concat d pathData</button>
</p>
<svg viewBox="0 0 50 10">
<path id="path1" d="m 20 0 10 0 0 10 -10 0z" />
<path id="path2" d="m 40 0 10 0 0 10 -10 0z" />
<path id="path3" d="m 0 0 10 0 0 10 -10 0z" />
</svg>
<svg viewBox="0 0 50 10">
<path id="pathConcat" d="" />
</svg>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill#1.0.4/path-data-polyfill.min.js"></script>
To fix the first relative m we can convert by changing the first command type
pathData[0].type='M';
Recommendation: only use relative m commands if they are actually relative:
if you need a shorthand for following l commands (like m 20 0 10 0 0 10 -10 0z)
for relative (subpath) starting points in compound paths – like the letter "o"
Actually merging shapes: removing overlapping shapes
If you need to merge shapes - paper.js has some powerful path operations like unite, subtract etc.
Explained here: "Merging two bezier-based shapes into one to create a new outline"

Categories

Resources