Search code examples
javascriptcssloopsanimationmarquee

Why does my infinite marquee animation have a jerky restart instead of smoothly looping?


I'm experiencing an issue with my infinite marquee animation. Whenever it reaches the end, instead of smoothly transitioning back to the beginning, it seems to abruptly restart.

    const scrollers = document.querySelectorAll(".services-brand");

    if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
        addScrolling();
    }

    function addScrolling() {
        scrollers.forEach((scroller) => {
            scroller.setAttribute("data-animated", true);

            const scrollerInner = scroller.querySelector('.scroller__inner')
            const scrollerContent = Array.from(scrollerInner.children);

            scrollerContent.forEach((item) => {
                const duplicatedItem = item.cloneNode(true);
                duplicatedItem.setAttribute("aria-hidden", true);
                scrollerInner.appendChild(duplicatedItem);
            })
        });
    }
section.services-brand {
    width: 100%;
    background: white;
    position: relative;
    margin-top: -100px;
}
.services-brand[data-animated="true"] {
    overflow: hidden;
}
.services-brand[data-animated="true"] .s-brand-wrapper {
    flex-wrap: nowrap;
}
.s-brand-wrapper {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    padding: 20px 0px;
    animation: brandScroller 5s linear infinite;
}
.s-brand-block span {
    font-size: 20px;
    font-family: var(--main-hard-font);
    font-weight: 800;
    color: var(--black-main-clr);
}

@keyframes brandScroller {
    0% {
        transform: translateX(0%);
    }
    100% {
        transform: translateX(-100%);
    }
}

.s-brand-block {
    display: flex;
    align-items: center;
    gap: 5px;
    justify-content: center;
    min-width: fit-content;
    margin-right: 50px;
    padding-right: 10px;
}

.s-brand-block img {
    height: 50px;
}
<section class="services-brand">
        <div class="s-brand-wrapper scroller__inner">
            <div class="s-brand-block">
                <img src="{{ asset('images/3d-icons/web-development.png') }}" alt="web development">
                <span>Web Development</span>
            </div>
            <div class="s-brand-block">
                <img src="{{ asset('images/3d-icons/marketing.png') }}" alt="Digital Marketing">
                <span>Digital Marketing</span>
            </div>
            <div class="s-brand-block">
                <img src="{{ asset('images/3d-icons/content-creation.png') }}" alt="Content Creation">
                <span>Content Creation</span>
            </div>
            <div class="s-brand-block">
                <img src="{{ asset('images/3d-icons/design.png') }}" alt="Design/Brending">
                <span>Design/Brending</span>
            </div>
            <div class="s-brand-block">
                <img src="{{ asset('images/3d-icons/seo.png') }}" alt="Seo">
                <span>SEO</span>
            </div>
            <div class="s-brand-block">
                <img src="{{ asset('images/3d-icons/ux-ui.png') }}" alt="UX/UI">
                <span>UX/UI</span>
            </div>
        </div>
    </section>

I attempted to adjust the animation properties, such as the animation-direction, to create a smoother transition between the end and the beginning of the marquee animation. However, despite my efforts, the animation still restarted abruptly without the desired seamless loop.


Solution

  • the added s-brand-blocks do not exactly double the width of the s-brand-wrapper (I can't figure out why) - you can get around that by adding the widths of the added divs, then calculating the percentage for the animation end translate value - I used a CSS variable named --perc for that

    however, you have to wait for the images to be loaded before you can do that. Also, need to consider the right margin when performing this calculation

    Note: I used external images as per Mehdi's edit to your question (though, his edit was broken)

    Also note the margin-top:-100px was preventing the output from being visible in the snippet

    I created a function imagePromise that returns a Promise that resolves to the width of the added element once the image has loaded

    This is then used to calculate the width of the added elements - this width divided by the original width of the container is used as the percentage of the translate in the animation

    const scrollers = document.querySelectorAll(".services-brand");
    
    if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      addScrolling();
    }
    function imagePromise(item) {
      const img = item.querySelector('img')
      return new Promise((resolve, reject) => {
        img.addEventListener('load', () => resolve(item.getBoundingClientRect().width));
        img.addEventListener('error', reject);
      });
    }
    function addScrolling() {
        scrollers.forEach((scroller) => {
            scroller.setAttribute("data-animated", true);
            const scrollerInner = scroller.querySelector('.scroller__inner')
            const scrollerContent = Array.from(scrollerInner.children);
            const promises = scrollerContent.map((item) => {
                const duplicatedItem = item.cloneNode(true);
                duplicatedItem.setAttribute("aria-hidden", true);
                scrollerInner.appendChild(duplicatedItem);
                return imagePromise(duplicatedItem);
            });
            Promise.all(promises).then((v) => {
              // the `50` below is your margin-right in .s-brand-block
              const tot = v.reduce((acc, width) => acc + width + 50, 0);
              const perc = tot/scrollerInner.getBoundingClientRect().width * 100;
              scroller.style.setProperty("--perc", `-${perc}%`);
            })
        });
    }
    section.services-brand {
        width: 100%;
        background: white;
        position: relative;
        margin-top: 0px;
    }
    .services-brand[data-animated="true"] {
        overflow: hidden;
    }
    .services-brand[data-animated="true"] .s-brand-wrapper {
        flex-wrap: nowrap;
    }
    .s-brand-wrapper {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        padding: 20px 0px;
        animation: brandScroller 5s linear infinite;
    }
    .s-brand-block span {
        font-size: 20px;
        font-family: var(--main-hard-font);
        font-weight: 800;
        color: var(--black-main-clr);
    }
    
    @keyframes brandScroller {
        0% {
            transform: translateX(0%);
        }
        100% {
            transform: translateX(var(--perc));
        }
    }
    
    .s-brand-block {
        display: flex;
        align-items: center;
        gap: 5px;
        justify-content: center;
        min-width: fit-content;
        margin-right: 50px;
        padding-right: 10px;
    }
    
    .s-brand-block img {
        height: 50px;
    }
    <section class="services-brand">
            <div class="s-brand-wrapper scroller__inner">
                <div class="s-brand-block">
                    <img src="https://picsum.photos/id/9/5000/3269.jpg" alt="web development">
                    <span>Web Development</span>
                </div>
                <div class="s-brand-block">
                    <img src="https://picsum.photos/id/7/4728/3168.jpg" alt="Digital Marketing">
                    <span>Digital Marketing</span>
                </div>
                <div class="s-brand-block">
                    <img src="https://picsum.photos/id/4/5000/3333.jpg" alt="Content Creation">
                    <span>Content Creation</span>
                </div>
                <div class="s-brand-block">
                    <img src="https://picsum.photos/id/20/3670/2462.jpg" alt="Design/Brending">
                    <span>Design/Brending</span>
                </div>
                <div class="s-brand-block">
                    <img src="https://picsum.photos/id/1/5000/3333.jpg" alt="Seo">
                    <span>SEO</span>
                </div>
                <div class="s-brand-block">
                    <img src="https://picsum.photos/id/48/5000/3333.jpg" alt="UX/UI">
                    <span>UX/UI</span>
                </div>
            </div>
        </section>