Marco
Marco

Reputation: 29

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>

Upvotes: 1

Views: 2026

Answers (3)

Marco
Marco

Reputation: 29

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],
                },
            );
        }
    });
});

Upvotes: 1

chrwahl
chrwahl

Reputation: 13145

I will suggest that you split up the path in different SVG images and use these images as background for your HTML elements.

In the first snippet you can see alle the SVG images that I use, and in the second these SVG mages as backgrounds in a simplified version of your code (the dot does not behave when scaling up and down the page, but I don't have time to fix it -- maybe later).

I have set preserveAspectRatio for the SVG and vector-effect for the paths. This combination ensures that the background image can be stretched if needed.

svg {
  border: thin solid gray;
}
<p>From top to left:</p>
<svg viewBox="0 0 100 10" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M 54 3 Q 54 5 50 5 Q 27 5 25 10" fill="none" stroke="black" stroke-width="4" vector-effect="non-scaling-stroke" />
</svg>
<p>and the starting dot:</p>
<svg viewBox="0 0 100 10" preserveAspectRatio="xMinyMid meet" xmlns="http://www.w3.org/2000/svg">
  <circle cx="54" cy="3" r="2" fill="black" />
</svg>
<p>From left to rigth:</p>
<svg viewBox="0 0 100 10" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M 25 0 Q 27 5 50 5 Q 73 5 75 10" fill="none" stroke="black" stroke-width="4" vector-effect="non-scaling-stroke" />
</svg>
<p>From right to left:</p>
<svg viewBox="0 0 100 10" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M 75 0 Q 73 5 50 5 Q 27 5 25 10" fill="none" stroke="black" stroke-width="4" vector-effect="non-scaling-stroke" />
</svg>

div.block {
  display: grid;
  grid-template-columns: 1fr 1fr;
}

div.box {
  background-color: red;
  min-height: 5em;
}

div.top {
  padding-top: 50px;
  background-image: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxwYXRoIGQ9Ik0gNTQgMyBRIDU0IDUgNTAgNSBRIDI3IDUgMjUgMTAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgo8L3N2Zz4K'), url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlueU1pZCBtZWV0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxjaXJjbGUgY3g9IjU0IiBjeT0iMyIgcj0iMiIgZmlsbD0iYmxhY2siIC8+Cjwvc3ZnPg==');
  background-repeat: no-repeat;
  background-size: 100% 50px,100% 50px;
}

div.left {
  padding-top: 50px;
  background-image: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxwYXRoIGQ9Ik0gMjUgMCBRIDI3IDUgNTAgNSBRIDczIDUgNzUgMTAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgo8L3N2Zz4K');
  background-repeat: no-repeat;
  background-size: 100% 50px;
}

div.left div.box {
  order: 2;
}
div.left div.text {
  order: 1;
}

div.right {
  padding-top: 50px;
  background-image: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxwYXRoIGQ9Ik0gNzUgMCBRIDczIDUgNTAgNSBRIDI3IDUgMjUgMTAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgo8L3N2Zz4=');
  background-repeat: no-repeat;
  background-size: 100% 50px;
}
<div class="top block">
  <div class="box"></div>
  <div class="text">Test</div>
</div>
<div class="left block">
  <div class="box"></div>
  <div class="text">Test<br>Test<br>Test<br>Test<br>Test<br>Test</div>
</div>
<div class="right block">
  <div class="box"></div>
  <div class="text">Test</div>
</div>

Upvotes: 0

Not an answer, just to get your creative juices flowing you can position elements without complex coding

<script>
customElements.define("svg-path-elements",class extends HTMLElement{
    connectedCallback(){
    let id = "curve" + this.getAttribute("id");
    let speed = 1; // set to 0.0001 for "instant" display
    let position = 0; // position of first element
    let elements = Array(~~this.getAttribute("count")||5).fill(0).map((_,idx,arr)=>{
      let inlineFunctionOnEnd = `this.closest('svg').parentNode.onend(${idx})`;
        let circle = `<circle id="circle${idx}" cx="0" cy="0" r="20" fill="green">
                      <animateMotion dur="${speed}s" keyPoints="0;${position}" 
                                     onend="${inlineFunctionOnEnd}"
                                     fill="freeze" keyTimes="0;1" calcMode="linear">
                        <mpath href="#${id}"></mpath>
                      </animateMotion>
                    </circle>`
     position += 1/(arr.length-1);
     return circle;
    }).join("");
    this.innerHTML = `<svg viewBox="-50 50 1300 200" style="background:pink">
                      <path id="${id}" fill="none" stroke="red" stroke-width="5"
                            d="M 0 150 
                                q 150 -150 300 0
                                q 150 150 300 0
                                q 150 -150 300 0
                                q 150 150 300 0"></path>${elements}</svg>`;
  }
  onend(idx){
    let circle = this.querySelector("#circle"+idx);
    //console.log("positioned", this.id, circle.id );
  }
})</script>
<svg-path-elements id="FIVE" count="5"></svg-path-elements>
<svg-path-elements id="TEN" count="10"></svg-path-elements>

Upvotes: 1

Related Questions