Marquee

https://www.react-fast-marquee.com/demo

如果不考虑兼容问题 建议使用css animation

enum Direction {
    Left = "left",
    Right = "right",
}
interface MarqueeOption {
    direction?: Direction,
    pauseOnHover: boolean,
    speed: number,
    el: HTMLElement
}

export class Marquee {
    private direction: Direction;
    private pauseOnHover: boolean = false;

    private speed: number = 0;
    private x: number = 0;
    private y: number = 0;
    private el: HTMLElement;
    private wrapper!: HTMLElement;
    private itemWidth: number = 0

    private pause = false;
    private rafId: number | null = null
    constructor(option: MarqueeOption) {

        this.direction = option?.direction ?? Direction.Left;

        this.speed = option.speed ?? 2

        this.pauseOnHover = option.pauseOnHover ?? false

        this.el = option.el

        this.init()
    }

    private init() {
        // 1. 此时 el 里面还是你原始的 HTML
        // 设置 whiteSpace 确保内容不换行,否则 scrollWidth 测量不准
        this.el.style.whiteSpace = 'nowrap';
        this.el.style.display = 'block';

        // 2. 直接获取原始内容的真实滚动宽度
        this.itemWidth = this.el.scrollWidth;

        // 3. 现在再进行克隆和包裹逻辑
        const content = this.el.innerHTML;

        this.wrapper = document.createElement('div');
        this.wrapper.style.display = 'inline-flex';
        this.wrapper.innerHTML = content + content + content; // 克隆

        this.el.innerHTML = '';
        this.el.appendChild(this.wrapper);

        // 4. 设置初始位置
        if (this.direction === Direction.Right) {
            this.x = -this.itemWidth;
        }

        this.start();

        if (this.pauseOnHover) {
            this.el.addEventListener('mouseenter', () => {
                this.stop()
            })
            this.el.addEventListener('mouseleave', () => {
                this.start()
            })

        }
    }

    private loop = () => {

        switch (this.direction) {
            case Direction.Left:
                this.x -= this.speed;
                // If we've scrolled past the first element's width, jump back to 0
                if (Math.abs(this.x) >= this.itemWidth) {
                    this.x = 0;
                }
                break;

            case Direction.Right:
                this.x += this.speed;
                // If we've moved right and reached 0, jump back to the negative width
                if (this.x >= 0) {
                    this.x = -this.itemWidth;
                }
                break;
        }

        // Apply transform to all children
        for (let i = 0; i < this.el.children.length; i++) {
            const el = this.el.children[i] as HTMLElement;
            el.style.transform = `translate3d(${this.x}px, ${this.y}px, 0)`;
        }

        if (this.pause) return;
        this.rafId = requestAnimationFrame(this.loop)
    }

    public start() {
        this.pause = false
        this.loop()
    }
    public stop() {
        this.pause = true
        if (this.rafId) cancelAnimationFrame(this.rafId);
    }
}