javascripthtmlanimationsvgsvg-animate

How to generate a dynamic SVG path where point coordinates are fixed to an HTML element and item follows the path?


My goal is to create something like this:

Sketch of the final result

The yellow boxes represent divs whose number is dynamic. I need to generate a bezier curve whose loops are fixed below the image (red) since the position can change depending on the screen width. In addition, an element (black) follows the path once it reaches 50vh.

I found an npm package called svg-path-generator I could use to generate the path, but I don't know how to tackle this: https://www.npmjs.com/package/svg-path-generator

This could be a way to let the element follow the path: https://animejs.com/documentation/#motionPath

How could I approach this?

This is the HTML I have so far with a working code pen: https://codepen.io/marcoluzi/pen/ZEjBBVo

.block {
  position: relative;
  padding-top: 128px;
}
.block__line-wrapper {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}
.block__line-wrapper svg {
  width: 100%;
  height: 100%;
}
.block-item {
  display: grid;
  gap: 32px;
  grid-template-columns: 1fr;
  align-items: center;
}
@media (min-width: 768px) {
  .block-item {
    grid-template-columns: 1fr 1fr;
  }
}
@media (min-width: 1024px) {
  .block-item {
    grid-template-columns: 1fr 4fr 2fr 4fr 1fr;
  }
}
.block-item:not(:first-child) {
  margin-top: 96px;
}
@media (min-width: 768px) {
  .block-item:not(:first-child) {
    margin-top: 144px;
  }
}
@media (min-width: 1024px) {
  .block-item:not(:first-child) {
    margin-top: 192px;
  }
}
@media (min-width: 1024px) {
  .block-item:nth-child(odd) .block-item__image-wrapper {
    grid-column: 2/4;
  }
  .block-item:nth-child(odd) .block-item__content {
    grid-column: 4/-1;
  }
}
@media (min-width: 768px) {
  .block-item:nth-child(even) .block-item__image-wrapper {
    order: 2;
  }
  .block-item:nth-child(even) .block-item__content {
    order: 1;
  }
}
@media (min-width: 1024px) {
  .block-item:nth-child(even) .block-item__image-wrapper {
    grid-column: 3/-2;
  }
  .block-item:nth-child(even) .block-item__content {
    grid-column: 1/3;
  }
}
.block-item__content {
  display: flex;
  flex-direction: column;
  gap: 8px;
  justify-content: center;
  align-items: center;
}
.block-item__content > * {
  margin: 0;
  width: 100%;
}
@media (min-width: 768px) {
  .block-item__content > * {
    max-width: 416px;
  }
}
<div class="block">
  <div class="block-item">
    <figure class="block-item__image-wrapper has-image">
      <picture>
        <img src="https://via.placeholder.com/639x354" width="641" height="354" />
      </picture>
    </figure>
    <div class="block-item__content">
      <h2 class="block-item__title">Title 1</h2>
      <p class="block-item__description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
    </div>
  </div>
  <div class="block-item">
    <figure class="block-item__image-wrapper has-image">
      <picture>
        <img src="https://via.placeholder.com/639x354" class="block-item__image" width="641" height="354" />
      </picture>
    </figure>
    <div class="block-item__content">
      <h2 class="block-item__title">Title 2</h2>
      <p class="block-item__description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
    </div>
  </div>
  <div class="block-item">
    <figure class="block-item__image-wrapper has-image">
      <picture>
        <img src="https://via.placeholder.com/639x354" class="block-item__image" width="641" height="354" />
      </picture>
    </figure>
    <div class="block-item__content">
      <h2 class="block-item__title">Title 3</h2>
      <p class="block-item__description">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
    </div>
  </div>
  <div class="block__line-wrapper">
    <svg>
      <path/>
    </svg>
  </div>
</div>


Solution

  • I figured out a solution. First, I split up the SVGs as @chrwahl did in his answer. In addition, I used a tool I found called leader-line to dynamically generate the SVGs. With this tool, you can pin the start and end point of a path to an HTML element, and it also offers a lot of customisation. In addition, I wrote a script that generates a circle for each path and fixes its position to that path.

    Here is a working codepen: https://codepen.io/marcoluzi/pen/XWBMzMY

    // generating the svg paths
    document.querySelectorAll('.block').forEach(block => {
        const startElement = block.querySelector('.block__start');
        const items = block.querySelectorAll('.block-item');
    
        const options = {
            color: '#ef7852',
            dash: true,
            endPlug: 'behind',
        };
    
        const startLeaderLine = new LeaderLine(
            startElement,
            items[0].querySelector('.line-anchor'),
            {
                ...options,
                startSocket: 'left',
                endSocket: 'top',
                startSocketGravity: window.innerWidth > 768 ? [-200, 100] : [-100, 0],
                endSocketGravity: window.innerWidth > 768 ? [0, -300] : [0, -100],
            },
        );
    
        window.addEventListener('resize', () => {
            startLeaderLine.setOptions({
                startSocketGravity: window.innerWidth > 768 ? [-200, 100] : [-100, 0],
                endSocketGravity: window.innerWidth > 768 ? [0, -300] : [0, -100],
            });
            startLeaderLine.position();
        });
    
        items.forEach((item, index) => {
            if (index < items.length - 1) {
                new LeaderLine(
                    item.querySelector('.line-anchor'),
                    items[index + 1].querySelector('.line-anchor'),
                    {
                        ...options,
                        startSocket: 'bottom',
                        endSocket: 'top',
                        startSocketGravity: [0, 400],
                        endSocketGravity: [0, -400],
                    },
                );
            }
        });
    });