Статьи

Создание 3D вращающейся карусели с помощью CSS и JavaScript

Много было сказано об использовании традиционных 2D-каруселей, например, эта статья в Smashing Magazine освещает эту тему. Там нет простого да или нет ответа на « я должен использовать карусель? вопрос; это зависит от конкретной ситуации.

карусель

Когда я начал исследовать эту тему, мне не понадобилась 3D-карусель, а скорее меня больше интересовали технические детали по ее реализации. Основные используемые методы, конечно, взяты из модуля преобразований CSS уровня 1 , но вместе с этим будет применена куча других технологий разработки интерфейса, затрагивающих различные темы в CSS, Sass и клиентском JavaScript .

Этот CodePen отображает различные версии компонента, который я покажу вам, как создать .

Чтобы проиллюстрировать настройку CSS 3D-преобразований, я покажу вам версию компонента только для CSS. Затем я покажу вам, как улучшить его с помощью JavaScript, разработав простой компонентный скрипт.

Для разметки изображения внутри компонента заключены в элемент <figure> , который обеспечивает базовый скелет:

 <div class="carousel"> <figure> <img src="..." alt=""> <img src="..." alt=""> ... <img src="..." alt=""> </figure> </div> 

Это будет нашей отправной точкой.

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

Элементы <img> должны быть расположены вокруг круга, обозначенного каруселью. Этот круг может быть аппроксимирован его ограниченным регулярным многоугольником и изображениями, размещенными на его сторонах:

Показ тета-угла

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

Что если в карусели меньше трех изображений? Многоугольник не может быть определен, и следующая процедура не может быть применена как есть. Во всяком случае, случай только одного изображения довольно бесполезен; два изображения немного более вероятны, и они могут быть размещены в двух диаметрально противоположных точках на окружности. Для простоты эти особые случаи не обрабатываются и предполагаются как минимум три изображения. Тем не менее, относительные модификации кода не будет сложно.

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

Карусель установки вид сверху

Таким образом, сторона, которая в настоящее время обращена к зрителю, будет находиться в плоскости экрана при z = 0, и переднее изображение, не подверженное ракурсу перспективы, будет иметь свой обычный 2D-размер. Буква d на рисунке представляет значение для свойства perspective CSS.

В этом разделе я покажу вам основные правила CSS, которые я расскажу шаг за шагом.

В следующих фрагментах кода некоторые переменные Sass используются для того, чтобы сделать компонент более настраиваемым. Я буду использовать $n для обозначения количества изображений в карусели и $item-width для указания ширины изображения.

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

 .carousel { display: flex; flex-direction: column; align-items: center; > * { flex: 0 0 auto; } .figure { width: $item-width; transform-style: preserve-3d; img { width: 100%; &:not(:first-of-type) { display: none /* Just for now */ } } } } 

Элемент <figure> имеет заданную ширину элемента карусели и имеет одинаковую высоту изображений (они могут иметь разные размеры, но должны иметь одинаковое соотношение сторон). Таким образом, высота контейнера карусели адаптируется в зависимости от высоты изображений. Кроме того, <figure> горизонтально центрирован в контейнере для карусели.

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

Карусель можно повернуть в трехмерном пространстве, применив преобразование вращения к элементу <figure> . Это вращение должно быть вокруг центра многоугольника, поэтому я изменю исходную точку преобразования по умолчанию <figure> :

 .carousel figure { transform-origin: 50% 50% (-$apothem); } 

Это значение отрицается, потому что в CSS положительное направление оси z находится вне экрана в направлении зрителя. Скобки необходимы, чтобы избежать синтаксических ошибок Sass. Вычисление апотема многоугольника будет объяснено позже.

Переведя систему отсчета элемента <figure> , можно повернуть всю карусель вращением вокруг своей (новой) оси y:

 .carousel figure { transform: rotateY(/* some amount here */rad); } 

Я вернусь к деталям этого вращения позже.

Давайте приступим к преобразованиям для других изображений. При абсолютном позиционировании изображения укладываются внутри <figure> :

 .carousel figure img:not(:first-of-type) { position: absolute; left: 0; top: 0; } 

Значения z-index игнорируются, поскольку это только предварительный шаг для следующих преобразований. Фактически, теперь каждое изображение можно поворачивать по оси Y карусели на угол поворота, который зависит от стороны многоугольника, которой назначено изображение. Сначала, как и в случае с элементом <figure> , источник преобразования изображений по умолчанию изменяется, перемещая его в центр многоугольника:

 .img:not(:first-of-type) { transform-origin: 50% 50% (-$apothem); } 

Затем изображения можно повернуть по их новой оси Y на величину, определяемую ($i - 1) * $theta радианами, где $i — это индекс (начиная с единицы) изображения, а $theta = 2 * $PI / $n , где $PI представляет математическую константу pi . Следовательно, второе изображение будет поворачиваться на $theta , третье — на 2 * $theta и т. Д. До последнего изображения, которое будет поворачиваться на ($n - 1) * $theta .

Карусель полигон

Это относительное расположение изображений будет сохраняться во время вращений карусели (то есть вращения вокруг модифицированной оси y в <figure> ) благодаря иерархической природе вложенных CSS-преобразований.

Эту величину поворота изображения можно назначить с @for управляющей директивы Sass @for :

 .carousel figure img { @for $i from 2 through $n { &:nth-child(#{$i}) { transform: rotateY(#{($i - 1) * $theta}rad); } } } 

При этом используется конструкция for...through а не for...to поскольку при for...to последнему значению, назначенному индексной переменной $i , будет n-1 вместо n .

Обратите внимание на два случая синтаксиса интерполяции Sass #{} . В первом случае он используется для индекса селектора :nth-child() ; во втором случае он используется для установки значения свойства вращения.

Вычисление Апофема

Вычисление апофема многоугольника зависит от количества сторон и ширины стороны, то есть от переменных $n и $item-width . Формула :

 $image-width / (2 * tan($PI/$n)) 

где tan()касательная тригонометрическая функция .

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

В этот момент изображения карусели «сшиваются» рядом, образуя требуемую многоугольную форму. Но здесь они плотно упакованы, в то время как часто в 3D-каруселях есть пространство между ними. Это расстояние улучшает восприятие трехмерного пространства, потому что оно позволяет вам видеть изображения на задней стороне карусели.

Можно дополнительно добавить этот разрыв между изображениями, введя другую конфигурационную переменную $item-separation и используя ее в качестве горизонтального отступа для каждого элемента <img> . Точнее, принимая половину этого значения для левого и правого заполнения:

 .carousel figure img { padding: 0 $item-separation / 2; } 

Окончательный результат можно увидеть в следующей демонстрации:

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

Чтобы упростить тестирование вращения карусели, я собираюсь добавить элемент управления пользовательского интерфейса для навигации между изображениями. Посмотрите демонстрацию CodePen для HTML, CSS и JavaScript, реализующих этот элемент управления; здесь я опишу только код, относящийся к ротации.

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

После обновления currImage вращение карусели выполняется с помощью:

 figure.style.transform = `rotateY(${currImage * -theta}rad)`; 

( Здесь и в следующих фрагментах шаблонные литералы ES6 используются для интерполяции выражений в строках; вы можете использовать традиционный оператор конкатенации ‘+’, если хотите )

где theta такая же, как и раньше:

 numImages = figure.childElementCount; theta = 2 * Math.PI / numImages; 

Вращение является - theta потому что для перехода к следующему элементу необходимо вращение против часовой стрелки, и такие значения вращения являются отрицательными в преобразованиях CSS.

Обратите внимание, что значение currImage не ограничено диапазоном [0, numImages — 1], но вместо этого оно может расти бесконечно, как в положительном, так и в отрицательном направлении. Фактически, если изображение на передней панели является последним (поэтому currImage == n-1), и пользователь нажимает следующую кнопку, если мы сбрасываем currImage в 0, чтобы перейти к первому изображению карусели, произошел бы переход угла поворота от (n-1)*theta до 0, и это повернёт карусель в противоположном направлении на всех предыдущих изображениях. Аналогичная проблема может возникнуть при нажатии кнопки prev, когда переднее изображение является первым.

Чтобы быть разборчивым, я должен даже проверить возможные переполнения currentImage , потому что тип данных Number не может принимать произвольно большие значения. Эти проверки не реализованы в демонстрационном коде.

А вот и вращающаяся карусель:

Улучшение с помощью JavaScript

Увидев базовый CSS, лежащий в основе карусели, теперь JavaScript можно использовать для улучшения компонента несколькими способами, такими как:

  • Произвольное количество изображений
  • Изображения с шириной в процентах
  • Несколько экземпляров карусели на странице
  • Конфигурации для каждого экземпляра, такие как размер зазора и видимость задней поверхности
  • Конфигурация с использованием HTML5 data- * атрибутов

Сначала я удаляю из таблицы стилей переменные и правила, относящиеся к источникам преобразования и поворотам, потому что это будет сделано с использованием JavaScript:

 $item-width: 40%; // Now we can use percentages $item-separation: 0px; // This now is set with Js $viewer-distance: 500px; .carousel { padding: 20px; perspective: $viewer-distance; overflow: hidden; display: flex; flex-direction: column; align-items: center; > * { flex: 0 0 auto; } figure { margin: 0; width: $item-width; transform-style: preserve-3d; transition: transform 0.5s; img { width: 100%; box-sizing: border-box; padding: 0 $item-separation / 2; &:not(:first-of-type) { position: absolute; left: 0; top: 0; } } } } 

Следующей в скрипте является функция carousel() которая заботится об инициализации экземпляра:

 function carousel(root) { // coming soon... } 

root аргумент относится к элементу DOM, который содержит карусель.

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

Чтобы создать несколько компонентов на одной странице, код ожидает загрузки всех изображений, регистрирует прослушиватель в объекте окна для события load , а затем вызывает carousel() для каждого элемента с классом carousel :

 window.addEventListener('load', () => { var carousels = document.querySelectorAll('.carousel'); for (var i = 0; i < carousels.length; i++) { carousel(carousels[i]); } }); 

carousel() выполняет три основные задачи:

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

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

 var figure = root.querySelector('figure'), images = figure.children, n = images.length, gap = root.dataset.gap || 0, bfc = 'bfc' in root.dataset ; 

Количество изображений ( n ) инициализируется на основе количества дочерних элементов элемента <figure> . Разделение между слайдами ( gap ) инициализируется из атрибута data-gap HTML5, если он установлен. Флаг видимости задней стороны ( bfc ) читается с использованием API набора данных HTML5. Это будет использовано позже, чтобы определить, должны ли изображения на обратной стороне карусели быть видимыми или нет.

Установка CSS-преобразований

Код, который устанавливает свойства, связанные с преобразованиями CSS, инкапсулирован в setupCarousel() . Эта вложенная функция принимает два аргумента. Первый — это количество элементов в карусели, то есть переменная n представленная выше. Второй параметр s — это длина стороны многоугольника карусели. Как я упоминал ранее, это равно ширине изображений, поэтому можно прочитать текущую ширину одного из них с помощью getComputedStyle() :

 setupCarousel(n, parseFloat(getComputedStyle(images[0]).width)); 

Таким образом, ширина изображения может быть установлена ​​с процентными значениями.

Чтобы сохранить отзывчивость карусели, я регистрирую слушателя для события resize окна, setupCarousel() снова вызывает setupCarousel() с измененным (в конечном итоге) измененным размером изображений:

 window.addEventListener('resize', () => { setupCarousel(n, parseFloat(getComputedStyle(images[0]).width)); }); 

Ради простоты я не опровергаю слушателя изменения размера.

Первое, что setupCarousel() , это вычисляет апофеон многоугольника, используя переданные параметры и ранее обсужденную формулу:

 apothem = s / (2 * Math.tan(Math.PI / n)); 

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

 figure.style.transformOrigin = `50% 50% ${-apothem}px`; 

Далее применяются стили для изображений:

 for (var i = 0; i < n; i++) { images[i].style.padding = `${gap}px`; } for (i = 1; i < n; i++) { images[i].style.transformOrigin = `50% 50% ${- apothem}px`; images[i].style.transform = `rotateY(${i * theta}rad)`; } if (bfc) { for (i = 0; i < n; i++) { images[i].style.backfaceVisibility = 'hidden'; } } 

Первый цикл назначает отступ для пространства между элементами карусели. Второй цикл устанавливает 3D-преобразования. Последний цикл обрабатывает задние грани, если соответствующий флаг был указан в конфигурации карусели.

Наконец, rotateCarousel() чтобы вывести текущее изображение вперед. Это небольшая вспомогательная функция, которая, учитывая индекс изображения, которое нужно показать, вращает элемент рисунка вокруг своей оси y, чтобы переместить целевое изображение вперед. Он также используется кодом навигации для перехода вперед и назад:

 function rotateCarousel(imageIndex) { figure.style.transform = `rotateY(${imageIndex * -theta}rad)`; } 

Вот конечный результат, демонстрация, где создаются несколько каруселей, каждая из которых имеет свою конфигурацию:

Источники и заключение

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

Если у вас есть какие-либо вопросы или комментарии по поводу кода или способа работы карусели, пожалуйста, оставляйте их ниже.