Draw a pattern along a path using SVG

Recently I wanted to create the effect of a cable for an application I’m working on. The cable should have a textile look, similar to a rope, which is quite common right now (for example the charging cable of the current MacBook Pro). I’m using SVG for rendering, so applying a pattern to a stroke path should be easy, right?

It turned out — it isn’t that easy 😂. Let’s start simple, I have a path and want to draw only its stroke. I’m using a quadratic Bézier curve to get a cable like shape:

Simple stroke path with solid red color.
<path d="M 199 311 Q 338.5 775.2 478 733" stroke="red" />

This looks quite boring, so I want to apply a pattern to it. SVG comes with the <pattern> element which repeats its content over the filled space. You can reference it as your stroke style via an id. Let’s try a simple arrow pattern:

A stroke path with a pattern applied, looks odd?
<defs>
  <pattern id="arrow" viewBox="0,0,20,20" width="10%" height="10%">
    <path d="M0 0 10 0 20 10 10 20 0 20 10 10Z" />
  </pattern>
</defs>

<path d="M 199 311 Q 338.5 775.2 478 733" stroke="url(#arrow)" />

Uh, that doesn’t work as expected. The pattern is applied uniformly over the space filled by the path stroke by repeating it at the global x and y axis. My goal is that the arrow pattern follows the path.

I searched a bit and found a nice answer on StackOverflow. The author is using markers instead of setting the stroke of the path. Markers can be used to draw arrow heads on lines or place joints between line segments. Let’s convert the pattern into a marker and give it a try:

Using markers to apply a pattern on the path instead of using a stroke style.
<defs>
  <pattern id="arrow" viewBox="0,0,20,20" width="10%" height="10%">
    <path d="M0 0 10 0 20 10 10 20 0 20 10 10Z" />
  </pattern>
</defs>

<path
  d="M 199 311 Q 338.5 775.2 478 733"
  markerStart="url(#arrow)"
  markerEnd="url(#arrow)"
  markerMid="url(#arrow)"
  stroke="#eee"
/>

Markers can be placed in three positions of a path: At the end, the start and between path segments. We can only see two of them because my path is pretty simple: <code>M 199 311 Q 338.5 775.2 478 733</code>. The path consists of only a single Q curve instructions and therefore only has a start and end, but no middle points. But the arrows are already properly oriented on the path, which is nice! To get the expected result, we just have to repeat the marker more often. For that we have to subdivide the part into smaller segments.

The Q instruction helps me to generate exactly the shape I want. I don’t want to define all points in between upfront. But how do I get the intermediate points? I could do the quadratic Bézier curve calculations myself, but there is a better way: The SVGPathElement interface has a getPointAtLength(t) method which allows us to query the points on the path at position t. Now we just have to get the length of the path (getTotalLength()) and divide it into equal steps where our marker should be placed. Now we have everything we need to generate a path with small segments:

export function subdividePath(path, stepSize = 16) {
  const p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  p.setAttribute('d', path);
  const count = Math.floor(p.getTotalLength() / stepSize + 1;
  const points = Array.from(
    Array(count)),
    (_, i) => i + 1
  ).map((i) => {
    return p.getPointAtLength(i * stepSize);
  });
  const initialOffset = p.getPointAtLength(0);
  return `M ${initialOffset.x} ${initialOffset.y} ${points
    .map(({ x, y }) => `L ${x} ${y}`)
    .join(' ')}`;
}
A subdivided path with multiple markers to create a pattern effect.

Now that we have more path segments the marker is repeated over the path, exactly like we want. Now I can implement my cable:

The final textile cable pattern.

I didn’t expect it to be that difficult, but I think that is a pretty cool trick.

This is the first time that I embed CodeSandbox links in this blog. It allows for even more interactive stuff, let’s see what I will do in the future.

Tags: svg