Тема, мягко говоря, не новая, существует ряд статей — на Smashing Magazine и в блогах, а так же просто реализации (исходный код, только та часть, которая касается анимации). Но, помимо фатального недостатка, у данных реализаций есть недостатки фактические — первые два варианта не предоставляют управления, а последний хоть и предоставляет, но при переключении слайдов анимация останавливается и её приходится запускать снова. Пожалуй, можно сказать что это фича, но мне хотелось полностью спародировать поведение слайдшоу как если бы оно было написано на javascript (что в итоге всё равно не удалось) — то есть при переклчении анимация продолжается, но начинается с выбранного слайда.
Кому лень читать — сразу конечный результат.
Смена слайдов
В варианте, представленном на 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
Результат на данном этапе.
$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 строк).
Материалы:
- Статья раз
- Статья два
- Красивый аналог того, что мы тут пилили
- CSS animation
- Документация Sass
- Управяющие директивы Sass
- Документация Jade
Ссылки:
Автор: linoleum