Referring to the code-snipplet below about getting data with on click on an svg path here, for my example which is displaying Number 2, I would like it to have the alert button pop up when the cursor is clicked only when close to number 2. My code below is wrong because when cursor is clicked anywhere which is far way from the Number 2, the pop up alert box is still shown. I will really appreciate any help I can get :)
function getKey(button) {
let key = button.querySelectorAll('path')[0].dataset.key;
alert('key is ' + key)
}
<div onClick="getKey(this)">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800pt" height="600pt" viewBox="0 0 800 600 " id="svg1">
<g enable-background="new">
<path data-key="12345" transform="matrix(1,0,0,-1,0,600)" stroke-width=".74" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke="#000000" d="M 224.34 585.57 L 224.34 586.6 L 225.22 588.68 L 226.1 589.71 L 227.87 590.75 L 231.39 590.75 L 233.15 589.71 L 234.04 588.68 L 234.92 586.6 L 234.92 584.53 L 234.04 582.46 L 232.27 579.35 L 223.46 568.98 L 235.8 568.98 "/>
</g>
</svg>
</div>
this in an element onclick attribute refers to the element
By setting an onclick attribute to the div containing the svg, a click anywhere within the div will call the function, and if this is sent to the function as an argument, it will always refer to the div element.
Once fired, your function is extracting the data attribute of the first path tag inside the div.
Instead, an eventListener can be attached to the path elements directly. When fired an event is created, which has a target property containing a reference to the tag that generated the event. It is that target element from which you need to extract the data attribute.
See: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
This working snippet demonstrates attaching event listeners to your paths. (I have had to change the line width of your path as the click must hit the line for the event to be triggered, see end note)
let paths = document.querySelectorAll('path');
// paths is an html collection of all paths;
// attach event listeners to each path;
for (let i=0; i<paths.length; i++) {
paths[i].addEventListener('click', event => alert(event.target.dataset.key));
}
<div>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800pt" height="600pt" viewBox="0 0 800 600 " id="svg1">
<g enable-background="new">
<path data-key="12345" transform="matrix(1,0,0,-1,0,600)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke="#000000" d="M 224.34 585.57 L 224.34 586.6 L 225.22 588.68 L 226.1 589.71 L 227.87 590.75 L 231.39 590.75 L 233.15 589.71 L 234.04 588.68 L 234.92 586.6 L 234.92 584.53 L 234.04 582.46 L 232.27 579.35 L 223.46 568.98 L 235.8 568.98 "/>
</g>
</svg>
</div>
If you have many paths, the above process may not be memory-efficient because the function is duplicated for each of the paths. Instead, a single handler function can be used and called from each of the event listeners. Like this:
let paths = document.querySelectorAll('path');
// paths is an html collection of all paths;
// attach event listeners to each path;
for (let i=0; i<paths.length; i++) {
paths[i].addEventListener('click', displayAlert);
}
function displayAlert(event) {
alert(event.target.dataset.key)
}
<div>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800pt" height="600pt" viewBox="0 0 800 600 " id="svg1">
<g enable-background="new">
<path data-key="12345" transform="matrix(1,0,0,-1,0,600)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke="#000000" d="M 224.34 585.57 L 224.34 586.6 L 225.22 588.68 L 226.1 589.71 L 227.87 590.75 L 231.39 590.75 L 233.15 589.71 L 234.04 588.68 L 234.92 586.6 L 234.92 584.53 L 234.04 582.46 L 232.27 579.35 L 223.46 568.98 L 235.8 568.98 "/>
</g>
</svg>
</div>
Note that the displayAlert function is referenced within the event listener without parentheses or argument. The event is passed automatically to the named external function (the declaration for which should include an argument if the automatically passed event is to be used inside the function). In practice, you are unlikely to have memory problems with such a trivial function but I've included this for completeness.
Alternatively, you can attach a single event listener to the div, and check that the target of the event is a path before displaying the data. This works because, although the event is attached to the div, the target of the event is set to whichever descendant of the div was clicked:
let div = document.getElementById('svg-container');
div.addEventListener('click', event => {
if (event.target.tagName == 'path') alert(event.target.dataset.key)
});
<div id="svg-container">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800pt" height="600pt" viewBox="0 0 800 600 " id="svg1">
<g enable-background="new">
<path data-key="12345" transform="matrix(1,0,0,-1,0,600)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none" stroke="#000000" d="M 224.34 585.57 L 224.34 586.6 L 225.22 588.68 L 226.1 589.71 L 227.87 590.75 L 231.39 590.75 L 233.15 589.71 L 234.04 588.68 L 234.92 586.6 L 234.92 584.53 L 234.04 582.46 L 232.27 579.35 L 223.46 568.98 L 235.8 568.98 "/>
</g>
</svg>
</div>
note
If you have to use narrow strokes for your paths, it may be difficult for a user to place the cursor exactly on the line and so clicks may be missed. One way to mitigate this is to duplicate the paths, set the stroke of the the lower (first) one of each pair much wider and make it invisible by using the background colour. In the case of characters like your number 2, it might be simpler to draw an appropriately sized invisible rectangle behind each character and attach event listener to them instead, or as well as, the character paths (remembering to include the data attribute).
Related
Thank you in advance.
i try to move un SVG curve element in html using javascript.
I would like to change the path of my svg so that my blue curve transforms like the red curve but with the transition to see the displacement of the curve.
i understand how to get or create an element but i am not sure how to set the attribude 'd' like change every 'c' in path.
alert(document.getElementById('s3').getAttribute('d'));
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<svg viewBox="0 0 990 180" height="200" width="1100" id="mySVG">
<g>
<path id="s2" d="M 241,128 C 272,113 293,152 369,125 C 434,80 471,72 580,114 "
fill="none" stroke="red" stroke-width="5px" />
<path id="s3" d="M 241,128 C266,131 298,100 369,125 C 441,150 482,151 580,114 "
fill="none" stroke="blue" stroke-width="5px" />
</g>
</svg>
<script src="app.js"></script>
</body>
</html>
I hope I understand your question: If you want to animate from one curve to the other you can use SMIL animations.
Since the paths in your code have the same number and the same type of commands you can use an <animate> element to animate the d attribute.
The values attribute is a list of values separated with semicolons (;) The first and the last value is the d attribute of the curve. The second value is the d attribute of the other one.
In my code the duration of the animation is 5 seconds: dur="5s
<svg viewBox="235 80 350 70" width="300" id="mySVG">
<g>
<path id="s2" d="M 241,128
C 272,113 293,152 369,125
C 434,80 471,72 580,114" fill="none" stroke="red" stroke-width="5px">
</path>
<path id="s3" d="M 241,128
C266,131 298,100 369,125
C 441,150 482,151 580,114" fill="none" stroke="blue" stroke-width="5px">
<animate attributeName="d" attributeType="XML" values="M 241,128
C266,131 298,100 369,125
C 441,150 482,151 580,114;
M 241,128
C 272,113 293,152 369,125
C 434,80 471,72 580,114;
M 241,128
C266,131 298,100 369,125
C 441,150 482,151 580,114; " dur="5s" repeatCount="indefinite" />
</path>
</g>
</svg>
UPDATE
The OP is commenting:
can i make it in javascript?
Making it in javascript is more complicated. You will need to set an array of values and an array of target values and recalculate each value of the curve with every frame of the animation. Next comes an example that is animating the blue curve on click:
Please read the comments in the code.
//I've hard coded the values and the target array
//you may want to do it dimamicaly from the d attribute
let vals = [
["M", 241, 128],
["C", 272, 113, 293, 152, 369, 125],
["C", 434, 80, 471, 72, 580, 114]
];
let target = [
["M", 241, 128],
["C", 266, 131, 298, 100, 369, 125],
["C", 441, 150, 482, 151, 580, 114]
];
//the request animation id
let rid = null;
//build the memory array used for the animation
let memory = [];
for (let i = 0; i < vals.length; i++) {
memory[i] = [];
memory[i][0] = target[i].slice();
memory[i][1] = vals[i].slice();
}
function Frame() {
rid = window.requestAnimationFrame(Frame);
updateValues();
updatePath();
}
window.addEventListener("load", updatePath, false);
// I'm animating the curve on click
svg.addEventListener(
"mousedown",
function () {
// if there is an animation running stop it before start another one
if (rid) {
window.cancelAnimationFrame(rid);
rid = null;
}
//reverse the animation
for (let i = 0; i < memory.length; i++) {
memory[i].reverse();
target[i] = memory[i][1].slice();
}
//call the Frame function
Frame();
},
false
);
function updateValues() {
//a function to update all the values of the curve except the move to part that is not changing anyway
for (let i = 1; i < vals.length; i++) {
for (let j = 1; j < vals[i].length; j++) {
let dist = target[i][j] - vals[i][j];
let vel = dist / 10;
vals[i][j] += vel;
}
}
}
//a function to reset the value of the d attribute
function updatePath() {
let d = `M${vals[0][1]},${vals[0][2]}`;
for (let i = 1; i < vals.length; i++) {
d += `C${vals[i][1]},${vals[i][2]},${vals[i][3]},${vals[i][4]},${vals[i][5]},${vals[i][6]}`;
}
s3.setAttributeNS(null, "d", d);
}
svg{border:1px solid}
<svg id="svg" viewBox="235 80 350 70" width="300" id="mySVG">
<g>
<path id="s2" d="M 241,128
C 272,113 293,152 369,125
C 434,80 471,72 580,114" fill="none" stroke="red" stroke-width="5px">
</path>
<path id="s3" d="M 241,128
C266,131 298,100 369,125
C 441,150 482,151 580,114" fill="none" stroke="blue" stroke-width="5px">
</path>
</g>
</svg>
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 4 years ago.
Improve this question
I want to have a svg dashed line drawn on scroll and got stuck on this for 4hrs.
If it's just a straight line, I can easily animate it by setting stroke-dasharray on css animation, but it doesn't work on a dashed line.
Since there's a background image on body, I cannot use a mask trick either.
I just want to have a simple 45 degree diagonal dashed line (about 100px) drawn on scroll.
Any advice?
In the next example I'm using the wheel event but you can use scroll instead. And I'm cheating. I'm drawing a dashed path over the path I want to animate. You'll see the animated path through the gaps. I hope it helps.
var svg = document.querySelector("svg");
var l = track.getTotalLength();
var dasharray = l;
var dashoffset = l;
theUse.style.strokeDasharray = dasharray;
theUse.style.strokeDashoffset = dashoffset;
document.addEventListener("wheel",
function(e) {
e.preventDefault();
if (dashoffset > 0 && e.deltaY > 0 ||
dashoffset < l && e.deltaY < 0) {
dashoffset -= e.deltaY;
}
if(dashoffset < 0)dashoffset = 0;
if(dashoffset > l)dashoffset = l;
theUse.style.strokeDashoffset = dashoffset;
}, false);
svg{border:1px solid}
<svg id="svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 -23 238 120">
<defs>
<path id="track" fill="none" d="M18.265,6.037c0,0,22.354-2.458,32.585,2.709c13.401,6.768,22.928,25.006,33.864,30.677c10.935,5.67,11.901,9.014,21.216,8.608c10.013-0.435,11.08-5.485,14.862-5.485
c9.801,0,25.631,24.662,36.168,24.115c14.971-0.777,9.135-0.936,22.096-0.531c12.959,0.406,29.501,7.144,41.247,4.309"/>
</defs>
<use xlink:href="#track" id="theUse" stroke="black" />
<use xlink:href="#track" stroke-dasharray="10 10" stroke="white" stroke-width="2" />
UPDATE
Someone commented:
what we are supposed to see .. because I see nothing when running the snippet
When moving the wheel of your mouse you should see something like this:
UPDATE 2
I'm updating again as an answer to another comment:
ok I don't have a wheel, but why not considering the scroll as stated in the question
Next comes a demo where I'm using the scroll event:
var l = thePath.getTotalLength();
var dasharray = l;
track.style.strokeDasharray = dasharray;
var dashoffset = l;
track.style.strokeDashoffset = dashoffset;
wrap.addEventListener("scroll", function() {
// - 10 is because I want to offset slightly the dashoffse
dashoffset =
-10 + l - this.scrollTop * l / (this.scrollHeight - this.clientHeight);
track.style.strokeDashoffset = dashoffset;
});
#wrap,svg{border:1px solid}
#wrap{height:200px; overflow:scroll}
use{fill:none}
<div id="wrap">
<svg id="svg"
width="100" viewBox="0 0 78.571 753.021" >
<defs>
<path id="thePath" d="M46.249,7c0,0-37.5,0-37.5,32.812s20.312,56.25,37.5,75
s23.881,51.525,6.25,73.438c-31.43,39.062,21.875,43.882,18.75,70.378s-35.938,63.997-45.312,90.559s17.08,37.5,23.438,81.25
s-23.438,75-18.75,118.75s45.312,75,26.562,103.125s-51.812,83.438-50.125,100"/>
</defs>
<use xlink:href="#thePath" id="track" stroke="black" />
<use xlink:href="#thePath" stroke-dasharray="10 10" stroke="white" stroke-width="2" />
</svg>
</div>
UPDATE num 3
Got another comment:
You should use a mask instead of a white path for hiding the dashed path, so that everything but the dashes remains transparent. Like here: Animate dashed SVG line
Inspired by this answer Animate dashed SVG line I'm using a mask instead of a white path.
var l = thePath.getTotalLength();
var dasharray = l;
mask.style.strokeDasharray = dasharray;
var dashoffset = l;
mask.style.strokeDashoffset = dashoffset;
wrap.addEventListener("scroll", function() {
dashoffset =
l - this.scrollTop * l / (this.scrollHeight - this.clientHeight);
mask.style.strokeDashoffset = dashoffset;
});
#wrap,svg{border:1px solid}
#wrap{height:200px; overflow:scroll}
use{fill:none;}
path{stroke-width:3px;}
#mask{stroke:white}
<div id="wrap">
<svg id="svg"
width="100" viewBox="0 0 78.571 753.021" >
<defs>
<path id="thePath" d="M46.249,7c0,0-37.5,0-37.5,32.812s20.312,56.25,37.5,75
s23.881,51.525,6.25,73.438c-31.43,39.062,21.875,43.882,18.75,70.378s-35.938,63.997-45.312,90.559s17.08,37.5,23.438,81.25
s-23.438,75-18.75,118.75s45.312,75,26.562,103.125s-51.812,83.438-50.125,100"/>
<mask id="mask1">
<use id="mask" xlink:href="#thePath" />
</mask>
</defs>
<use xlink:href="#thePath" stroke-dasharray="10 10" stroke="black" mask="url(#mask1)" />
</svg>
</div>
I want to auto generate an imagemap type of result for a raster image. I was able to supply this image as a PNG:
The original SVG for this looks like this:
<svg width="580" height="400" xmlns="http://www.w3.org/2000/svg">
<g>
<rect fill="#fff" id="canvas_background" height="402" width="582" y="-1" x="-1"/>
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
</g>
</g>
<g>
<rect id="svg_1" height="67" width="54" y="119.5" x="125.5" stroke-width="1.5" stroke="#000" fill="#fff"/>
<rect id="svg_3" height="67" width="54" y="119.5" x="180.5" stroke-width="1.5" stroke="#000" fill="#fff"/>
</g>
</svg>
Once I traced it using the library: https://github.com/jankovicsandras/imagetracerjs I get back path data like this:
<svg width="156" height="114" version="1.1" xmlns="http://www.w3.org/2000/svg" desc="Created with imagetracer.js version 1.2.3" >
<path fill="rgb(60,60,60)" stroke="rgb(60,60,60)" stroke-width="1" opacity="1" d="M 20 20 L 131 20 L 131 89 L 20 89 L 20 20 Z M 22 22 L 22 87 L 74 87 L 74 22 L 22 22 Z M 77 22 L 77 87 L 129 87 L 129 22 L 77 22 Z " />
<path fill="rgb(255,255,255)" stroke="rgb(255,255,255)" stroke-width="1" opacity="1" d="M 0 0 L 156 0 L 156 114 L 0 114 L 0 0 Z M 20 20 L 20 89 L 131 89 L 131 20 L 20 20 Z " />
<path fill="rgb(255,255,255)" stroke="rgb(255,255,255)" stroke-width="1" opacity="1" d="M 22 22 L 74 22 L 74 87 L 22 87 L 22 22 Z " />
<path fill="rgb(255,255,255)" stroke="rgb(255,255,255)" stroke-width="1" opacity="1" d="M 77 22 L 129 22 L 129 87 L 77 87 L 77 22 Z " />
</svg>
I would like to go back to the rect or polygon method so I can measure the area of each object so that if there were traced text I could exclude it / flatten it by saying it's total area is lower than allowed as a polygon / rect object.
Is there a way to convert the path data back to where I have 2 separate objects? I want to be able to overlay the results over the original image and allow targeting each square
I try to answer your question as best as I can, but there are multiple solutions here.
If you force imagetracerjs to use only straight line edges (with qtres = 0.00001) , then SVG path elements are polygons, their coordinates are defined in the d attribute: d="M 20 20 L 131 20 ... " in the first example. (More info: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d)
But 1 d attribute is often not just 1 polygon. The shape can include holes, and they are represented as smaller polygons. (More info: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule)
You can extract the d attributes with e.g. Regex, split them by the Z tags (which mean roughly: this shape ends, a hole shape begins) , then use the Gauss's area formula (https://en.wikipedia.org/wiki/Shoelace_formula) on the coordinates to get the areas. You might need to subtract the areas of the holes from the area of the parent shape.
You have 4 separate objects on your image, but some are not trivial. I try to "translate" your SVG result so you can decide which path to process.
ImageTracer output is sorted by colors first. The 1. path is the blackish frame around the two smaller rectangles, technically this is a blackish rectangle with two rectangle holes. The 2. path is white frame around the blackish frame, technically a white rectangle with a big rectangle hole. The 3. and 4. paths are the smaller white rectangles, which might be relevant for you.
There's an alternative to splitting and parsing the SVG string, but it's a bit undocumented, sadly. You can get a tracedata object first, process it, and optionally render it to SVG string later. (More info: https://github.com/jankovicsandras/imagetracerjs#examples )
var options = {qtres:0.0001};
ImageTracer.imageToTracedata(
'example.png',
function(tracedata){
// color layers loop
for(var i=0; i<tracedata.layers.length; i++){
// paths loop
for(var j=0; j<tracedata.layers[i].length; j++){
var thispath = tracedata.layers[i][j];
// processing this path if it's not a hole
if( !thispath.isholepath ){
// accumulating coordinates in [ [x,y] , [x,y] , ... ] polygon
var thispolygon = [];
thispolygon.push( [ thispath.segments[0].x1 , thispath.segments[0].y1 ] );
for(var k=0; k<thispath.segments.length; k++){ thispolygon.push( [ thispath.segments[k].x2 , thispath.segments[k].y2 ] ); }
// TODO: calculate thispolygon area with https://en.wikipedia.org/wiki/Shoelace_formula here
}
}// End of paths loop
}// End of color layers loop
},// End of imageToTracedata callback()
options
);// End of ImageTracer.imageToTracedata()
I hope these help. :)
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.
I have this SVG code:
<svg id="svgSurface" width="500" height="500">
<defs>
<marker id="Triangle" viewBox="0 0 20 20" refX="0" refY="0" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto">
<path d="M 0 0 L 20 10 L 0 20 z" fill="red" fill-opacity="1">
</path>
</marker>
</defs>
<line x1="10" y1="10" x2="100" y2="100" class="Line" marker-end="url(#Triangle)"></line>
</svg>
and some javascript code:
var svg = document.getElementById("svgSurface");
var svgRect = svg.createSVGRect();
svgRect.x = 0;
svgRect.y = 0;
svgRect.width = 50;
svgRect.height = 50;
var nodes = svg.getIntersectionList(svgRect, null);
alert(nodes.length);
Here is a working example in fiddle http://jsfiddle.net/gYaEX/1/
As you can see I try to get all nodes whose rendered content intersects the specified rectangle svgRect. In Chrome it works properly but in IE it always crashes and I don't understand why.
If it crashes that's a bug in IE which you should report to Microsoft, having said that you are using it incorrectly as you should be passing in an element and not null as the last argument. In your case you probably want this:
var nodes = svg.getIntersectionList(svgRect, svg);