Когда я начал изучать эту тему, мне не нужна была трехмерная карусель, но я больше интересовался техническими деталями ее реализации. Разумеется, основные используемые методы, конечно же, относятся к модулю CSS Transforms Module Level 1 , но по мере того, как будет применена группа других технологий разработки переднего плана, затрагивая различные темы в CSS, Sass и клиентском JavaScript .
Демо
Чтобы проиллюстрировать настройку 3D-преобразований CSS, я покажу вам версию компонента только для CSS. Затем я покажу вам, как улучшить его с помощью JavaScript, разработав простой скрипт компонента.
Разметка для карусели с восемью изображениями
Для разметки изображения внутри компонента обернуты внутри <figure>
элемента, который обеспечивает базовый скелет:
<figure style="transform-origin: 50% 50% -600.159px; transform: rotateY(0rad);">
<img src="https://source.unsplash.com/nvzvOPQW0gc/800x533" alt="" style="padding: 0px;">
<img src="https://source.unsplash.com/EbuaKnSm8Zw/800x533" alt="" style="padding: 0px; transform-origin: 50% 50% -600.159px; transform: rotateY(0.785398rad);">
<img src="https://source.unsplash.com/kG38b7CFzTY/800x533" alt="" style="padding: 0px; transform-origin: 50% 50% -600.159px; transform: rotateY(1.5708rad);">
<img src="https://source.unsplash.com/mCg0ZgD7BgU/800x533" alt="" style="padding: 0px; transform-origin: 50% 50% -600.159px; transform: rotateY(2.35619rad);">
<img src="https://source.unsplash.com/VkwRmha1_tI/800x533" alt="" style="padding: 0px; transform-origin: 50% 50% -600.159px; transform: rotateY(3.14159rad);">
<img src="https://source.unsplash.com/1FWICvPQdkY/800x533" alt="" style="padding: 0px; transform-origin: 50% 50% -600.159px; transform: rotateY(3.92699rad);">
<img src="https://source.unsplash.com/bjhrzvzZeq4/800x533" alt="" style="padding: 0px; transform-origin: 50% 50% -600.159px; transform: rotateY(4.71239rad);">
<img src="https://source.unsplash.com/7mUXaBBrhoA/800x533" alt="" style="padding: 0px; transform-origin: 50% 50% -600.159px; transform: rotateY(5.49779rad);">
</figure>
Это будет нашим фундаментом.
Геометрическая структура карусели
Прежде чем смотреть в CSS, давайте рассмотрим план, который будет разработан в следующих разделах.
Эти <img>
элементы должны быть расположены по кругу , очерченном карусели. Этот круг может быть аппроксимирован его ограниченным регулярным многоугольником и изображениями, расположенными по бокам:
Таким образом, количество сторон такого многоугольника совпадает с количеством изображений в карусели:
С тремя изображениями многоугольник является равносторонним треугольником;
С четырьмя изображениями это квадрат;
С пятью пятиугольниками; и так далее:
Что делать, если в карусели меньше трех изображений?
Полигон не может быть определен, и следующая процедура не может применяться как есть. Во всяком случае, случай с одним изображением довольно бесполезен; Два изображения несколько более вероятны, и их можно было разместить на двух диаметрально противоположных точках на круге. Для простоты эти особые случаи не обрабатываются и предполагается как минимум три изображения. Однако относительные модификации кода не были бы трудными.
Этот воображаемый опорный многоугольник будет располагаться в трехмерном пространстве, перпендикулярном плоскости окна просмотра, и его центр будет оттеснен назад в экран на расстоянии, равном его апотему, расстоянии стороны многоугольника от его центра, как показано на рис. Этот сверху вниз вид карусели:
Таким образом, сторона, которая в настоящее время обращена к зрителю, будет находиться на плоскости экрана при z = 0, а переднее изображение, не подверженное перспективой ракурса, будет иметь свой обычный 2D размер. d
Письмо на картинке представляет значение для CSS perspective
свойства.
Построение геометрии карусели
В этом разделе я покажу вам ключевые правила 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. Вычисление многоугольника apothem будет объяснено позже.
Переведя систему отсчета <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 :
.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()
селектора;
Во втором случае он используется для установки значения свойства вращения.
Вычисление Apothem
Вычисление апофемы полигона зависит от количества сторон и ширины стороны, то есть, на $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 тип данных не может принимать сколь угодно большие значения. Эти проверки не реализованы в демо-коде.
Увидев базовый CSS, лежащий в основе карусели, теперь JavaScript можно использовать для улучшения компонента несколькими способами, такими как:
- Произвольное количество изображений
- Изображения с процентной шириной
- Несколько экземпляров карусели на странице
- Конфигурации для каждого экземпляра, такие как размер зазора и видимость на обратной стороне
- Конфигурация с использованием атрибутов данных HTML5 *
Сначала я удаляю из таблицы стилей переменные и правила, связанные с происхождением преобразования и поворотами, потому что это будет сделано с использованием 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() Выполняет три основные задачи:
- Настройка навигации.
- Настройка трансформаций
- Зарегистрируйте слушателя с изменением размера окна, чтобы сохранить отзыв карусели, адаптировав его к новому размеру видового экрана
Перед рассмотрением кода настройки преобразования я рассмотрю некоторые ключевые переменные и как они инициализируются на основе конфигурации экземпляра:
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() снова вызывает изменения (в конечном итоге) измененных размеров изображений:
window.addEventListener('resize', () => {
setupCarousel(n, parseFloat(getComputedStyle(images[0]).width));
});
Для простоты я не разбираюсь в слушателе изменения размера.
Первое, что setupCarousel() нужно сделать, - вычислить apothem многоугольника, используя переданные параметры и ранее обсуждавшуюся формулу:
apothem = s / (2 * Math.tan(Math.PI / n));
Затем это значение используется для изменения начала преобразования фигурного элемента для получения новой оси вращения карусели:
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)`;
}
Вот окончательный результат, демонстрация, в которой создаются несколько каруселей, каждая из которых имеет другую конфигурацию: