Sample points on an svg path

Viewed 688

Given an SVG (which contains a single path object), I want to divide the path into n segments of equal length. The returned value should be list of (n+1) points (x, y) coordinates in rasterized svg indicating the ends of every segment

Example:

Given SVG : (this example has only straight lines but the solution must work for all types of paths including cubic and quadratic beziers)

<svg height="210" width="400">
  <path d="M150 0 L75 200 L225 200 Z" fill="none" stroke="black" stroke-width="4" />
</svg>

renders as

given-svg

Expected output : coordinates of each of the red dots below considering bottom left corner as origin. The SVG is rendered at width=100px always

expected-output

I am using python and svgpathtools, how can I achieve this ?

My approaches :

  1. Take a look at SVG rasterizers. See how they handle paths. I took a look at this but sadly couldn't understand the code well enough to implement it myself
  2. Add a stroke-dasharray to my path. stroke-dasharray will "break" the continuous stroke into equal sized segments automatically. Then rasterize this modified SVG, then use something like OpenCV to cluster the rendered stroke. Then find midpoint of these clusters to get the answer. This approach is terribly slow due to multiple slow processes (rasterization + clustering) involved in it. It is also not very flexible in case a path crosses itself (infinity sign) since the stroke-dasharray will also intersect giving a weird intersection point
  3. I had a look at this question but I am not sure how this will work in my case where the SVG is always rendered at width=100px (height autocalculated based on aspect ratio of SVG), moreover the approach mentioned here is supposedly "slow". Moreover, I am not sure if this linked question gets me points "on" the path or the control points for svg path

While approach 2 works for many images, I certainly don't want to go that way because it's horribly slow. Is there a better approach ?

1 Answers

Given an SVG (which contains a single path object), I want to divide the path into n segments of equal length.

I used the effect of the appearance of dots when two conditions were met:

  1. There must be zero stroke (dash) length in the attribute stroke-dasharray = "(0, N)"
  2. stroke-lineCap="round"

I used getTotalLength() JS method to get the full line length of an SVG element

As you can see in the console, the result is ~= 577.2px

Let's say we need to divide the length of the line into 11 equal segments:

577,2 / 11 = 52.47 In this case, stroke-dasharray =" 0, 52.47 "

<svg xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink" height="210" width="400">
  <path stroke-dasharray="0,52.47"  stroke-lineCap="round" id="p1" d="M150 0 L75 200 L225 200 Z" fill="none" stroke="black" stroke-width="4" />
</svg>
<script>
console.log(p1.getTotalLength() / 11); 
</script>

For 22 equal line segments:

<svg xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink" height="210" width="400">
  <path stroke-dasharray="0,26.23"  stroke-lineCap="round" id="p1" d="M150 0 L75 200 L225 200 Z" fill="none" stroke="black" stroke-width="4" />
</svg>
<script>
console.log(p1.getTotalLength() /22); 
</script>

You can increase the stroke-width, the diameter of the points will increase proportionally, but the distance between the centers of the points will remain unchanged.

stroke-width="8"

<svg xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink" height="210" width="400">
  <path stroke-dasharray="0,26.23"  stroke-lineCap="round" id="p1" d="M150 0 L75 200 L225 200 Z" fill="none" stroke="black" stroke-width="8" />
</svg>
<script>
console.log(p1.getTotalLength() / 22); 
</script>

UPDATE

Commented @ashutoshbsathe

I'll give this a try with some more SVGs before accepting this as a final answer

To test the universality of this technique of dividing the perimeter into N equal parts for different figures of the SVG

#1. Divide the square into 4 parts

<svg xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink" height="300" width="300">
        <!-- Background rectangle -->
 <path  id="back" d="M50 10 L250 10 L250 210 50 210Z" fill="none" stroke="grey" stroke-width="2" /> 
             <!-- Divide into 4 equal parts using the stroke-dasharray  -->
 <path id="p1" stroke-dasharray="0,200"  stroke-lineCap="round"  d="M50 10 L250 10 L250 210 50 210Z" fill="none" stroke="red" stroke-width="8" />
</svg>
<script>
console.log(p1.getTotalLength() / 4); 
</script>

#2. Divide the square into 16 parts

<svg xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink" height="300" width="300">
        <!-- Background rectangle -->
 <path  id="back" d="M50 10 L250 10 L250 210 50 210Z" fill="none" stroke="grey" stroke-width="2" /> 
             <!-- Divide into 8 equal parts using the stroke-dasharray  -->
 <path id="p1" stroke-dasharray="0,50"  stroke-lineCap="round"  d="M50 10 L250 10 L250 210 50 210Z" fill="none" stroke="red" stroke-width="8" />
</svg>
<script>
console.log(p1.getTotalLength() / 16); 
</script>

#3. Divide the star into 5 parts

<style>
#back {
fill:none;
stroke:black;
stroke-width:2;
}
#p1 {
fill:none;
stroke:red;
stroke-width:8;
stroke-dasharray:0,179.95;
stroke-lineCap:round;
}

</style>
<svg xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink" height="300" width="300" viewBox="0 0 300 300">
   <!-- Background star -->
   <polygon id="back" points = "279.1,160.8 195.2,193.3 174.4,280.8   117.6,211.1 27.9,218.3 76.7,142.7 42.1,59.6 129.1,82.7 197.4,24.1 202.3,114 "/>
      <!--  red dots 5 -->
  <polygon id="p1" points = "279.1,160.8 195.2,193.3 174.4,280.8   117.6,211.1 27.9,218.3 76.7,142.7 42.1,59.6 129.1,82.7 197.4,24.1 202.3,114 "/>
</svg>
<script>
console.log(p1.getTotalLength() / 5); 
</script>

#4. Divide the star into 10 parts

<style>
#back {
fill:none;
stroke:black;
stroke-width:2;
}
#p1 {
fill:none;
stroke:red;
stroke-width:8;
stroke-dasharray:0,89.978;
stroke-lineCap:round;
}

</style>
<svg xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink" height="300" width="300" viewBox="0 0 300 300">
   <!-- Background star -->
   <polygon id="back" points = "279.1,160.8 195.2,193.3 174.4,280.8   117.6,211.1 27.9,218.3 76.7,142.7 42.1,59.6 129.1,82.7 197.4,24.1 202.3,114 "/>
      <!--  red dots 10 -->
  <polygon id="p1" points = "279.1,160.8 195.2,193.3 174.4,280.8   117.6,211.1 27.9,218.3 76.7,142.7 42.1,59.6 129.1,82.7 197.4,24.1 202.3,114 "/>
</svg>
<script>
console.log(p1.getTotalLength() / 10); 
</script>

#5. Divide the hexagon into 6 parts

<style>
#back {
fill:none;
stroke:black;
stroke-width:1;
}
#p1 {
fill:none;
stroke:red;
stroke-width:4;
stroke-dasharray:0,31.64;
stroke-lineCap:round;
}

</style>
<svg xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink" height="300" width="300" viewBox="-5 0 100 100">
   <!-- Background hexagon -->
   <path id="back"  d="M48.9 60.9 16.9 60.9 1.2 33.4 17.1 6.1 48.7 6.2 64.4 33.6z"/>
      <!-- Six red dots -->
  <path id="p1"  d="M48.9 60.9 16.9 60.9 1.2 33.4 17.1 6.1 48.7 6.2 64.4 33.6z"/>
</svg>
<script>
console.log(p1.getTotalLength() / 6); 
</script>

#6. Divide the hexagon into 12 parts

#back {
fill:none;
stroke:black;
stroke-width:1;
}
#p1 {
fill:none;
stroke:red;
stroke-width:4;
stroke-dasharray:0,15.82;
stroke-lineCap:round;
}
<svg xmlns="http://www.w3.org/2000/svg"  xmlns:xlink="http://www.w3.org/1999/xlink" height="300" width="300" viewBox="-5 0 100 100">
   <!-- Background hexagon -->
   <path id="back"  d="M48.9 60.9 16.9 60.9 1.2 33.4 17.1 6.1 48.7 6.2 64.4 33.6z"/>
      <!--  red dots 12 -->
  <path id="p1"  d="M48.9 60.9 16.9 60.9 1.2 33.4 17.1 6.1 48.7 6.2 64.4 33.6z"/>
</svg>
<script>
console.log(p1.getTotalLength() /12); 
</script>

Related