Determine if two non-rectangles overlap in JS - javascript

I have two html elements (in my case they are SVG path elements). Each of these has a different stroke width. Is there any way to determine if two of these paths overlap? I have tried using the npm path intersection library, but it doesn't allow me to specify a width. Also, the results are approximate, but I need more exact data.
I don't need to calculate the actual intersection area, or coordinates. I just need to know if an intersection occurs between these two paths with definite widths. Is there a solution?
edit:
here is an example to demonstrate something i want
<svg xmls="http://www.w3.org/2000/svg">
<path id="p1" d="M10,10 C20,30, 50,70, 100,100" stroke-width="3" stroke='black'></path>
<path id="p2" d="M20,0 Q200,30,0,100" stroke-width="5" stroke="red"></path>
</svg>
path {
fill: none;
}
svg {
position: absolute;
top: 100px;
left: 100px;
}
in javascript, i have something like this:
let p1 = document.getElementById("p1"), p2 = document.getElementById("p2");
if (p1 intersects/overlaps with p2) {
//run some code
}
example 1
it produces the above, which i classify as an intersection, as the paths themselves overlap
example 2
here, i produced the same lines, but one is shifted. I do not classify this as an overlap

Related

How do I continue a text over multiple SVG paths? (text behavior on paths)

I would like to build a site where the text is not displayed in straight lines but on curved lines instead. I work with a single SVG path at the moment, that spans over multiple rows and put the text on that path with the textPath-element. I then scale the fontsize with JavaScript, to make it "responsive".
Here is what I have so far:
$(function() {
var myFontsize = window.innerWidth * 0.015 + ((window.innerWidth - 1300) / -30);
$( "text" ).css("font-size", myFontsize)
});
$( window ).resize(function() {
var myFontsize = window.innerWidth * 0.015 + ((window.innerWidth - 1300) / -30);
$( "text" ).css("font-size", myFontsize)
});
body {
margin: 1vw;
}
path {
fill: none;
}
#line-box {
position: fixed;
left: 1vw;
top: 0;
width: 101vw;
}
text {
fill: black;
font-size: 1.3rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="line-box" viewBox="0 -30 1200 1200" preserveAspectRatio="none">
<path vector-effect="non-scaling-stroke" id="curve" d="M0 0C449.6667 0 857 0 1160 0M0 35C292 35 584 35 876 55 953.6667 55 1031.3333 55 1160 35"/>
<text text-anchor="start">
<textPath xlink:href="#curve">
Are you a human being? We apologize for the confusion, but we can't quite tell if you're a person or a script. Please don't take this personally. Bots and scripts can be remarkably lifelike these days!
</textPath>
</text>
</svg>
(Change the width of the view box to see the behavior I am talking about)
Now the rendered text on the path of course doesn't behave like a text in a p-element for example, so where there is a "line-break" it wraps around the edges in the middle of the words (which are not words but single rendered letters).
I thought about putting each word into a tspan-element, thinking I could get a grip on each word this way, but the rows of text are put on one consecutive path, so I don't really know how to calculate or detect wether a word is leaving or entering a specific row of the path.
Is there a way to continue a text over multiple paths instead of having one path drawing multiple rows? Maybe this way it could be somehow calculated when a tspan-element is leaving a path and must enter the next?
I basically want the text and it's "line-breaks" to work similarly to a normal paragraph without hyphenation, just that the lines move on paths. That idea doesn't sound so complicated to me, but I don't really know what the best way is to make it work.
Maybe SVG-paths are just not the right tool for this. In that case, does anyone have a better Idea where I should look? I am not hesitant to look into some js-libraries for example.
Thank you so much :)
You'll definitely need Javascript to implement this. SVG is a relatively low-level format for describing vector graphics, not a text layout format. Responsive line breaks is a feature in the pipeline, but not fully implemented yet AFAIK. Responsive line breaks on text paths? That is far too specialised.
That idea doesn't sound so complicated to me
Well, it is for various reasons. Here is a somewhat naive implementation:
const nssvg = "http://www.w3.org/2000/svg";
const nsxlink = "http://www.w3.org/1999/xlink";
const text = document.querySelector('text');
const originalText = text.textContent.trim();
function nextCurve(curves) {
const currentCurve = curves.shift();
const currentLength = currentCurve.getTotalLength();
return [currentCurve, currentLength, /\s+/g, 0];
}
function makeTextPath(content, id) {
const textPath = document.createElementNS(nssvg, 'textPath');
textPath.setAttributeNS(nsxlink, 'xlink:href', '#' + id);
textPath.textContent = content;
text.appendChild(textPath);
}
function onResize() {
text.style.fontSize = (window.innerWidth * 0.015 + ((window.innerWidth - 1300) / -30)) + 'px';
// flush old layout
text.innerHTML = "";
let currentText = originalText;
// set text in a provisorial tspan so it can be measured
const tspan = document.createElementNS(nssvg, 'tspan');
tspan.textContent = currentText;
text.appendChild(tspan);
const curves = [...document.querySelectorAll('.curve')];
let [currentCurve, currentLength, re, previousIndex] = nextCurve(curves);
// search for spaces
while (re.exec(currentText)) {
// measure the position of the space
const length = tspan.getSubStringLength(0, re.lastIndex)
if ( (length < currentLength) ) {
previousIndex = re.lastIndex;
} else {
// if the space is beyond the end, add a textPath with one word less,
// provided there is still a curve left to use
if (currentCurve) {
makeTextPath(currentText.substring(0, previousIndex), currentCurve.id);
}
// remove the already used part from the tspan
// and initialize for next line
tspan.textContent = currentText = currentText.substring(previousIndex).trim();
[currentCurve, currentLength, re, previousIndex] = nextCurve(curves);
}
}
// there is a rest that still needs to be layed out
if (currentCurve) {
makeTextPath(currentText, currentCurve.id);
}
//the provisorial tspan can be removed now
tspan.remove();
}
onResize();
window.addEventListener('resize', onResize);
#line-box {
left: 1vw;
width: 98vw;
}
text {
text-anchor: start;
fill: black;
font-size: 1.3rem;
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="line-box" viewBox="0 -30 1200 1200" preserveAspectRatio="none">
<defs>
<path class="curve" id="c1" d="M0 0C449.6667 0 857 0 1160 0"/>
<path class="curve" id="c2" d="M0 35C292 35 584 35 876 55 953.6667 55 1031.3333 55 1160 35"/>
<path class="curve" id="c3" d="M 0,70 C 292,70 584,90 876,110 953.67,110 1031.3,90 1160,70"/>
</defs>
<text>
Are you a human being? We apologize for the confusion, but we can't quite tell if you're a person or a script. Please don't take this personally. Bots and scripts can be remarkably lifelike these days!
</text>
</svg>
One simplification about this you mentioned yourself: the potential line breaks are found with the regular expression /\s+/g, which will identify various space characters. This is even less sophisticated than the algorithms used for line breaks in HTML (with the default hyphens: none).
The big problem is identifying what is fitting on the path used in <textPath>. There are a few interfaces defined for SVGTextContentElement that look like they could be helpful. But in practice, they fall short. The algorithm for describing how to position glyphs is quite longish, but it doesn't say much about glyphs that cannot be positioned:
Glyphs whose midpoint-on-the-path are off the path are not rendered.
Continue rendering glyphs until there are no more glyphs (or no more space on path).
Suppose the nth character is not rendered. The quote above doesn't say anything about the return values of textPath.getSubStringLength(0, n) or textPath.getStartPositionOfChar(n). Firefox, for example, returns values as if they were all lumped up somewhere near the end of the path. Other browsers may return something completely different, there is no guarantee what.
I decided to circumvent this problem by first laying out the string in a straight line, then measuring where the spaces end up, and break at the last space whose position is still smaller than the length of the path it will be laid out with. But this is only a rough estimate. In reality the character spacing differs between straight and curved layout. Especially for narrow curves the results will be wrong, and when the text is positioned on its inside, some letters may disappear.
Another problem arises from the concept of "addressable characters" used for producing character indices in SVG:
A character that is addressable by text positioning attributes and SVG DOM text methods. Characters discarded during layout such as collapsed white space characters are not addressable, neither are characters within an element with a value of none for the display property. Addressable characters are addressed by their index measured in UTF-16 code units (thus, a single Unicode code point above U+FFFF will map to two addressable characters as a UTF-16 code unit consists of 16 bits).
If you have a string as returned from the textContent property, and apply methods of the Javascript String prototype, the same indices may point to completely different characters.
To completely solve both problems, a lot more code will be needed, and all the special cases to be considered are quite beyond the scope of this answer. Maybe there is a library out there somewhere that attempts this, but I am not aware of it.

d3.js extract coordinates of SVG path elements

After spending quite some time on this problem without any success, I decided to post my (first!) question here.
For a project I used topojson to plot part of a map. In my SVG I have some groups containing some points (which I placed using the geojson.io tool).
The map looks like this: plotted map
The structure of the code looks like this:
<svg width='1300' height='900'>
<g class="gate" id="D5">
<path d="M645.3426080089994,434.9821086442098m0,4.5a4.5,4.5 0 1,1 0,-9a4.5,4.5 0 1,1 0,9z"></path>
</g>
<g class='gate' id='d4'>
<path d="M605.3552137160441,383.2755208062008m0,4.5a4.5,4.5 0 1,1 0,-9a4.5,4.5 0 1,1 0,9z"></path>
</g>
</svg>
What I'd like to do is append some SVG based on some condition on the exact location of these points. Now while I know how to append SVG's, I'm having trouble extracting the coordinates of the point on the map. The SVG that I would like to append must be placed upon the points in the map. However, when for example I append a circle to the group, the coordinates that I enter are not considered relative to the point, but rather absolute to the whole SVG area. The circle ends up in the upper left corner instead of on the actual point.
I attempted to extract the coordinates of each point in order to use this data for plotting, but I have not yet succeeded. I managed to get the node by using:
d3.selectAll('#D5').select('path').node()
Which returns:
<path d="M605.3552137160441,383.2755208062008m0,4.5a4.5,4.5 0 1,1 0,-9a4.5,4.5 0 1,1 0,9z"></path>
When I look inside the path object, I see that deep inside the __ data__ element there are some coordinates, but I'm not able to extract these.
This issue wouldn't be a problem if I defined the coordinates for the point at start. However, due to the fact that this is topojson, the whole image is just a composition of path elements.
How can I plot a SVG on the points without explicitly defining coordinates beforehand? If any more information is required, please let me know.
Thanks!
What you are looking for is the getBBox() function
d3.selectAll('#D5').select('path').node().getBBox()
// { x: 640.8425903320312, y: 430.48211669921875, width: 9, height: 9 }
See https://jsfiddle.net/7zahj9gt/

How to combine two SVG paths together (without spaces)

I was able to merge two separate paths together using this technique. However, my animation is still treating this as two separate paths.
Is there a way to combine these two paths without using spaces?
M3322.09,361.23V473.45c0,14,2,23.41,23.41,23.41H3809.63
M3809.63,496.31c21.41,0,166.41-1,166.41-1s13.07.87,82.08.87c31.75,0,63.51-36.21,95.26-75.31"/>
svg {
fill: none;
}
path {
stroke: tomato;
stroke-width: 100;
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 25 14905 623">
<path d="M3322.09,361.23V473.45c0,14,2,23.41,23.41,23.41H3809.63 M3809.63,496.31c21.41,0,166.41-1,166.41-1s13.07.87,82.08.87c31.75,0,63.51-36.21,95.26-75.31"/>
</svg>
The originally separate paths can be viewable here:
svg {
fill: none;
}
path {
stroke: tomato;
stroke-width: 100;
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 25 14905 623">
<path d="M3322.09,361.23V473.45c0,14,2,23.41,23.41,23.41H3511.9" />
<path d="M3809.63,496.31c21.41,0,166.41-1,166.41-1s13.07.87,82.08.87c31.75,0,63.51-36.21,95.26-75.31" />
</svg>
The goal is to merge these paths to match the above svg snippet -- without using spaces in the path.
I found an awesome online editor for path manipulations:
yqnn.github.io/svg-path-editor/
Paste your path segments
[optional] Check parsed path commands (e.g. Remove the M between segments)
[optional] Convert to either 'abs' or 'rel' commands
[optional] Set 'minify output' checkbox
Copy output
m3322.09 361.23v112.22c0 14 2 23.41 23.41 23.41h464.13c21.41 0 166.41-1 166.41-1s13.07.87 82.08.87c31.75 0 63.51-36.21 95.26-75.31
The answer is
M3322.09,361.23V473.45c0,14,2,23.41,23.41,23.41H3809.63 c21.41,0,166.41-1,166.41-1s13.07.87,82.08.87c31.75,0,63.51-36.21,95.26-75.31
H3511.9 means draw a horizontal line until x point at 3511.9 (with whatever y was previously inherited)
M3809.63,496.31 means move the "cursor" to an x, y coordinate.
I changed H3511.9 to H3809.63 and removed M3809.63,496.31 and continued with c21.41... which is a draw curve command.
These resources helped me to understand the draw commands for the d path attribute.
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
https://www.youtube.com/watch?v=k6TWzfLGAKo
I found the easiest solution was:
Open the SVG file in Inkscape (Free software, cross platform https://inkscape.org)
Select the paths to merge
From the Path menu, choose Union
Save the file
For those who came upon this question while researching to make sure there would be no problems from merging paths like I was, the problem here is just a typo.
In the above merged snippet, the ending of the first path was changed from its original 23.41H3511.9 to 23.41H3809.63.
Putting paths together by putting a space (or no space at all, just don't have a comma between the paths when placing them in the d attribute value) should always work without modification.
(On a side note, Raphael's answer made SVGs a lot less cryptic to me now after reading his links and tips)

Aligning SVG polygons

I have a list with all our team members.
In the design there are 15 different shapes, which fit into each other. So in the perfect situation you would have:
|shape1|shape2|shape3|shape4|shape5|
|shape6|shape7|shape8|shape9|shape10|
|shape11|shape12|shape13|shape14|shape15|
But the width of that section is 100% so what happens is:
|shape1|shape2|shape3|shape4|shape5|shape6|shape7|
|shape8|shape9|shape10|shape11|shape12|
|shape13|shape14|shape15|shape1|shape2|
So what happens is that shape 6 floats next to 5. But that shape6 should be shape1 again. Because that would fit.
So it would be
|shape1|shape2|shape3|shape4|shape5|shape1|shape2|
|shape6|shape7|shape8|shape9|shape10|
|shape11|shape12|shape13|shape14|shape15|
What I basicly need is that every LI element next to a LI element with shape5 turns into a shape1 instead of shape6.
the shapes are added through svg elements(this x 15)
HTML
<svg width="0" height="0">
<clipPath id="polygon-1">
<polygon fill="#939598" points="183,172.776 89.16,172.776 -6.231,189.785 -0.174,1.02 170.255,37.688 "/>
</clipPath>
</svg>
CSS
.team article.image-shape.polygon-1{
-webkit-clip-path: url(#polygon-1);
}
I am not quit sire how to tackle this problem. If you guys have any suggestions I could look into, it would be very helpful.
What have I been trying?
I have been counting the LI elements within the UL and adding the 6th one class one, but obviously that isn't working when you are scaling down because you would get:
|shape1|shape2|shape3|shape4|shape5|
|shape1|shape7|shape8|shape9|shape10|
|shape11|shape12|shape13|shape14|shape15|

using document.elementFromPoint to get nearby path

For my website, I want the users to be able to click on various svg elements.
I've coded up a quick function to let users click on enclosed shapes. I basically just run document.elementFromPoint(x, y) on them. This works great for any enclosed shape, since they have clickable area. Open paths, however, are a pain to click because they're only 1-2 pixels in size.
I would like to allow the user to select them by clicking 'close enough' to it, such that if the click is, for example, 10 or fewer pixels away from the path, the path would be selected. I could write a mathematical algorithm for this (using Ray-Casting that only goes 10 pixels out for example, or turn points into 10px-radius circles and check for intersections with the path), but my intuition tells me there must be an easier way, perhaps via CSS.
One idea I was thinking of is if the stroke for this path was 10 pixels thick for selection purposes, but only 1 pixel thick visibly (or having a 'padding' that would apply to the path rather than the bounding box). I've also looked at pointer-events, but aside from bounding box (which I would rather not use to avoid selecting other shapes) I don't see any useful settings there. Is there something I could leverage in JavaScript or CSS that can help me accomplish this without doing the math for the paths myself (since it may get ugly for cubic beziers, etc.)?
I should also mention that this is an HTML5 app, so I don't care about backwards-compatibility.
If the paths have no stroke then you can create an invisible stroke (stroke-opacity="0") and use pointer-events="all" to give the shape an extended hit area.
If the paths do have strokes then you need to create a second invisible path on top of the visible path with the same coordinates but a bigger stroke-width. You can then use pointer-events="all" to make this invisible path the hit target and if it is hit you react in the event handler as if the visible shape was hit.
Robert Longson describes how you can do this, but I wanted to add some more details.
First, you don't need to duplicate or create new geometry if you don't want to. You can also use a <use> element. You can use some css to give shapes a different look even when some other invisible element is hovered.
<style>
g { fill: #ddd; stroke: black; pointer-events: none; }
use { pointer-events: all; stroke-width: 40; fill: none; stroke-opacity: 0; }
use:hover + * { fill: cornflowerblue; }
<g>
<use xlink:href="#c"/>
<circle id="c" cx="50" cy="60" r="20"/>
</g>
See live example.

Categories