Недавно я столкнулся с задачей создания обновления бесшовной бегущей строки изображений на сайте. Изначальный скрипт был достаточно старый и приводил к тому, что изображения выходящие за пределы контейнера накладывались друг на друга. Поэтому я решил переписать его с использованием библиотеки 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>
Проблема заключалась в том, что при добавлении большого количества изображений они начинали накладываться друг на друга вместо того, чтобы плавно прокручиваться.
Первым шагом было понять, почему изображения накладываются друг на друга. Я заметил, что при расчёте ширины содержимого бегущей строки использовался getComputedStyle
, который не всегда возвращает полную ширину элемента, особенно если часть содержимого выходит за пределы видимой области.
Решение: заменить использование getComputedStyle
на свойство scrollWidth
, которое возвращает полную ширину элемента, включая невидимые части.
Изменения в коде:
const width = marqueeContent.scrollWidth;
Далее я столкнулся с тем, что изображения могут не успеть загрузиться к моменту выполнения скрипта, что приводит к неправильному расчёту ширины.
Решение: убедиться, что скрипт выполняется после полной загрузки всех изображений. Для этого я заменил событие DOMContentLoaded
на window.addEventListener('load', init);
.
Однако мой фреймворк (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));
После того как скрипт начал выполняться, проблема с наложением изображений всё ещё сохранялась. Я решил обратить внимание на 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;
}
Чтобы гарантировать правильный расчёт ширины бегущей строки, я добавил функцию, которая отслеживает загрузку всех изображений внутри 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();
}
}
}
Я заметил, что элементы 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;
}
После успешного решения проблемы я решил упростить код, убрав проверку загрузки изображений, так как скрипт теперь выполнялся после полной загрузки страницы, и изображения успевали загрузиться.
Мне потребовалось добавить управление направлением и скоростью движения бегущей строки через параметры 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;
}
В конце хочу дать ссылку на исправленный компонент. Надеюсь он будет корректно работать и позволит вам реализовывать задуманные решения на сайтах клиентов.
Работает на Creatium