Бесконечная прокрутка логотипов на чистом HTML и CSS

Источник: «Infinite-Scrolling Logos In Flat HTML And Pure CSS»
Помните HTML-элемент marquee? Он устарел, поэтому вряд ли стоит его использовать, когда понадобится что-то вроде горизонтальной автопрокрутки. Вот тут-то и приходит на помощь CSS, потому что в нём есть все необходимые инструменты для реализации этой функции.

Когда меня попросили сделать автопрокручивающуюся ферму логотипов, пришлось спросить себя: Вы имеете в виду, как <marquee>?. Это не самая странная просьба, но мысль о <marquee> навевает воспоминания о старых веб днях, когда правили Geocities. Что было дальше, повторяющийся сверкающий GIF-фон с единорогом?

Если у вас возникнет соблазн достать элемент <marquee>, не делайте этого. В MDN есть суровое предупреждение об этом в верхней части страницы:

Устарело: Эта функция больше не рекомендуется. Хотя некоторые браузеры всё ещё поддерживают её, возможно, она уже удалена из соответствующих веб-стандартов, может быть в процессе исключения или сохранена только в целях совместимости. Избегайте её использования и по возможности обновляйте существующий код […] Имейте в виду, что эта функция может перестать работать в любой момент.

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

Это нормально, потому что функцию бесконечной прокрутки, предлагаемую <marquee>, наверняка можно реализовать в CSS. Но когда я искал примеры, которые помогли бы мне сориентироваться, я с удивлением обнаружил очень мало информации по этому вопросу. Возможно, элементы с автоматической прокруткой сейчас не в моде. Возможно, сама природа поведения автопрокрутки является красным флажком доступности, чтобы отпугнуть нас.

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

Что мы создаём

Для начала — пример готового результата:

See the Pen

Идея довольно проста. Нам нужен некий контейнер, а в нем — серия бесконечно прокручиваемых изображений. Другими словами, когда последнее изображение скользит по экрану, необходимо, чтобы первое изображение в серии следовало непосредственно за ним в бесконечном цикле.

Итак, план таков: Сначала создадим HTML, затем разберёмся с контейнером и убедимся, что изображения в нём правильно размещены, после чего перейдём к написанию CSS-анимации, которая соберёт всё воедино.

Существующие примеры

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

«Sliding Background Effect» Geoff Graham близок к тому, что хотелось мне. Хотя он устарел, но помог увидеть, как можно намеренно использовать overflow, позволяя изображениям выскальзывать из контейнера, и анимацию, зацикливающуюся это до бесконечности. Однако это фоновое изображение, и оно опирается на суперспецифические числовые значения, что затрудняет повторное использование в других проектах.

See the Pen

Если в данном примере фоновое изображение не загружается. Используйте прокси или VPN.

Есть ещё один отличный пример от Coding Journey на CodePen:

See the Pen

Конечно, этот эффект — то, что нужно, но он использует немного JavaScript, и, хотя это всего лишь лёгкое вкрапление, хотелось бы обойтись без JavaScript.

«CSS Marquee Logo Wall» от Ryan Mulligan — самое близкое к этому решение. Это не только ферма логотипов с отдельными изображениями, но и демонстрация того, как можно использовать CSS маски, чтобы скрыть изображения, когда они вдвигаются и выдвигаются из контейнера. Я смог интегрировать эту же идею в свою работу.

See the Pen

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

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

HTML

Давайте прежде всего создадим HTML-структуру. Опять, я хочу, чтобы всё было как можно более просто, то есть очень мало элементов с максимально коротким семейным древом. Можно обойтись только контейнером marquee и изображениями логотипа в нем.

<figure class="marquee">
<img class="marquee__item" src="logo-1.png" width="100" height="100" alt="Company 1">
<img class="marquee__item" src="logo-2.png" width="100" height="100" alt="Company 2">
<img class="marquee__item" src="logo-3.png" width="100" height="100" alt="Company 3">
</figure>

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

Настройка контейнера

Flexbox — это, пожалуй, самый простой подход к созданию ряда изображений с промежутком между ними. Даже не нужно указывать ему направление потока в ряд, потому что он используется по умолчанию.

.marquee {
display: flex;
}

Я уже знаю, что планирую использовать абсолютное позиционирование для элементов изображения, поэтому имеет смысл установить относительное позиционирование для контейнера, чтобы, как вы понимаете, содержать их. А поскольку изображения абсолютно позиционируются, у них нет зарезервированных размеров высоты или ширины, влияющих на размер контейнера. Поэтому придётся явно объявить block-size (логический эквивалент height). Также нужна максимальная ширина, чтобы была граница, по которой изображения могут скользить и исчезать из виду, поэтому используем max-inline-size (логический эквивалент max-width):

.marquee {
--marquee-max-width: 90vw;

display: flex;
block-size: var(--marquee-item-height);
max-inline-size: var(--marquee-max-width);
position: relative;
}

Обратите внимание, что здесь используется несколько CSS переменных: одна определяет высоту marquee на основе высоты одного из изображений (--marquee-item-height), а ещё одна — максимальную ширину marquee (--marquee-max-width). Можно задать значение максимальной ширины marquee уже сейчас, но нужно будет формально зарегистрировать и присвоить значение высоты изображения, что мы и сделаем в ближайшее время. Мне просто нравится знать, с какими переменными я собираюсь работать по ходу дела.

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

.marquee {
--marquee-max-width: 90vw;

display: flex;
block-size: var(--marquee-item-height);
max-inline-size: var(--marquee-max-width);
overflow-x: hidden;
position: relative;
}

Мне очень понравился подход Ryan Mulligan к использованию CSS маски. Она создаёт впечатление, что изображения постепенно появляются и исчезают из виду. Давайте добавим это в микс:

.marquee {
display: flex;
block-size: var(--marquee-item-height);
max-inline-size: var(--marquee-max-width);
overflow-x: hidden;
position: relative;
mask-image: linear-gradient(
to right,
hsl(0 0% 0% / 0),
hsl(0 0% 0% / 1) 20%,
hsl(0 0% 0% / 1) 80%,
hsl(0 0% 0% / 0)
);
position: relative;
}

Вот что получилось на данный момент:

See the Pen

Позиционирование прокручиваемых элементов

Абсолютное позиционирование — это то, что позволяет выдернуть изображения из потока документа и вручную расположить их так, чтобы можно было запустить прокрутку.

.marquee__item {
position: absolute;
}

Из-за этого кажется, что изображения полностью исчезли. Но они есть — изображения расположены прямо друг на друге.

Помните CSS переменную для контейнера --marquee-item-height? Теперь её можно использовать для определения высоты marquee__item:

.marquee__item {
position: absolute;
inset-inline-start: var(--marquee-item-offset);
}

Чтобы вывести прокручиваемые изображения за пределы контейнера, нужно задать параметр --marquee-item-offset, но этот расчёт не является тривиальным, поэтому как это сделать, рассмотрим в следующем разделе. Мы знаем, какой должна быть animation: что-то, что движется линейно в течение определённого времени после начальной задержки, а затем бесконечно повторяется. Давайте добавим переменные.

.marquee__item {
position: absolute;
inset-inline-start: var(--marquee-item-offset);
animation: go linear var(--marquee-duration) var(--marquee-delay, 0s) infinite;
}

Для анимации бесконечной прокрутки элементов нужно определить две CSS переменные, одну для длительности (--marquee-duration) и ещё одну для задержки (--marquee-delay). Длительность может быть любой, но задержка должна быть вычисляемой, что и выясним в следующем разделе.

.marquee__item {
position: absolute;
inset-inline-start: var(--marquee-item-offset);
animation: go linear var(--marquee-duration) var(--marquee-delay, 0s) infinite;
transform: translateX(-50%);
}

Наконец, мы переместим элемент marquee на -50% по горизонтали. Этот небольшой «хак» позволяет справиться с ситуациями, когда размеры изображений неодинаковы.

See the Pen

Анимация изображений

Чтобы анимация работала, необходима следующая информация:

Давайте используем следующую конфигурацию для настройки переменных:

.marquee--8 {
--marquee-item-width: 100px;
--marquee-item-height: 100px;
--marquee-duration: 36s;
--marquee-items: 8;
}

Примечание: Я использую BEM-модификатор .marquee--8, чтобы задать анимацию восьми логотипов. Теперь можем определить ключевые кадры анимации, зная значение --marquee-item-width.

@keyframes go {
to {
inset-inline-start: calc(var(--marquee-item-width) * -1);
}
}

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

Теперь необходимо задать параметр --marquee-item-offset. Нужно, сдвинуть элемент прокрутки к правой стороне контейнера marquee, противоположной состоянию окончания анимации.

Может показаться, что смещение должно быть 100% + var(--marquee-item-width), но в этом случае логотипы будут накладываться друг на друга на маленьких экранах. Чтобы предотвратить это, необходимо знать минимальную ширину всех вместе взятых логотипов. Это можно сделать следующим образом:

calc(var(--marquee-item-width) * var(--marquee-items))

Но этого недостаточно. Если контейнер marquee слишком велик, логотипы будут занимать меньше максимального пространства, и смещение будет происходить внутри контейнера, что сделает логотипы видимыми внутри контейнера marquee. Чтобы предотвратить это, используем функцию max(), как показано ниже:

--marquee-item-offset: max(
calc(var(--marquee-item-width) * var(--marquee-items)),
calc(100% + var(--marquee-item-width))
);

Функция max() проверяет, какое из двух значений в её аргументах больше: общая ширина всех логотипов или максимальная ширина контейнера плюс ширина одного логотипа, которую мы определили ранее. Последнее будет верно на больших экранах, а первое — на маленьких.

See the Pen

Наконец, с помощью этой формулы зададим сложную задержку анимации (--marquee-delay):

--marquee-delay: calc(var(--marquee-duration) / var(--marquee-items) * (var(--marquee-items) - var(--marquee-item-index)) * -1);

Задержка равна длительности анимации, делённой на квадратичный полином (по крайней мере, так утверждает ChatGPT). Квадратичный полином — это следующая часть, в которой перемножаются количество элементов и количество элементов минус текущий индекс элемента:

var(--marquee-items) * (var(--marquee-items) - var(--marquee-item-index))

Обратите внимание, что используется отрицательная задержка (* -1), чтобы анимация началась, образно говоря, в прошлом. Осталось задать переменную --marquee-item-index (текущая позиция элемента области):

.marquee--8 .marquee__item:nth-of-type(1) {
--marquee-item-index: 1;
}
.marquee--8 .marquee__item:nth-of-type(2) {
--marquee-item-index: 2;
}

/* и т.д. */

.marquee--8 .marquee__item:nth-of-type(8) {
--marquee-item-index: 8;
}

Вот снова финальная демонстрация:

See the Pen

Чувствительность к движению

Хотя анимация не самая сложная и дикая, она всё же может стать триггером для тех, у кого чувствительность к движению вызвана расстройством вестибулярного аппарата. Можно замедлить или устранить анимацию с помощью медиа-запроса prefers-reduced-motion:

@media (prefers-reduced-motion) {
.marquee__item {
animation-play-state: paused;
}
}

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

@media (prefers-reduced-motion) {
.marquee {
justify-content: space-evenly;
mask-image: unset;
}

.marquee__item {
position: unset;
inset-inline-start: unset;
transform: unset;
}

@keyframes go {
to {
inset-inline-start: unset;
}
}
}

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

Дополнительные усовершенствования

Это решение могло бы быть лучше, особенно если логотипы не одинаковой ширины. Чтобы скорректировать промежутки между изображениями несоответствующего размера, можно было бы более точно рассчитать задержку анимации. Это возможно, поскольку анимация линейна. Я попытался найти формулу, но, как видите, она нуждается в более тонкой настройке:

See the Pen

Ещё одно улучшение, которого можно добиться с помощью тонкой настройки, — это предотвращение больших разрывов на широких экранах. Для этого установите max-inline-size и объявите margin-inline: auto для контейнера .marquee:

See the Pen

Заключение

Что вы думаете? Это то, что вы можете использовать в своём проекте? Вы бы использовали другой подход? Я всегда рад, когда мне попадается что-то с чистой HTML структурой и чистом CSS решением. Окончательную реализацию вы можете увидеть на сайте Heyflow.

Дополнительные материалы

Предыдущая Статья

Магические методы PHP

Следующая Статья

Происходит ли утечка информации через ваш Referer