Слайдшоу на CSS (Sass)

в 8:46, , рубрики: css, Веб-разработка, ненормальное программирование

Тема, мягко говоря, не новая, существует ряд статей — на Smashing Magazine и в блогах, а так же просто реализации (исходный код, только та часть, которая касается анимации). Но, помимо фатального недостатка, у данных реализаций есть недостатки фактические — первые два варианта не предоставляют управления, а последний хоть и предоставляет, но при переключении слайдов анимация останавливается и её приходится запускать снова. Пожалуй, можно сказать что это фича, но мне хотелось полностью спародировать поведение слайдшоу как если бы оно было написано на javascript (что в итоге всё равно не удалось) — то есть при переклчении анимация продолжается, но начинается с выбранного слайда.
Кому лень читать — сразу конечный результат.

Слайдшоу на CSS (Sass) - 1

Смена слайдов

В варианте, представленном на Smashing Magazine, для каждого слайда саздается своя анимация —

#slider li.firstanimation  { animation: cycle      25s linear infinite; }
#slider li.secondanimation { animation: cycletwo   25s linear infinite; }
#slider li.thirdanimation  { animation: cyclethree 25s linear infinite; }
#slider li.fourthanimation { animation: cyclefour  25s linear infinite; }
#slider li.fifthanimation  { animation: cyclefive  25s linear infinite; }

и далее для всех анимаций cycle, cycletwo и т.д. описывается @keyframes и код получается достаточно объемным.

Если все слайды анимируются одинаково, то такой вариант избыточен — достаточно создать одну анимацию и задать её для каждого слайда с разным animation‑delay. До n-го элемента будем добираться с помощью :nth‑child. Соответственно, если каждый слайд отображается в течение 3 секунды, то для i‑го слайда задержка будет 3 * (i ‑ 1), например li:nth‑child(1) { animation‑delay: 0s }. В течение всей анимации слайд сначала отображается некоторое время, потом прячется и остается скрытым до конца итерации.

Переменные:

// здесь и далее используется camelCase, т.к. подсветска синтаксиса для php
// не понимает дефис в идентификаторах

// количество слайдов
$sliderLength: 4
// время, в течение которого слайд отображается
$delay: 3s
// общая продолжительность анимации
$duration: $sliderLength * $delay
// время, в течение которого слайд отображается (в процентах)
$displayTime: 100% / $sliderLength

@keyframes toggle
  // изначально элемент скрыт
  0%
    opacity: 0
  // затем плавно появляется в течение 10% процентов времени.
  // Проценты берутся от $delay, а не от общей продолжительности анимации
  #{$displayTime * 0.1}
    opacity: 1
  // 80% времени мы его видим
  #{$displayTime * 0.9}
    opacity: 1
  // последние 10% элемент плавно исчезает
  #{$displayTime}
    opacity: 0
  // и остается скрытым до конца анимации
  100%
    opacity: 0

Сама анимация:

// у всех слайдов одна и та же анимация
.slider li
  animation-name: toggle
  animation-duration: $duration
  animation-iteration-count: infinite
  // сокращенная запись
  // animation: toggle $duration infinite

@for $i from 0 to $sliderLength
  .slider li:nth-child(#{$i + 1})
    // устанавливаем задержку перед анимацией в зависимости от номера элемента
    animation-delay: $delay * $i

Результат на данном этапе.

Для тех, кто не знаком с Sass/SCSS
$sliderLength: 4 // объявление переменной
$delay: 3s // допускаются разные единицы измерения
$duration: $sliderLength * $delay
$displayTime: 100% / $sliderLength // в том числе проценты

@keyframes toggle // фигурные скобки опускаются
  0%
    opacity: 0
  // #{...} используется для вывода данных в строку
  #{$displayTime * 0.1}
    opacity: 1
  #{$displayTime * 0.9}
    opacity: 1
  #{$displayTime}
    opacity: 0
  100%
    opacity: 0

.slider li
  animation: toggle $duration infinite

// обычный for. С одной оговоркой - итерировать будет до $sliderLength - 1
@for $i from 0 to $sliderLength
   // будет выведено для каждой итерации
  .slider li:nth-child(#{$i + 1})
    animation-delay: $delay * $i

Следует понимать, что этот код не совсем честный — не смотря на то, что цикл всего три строчки, для каждого $i будет создано свое css правило —

.slider li:nth-child(1) { animation-delay: 0s; }
.slider li:nth-child(2) { animation-delay: 3s; }
.slider li:nth-child(3) { animation-delay: 6s; }
.slider li:nth-child(4) { animation-delay: 9s; }

Таким образом, объем css будет [на данном этапе] расти линейно относительно количества слайдов. А посему предлагаю временно забыть о том, что есть css, как будто мы в будущем и у нас умные браурезы, способные эффективно обрабатывать Sass. Без этого допущения читать статью дальше будет страшно.

Переход к слайду

Для того, чтобы организовать переход к слайду, нам нужно научиться, во-первых, хранить текущее состояние, а во-вторых — уметь это состояние менять. Сделать это можно с помощью radio-инпутов. В зависимости от того, какой инпут активен в данный момент, мы будем просто менять очередность появления элементов. Так, если активен первый инпут, то очеред будет 1, 2, 3, 4, если активен второй — 4, 1, 2, 3 и т.д. То есть, при активном n-ом инпуте мы циклически сдвигаем последовательность на n ‑ 1 позицию вправо. Проверка будет производиться с помощью соседского селектора ~. Например, input:nth‑of‑type(1):checked ~ .slider li (читать — если перед списком первый инпут активен — применить такой-то стиль). При этом инпуты должны располагаться перед списком, в котором лежат слайды.

Переменные и @keyframes те же, что и в предыдущем случае.

// при клике на инпут необходимо отменить текущую анимацию.
input:active ~ .slider li
  animation: none !important

.slider li
  animation: toggle $duration infinite

// для каждого состояния генерируем свой набор правил
@for $ctrlNumber from 0 to $sliderLength
  // все селекторы привязаны к конкретному активному инпуту
  input:nth-of-type(#{$ctrlNumber + 1}):checked
    @for $slideNumber from 0 to $sliderLength
      // получаем сдвиг
      $position: $slideNumber - $ctrlNumber
      // сдвиг должен быть циклическим
      @if $position < 0
        $position: $position + $sliderLength
      
      ~ .slider li:nth-child(#{$slideNumber + 1})
        animation-delay: $delay * $position

Результат на данном этапе.

Правда, если в предыдущем случае css рос линейно при увеличении количества слайдов, то теперь имеет место квадратичная зависимость. Но, мы договорились об этом не думать.

Подсветка текущего инпута...

… невозможна сама по себе. То есть мы не можем переключить их с помощью CSS. Но мы можем анимировать <label>, связанный с инпутом, а сами инпуты можно скрыть. Но вот незадача — хром и другие вебкиты реагируют на клик по интпуту и клик по метке по-разному — во втором случае анимация меняется только после второго клика по метке (быть может, кто-нибудь в курсе, почему так происходит?) Причем инпут переключается, но анимация при этом не меняется. Соответственно, нам нужен именно клик по инпуту. Для этого мы можем расположить его над меткой и сделать его прозрачным. Нужно понимать, что подсвеченная метка никак не связана с активным инпутом — это просто анимация, которая идет независимо, но задержки анимаций точно такие же, как и для слайдов. Важно, чтобы метки шли после всех инпутов.

// добавляется ещё один @keyframes

@keyframes toggle-ctrl
  #{$display-time * 0.1},
  #{$display-time * 0.9}
    background-color: #555
  #{$display-time},
  100%
    background-color: #ccc

Сама же анимация изменится незначительно

input:active
  ~ .slider li,
  ~ label 
    animation: none !important

.slider li
  animation: toggle-slide $duration infinite

label
  animation: toggle-ctrl $duration infinite

@for $ctrlNumber from 0 to $sliderLength
  input:nth-of-type(#{$ctrlNumber + 1}):checked
    @for $slideNumber from 0 to $sliderLength
      $position: $slideNumber - $ctrlNumber
      @if $position < 0
        $position: $position + $sliderLength

      ~ .slider li:nth-child(#{$slideNumber + 1}),
      // добавляем правило для лейбла
      ~ label:nth-of-type(#{$slideNumber + 1})
        animation-delay: $delay * $position

Результат на данном этапе.

Переход к следующему/предыдущему слайду.

Как было сказано выше, для смены нужно уметь хранить и менять состояние. Хранить мы его уже умеем, значит стрелочки влево-вправо должны лишь менять текущий активный инпут. Первый вариант — это в каждом состоянии (при каждом активном инпуте) у нас будет по две метки — первая привязана в предыдущему инпуту, вторая — к следующему (циклически). (В качестве идеи: можно не создавать дополнительные инпуты, а стрелочки положить в :after и :before соответствующей метки внизу. Правда, здесь могут (?) возникнуть приблемы с анимированием псевдоэлементов.)
При этом нужно учитывать, что при клике по метке анимация остановится, а вне анимации по умолчанию размеры метки равны нулю, то есть она невидима. И поэтому ничего не сработает — оказалось, мы должны не только кликнуть по лейблу, но и отпустить кнопку мыши над тем же элементом. Так что активную метку нужно показывать всегда — label:active { font‑size: 30px; }

Появление 'правильной' стрелочки можно добиться, например, циклическим сдвигом атрибутов for тега label:

// html (jade)
 - for (var i = 0; i < sliderLength; i++)
    - var id = i - 1
    - if (id < 0) id = sliderLength - 1
    label.prev(for='c#{id}') ⇚

  - for (var i = 0; i < sliderLength; i++)
    - var id = i + 1
    - if (id === sliderLength) id = 0
    label.next(for='c#{id}') ⇛

А можно сделать сдвиг анимаций в CSS:

input:active
  ~ .slide li,
  ~ .label,
  ~ .prev,
  ~ .next
    animation: none !important

.slide li
  animation: toggle-slide $duration infinite

.label
  animation: toggle-ctrl $duration infinite
    
.prev,
.next
  animation: toggle-arrow $duration infinite

@for $ctrlNumber from 0 to $length
  input:nth-of-type(#{$ctrlNumber + 1}):checked
    @for $slideNumber from 0 to $length
      $position: $slideNumber - $ctrlNumber
      @if $position < 0
        $position: $position + $length

      ~ .slide li:nth-child(#{$slideNumber + 1}),
      ~ .label:nth-of-type(#{$slideNumber + 1})
        animation-delay: $delay * $position
       
      // сдвиг для предыдущего
      $prev: $slideNumber - 1
      @if $prev < 0
        $prev: $length - 1
          
      // сдвиг для следующего
      $next: $slideNumber + 1
      @if $next == $length
        $next: 0
        
      // 'type' в данном слечае - это label,
      //  так что необходимо учитывать все предшествующие элементы
      ~ .prev:nth-of-type(#{$prev + 1 + $length}),
      ~ .next:nth-of-type(#{$next + 1 + $length * 2})
        animation-delay: $delay * $position

Здесь мы снова сталкиваемся с тем, что в вебкитах клик по лейблу срабатывает только со второго раза (все-таки, почему?), то есть стрелочки реагируют только на двойной клик. В мозиле работает как надо.
Конечный результат.

Итого

Ничего хорошего. CSS даже для четырех элементов получается огромный (260 строк).

Материалы:

Ссылки:

Автор: linoleum

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js