В предыдущей статье у нас получился красивый слайдер («карусель») с круговым вращением. А сегодня я создам слайдер, пролистывающий стопку «полароидных» снимков.


Пока не смотрите код, сначала я должен вам многое про него рассказать. Поехали! К старту нашего курса по Fullstack-разработке на Python.


Основные настройки


Большую часть HTML и CSS для этого проекта я возьму из кода кругового слайдера из предыдущей статьи. HTML-разметка абсолютно та же:


<div class="gallery">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
</div>

А ниже — исходный CSS родительского контейнера .gallery. Контейнер — это грид, где изображения находятся в стопке одно над другим:


.gallery  {
  display: grid;
  width: 220px; /* контроль размера контейнера */
}
.gallery > img {
  grid-area: 1 / 1;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border: 10px solid #f2f2f2;
  box-shadow: 0 0 4px #0007;
}

Пока ничего сложного. Для эффекта «полароидности» я использую свойства border и box-shadow. Попробуйте поиграть со стилями, возможно, вам удастся добиться большего! Я же приступлю к самому сложному — к анимации.


В чём фокус?


Логика слайдера основана на порядке размещения изображений в стопке. Да, мы будем работать с z-index. Сначала у всех картинок будет один и тот же z-index, равный 2. Так последнее изображение будет наверху стопки.


Затем последняя картинка смещается вправо, пока не появится следующая. После z-index картинки уменьшается и она помещается обратно в стопку. Теперь её z-index меньше, чем у других изображений, и она перемещается вниз стопки.


Посмотрите на упрощённую демоверсию слайдера. Наведите мышь на изображение, чтобы увидеть анимацию.


Так же нужно поступить с остальными картинками. Вот алгоритм с псевдоселектором :nth-child() для разных изображений:


  • Смещаем последнюю картинку (N). Становится видна следующая (N − 1).
  • Смещаем следующую картинку (N − 1). Становится видна ещё одна (N − 2).
  • Смещаем следующую картинку (N − 2). Становится видна ещё одна (N − 3).
  • (Продолжаем, пока не увидим первую картинку)
  • Смещаем первое изображение (1) и снова видим последнее (N).

Бесконечный слайдер готов!


Разбираемся с анимацией


В предыдущей статье я написал лишь одну анимацию и проигрывал её с различными задержками для каждого изображения. Здесь будет такой же подход. Для начала давайте представим временную шкалу анимации сначала для трёх картинок, а затем и для любого их числа (N).


Схема трех этапов анимации


Анимация разбита на три этапа — «смещение вправо», «смещение влево» и «стоп». Задержки между изображениями определяются так: если анимация первой картинки начинается в 0s (0 с), а её продолжительность — 6s (6 с), то анимация второй картинки начинается в -2s (минус 2 с), а третьей — -4s (минус 4 с).


.gallery > img:nth-child(2) { animation-delay: -2s; } /* -1 * 6с / 3 */
.gallery > img:nth-child(3) { animation-delay: -4s; } /* -2 * 6с / 3 */

Также видно, что этап «стоп» занимает две трети времени всей анимации (2*100%/3), а «смещение вправо» и «смещение влево» в сумме занимают треть времени. Следовательно, длительность каждого смещения равна 100%/6 от времени всей анимации.


Ключевые кадры анимации выглядят так:


@keyframes slide {
  0%     { transform: translateX(0%); }
  16.67% { transform: translateX(120%); }
  33.34% { transform: translateX(0%); }
  100%   { transform: translateX(0%); } 
}

120% — произвольная цифра. Нужно значение больше 100%. Изображения должны смещаться вправо за пределы границ других картинок. Необходимо сместить изображения по крайней мере на 100% от их размера. Вот почему я выбрал число 120% — чтобы получить немного дополнительного места.


Далее нужно прописать значения z-index. Помните, что необходимо обновлять z-indexкартинки после того, как она сместится вправо.


@keyframes slide {
  0%     { transform: translateX(0%);   z-index: 2; }
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; } /* здесь обновляем порядок наложения по оси z */
  33.34% { transform: translateX(0%);   z-index: 1; }
  100%   { transform: translateX(0% );  z-index: 1; }  
}

Вместо того чтобы определить одно состояние на точке временной линии, равной 16.67% (100%/6), определим два состояния почти в одной и той же точке (16.66 и 16.67%), где z-index уменьшается перед тем, как изображение смещается обратно в стопку.


Вот что произойдёт после того, как мы соединим всё вместе.


Странно, смещение работает нормально, но порядок изображений в стопке неправильный. Анимация начинается правильно, верхнее изображение перемещается назад… Но следующие изображения не следуют этому примеру. Обратите внимание на вторую картинку — она возвращается на верх стопки, а затем следующее фото появляется поверх неё.


Давайте внимательно посмотрим на изменения z-index. Изначально у всех изображений z-index равен 2. Это значит, что порядок картинок в стопке выглядит так…


Наш взгляд ???? --> третье фото (2) | второе фото (2) | первое фото (2)

Третья картинка смещается, её z-index обновляется. Теперь порядок картинок таков:


Наш взгляд ???? --> второе фото (2) | первое фото (2) | третье фото (1)

То же самое делаем со второй картинкой:


Наш взгляд ???? --> первое фото (2) | третье фото (1) | второе фото (1)

…и с первой:


Наш взгляд ???? --> третье фото (1) | второе фото (1) | первое фото (1)

Вроде бы всё должно быть в порядке, но на самом деле нет! Когда первая картинка перемещается назад, третья начинает новую итерацию, что означает, что её z-index снова становится равным 2:


Наш взгляд ???? --> третье фото (2) | второе фото (1) | первое фото (1)

На самом деле все изображения никогда не получали значение z-index, равное 2! Когда изображения неподвижны (к примеру, на этапе «стоп»), z-index равен 1. Если третье фото смещается, а его z-index изменяется с 2 до 1, оно остается поверх стопки! Если у всех изображений одинаковый `z-index, последнее в исходной последовательности — в данном случае третье — оказывается сверху. Смещение третьего изображения приводит к следующему:


Наш взгляд ???? --> третье фото (1) | второе фото (1) | первое фото (1)

Третье изображение всё ещё поверх стопки, а следом наверх перемещается второе, когда его анимация перезапускается при z-index: 2:


Наш взгляд ???? --> второе фото (2) | третье фото (1) | первое фото (1)

Когда оно смещается, получается вот что:


Наш взгляд ???? --> третье фото (1) | второе фото (1) | первое фото (1)

А затем сверху оказывается первая картинка:


Наш взгляд ???? --> первое фото (2) | третье фото (1) | второе фото (1)

Я запутался. Получается, что вся логика неверна?


Знаю, это сложно. Но наша логика отчасти правильна. Нужно немного изменить анимацию, и всё заработает. Фокус в том, чтобы верно сбросить значение z-index.


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


Наш взгляд ???? --> третье фото (2) | второе фото (1) | первое фото (1)

После смещения третьей картинки и изменения её z-indexона остаётся сверху. Нам же необходимо обновить z-index второй картинки. Давайте перед смещением третьего изображения за пределы стопки сделаем z-index второго изображения равным 2.


Другими словами, z-index второй картинки сбрасывается перед окончанием анимации.


Диаграмма этапов анимации с обозначениями того, где z-index уменьшается или увеличивается


Зелёный плюс означает увеличение z-index до 2, а красный минус — уменьшение до 1. Второе фото сначала имеет z-index, равный 2, а при смещении из стопки он уменьшается до 1. Но, прежде чем первое изображение смещается из стопки, сделаем z-index второго изображения равным 2. Таким образом, z-index первого и z-index второго изображения получаются одинаковыми. Но третье изображение всё равно окажется сверху, так как в DOM оно появляется позже. После смещения третьего фото и обновления его z-indexоно оказывается внизу стопки.


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


@keyframes slide {
  0%     { transform: translateX(0%);   z-index: 2; }
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; } /* здесь обновляем порядок наложения по оси z... */
  33.34% { transform: translateX(0%);   z-index: 1; }
  66.33% { transform: translateX(0%);   z-index: 1; }
  66.34% { transform: translateX(0%);   z-index: 2; } /* ...а также здесь */
  100%   { transform: translateX(0%);   z-index: 2; }  
}

Уже лучше, но все ещё не совсем


Это никогда не кончится


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


Когда первое фото находится сверху, происходит следующее:


Наш взгляд ???? -->  первое фото (2) | третье фото (1) | второе фото (1)

После предыдущего изменения третье изображение появится сверху перед смещением первого. Так происходит только сейчас, потому что следующее изображение, которое двигается после первого, — это последнее изображение, которое находится выше в DOM. Остальные фото работают, как нужно, потому что сначала идёт N, затем — N - 1, потом 3 меняется на 2, а 2 на 1… Но затем происходит переход от 1 к N.


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


@keyframes slide-last {
  0%     { transform: translateX(0%);   z-index: 2;}
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; } /* здесь обновляем порядок наложения по оси z... */
  33.34% { transform: translateX(0%);   z-index: 1; }
  83.33% { transform: translateX(0%);   z-index: 1; }
  83.34% { transform: translateX(0%);   z-index: 2; } /* ...а также здесь */
  100%   { transform: translateX(0%);   z-index: 2; }
}

z-index сбрасывается на 5/6 анимации (а не на 2/3), когда первое фото находится вне стопки. Больше никакого выпрыгивания картинки!


Ура. Теперь всё работает безупречно! Вот окончательный код во всём его великолепии:


.gallery > img {
  animation: slide 6s infinite;
}
.gallery > img:last-child {
  animation-name: slide-last;
}
.gallery > img:nth-child(2) { animation-delay: -2s; } 
.gallery > img:nth-child(3) { animation-delay: -4s; }

@keyframes slide {
  0% { transform: translateX(0%); z-index: 2; }
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; } 
  33.34% { transform: translateX(0%); z-index: 1; }
  66.33% { transform: translateX(0%); z-index: 1; }
  66.34% { transform: translateX(0%); z-index: 2; } 
  100% { transform: translateX(0%); z-index: 2; }
}
@keyframes slide-last {
  0% { transform: translateX(0%); z-index: 2; }
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; }
  33.34% { transform: translateX(0%); z-index: 1; }
  83.33% { transform: translateX(0%); z-index: 1; }
  83.34% { transform: translateX(0%); z-index: 2; } 
  100%  { transform: translateX(0%); z-index: 2; }
}

Поддержка любого числа изображений


Теперь, когда анимация правильно отображает три фотографии, давайте перепишем код так, чтобы он работал с любым числом (N) картинок. Но сначала оптимизируем и уберём избыточный код. Для этого разделяем анимацию:


.gallery > img {
  z-index: 2;
  animation: 
    slide 6s infinite,
    z-order 6s infinite steps(1);
}
.gallery > img:last-child {
  animation-name: slide, z-order-last;
}
.gallery > img:nth-child(2) { animation-delay: -2s; } 
.gallery > img:nth-child(3) { animation-delay: -4s; }

@keyframes slide {
  16.67% { transform: translateX(120%); }
  33.33% { transform: translateX(0%); }
}
@keyframes z-order {
  16.67%,
  33.33% { z-index: 1; }
  66.33% { z-index: 2; }
}
@keyframes z-order-last {
  16.67%,
  33.33% { z-index: 1; }
  83.33% { z-index: 2; }
}

Теперь кода намного меньше! Я создал отдельную анимацию для смещения, а другую — для обновления z-index. Обратите внимание, что при анимации z-index используется steps(1). Я использую steps(1), потому что хочу резко изменить значение z-index, а не постепенно, как в анимации сдвига.


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


@for $i from 2 to ($n + 1) {
  .gallery > img:nth-child(#{$i}) {
    animation-delay: calc(#{(1 — $i)/$n}*6s);
  }
}

От ванильного CSS переходим к SASS. Затем представим, как временная шкала изменяется при наличии N количества изображений. Не стоит забывать, что анимация состоит из трёх этапов:


Демонстрация трех этапов анимации с помощью стрелок.


После «смещения вправо» и «смещения влево» изображение должно оставаться неподвижным, пока другие картинки не пройдут через анимации. Так что этап «стоп» должен длиться столько же, сколько и этапы (N − 1) «смещения вправо» и «смещения влево». В течение одной итерации «сместятся» N изображений. Таким образом, «смещение вправо» и «смещение влево» вместе длятся 100%/N от общего времени анимации. Изображение выходит из стопки в (100%/N)/2 и входит обратно в стопку в 100%/N.


Этот код:


@keyframes slide {
  16.67% { transform: translateX(120%); }
  33.33% { transform: translateX(0%); }
}

заменяем на этот:


@keyframes slide {
  #{50/$n}%  { transform: translateX(120%); }
  #{100/$n}% { transform: translateX(0%); }
}

Если заменить N на 3, получается 16.67 и 33.33%, если в стопке три фотографии. Применяя такую же логику для порядка наложения, получаем следующее:


@keyframes z-order {
  #{50/$n}%,
  #{100/$n}% { z-index: 1; }
  66.33% { z-index: 2; }
}

Нам всё ещё нужно обновить значение 66.33%. В этой точке сбрасывается z-index изображения перед окончанием анимации и одновременно следующее изображение начинает смещаться. Смещение занимает 100%/N, а сброс z-index происходит в 100% — 100%/N:


@keyframes z-order {
  #{50/$n}%,
  #{100/$n}% { z-index: 1; }
  #{100 — 100/$n}% { z-index: 2; }
}

Но для работы анимации z-order-last сброс z-index должен произойти чуть позже. Помните о фиксе для последней фотографии? Сброс z-index должен происходить, когда первая картинка находится вне стопки, а не когда она начинает смещение. Здесь используется схожий подход:


@keyframes z-order-last {
  #{50/$n}%,
  #{100/$n}% { z-index: 1; }
  #{100 — 50/$n}% { z-index: 2; }
}

Готово! Вот слайдер с пятью картинками:


Для красоты добавим поворот:



Я всего лишь добавил rotate(var(--r)) к свойству transform. В цикле --r — случайный угол:


@for $i from 1 to ($n + 1) {
  .gallery > img:nth-child(#{$i}) {
    --r: #{(-20 + random(40))*1deg}; /* случайный угол между -20 и 20 градусами */
  }
}

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


Заключение


Вся работа с z-index потребовала точной настройки. Если раньше вы не были уверены, как работает порядок наложения, то теперь, надеюсь, вы понимаете принцип его работы гораздо лучше! Если вам что-то показалось непонятным, настоятельно советую вам ещё раз прочитать эту статью и расписать всё на бумаге. Попробуйте проиллюстрировать каждый шаг анимации, используя разное количество изображений, чтобы лучше понять, как всё работает.


В прошлой статье я показал несколько геометрических хитростей и создал закольцованный круговой слайдер. Сегодня — сделал что-то похожее с помощью z-index. В обоих случаях я не дублировал картинки для создания непрерывной анимации и не использовал JavaScript.


А в следующей статье я покажу вам, как создать трёхмерный слайдер. Не переключайтесь!




Комментарии (1)


  1. Perfecti-ist
    20.01.2023 11:15

    Это всё, конечно, круто с точки зрения разработки, но что это, что первый вариант, выглядят очень плохо и «колхозно»