1. Главная страница
  2. ››
  3. Документация
  4. ››
  5. Создание бесшовной бегущей строки изображений с помощью GSAP

Навигация

Разделы доки

Demo компонента

Создание бесшовной бегущей строки изображений с помощью GSAP

Мой опыт решения проблемы наложения

Недавно я столкнулся с задачей создания обновления бесшовной бегущей строки изображений на сайте. Изначальный скрипт был достаточно старый и приводил к тому, что изображения выходящие за пределы контейнера накладывались друг на друга. Поэтому я решил переписать его с использованием библиотеки GSAP (GreenSock Animation Platform). Первоначально я нашёл скрипт, который должен был реализовать эту функциональность, но столкнулся с проблемой: когда количество изображений превышало ширину экрана, они опять начинали накладываться друг на друга. В этой статье я расскажу о том, как я решил эту проблему, и поделюсь полученным опытом.

Исходная проблема

Изначально мой код выглядел следующим образом:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Бегущая строка с GSAP</title>
    <style>
        /* Стили */
    </style>
    <!-- Подключение GSAP -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
</head>
<body>
<div class="container">
    <div class="marquee" wb-data="marquee" duration="20">
        <div class="marquee-content">
            <!-- Изображения -->
        </div>
    </div>
</div>

<script>
    // JavaScript-код для бегущей строки
</script>
</body>
</html>

Проблема заключалась в том, что при добавлении большого количества изображений они начинали накладываться друг на друга вместо того, чтобы плавно прокручиваться.

Анализ и поиск решения

1. Вычисление ширины содержимого

Первым шагом было понять, почему изображения накладываются друг на друга. Я заметил, что при расчёте ширины содержимого бегущей строки использовался getComputedStyle, который не всегда возвращает полную ширину элемента, особенно если часть содержимого выходит за пределы видимой области.

Решение: заменить использование getComputedStyle на свойство scrollWidth, которое возвращает полную ширину элемента, включая невидимые части.

Изменения в коде:

const width = marqueeContent.scrollWidth;

2. Обработка загрузки изображений

Далее я столкнулся с тем, что изображения могут не успеть загрузиться к моменту выполнения скрипта, что приводит к неправильному расчёту ширины.

Решение: убедиться, что скрипт выполняется после полной загрузки всех изображений. Для этого я заменил событие DOMContentLoaded на window.addEventListener('load', init);.

3. Проблемы с фреймворком Creatium

Однако мой фреймворк (Creatium) не позволял использовать событие load должным образом, и скрипт перестал работать. Я заметил, что мой скрипт вообще не выполняется, и в консоли отсутствуют сообщения console.log.

Решение: реализовать динамическую загрузку библиотеки GSAP с помощью промисов и выполнить основной скрипт после успешной загрузки библиотеки.

Код для динамической загрузки GSAP:

function loadScript(src) {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = src;
        script.onload = () => resolve();
        script.onerror = () => reject(new Error(`Failed to load script ${src}`));
        document.head.appendChild(script);
    });
}

loadScript('https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js')
    .then(() => {
        // Ваш код инициализации
    })
    .catch(error => console.error(error));

4. Предотвращение наложения изображений

После того как скрипт начал выполняться, проблема с наложением изображений всё ещё сохранялась. Я решил обратить внимание на CSS-стили.

Решение:

  • Добавить flex-shrink: 0; к изображениям, чтобы они не сжимались внутри Flex-контейнера.
  • Использовать margin-right вместо gap для создания отступов между изображениями, учитывая кросс-браузерную совместимость.

Изменения в стилях:

.marquee-content {
    display: flex;
    /* gap: 1rem; */ /* Убираем для кросс-браузерности */
}

.marquee-content img {
    flex-shrink: 0;
    margin-right: 1rem;
}

.marquee-content img:last-child {
    margin-right: 0;
}

5. Убедиться в загрузке всех изображений

Чтобы гарантировать правильный расчёт ширины бегущей строки, я добавил функцию, которая отслеживает загрузку всех изображений внутри marquee-content.

Код функции imagesLoaded:

function imagesLoaded(container, callback) {
    const images = container.getElementsByTagName('img');
    let loaded = 0;
    const count = images.length;

    if (count === 0) {
        callback();
        return;
    }

    for (let i = 0; i < count; i++) {
        if (images[i].complete) {
            incrementCounter();
        } else {
            images[i].addEventListener('load', incrementCounter);
            images[i].addEventListener('error', incrementCounter);
        }
    }

    function incrementCounter() {
        loaded++;
        if (loaded === count) {
            callback();
        }
    }
}

6. Решение проблемы с шириной элементов

Я заметил, что элементы marquee и marquee-content имеют ширину меньше ожидаемой, что приводит к неправильному отображению.

Решение: убедиться, что родительские элементы не ограничивают ширину дочерних элементов, и установить для них width: 100%; и overflow: hidden;.

Изменения в стилях:

.marquee-container {
    width: 100%;
    overflow: hidden;
}

.marquee {
    display: flex;
    overflow: hidden;
    width: 100%;
}

.marquee-content {
    display: flex;
    flex-wrap: nowrap;
    flex-shrink: 0;
    width: auto;
}

7. Упрощение кода

После успешного решения проблемы я решил упростить код, убрав проверку загрузки изображений, так как скрипт теперь выполнялся после полной загрузки страницы, и изображения успевали загрузиться.

8. Добавление возможности управления направлением и скоростью

Мне потребовалось добавить управление направлением и скоростью движения бегущей строки через параметры params.direction и params.speed.

Решение:

  • Использовать параметры для задания направления ('left' или 'right') и скорости.
  • Рассчитать длительность анимации на основе скорости и длины содержимого, чтобы скорость не зависела от количества элементов.

Изменения в коде:

const direction = params.direction || 'left';
const speed = parseFloat(params.speed) || 50;

const totalDistance = width;
const duration = totalDistance / speed;

let fromX, toX;

if (direction === 'left') {
    fromX = 0;
    toX = -totalDistance;
} else if (direction === 'right') {
    fromX = -totalDistance;
    toX = 0;
}

tween = gsap.fromTo(
    marquee.children,
    { x: fromX },
    { x: toX, duration, ease: "none", repeat: -1 }
);

Итоговый код

JavaScript:

    function loadScript(src) {
        return new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = src;
            script.onload = () => resolve();
            script.onerror = () => reject(new Error(`Failed to load script ${src}`));
            document.head.appendChild(script);
        });
    }

    function init() {
        const marquee = document.querySelector('[wb-data="marquee"]');
        if (!marquee) return;
        const marqueeContent = marquee.querySelector(".marquee-content");
        if (!marqueeContent) return;

        const direction = params.direction || 'left';
        let speed = parseFloat(params.speed);
        if (isNaN(speed) || speed < 1) speed = 1;
        if (speed > 100) speed = 100;

        const marqueeContentClone = marqueeContent.cloneNode(true);
        marquee.append(marqueeContentClone);

        let tween;

        const playMarquee = () => {
            let progress = tween ? tween.progress() : 0;
            tween && tween.progress(0).kill();

            const width = marqueeContent.getBoundingClientRect().width;
            const totalDistance = width;
            const duration = totalDistance / speed;

            let fromX, toX;

            if (direction === 'left') {
                fromX = 0;
                toX = -totalDistance;
            } else if (direction === 'right') {
                fromX = -totalDistance;
                toX = 0;
            }

            tween = gsap.fromTo(
                marquee.children,
                { x: fromX },
                { x: toX, duration, ease: "none", repeat: -1 }
            );
            tween.progress(progress);
        };
        playMarquee();

        window.addEventListener("resize", playMarquee);
    }

    loadScript('https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js')
        .then(() => {
            if (document.readyState === 'complete') {
                init();
            } else {
                window.addEventListener('load', init);
            }
        })
        .catch(error => console.error(error));

CSS:

.marquee-container {
    width: 100%;
    overflow: hidden;
}

.marquee {
    display: flex;
    overflow: hidden;
    width: 100%;
}

.marquee-content {
    display: flex;
    flex-wrap: nowrap;
    flex-shrink: 0;
    width: auto;
}

.marquee-content img {
    flex-shrink: 0;
    margin-right: 1rem;
}

.marquee-content img:last-child {
    margin-right: 0;
}

Заключение

В конце хочу дать ссылку на исправленный компонент. Надеюсь он будет корректно работать и позволит вам реализовывать задуманные решения на сайтах клиентов.

Logo Upline Studio Creatium

Наша команда Uplinestudio разрабатывает сайты под разные потребности клиентов. Одним из ключевых направлений в нашей разработке является создание сайтов на Creatium.

Оставить заявку

Работает на Creatium