Ad

How Do I Make An Infinite Marquee That Speeds Up On Scroll?

I'm trying to make an Infinite marquee that speeds up on scroll, https://altsdigital.com/ you can see the effect on this website, the text says "Not your usual SEO agency" and when you scroll it speeds up.

Here's what I've tried but it does not work. It does not loop properly without overlapping (keep your eye on the left side of the page, you'll notice the text briefly overlaps and then translates left to create a gap) and I am unsure on how to fix it:

Here's the code (TEXT ONLY VISIBLE ON "FULL PAGE" view):

const lerp = (current, target, factor) => {
    let holder = current * (1 - factor) + target * factor;
    holder = parseFloat(holder).toFixed(3);
    return holder;
};

class LoopingText {
    constructor(DOMElements) {
        this.DOMElements = DOMElements;
        this.lerpingData = {
            counterOne: { current: 0, target: 0 },
            counterTwo: { current: 100, target: 100 },
        };
        this.interpolationFactor = 0.1;
        this.speed = 0.2;
        this.render();
        this.onScroll();
    }

    onScroll() {
        window.addEventListener("scroll", () => {
            this.lerpingData["counterOne"].target += this.speed * 5;
            this.lerpingData["counterTwo"].target += this.speed * 5;
        });
    }

    lerp() {
        for (const counter in this.lerpingData) {
            this.lerpingData[counter].current = lerp(
                this.lerpingData[counter].current,
                this.lerpingData[counter].target,
                this.interpolationFactor
            );
        }

        this.lerpingData["counterOne"].target += this.speed;
        this.lerpingData["counterTwo"].target += this.speed;

        if (this.lerpingData["counterOne"].target < 100) {
            this.DOMElements[0].style.transform = `translate(${this.lerpingData["counterOne"].current}%, 0%)`;
        } else {
            this.lerpingData["counterOne"].current = -100;
            this.lerpingData["counterOne"].target = -100;
        }

        if (this.lerpingData["counterTwo"].target < 100) {
            this.DOMElements[1].style.transform = `translate(${this.lerpingData["counterTwo"].current}%, 0%)`;
        } else {
            this.lerpingData["counterTwo"].current = -100;
            this.lerpingData["counterTwo"].target = -100;
        }
    }

    render() {
        this.lerp();

        window.requestAnimationFrame(() => this.render());
    }
}

let textArray = document.getElementsByClassName("item");
new LoopingText(textArray);
@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,[email protected],100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: "Poppins";
}

.hero-section {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100vh;
    overflow: hidden;
    position: relative;
    width: 100%;
}

.loop-container {
    position: relative;
    width: 100%;
    display: flex;
    /* padding-right: 24px; */
}

.item {
    position: absolute;
    font-size: 15rem;
    white-space: nowrap;
    margin: 0;
}

span {
    transition: all 0.2s;
    cursor: default;
}

.hover:hover {
    color: gray;
    transition: all 0.2s;
}
<body>
    <section class="hero-section">
        <div class="loop-container">
            <div class="item">Infinite Horizontal Looping Text</div>
            <div class="item">Infinite Horizontal Looping Text</div>
        </div>
    </section>
    <section class="hero-section">
    </section>
</body>

Ad

Answer

Your items are overlapping because you're not allowing any lerping diffing when the items should switch positions.

The current value should never equal the target value. If the values match, than the current value needs to catch up or when the target giving that erratic movement and wrong calculations, additionally aggravated for the two sibling elements — which should be perfectly in sync to give that immediate snap-back - perceived as a fluid continuous motion.

Solution

  • Instead of animating two children independently,
    animate only the parent container .loop-container.
  • The container should be as wide as one child element exactly.
  • "Push" one child element to the far left using position: absolute; left: -100%
  • To allow the target value to be always greater than the current value:
    when the target value is greater than 100 — set current to the negative difference of the two values, and target to 0

Demo time:

const lerp = (current, target, factor) => current * (1 - factor) + target * factor;

class LoopingText {
  constructor(el) {
    this.el = el;
    this.lerp = {current: 0, target: 0};
    this.interpolationFactor = 0.1;
    this.speed = 0.2;
    this.events();
    this.render();
  }

  events() {
    window.addEventListener("scroll", () => this.lerp.target += this.speed * 5);
  }

  animate() {
    this.lerp.target += this.speed;
    this.lerp.current = lerp(this.lerp.current, this.lerp.target, this.interpolationFactor);

    if (this.lerp.target > 100) {
      this.lerp.current -= this.lerp.target;
      this.lerp.target = 0;
    }

    this.el.style.transform = `translateX(${this.lerp.current}%)`;
  }

  render() {
    this.animate();
    window.requestAnimationFrame(() => this.render());
  }
}

document.querySelectorAll(".loop-container").forEach(el => new LoopingText(el));
/* QuickReset */ * { margin: 0; box-sizing: border-box; }

body { min-height: 400vh;  /* force some scrollbars */ }

.hero-section {
  position: relative;
  overflow: hidden;
  display: flex;
  min-height: 100vh;
}

.loop-container {
  margin: auto 0;
  white-space: nowrap;
  font: 900 9vw/1 sans-serif;
}

.item:first-child {
  position: absolute;
  left: -100%;
  top: 0;
}
<section class="hero-section">
  <div class="loop-container">
    <div class="item">Infinite Horizontal Looping Text&nbsp;</div>
    <div class="item">Infinite Horizontal Looping Text&nbsp;</div>
  </div>
</section>

PS:

When animating, (unless you want an element static / immovable) you should never put an elements transformations inside an if/else logic. The element should always receive the updated transformations. Put inside the conditional logic only the values that you actually want to modify (as I did in the example above).

Ad
source: stackoverflow.com
Ad