Source

src/Marquee.js

import "../../css/components/_marquee.scss";
import gsap from "gsap";
import $ from "jquery";

/**
 * Marquee options
 * @typedef {Object} Marquee#MarqueeOptions
 * @property {number} speed
 * @property {('left'|'right')} direction
 * @property {boolean} pauseOnHover
 */

/**
 * Default marquee options
 * @type Marquee#MarqueeOptions
 * @defaultvalue
 * @private
 */
const _defaults = {
  speed: 1,
  direction: "left",
  pauseOnHover: false,
};
/**
 * @class
 * @example
 * const marquee = new Marquee($(".marquee"), {});
 * // or
 * const marquee = new Marquee(document.querySelector(".marquee")), {});
 */
class Marquee {
  /**
   * @constructor
   * @param {jQuery|HTMLElement} el - jQuery object or HTMLElement instance of the marquee wrapper
   * @param {Marquee#MarqueeOptions} [options = {}] - Options for the marquee
   */
  constructor(el, options = {}) {
    this.$el = $(el);
    this.options = { ..._defaults, ...options };

    if (this.options.pauseOnHover) {
      this.$el.hover(
        () => {
          this.tl.pause();
        },
        () => {
          this.tl.resume();
        },
      );
    }

    this.tl = gsap.timeline({
      repeat: -1,
      defaults: { ease: "none" },
      onReverseComplete: () =>
        this.tl.totalTime(this.tl.rawTime() + this.tl.duration() * 100),
      reversed: this.options.direction === "right",
    });

    this.refresh();

    if (this.options.direction === "right") {
      this.tl.vars.onReverseComplete();
      this.tl.reverse();
    }
  }

  /**
   * Refresh the marquee
   * @example
   * const marquee = new Marquee($(".marquee"), {});
   * marquee.refresh();
   */
  refresh() {
    if (!this.$el.length) return;
    this.$items = this.$el.children();
    gsap.set(this.$items, { x: 0 });

    const progress = this.tl.progress();

    // Kill and remove styles
    this.tl.revert();
    this.tl.clear();

    this.length = this.$items.length;
    this.startX = this.$items.eq(0).offset().left;
    this.widths = [];
    this.pixelsPerSecond = (this.options.speed || 1) * 100;

    const _this = this;
    let $item, i, distanceToStart, distanceToLoop;

    this.$items.map(function (i) {
      _this.widths[i] = $(this).outerWidth();
    });

    this.totalWidth =
      this.$items.eq(this.length - 1).offset().left -
      this.startX +
      this.$items.eq(this.length - 1).outerWidth();

    for (i = 0; i < this.length; i++) {
      $item = $(this.$items[i]);
      distanceToStart = $item.offset().left - this.startX;
      distanceToLoop = distanceToStart + this.widths[i];
      this.tl
        .to(
          $item,
          {
            xPercent: (-distanceToLoop / this.widths[i]) * 100,
            duration: distanceToLoop / this.pixelsPerSecond,
          },
          0,
        )
        .fromTo(
          $item,
          {
            xPercent:
              ((-distanceToLoop + this.totalWidth) / this.widths[i]) * 100,
          },
          {
            xPercent: 0,
            duration:
              (-distanceToLoop + this.totalWidth) / this.pixelsPerSecond,
            immediateRender: false,
          },
          distanceToLoop / this.pixelsPerSecond,
        );
    }

    // pre-render for performance
    this.tl.progress(1, true).progress(progress, true);
  }

  /**
   * Kill the marquee
   * @example
   * const marquee = new Marquee($(".marquee"), {});
   * marquee.kill();
   */
  kill() {
    this.tl.revert();
    this.tl = null;
    gsap.set(this.$items, { clearProps: "all" });
  }
}
export { Marquee as default };