Галерея с эффектом Cover Flow на JavaScript.
Вступление.
Несколько лет назад, просматривая в инете галереи с эффектом «Cover Flow», наткнулся на сайт, где эффект «Cover Flow» был реализован очень красиво, в различных вариантах и возможностью переключения между ними. Скрипты были написаны на jQuery, при этом ещё минифицированными, зашифрованными и платными.
Мне захотелось сделать тоже самое, но на чистом JavaScript. Признаюсь честно, я взял с этого сайта саму идею эффекта и несколько картинок.
За прошедшее с тех пор время, плагин Simple 3D Coverflow был значительно улучшен его авторами — эффект «Cover Flow» стал более плавным, в галерее появилась адаптивность под различные разрешения и т. д.
Тем не менее, в этой статье мы рассмотрим галерею с эффектом «Cover Flow» в том виде, каким он был на момент написания мною скрипта пару лет назад.
Эффект «Cover Flow» основан на свойстве CSS3 transform
, которое позволяет преобразовывать элемент в трехмерной системе координат. Если вам не приходилось сталкиваться с CSS-трансформацией, рекомендую предварительно ознакомиться с парой статей: CSS3-трансформации и CSS3 3D-трансформации.
Надеюсь всё всем понятно. Ну а теперь составим техническое задание, чтобы ничего не забыть и не пропустить.
-
1
Галерея будет фиксированной ширины, чтобы упростить скрипт и сконцентрироваться непосредственно на реализации эффекта Cover Flow при листании галереи.
Если вас интересует создание адаптивной галереи, то можете изучить статью Адаптивная галерея изображений для сайта на JavaScript. -
2
Для листания галереи используем следующие варианты управления:
— кнопками навигации «prev» и «next»;
— клавишами клавиатуры со стрелочками «←» и «→»;
— вращением колёсика мыши;
— клик по изображению, к которому необходимо прокрутить галерею. -
3
Создадим несколько вариантов эффекта Cover Flow и обеспечим переключение между ними без перезагрузки страницы.
-
4
Возможно использование нескольких галерей на одной странице.
HTML-вёрстка галереи с эффектом Cover Flow.
Полностью вёрстку приводить не буду — надеюсь, что вы в состоянии самостоятельно прописать необходимые метатеги, подключить таблицу стилей, шрифты и js-скрипт.
HTML-вёрстка галереи очень простая и состоит из трёх основных блоков:
- непосредственно сама галерея;
- навигация по галерее с кнопками «prev» и «next»;
- переключение между вариантами эффекта Cover Flow.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
<div id="content" class="content"> <div id="container" class="container"> <!-- галерея Cover Flow --> <div id="coverflow" class="coverflow"> <div> <img src="https://via.placeholder.com/400x266/09f/fff.png?text=Element+1" alt=""> <div></div> </div> <div> <img src="https://via.placeholder.com/400x266/09f/fff.png?text=Element+2" alt=""> <div></div> </div> <div> <img src="https://via.placeholder.com/400x266/09f/fff.png?text=Element+3" alt=""> <div></div> </div> <div> <img src="https://via.placeholder.com/400x266/09f/fff.png?text=Element+4" alt=""> <div></div> </div> <div> <img src="https://via.placeholder.com/400x266/09f/fff.png?text=Element+5" alt=""> <div></div> </div> <div> <img src="https://via.placeholder.com/400x266/09f/fff.png?text=Element+6" alt=""> <div></div> </div> <div> <img src="https://via.placeholder.com/400x266/09f/fff.png?text=Element+7" alt=""> <div></div> </div> <div> <img src="https://via.placeholder.com/400x266/09f/fff.png?text=Element+8" alt=""> <div></div> </div> <div> <img src="https://via.placeholder.com/400x266/09f/fff.png?text=Element+9" alt=""> <div></div> </div> <div> <img src="https://via.placeholder.com/400x266/09f/fff.png?text=Element+10" alt=""> <div></div> </div> <div> <img src="https://via.placeholder.com/400x266/09f/fff.png?text=Element+11" alt=""> <div></div> </div> <div> <img src="https://via.placeholder.com/400x266/09f/fff.png?text=Element+12" alt=""> <div></div> </div> <div> <img src="https://via.placeholder.com/400x266/09f/fff.png?text=Element+13" alt=""> <div></div> </div> </div> <!-- /галерея Cover Flow --> <!-- навигация по галерее --> <div id="control" class="control"> <span data-show="prev">prev</span> <span data-show="next">next</span> </div> <!-- /навигация по галерее --> </div> <!-- переключение вида анимации Cover Flow --> <div class="type"> <ul> <li data-type="type1" class="active"><img src="img/coverflow/type_1.jpg" alt=""></li> <li data-type="type2"><img src="img/coverflow/type_2.jpg" alt=""></li> <li data-type="type3"><img src="img/coverflow/type_3.jpg" alt=""></li> </ul> </div> <!-- /переключение вида анимации Cover Flow --> </div> |
В качестве комментария к HTML-вёрстке галереи с эффектом Cover Flow, хочу обратить внимание на пустой DIV
, расположенный после каждого изображения галереи. С его помощью создаётся градиентное затенение изображения.
Таблица стилей для галереи с эффектом Cover Flow.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
html, body, div, span, img, ul, li { margin: 0; padding: 0; border: 0; vertical-align: baseline; } html, body, .wrap { width: 100%; height: 100%; line-height: 1; } body { font-family: Roboto, 'Helvetica CY', 'Nimbus Sans L', sans-serif; } .wrap { width: 1280px; margin: 0 auto; } *, :after, :before { -webkit-box-sizing: border-box; box-sizing: border-box; } ul { list-style: none; display: inline-block; } img { display: block; } .btn, .type span { transition: all 0.3s; } .content { width: 100%; height: 100%; position: absolute; left: 0; top: 0; } .container { width: 100%; height: 470px; position: absolute; overflow: hidden; background: #000; } /* непосредственно сама галерея */ .coverflow { width: 1280px; height: 470px; position: relative; margin: 0 auto; top: 25px; transform-style: preserve-3d; perspective: 1600px; } .coverflow > div { width: 400px; height: 266px; position: absolute; left: 0; top: 0; transform-origin: 50% 100% 0; cursor: pointer; box-shadow: rgb(17, 17, 17) 0px 2px 2px; } .coverflow > div > div { width: 400px; height: 266px; position: absolute; left: 0; top: 0; z-index: 2; } .coverflow img { width: 400px; height: 266px; } /* управление галереей кнопками навигации 'prev' и 'next' */ .control { width: 100%; text-align: center; user-select: none; position: absolute; top: 430px; z-index: 100; } .control span { font-size: 17px; line-height: 20px; color: #fff; display: inline-block; margin: 0 20px; cursor: pointer; } /* переключение вариантов эффекта Cover Flow */ .type { width: 100%; height: 72px; text-align: center; position: absolute; left: 0; top: 500px; overflow: hidden; } .type li { display: block; float: left; cursor: pointer; opacity: 0.3; } .type li:hover { opacity: 1; } .type .active { opacity: 0.75; } .type li + li { margin-left: 20px; } |
Конструктор для адаптивной галереи с эффектом Cover Flow.
Первое, что мы сделаем, это создадим анонимную самозапускающуюся функцию, внутри которой и будет расположен наш код.
Нужно взять за правило ограничивать область видимости скрипта, чтобы исключить конфликты с другими JS-скриптами подключенными к странице.
1 2 3 4 5 6 |
;(function() { 'use strict'; })(); |
При написании JS-скрипта мы будем использовать прототипное наследование. Это позволит создать несколько галерей на одной странице.
Для создания объектов галерей с эффектом Cover Flow мы будем использовать конструкторы, т. к. они хорошо подходят для создания множества похожих объектов, имеющих одинаковую структуру, с общими именами свойств и методов. В JavaScript это называется наследованием через прототипы, а объект от которого наследуются свойства и методы, называется прототипом. В нашем случае, это объект созданный конструктором. Другими словами, механизм работы наследования через прототипы заключается в наследовании и повторном использовании свойств и методов созданных в конструкторе, с возможностью расширения свойств во вновь созданном объекте.
Конструктор представляет из себя функциональное выражение (Function Expression) с набором настроек, определяющих внешний вид галереи Cover Flow, её поведение и способы управления. Эти настройки можно изменить во время инициализации галереи, заменив их значения на пользовательские.
Кроме этого, в конструкторе вызывается функция init
, которая формирует разновидность эффекта Cover Flow исходя из полученных настроек.
Конструктор принимает два аргумента:
id
— идентификатор галерей, инициализация которой происходит в данный момент;setup
— ассоциативный массив с пользовательскими настройками.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
;(function() { 'use strict'; // функция-конструктор var Coverflow = function(id, setup) { // настройки по-умолчанию и основные DOM-элементы // галереи определяющие её каркас ... // построение галереи исходя из полученных настроек this.init(this.setup.type); }; })(); |
Для того, чтобы была возможность обратиться к конструктору из вне анонимной функции, запишем функцию-конструктор, как свойство объекта Window
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
;(function() { 'use strict'; // функция-конструктор var Coverflow = function(id, setup) { // настройки по-умолчанию и основные DOM-элементы // галереи определяющие её каркас ... // построение галереи исходя из полученных настроек this.init(this.setup.type); }; // запишем конструктор в свойство 'window.Coverflow', чтобы обеспечить // доступ к нему снаружи анонимной функции window.Coverflow = Coverflow; })(); |
Теперь рассмотрим полный JS-код конструктора, дополненный комментариями:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
var Coverflow = function(id, setup) { // настройки по-умолчанию this.defaults = { w: 400, // ширина элемента, h: 266, // высота элемента shiftY: 0, // сдвиг по оси Y shiftZ: 100, // сдвиг по оси Z при наведении initShiftZ: 190, // сдвиг по оси Z при инициализации rotateX: 0.1, // угол поворота по оси X duration: 400, // время анимации при листании durationZ: 200, // время анимации при наведении flag: true, // блокировка управления на время анимации isctrl: false // блокировка повторной регистрации обработчиков }; this.setup = setup; // основные DOM-элементы галереи определяющие её каркас // родительский элемент галереи this.content = document.getElementById(id); this.coverflow = this.content.querySelector('.coverflow'); // коллекция элементов галереи this.elements = this.coverflow.querySelectorAll('.coverflow > div'); // коллекция контейнеров внутри элементов галереи, к которым будет применяться // свойство CSS 'background' this.elementsBg = this.coverflow.querySelectorAll('.coverflow > div > div'); // родительский элемент переключателей типа эффекта Cover Flow this.parentSwitches = document.getElementById('switches_type'); // коллекция элементов, переключающих тип эффекта Cover Flow // эти элементы необходимы только для демонстрации эффектов this.tabs = this.parentSwitches.querySelectorAll('li'); // массив для хранения координат z0 каждого элемента // используется при наведении курсора на элемент this.arrayZ0 = []; // построение галереи исходя из полученных настроек this.init(this.setup.type); }; |
Как видно из приведённого кода, конструктор содержит в себе настройки по умолчанию — это объект defaults
. Эти настройки могут быть переназначены при вызове конструктора и их новые значения будут передаваться в аргументе setup
. Позже это будет рассмотрено.
Кроме этого, в конструкторе определяются основные DOM-элементы и коллекции DOM-элементов от которых зависит каркас галереи с эффектом Cover Flow.
Вызывать функцию-конструктор будем из HTML-страницы, для чего в конце HTML-вёрстки, перед закрывающим тегом </BODY>
запишем следующий JS-код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// настраиваем галерею с учётом входных данных, аргументы: id родительского блока // галереи и объект с пользовательскими настройками. <script type="text/javascript"> var coverflow = new Coverflow('coverflow', { // устанавливаем первый вариант эффекта Cover Flow type: 'type1' // здесь можно переназначить настройки по-умолчанию, // установив значения уникальные именно для данного // экземпляра галереи ... }); </script> |
В результате мы получим экземпляр адаптивной галереи, дополненный новыми свойствами и от которых будут наследоваться методы, отвечающие за формирование каркаса и навигации галереи, а также, её работу.
Т. к. все методы наследуются от своего прототипа, то их запись будет выглядеть следующим образом:
1 2 3 |
Coverflow.prototype.someMethod = function() { ... } |
Давайте сократим эту запись, создав переменную, которая будет ссылаться на прототип Coverflow
, для этого в конец нашей анонимной функции добавим следующий JS-код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
;(function() { 'use strict'; // функция-конструктор var Coverflow = function(id, setup) { // настройки по-умолчанию и основные DOM-элементы // галереи определяющие её каркас ... // построение галереи исходя из полученных настроек this.init(this.setup.type); }; // запишем конструктор в свойство 'window.Coverflow', чтобы обеспечить // доступ к нему снаружи анонимной функции window.Coverflow = Coverflow; // для сокращения записи, создадим переменную, которая будет ссылаться // на прототип 'Coverflow' var fn = Coverflow.prototype; })(); |
Теперь запись функции будет выглядеть следующим образом:
1 2 3 |
fn.someMethod = function() { ... } |
Все функции, которые мы будем создавать в дальнейшем, должны помещаться в конец анонимной самозапускающейся функции. В дальнейшем акцентрировать внимание на этом не будем.
Инициализация галереи с эффектом Cover Flow.
Инициализация галереи начинается вызовом функции init
из функции-конструктора Coverflow
. В качестве аргумента функция init
получает тип эффекта Cover Flow и решает следующие задачи:
- Устанавливает настройки в зависимости от типа эффекта Cover Flow.
- Объединяет настройки эффекта Cover Flow с пользовательскими и дефолтными настройками, записывая результат в объект настроек
options
. - Исходя из количества элементов в галерее, вычисляет центральный элемент и его координаты по осям X, Y и Z.
- Создаёт каркас галереи и размещает в нём элементы в зависимости от выбранного типа эффекта Cover Flow.
- Устанавливает обработчики событий для управления галереей.
JS-код функции init
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
fn.init = function(type) { var settings; // устанавливаем настройки в зависимости от типа эффекта Cover Flow switch(type) { case 'type1': settings = { shiftX: 160, shiftZ: 190, shiftXlong: 320, shiftZlong: 570, rotateY: 70, rotateXOut: 0 }; break; case 'type2': settings = { shiftX: 40, shiftZ: 62, shiftXlong: 240, shiftZlong: 160, rotateY: 60, rotateXOut: -15 }; break; case 'type3': settings = { shiftX: 0, shiftZ: 70, shiftXlong: 520, shiftZlong: 0, rotateY: 0, rotateXOut: -15 }; break; }; // объединяем настройки эффекта Cover Flow с пользовательскими настройками settings = this.extend({}, settings, this.setup); // объединяем настройки по умолчанию с полученными this.options = this.extend({}, this.defaults, settings); // явным образом добавляем тип эффекта Cover Flow в настройки this.options.type = type; // количество элементов в галерее this.options.count = this.elements.length; // индекс текущего элемента this.options.currIndex = this.options.count >> 1; // координаты центра галереи coverflow по трём осям this.options.x0 = (this.coverflow.clientWidth - this.options.w) / 2; this.options.y0 = (this.coverflow.clientHeight - this.options.h) / 2; this.options.z0 = 0; // создание каркаса галереи и размещение элементов в ней // в соответствии с выбранным типом this.creatGallery(); // если обработчики событий ещё не устанавливались, то регистрируем их if (this.options.isctrl === false) { this.galleryControl(); } }; |
В приведённом JS-коде встретился вызов трёх новых функций — extend
, creatGallery
и galleryControl
. Рассмотрим подробнее назначение и работу данных функций.
Функция extend
объединяет содержимое двух объектов, дополняя и перезаписывая содержимое исходного объекта свойствами объекта-источника. Эта функция вызывается дважды. При первом вызове объединяем настройки эффекта Cover Flow с пользовательскими настройками, а затем — полученный результат с настройками по умолчанию.
Запишем в конец анонимной самозапускающейся функции следующий JS-код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
fn.extend = function(out) { out = out || {}; for (var i = 1; i < arguments.length; i++) { if (!arguments[i]) continue; for (var key in arguments[i]) { if (arguments[i].hasOwnProperty(key)) out[key] = arguments[i][key]; } } return out; }; |
Необходимо обратить внимание ещё на такой момент — при вызове функции extend
, формируется свойство options.type
, значение которого берётся из пользовательских настроек. При переключении типа эффекта Cover Flow значение этого свойства не изменится, т. к. пользовательские настройки при этом остаются неизменными, поэтому значение свойства options.type
пропишем явно, используя полученный аргумент type
функции init
.
Функции creatGallery
и galleryControl
рассмотрим отдельно, т. к. они достаточно сложные и в своей работе используют ряд вспомогательных функций.
Формирование каркаса и размещение элементов галереи с эффектом Cover Flow.
В этом разделе мы рассмотрим функцию creatGallery
, которая формирует каркас галереи и запускает анимацию размещения её элементов в зависимости от выбранного типа эффекта Cover Flow.
Рассмотрим алгоритм работы функции creatGallery
:
-
1
Фиксируется время начала анимации размещения элементов галереи и регистрируется ряд переменных:
— zIndex, Z-индекс элемента галереи;
— shiftLX, смещение по оси X элементов, расположенных слева от центрального (левая ветвь элементов);
— shiftRX, смещение по оси X элементов, расположенных справа от центрального (правая ветвь элементов);
— shiftZ, смещение элементов по оси Z;
— alignment, содержание атрибутаstyle
элемента галереи. -
2
Запускается функция анимации.
-
3
Вся галерея поворачивается по оси X на угол равный
options.rotateXOut
и зависящий от типа эффекта Cover Flow. -
4
Позиционируется центральный элемент галереи, имеющий индекс
options.currIndex
. Координаты его середины совпадают с центром галереи и являются начальной точкой отсчёта для позиционирования остальных элементов галереи. -
5
Устанавливаются ближайшие элементы с правой и левой стороны от центрального (текущего). У этих элементов увеличенный в 2 раза сдвиг по оси Х (
options.shiftXlong
) и в 3 раза по оси Z (options.shiftZlong
), по сравнению с другими элементами галереи. Это обусловлено тем, что в противном случае, центральный (текущий) элемент будет закрывать собою соседние элементы. -
6
Позиционируются остальные элементы левой и правой ветвей галереи.
-
7
После окончания анимации сохраняется в массив координата Z каждого элемента. Эта координата понадобится для анимации изменения положения элемента при наведении на него курсора.
Теперь рассмотрим JS-код, реализующий данный алгоритм.
Создадим функцию creatGallery
и зарегистрируем в ней ряд переменных:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fn.creatGallery = function() { // запрещаем управление галереей на время формирования каркаса this.options.flag = false; var start = Date.now(), // время начала анимации duration = this.options.duration * 2, // продолжительность анимации zIndex = 0, // исходное значение 'z-index' для элементов галереи shiftLX, // сдвиг по оси X для элементов левой части галереи shiftRX, // сдвиг по оси X для элементов правой части галереи shiftZ, // сдвиг по оси Z для элементов левой и правой части галереи alignment; // текущие значения атрибута 'style' } |
Теперь необходимо запустить функцию анимации, которая плавно расставит элементы галереи в соответствии с настройками эффекта Cover Flow.
При создании анимации на JavaScript вместо функции
setInterval
используйте функцию requestAnimationFrame
. Эта функция позволяет синхронизировать анимацию со встроенными в браузер механизмами обновления страницы. Результатом будет более эффективное использование графического ускорителя, исключена повторная обработка одних и тех же участков страницы, меньше будет загрузка процессора и, самое главное, анимация будет более плавная, без рывков и дёрганий.
Для этого необходимо создать возможность кроссбраузерного использование функции requestAnimationFrame
. Добавим вовнутрь созданной анонимной функции следующий код:
1 2 3 4 5 6 7 8 9 |
// обеспечиваем кроссбраузерноть для использования встроенного // в браузеры API requestAnimationFrame var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; window.requestAnimationFrame = requestAnimationFrame; |
Зарегистрируем внутри функции creatGallery
функцию анимации animate
. Она представляет из себя именованное функциональное выражение. Это сделано для работы с рекурсией, т. е., чтобы позволить функции изнутри вызвать саму себя.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
fn.creatGallery = function() { // запрещаем управление галереей на время формирования каркаса this.options.flag = false; var start = Date.now(), // время начала анимации duration = this.options.duration * 2, // продолжительность анимации zIndex = 0, // исходное значение 'z-index' для элементов галереи shiftLX, // сдвиг по оси X для элементов левой части галереи shiftRX, // сдвиг по оси X для элементов правой части галереи shiftZ, // сдвиг по оси Z для элементов левой и правой части галереи alignment; // текущие значения атрибута 'style' // функция анимации расстановки элементов галереи var animate = function() { var timePassed = Date.now() - start, // время прошедшее с момента старта // коэффициент выполнения анимации progress = (timePassed / duration < 1) ? timePassed / duration : 1; // Здесь позже напишем js-код управления размещением элементов галереи ... // если анимация ещё не закончилась, т.е. время анимации не истекло // рекурсивно вызываем функцию анимации с передачей контекста вызова if (progress < 1) { return requestAnimationFrame(animate.bind(this)); } }; // первоначальный запуск функции анимации с передачей контекста вызова requestAnimationFrame(animate.bind(this)); }; |
Первое, что делает функция animate
— поворачивает всю галерею по оси X на угол, заданный в настройках.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
fn.creatGallery = function() { // запрещаем управление галереей на время формирования каркаса this.options.flag = false; var start = Date.now(), // время начала анимации duration = this.options.duration * 2, // продолжительность анимации zIndex = 0, // исходное значение 'z-index' для элементов галереи shiftLX, // сдвиг по оси X для элементов левой части галереи shiftRX, // сдвиг по оси X для элементов правой части галереи shiftZ, // сдвиг по оси Z для элементов левой и правой части галереи alignment; // текущие значения атрибута 'style' // функция анимации расстановки элементов галереи var animate = function() { var timePassed = Date.now() - start, // время прошедшее с момента старта // коэффициент выполнения анимации progress = (timePassed / duration < 1) ? timePassed / duration : 1; // поворот по оси Х родительского элемента ('#coverflow') var rotateXOut = (this.options.rotateXOut * progress > this.options.rotateXOut) ? this.options.rotateXOut * progress : this.options.rotateXOut; alignment = 'transform:rotateX(' + rotateXOut + 'deg)'; this.coverflow.style.cssText = alignment; // Здесь позже напишем js-код управления размещением элементов галереи ... // если анимация ещё не закончилась, т.е. время анимации не истекло // рекурсивно вызываем функцию анимации с передачей контекста вызова if (progress < 1) { return requestAnimationFrame(animate.bind(this)); } }; // запуск функции анимации с передачей контекста вызова requestAnimationFrame(animate.bind(this)); }; |
Давайте, на примере поворота галереи по оси X, разберёмся, как работает анимация на JavaScript. На этом принципе построена вся анимация эффекта Cover Flow.
Итак, у нас есть ряд переменных:
- timePassed
- равна разности текущего времени и времени старта анимации. Другими словами, эта переменная содержит длительность анимации на данный момент.
- duration
- содержит время, отведённое на анимацию, т. е. продолжительность анимации, зависит от настроек галереи.
- progress
- коэффициент выполнения анимации. В процессе работы функции
animate
и роста значения переменнойtimePassed
, меняет своё значение от 0 до 1. - options.rotateXOut
- угол, на который должна повернуться галерея по оси X, зависит от настроек галереи.
Текущий угол поворота галереи равен произведению options.rotateXOut * progress. С ростом времени анимации timePassed
, растёт коэффициент выполнения анимации progress
и, как следствие, результат произведения options.rotateXOut * progress стремится к заданному углу поворота options.rotateXOut
.
При каждом вызове функции animate
коэффициент выполнения анимации сравнивается с 1. Если progress < 1, считаем, что анимация ещё не закончена и рекурсивно вызываем функцию анимации.
По такому же принципу, используя зависимость коэффициента выполнения анимации от времени анимации, вычисляются и другие свойства css-трансформации.
Теперь рассмотрим анимацию позиционирования элементов галереи при её начальном построении.
Обращаться к элементам коллекции elements
для применения к ним css-трансформации, можно по их индексам в коллекции. В связи с тем, что css-трансформация у элементов левой и правой части галереи разная, необходимо определиться, какие индексы относятся к левой ветви галереи, а какие к правой.
Нам известен индекс центрального элемента — options.currIndex
. Логично предположить, что элементы, у которых индекс меньше options.currIndex
, относятся к левой части, а у которых больше — к правой. При этом, к элементам, у которых индекс больше или меньше на единицу индекса центрального элемента, применяется своя трансформация.
Ещё раз вкратце, т. к. это очень важно — в галерее с эффектом Cover Flow есть три группы элементов:
— центральный элемент,
— два элемента рядом с центральным,
— остальные элементы левой и правой части галереи,
к которым применяется разные значения свойств css3-трансформации.
Для получения индексов, используем цикл от 0 до options.currIndex. Используя полученные индексы, можно разделить элементы на группы и далее, применить к каждой группе свою css-трансформацию.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
for (var i = 0, j = this.options.currIndex; i <= j; i++) { var leftIndex = this.options.currIndex - i, // левая сторона rightIndex = this.options.currIndex + i; // правая сторона // уменьшаем z-index элемента, чтобы он находился под // предыдущим элементом, таким образом создаётся видимость стопки элементов zIndex = -i; if (i == this.options.currIndex) { // позиционируем центральный элемент галереи // относительно его будут позиционироваться остальные элементы // слева и справа ... } if (leftIndex == this.options.currIndex - 1 || rightIndex == this.options.currIndex + 1) { // устанавливаем ближайшие элементы с правой и левой стороны от центрального (текущего) // у этих элементов увеличенный сдвиг по оси Х (shiftXlong) и по оси Z (shiftZlong) // сдвиг отсчитывается от координат x0 и z0 - центра галереи ... } else { // устанавливаем остальные элементы слева и справа от центрального ... } // прописываем полученные значения свойств css-трансформации и наложение затемнения // для элементов левой части галереи ... // прописываем полученные значения свойств css-трансформации и наложение затемнения // для элементов правой части галереи ... } |
Галерея с эффектом Cover Flow. Позиционирование центрального элемента.
Центральный элемент располагается по центру галереи, что в принципе понятно из его названия. Соответственно, его координаты x0 и y0 совпадают с центром галереи и являются начальной точкой отсчёта для остальных элементов.
Для позиционирования центрального элемента, помещаем в переменную alignment
данные для 3D-преобразования и вызываем функцию setAlignment
, передавая ей в качестве аргументов индекс центрального элемента и alignment
.
Устанавливаем затенение элемента, вызвав функцию setbg
. Первым параметрами данной функции будет индекс элемента, у которого устанавливается затенение, а второй параметр указывает прозрачность (opacity
) затенения. Т. к. центральному элементу затенение не нужно, то второй параметр будет равен 0.
Работу функций setAlignment
и setbg
мы рассмотрим позже.
1 2 3 4 5 6 7 8 9 10 |
// x0 и y0 - координаты центра галереи. С ними совпадает середина центрального // элемента по-умолчанию, эти координаты являются начальной точкой отсчёта // для остальных элементов при создании галереи alignment = 'transform:translate3d(' + this.options.x0 + 'px,' + this.options.y0 + 'px, 0) rotateX(0.1deg) rotateY(0deg); z-index:0;'; // добавляем атрибут 'style' центральному элементу this.setAlignment(this.options.currIndex, alignment); // убираем затенение у центрального элемента this.setbg(this.options.currIndex, 0); |
Галерея с эффектом Cover Flow. Подготовка данных для позиционирование элементов соседних с центральным.
устанавливаем ближайшие элементы с правой и левой стороны от центрального (текущего). У этих элементов увеличенный сдвиг по оси Х (shiftXlong
) и по оси Z (shiftZlong
). Сдвиг этих элементов отсчитывается от центра галереи.
Напомню, увеличенный по осям X и Z сдвиг нужен для того, чтобы центральный элемент не закрывал соседние элементы.
1 2 3 4 5 6 7 8 |
// устанавливаем ближайшие элементы с правой и левой стороны от центрального (текущего) // у этих элементов увеличенный сдвиг по оси Х (shiftXlong) и по оси Z (shiftZlong) // сдвиг отсчитывается от координат x0 и z0 shiftLX = this.options.x0 - this.options.shiftXlong * progress; shiftRX = this.options.x0 + this.options.shiftXlong * progress; shiftZ = this.options.z0 - this.options.initShiftZ - this.options.shiftZlong * progress; |
Галерея с эффектом Cover Flow. Подготовка данных для позиционирование остальных элементов.
Для этих элементов позиционирование начинается с координат полученных для элементов, соседних с центральным: shiftLX
, shiftRX
и shiftZ
1 2 3 4 5 |
shiftLX = shiftLX - this.options.shiftX * progress; shiftRX = shiftRX + this.options.shiftX * progress; shiftZ = shiftZ - this.options.shiftZ * progress; |
Галерея с эффектом Cover Flow. Применение css-трансформации к элементам галереи.
При каждой итерации цикла мы получаем индексы трёх элементов (центрального и по одному из левой и правой части галереи) и их смещение за прошедшее время анимации.
Теперь эти данные необходимо использовать для формирования свойства transform
и, после этого, прописать в качестве значения атрибута style
соответствующего элемента галереи Cover Flow. Для центрального элемента это делается сразу же, при совпадении текущего индекса с options.currIndex
. Для остальных элементов галереи необходимо написать отдельный JS-код, причём разный для левой и правой ветви.
1 2 3 4 5 6 7 8 9 10 11 |
// левая сторона галереи alignment = this.getAlignment(shiftLX, this.options.y0, shiftZ, this.options.rotateX, this.options.rotateY, zIndex); this.setAlignment(leftIndex, alignment); this.setbg(leftIndex, 1); // правая сторона галереи alignment = this.getAlignment(shiftRX, this.options.y0, shiftZ, this.options.rotateX, -this.options.rotateY, zIndex); this.setAlignment(rightIndex, alignment); this.setbg(rightIndex, 1); |
Мы рассмотрели отдельные части JS-кода функции creatGallery
, теперь соберём их вместе:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
fn.creatGallery = function() { // запрещаем управление галереей на время формирования каркаса this.options.flag = false; var start = Date.now(), // время начала анимации duration = this.options.duration * 2, // продолжительность анимации zIndex = 0, // исходное значение 'z-index' для элементов галереи shiftLX, // сдвиг по оси X для элементов левой части галереи shiftRX, // сдвиг по оси X для элементов правой части галереи shiftZ, // сдвиг по оси Z для элементов левой и правой части галереи alignment; // текущие значения атрибута 'style' // функция анимации расстановки элементов галереи var animate = function() { // время прошедшее с момента старта var timePassed = Date.now() - start, // коэффициент выполнения анимации progress = (timePassed / duration < 1) ? timePassed / duration : 1; // поворот по оси Х родительского элемента ('#coverflow') var rotateXOut = (this.options.rotateXOut * progress > this.options.rotateXOut) ? this.options.rotateXOut * progress : this.options.rotateXOut; alignment = 'transform:rotateX(' + rotateXOut + 'deg)'; this.coverflow.style.cssText = alignment; // расстановка элементов галереи в первоначальное положение // в зависимости от эффекта coverflow for (var i = 0, j = this.options.currIndex; i <= j; i++) { var leftIndex = this.options.currIndex - i, // left side rightIndex = this.options.currIndex + i; // right side // уменьшаем z-index элемента, чтобы он находился под // предыдущим элементом, таким образом создаётся видимость стопки элементов zIndex = -i; // позиционируем центральный элемент галереи // относительно его будут позиционироваться остальные элементы // слева и справа if (i == this.options.currIndex) { // x0 и y0 - координаты центра галереи. С ними совпадает середина центрального // элемента по-умолчанию, эти координаты являются начальной точкой отсчёта // для остальных элементов при создании галереи alignment = 'transform:translate3d(' + this.options.x0 + 'px,' + this.options.y0 + 'px, 0) rotateX(0.1deg) rotateY(0deg); z-index:0;'; // добавляем атрибут 'style' цетральному элементу this.setAlignment(this.options.currIndex, alignment); this.setbg(this.options.currIndex, 0); } if (leftIndex == this.options.currIndex - 1 || rightIndex == this.options.currIndex + 1) { // устанавливаем ближайшие элементы с правой и левой стороны от центрального (текущего) // у этих элементов увеличенный сдвиг по оси Х (shiftXlong) и по оси Z (shiftZlong) // сдвиг отсчитывается от координат x0 и z0 shiftLX = this.options.x0 - this.options.shiftXlong * progress; shiftRX = this.options.x0 + this.options.shiftXlong * progress; shiftZ = this.options.z0 - this.options.initShiftZ - this.options.shiftZlong * progress; } else { // устанавливаем остальные элементы слева и справа от центрального shiftLX = shiftLX - this.options.shiftX * progress; shiftRX = shiftRX + this.options.shiftX * progress; shiftZ = shiftZ - this.options.shiftZ * progress; } // левая сторона галереи alignment = this.getAlignment(shiftLX, this.options.y0, shiftZ, this.options.rotateX, this.options.rotateY, zIndex); this.setAlignment(leftIndex, alignment); this.setbg(leftIndex, 1); // правая сторона галереи alignment = this.getAlignment(shiftRX, this.options.y0, shiftZ, this.options.rotateX, -this.options.rotateY, zIndex); this.setAlignment(rightIndex, alignment); this.setbg(rightIndex, 1); } if (progress < 1) { return requestAnimationFrame(animate.bind(this)); } else { // сохраняем новые значения z0 все элементов this.saveCoordinatesZ(); // снимаем запрет на управление галереей this.options.flag = true; } }; // запуск функции анимации с передачей контекста вызова requestAnimationFrame(animate.bind(this)); }; |
Галерея с эффектом Cover Flow. Сохранение координаты Z элементов галереи.
Как видно из кода функции, по окончанию анимации формирования галереи, вызывается функция saveCoordinatesZ
. Эта функция сохраняет координаты Z всех элементов галереи в массив arrayZ0
. Данные из этого массива понадобятся при создании анимации при наведении на эти элементы.
Функция saveCoordinatesZ
очень простая, отдельно рассматривать её работу я не буду, вполне достаточно будет комментариев в коде.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
fn.saveCoordinatesZ = function() { // получаем значение атрибута 'style' каждого элемента из коллекции // элементов галереи, т.е., текущее расположение каждого элемента var styles = this.getStylesElements(); for (var i = 0, j = this.elements.length; i < j; i++) { // значение атрибута 'style' i-го элемента var style = styles[i]; // сохраняем в массив значение z0 каждого элемента галереи this.arrayZ0[i] = style.z0; } }; |
В своей работе функция saveCoordinatesZ
вызывает функцию getStylesElements
, а она в свою очередь getStylesElement
. Эти функции так же очень просты и нет необходимости рассматривать каждую строчку кода. Обойдёмся комментариями, встроенными в код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
fn.getStylesElements = function() { var arr = []; // перебираем всю коллекцию элементов галереи for (var i = 0, j = this.elements.length; i < j; i++) { // получаем объект с набором координат каждого элемента и // помещаем этот объект в массив arr.push(this.getStylesElement(this.elements[i])); } return arr; }; fn.getStylesElement = function(el) { // используя встроенный метод 'getComputedStyle' получаем полную информацию // о стилях элемента var style = window.getComputedStyle(el), obj = {}; // если у элемента галереи есть атрибут 'style', получаем // из него текущие координаты элементов галереи и помещаем // их в объект 'obj' if (style.transform !== 'none') { // довольно распространённый способ распарсить данные полученные // с помощью метода 'getComputedStyle' // многократное использование 'split' позволяет удалить из строки // лишние символы, выдавая на выходе массив, содержащий только // числовые данные var arr = style.transform.split('(')[1].split(')')[0].split(','); obj.x0 = Number(arr[12]), obj.y0 = Number(arr[13]), obj.z0 = Number(arr[14]), obj.zIndex = Number(style.zIndex); // в противном случае берём информацию из объекта 'options' } else { obj.x0 = this.options.x0, obj.y0 = this.options.y0, obj.z0 = this.options.z0, obj.zIndex = 0; } return obj; }; |
Назначение встроенных стилей элементам галереи с эффектом Cover Flow.
Рассмотрим три важные функции, упомянутые в выше приведённом JS-коде, которые, получив текущие значения сдвигов и углов поворота, прописывают CSS-код трансформации непосредственно внутри тега элемента с помощью атрибута style
. Эти функции используются не только при построении, но и при листании галереи, создавая эффект Cover Flow.
Прежде чем прописывать в атрибут style
CSS-код, этот код нужно создать. За это отвечает функция getAlignment
. Принимая в качестве аргумента вычисленные данные, она возвращает готовый CSS-код, собранный по имеющемуся шаблону.
1 2 3 4 5 6 7 8 |
fn.getAlignment = function(x0, y0, z0, rotateX, rotateY, zIndex) { return 'transform:translate3d(' + x0 + 'px,' + y0 + 'px,' + z0 + 'px) ' + 'rotateX(' + rotateX + 'deg) ' + 'rotateY(' + rotateY + 'deg); ' + 'z-index:' + zIndex + ';'; }; |
Далее, полученный CSS-код необходимо прописать в атрибут style
конкретного элемента галереи. Используем для этой цели функцию setAlignment
. Аргументами этой функции является CSS-код и индекс элемента, которому его нужно применить.
1 2 3 4 5 |
fn.setAlignment = function(index, style) { this.elements[index].style.cssText = style; }; |
Как видно на скриншоте, приведённом в начале статьи, элементы галереи имеют затенения. Причём у элементов левой ветви затенена правая часть, а у элементов правой ветви — левая. Этот эффект создаётся путём применения линейного градиента к контейнеру <DIV>
, расположенному над элементом галереи.
Для реализации затенения создадим функцию setbg
. Аргументами у этой функции будут: индекс текущего элемента и прозрачность.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fn.setbg = function(index, opacity) { var side; // в зависимости от индекса элемента определяем, с какой стороны // будет затенение if (index <= this.options.currIndex) { side = (this.options.type === 'type3') ? 'left' : 'right'; } else { side = (this.options.type === 'type3') ? 'right' : 'left'; } this.elementsBg[index].style.cssText = 'background:-webkit-linear-gradient(' + side + ', rgb(0, 0, 0), rgba(0, 0, 0, 0)); opacity:' + opacity + ';'; }; |
Как видно из приведённого кода, направление градиента зависит не только от индекса элемента, но и от типа эффекта Cover Flow.
Переключение вариантов эффекта Cover Flow.
После рассмотрения анимации формирования каркаса галереи при загрузке страницы, логично разобраться, как галерея меняет свой вид при переключении типа эффекта Cover Flow.
Если при загрузке страницы, вид эффекта Cover Flow мы передаём, вызывая функцию-конструктор Coverflow
, то в дальнейшем мы сможем менять разновидность эффекта при помощи созданного нами блока управления. Напомню вам его HTML-код:
1 2 3 4 5 6 7 8 9 |
<div class="type"> <ul> <li id="type1" class="active"><img src="img/coverflow/type_1.jpg" alt=""></li> <li id="type2"><img src="img/coverflow/type_2.jpg" alt=""></li> <li id="type3"><img src="img/coverflow/type_3.jpg" alt=""></li> </ul> </div> |
Визуально блок переключения разновидностей анимации Cover Flow выглядит так:
Интерактивными элементами являются элементы списка <LI>
. Клик именно по этим элементам будет вызывать переключение типа эффекта Cover Flow.
Как с точки зрения семантики, так и с точки зрения СЕО — тэг
<a>
должен использоваться только для формирования ссылок, ведущих на другие страницы сайта или другой интернет-ресурс. Для управления элементами текущей страницы (показать / скрыть, изменить стиль, переместить, подгрузить и т.д.) должны использоваться теги <span>
, <button>
, <div>
, <li>
. Именно они должны быть интерактивными элементами страницы.
Для упрощения и сокращения JS-кода мы не будем вешать обработчики событий на каждый элемент <LI>
, а воспользуемся делегированием и с помощью функции addEventListener
вешаем один обработчик на их родительский элемент <UL>
. При возникновении события click
на ненумерованном списке <UL>
, обработчик вызовет функцию switchGalleryType
.
Регистрация обработчика происходит в функции galleryControl
, которую мы рассмотрим, когда будем разбираться с реализацией функционала листания галереи.
1 2 3 |
this.parentSwitches.addEventListener('click', this.switchGalleryType.bind(this)); |
При использовании метода
addEventListener
теряется контекст вызова — this
, который ссылается на текущий объект. Используя встроенный в JavaScript метод bind
, можно напрямую передать контекст вызова в функцию обработчика события.
Подробно разбирать, как работает делегирование, основанное на всплытии событий, я не буду. Вы можете прочитать об этом здесь.
Алгоритм работы функции switchGalleryType
:
-
1
При срабатывании события (по блоку переключения был сделан клик) и используя делегирование, определяем, что клик произошел именно по элементу
<LI>
, а не по пустой области его родительского элемента. Получаем значение атрибутаdata-type
у элемента<LI>
, по которому был сделан клик, значение атрибута содержит тип эффекта Cover Flow. -
2
Перебираем коллекцию переключателей и удаляем у них класс
active
. Присваиваем классactive
элементу, по которому был сделан клик. -
3
Вызываем функцию
init
, передавая в качестве параметра тип выбранной анимации эффекта Cover Flow.
Полный JS-код функции switchGalleryType
с комментариями:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
fn.switchGalleryType = function(e) { // запрещаем переключение эффекта Cover Flow на время анимации if (this.options.flag === false) return; // определяем, что клик сделан по элементу <li> var target = e.target; while (target !== this.parentSwitches) { if (target.tagName == 'LI') break; target = target.parentNode; } // если клик сделан не по интерактивному элементу, выходим из функции if (target === this.parentSwitches) return; // получаем значение атрибута 'data-type' у элемента <li>, по которому // был сделан клик, значение атрибута будет соответствовать выбранному // типу эффекта Cover Flow var type = target.getAttribute('data-type'); // визуально отображаем переключение между эффектами [].forEach.call(this.tabs, function(el) { el.classList.remove('active'); }); target.classList.add('active'); // инициализируем галерею, для установки параметров соответствующих // новому эффекту Cover Flow this.init(type); }; |
Управление листанием галереи с эффектом Cover Flow.
В техническом задании мы определили четыре способа управления листанием галереи. Напомню их:
— кнопками навигации «prev» и «next»;
— клавишами клавиатуры со стрелочками «←» и «→»;
— вращением колёсика мыши;
— клик по изображению, к которому необходимо прокрутить галерею.
Всё управление галереей реализовано в функции galleryControl
. В этой функции регистрируются обработчики событий, а в случае наступления этих событий, определяется направление перемещения и запускается прокручивание галереи. Кроме этого, здесь регистрируется обработчик события переключения типа анимации эффекта Cover Flow.
Галерея с эффектом Cover Flow. Управление кнопками «prev» и «next».
Давайте вспомним HTML-код блока навигация по галерее кнопками «prev» и «next»:
1 2 3 4 5 6 |
<div id="control" class="control"> <span data-show="prev">prev</span> <span data-show="next">next</span> </div> |
Мы опять воспользуемся делегированием и с помощью функции addEventListener
повесим обработчик на родительский контейнер элементов <SPAN>
. В атрибуте [data-show]
этих элементов содержится информация о направлении листания галереи.
Запишем следующий JS-код в функцию galleryControl
:
1 2 3 4 5 6 7 8 9 10 |
document.getElementById('control').addEventListener('click', function(e) { // если клик сделан по объекту 'control', но мимо элементов 'span', // прекращаем работу функции if (e.target.tagName != 'SPAN') return; dir = (e.target.getAttribute('data-show') === 'next') ? -1 : 1; // запускаем смещение на один элемент в направлении dir this.shifGallery(dir, this.options.duration); }.bind(this)); |
Как видите, здесь цикл while
не используется, т. к. у <SPAN>
нет дочерних элементов, по которым может быть сделан клик.
Галерея с эффектом Cover Flow. Управление с клавиатуры.
Обработчик события устанавливается на объект window
, который представляет собой окно, содержащее DOM документ. Будет отслеживаться «клавиатурное» событие keydown
, которое будет происходить при нажатии клавиши.
Нас интересует нажатие только на стрелочные клавиши «←» и «→», которые имеют код 37 и 39 соответственно. При нажатии на другие клавиши произойдёт выход из обработчика события.
Направление листания галереи зависит от кода нажатой клавиши.
1 2 3 4 5 6 7 8 9 10 11 12 |
window.addEventListener('keydown', function(e) { // коды клавиш var keys = {left: 37, right: 39}; // если нажата другая клавиша, прекращаем работу функции if (e.which !== keys.right && e.which !== keys.left) return; // в зависимости от нажатой клавиши, устанавливаем направление перемещения dir = (e.which === keys.right) ? -1 : 1; // запускаем смещение на один элемент в направлении dir this.shifGallery(dir, this.options.duration); }.bind(this)); |
Галерея с эффектом Cover Flow. Управление колёсиком мыши.
Обработчик события устанавливается на объект coverflow
, т. е. галерея будет пролистываться только, если указатель мыши находится над ней. Направление листания галереи зависит от направления вращения колёсика мыши.
1 2 3 4 5 6 7 8 9 10 11 |
// управление колёсиком мыши работает, если указатель // мыши находится над DIV'ом '.coverflow' this.coverflow.addEventListener('wheel', function(e) { // в зависимости от направления вращения колёсика мыши, // устанавливаем направление перемещения галереи dir = (e.deltaY > 0) ? -1 : 1; // запускаем листание галереи в направлении dir this.shifGallery(dir, this.options.duration); }.bind(this)); |
Галерея с эффектом Cover Flow. Скролл к элементу по которому был сделан клик.
Предыдущие способы листания галереи, при наступлении события, смещают её влево или вправо на один элемент. Сейчас мы рассмотрим прокручивание галереи к элементу, по которому был сделан клик. При этом не важно, на каком расстоянии этот элемент находится от текущего.
В данном случае я решил отойти от делегирования события к регистрации обработчика на каждом элементе. По ряду причин связанных с упрощением HTML-вёрстки, в данном случае будет трудно идентифицировать выбранный элемент и определить его позицию (индекс) в галерее. Конечно, можно изменить вёрстку, например, представив галерею в виде немаркированного списка, где элементами галереи будут элементы списка <LI>
. Можно добавить им атрибуты, которые будут содержать информацию об индексе элемента и т.д. Тем не менее я решил зарегистрировать обработчики событий на каждом элементе галереи.
Итак, у нас есть коллекция элементов галереи elements
, которую мы переберём с помощью метода forEach
, каждому элементу назначим свой обработчик события click
и получим индекс каждого элемента.
1 2 3 4 5 6 |
[].forEach.call(this.elements, function(el, index) { // запускается скролл галереи к выбранному элементу el.addEventListener('click', this.scroll.bind(this, index)); }.bind(this)); |
Давайте анимируем выбор элемента. Пусть элемент, при наведении, плавно выдвигается из общего ряда галереи, а если убрать курсор, так же плавно встаёт на место. Для этого назначим каждому элементу ещё по два обработчика событий — на события mouseenter
и mouseleave
:
1 2 3 4 5 6 7 8 9 10 |
[].forEach.call(this.elements, function(el, index) { // элемент выдвигается при наведении курсора el.addEventListener('mouseenter', this.hoverShift.bind(this, el, index, 'mouseover')); // элемент встаёт на место, когда курсор уходит с него el.addEventListener('mouseleave', this.hoverShift.bind(this, el, index, 'mouseleave')); // запускается скролл галереи к выбранному элементу el.addEventListener('click', this.scroll.bind(this, index)); }.bind(this)); |
Функции shifGallery
, hoverShift
и scroll
, которые вызываются при срабатывании обработчиков событий, мы рассмотрим чуть позже, а пока — полный JS-код функции galleryControl
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
fn.galleryControl = function() { // хранит направление листания галереи var dir; // устанавливаем обработчики событий для управления галереей // управление кнопками 'prev' / 'next' document.getElementById('control').addEventListener('click', function(e) { // если клик сделан по объекту 'control', но мимо элементов 'span', // прекращаем работу функции if (e.target.tagName != 'SPAN') return; dir = (e.target.getAttribute('data-show') === 'next') ? -1 : 1; // запускаем смещение на один элемент в направлении dir this.shifGallery(dir, this.options.duration); }.bind(this)); // управление клавишами ← и → window.addEventListener('keydown', function(e) { // коды клавиш var keys = {left: 37, right: 39}; // если нажата другая клавиша, прекращаем работу функции if (e.which !== keys.right && e.which !== keys.left) return; // в зависимости от нажатой клавиши, устанавливаем направление перемещения dir = (e.which === keys.right) ? -1 : 1; // запускаем смещение на один элемент в направлении dir this.shifGallery(dir, this.options.duration); }.bind(this)); // управление колёсиком мыши, управление работает, если указатель // мыши находится над DIV'ом '.coverflow' this.coverflow.addEventListener('wheel', function(e) { dir = (e.deltaY > 0) ? -1 : 1; this.shifGallery(dir, this.options.duration); }.bind(this)); // листание галереи к элементу, по которому был сделан клик [].forEach.call(this.elements, function(el, index) { // элемент выдвигается при наведении курсора el.addEventListener('mouseenter', this.hoverShift.bind(this, el, index, 'mouseover')); // элемент встаёт на место, когда курсор уходит с него el.addEventListener('mouseleave', this.hoverShift.bind(this, el, index, 'mouseleave')); // запускается скролл галереи к выбранному элементу el.addEventListener('click', this.scroll.bind(this, index)); }.bind(this)); // переключаем эффект Cover Flow this.parentSwitches.addEventListener('click', this.switchGalleryType.bind(this)); }; |
Листание галереи с эффектом Cover Flow.
Эффект Cover Flow при листании галереи создаётся в функции shifGallery
. Алгоритм работы этой функции и её JS-код во многом совпадают с алгоритмом и кодом функции creatGallery
, поэтому отдельные участки кода я буду публиковать без объяснения, чтобы избежать ненужного повторения.
Функция shifGallery
имеет два входных параметра:
1. dir — направление листания галереи;
2. duration — продолжительность анимации при смещении на один элемент.
Кроме этого, функция использует значения из объекта options
.
Рассмотрим алгоритм работы функции shifGallery
:
-
1
Проверяется индекс текущего элемента и направление перемещения. Если текущим является первый или последний элемент, а направление перемещение указано за пределы галереи, работа функции прекращается.
-
2
Устанавливается запрет на листание галереи до окончания текущей анимации.
-
3
Фиксируется время начала анимации и регистрируется ряд переменных:
— zIndex, Z-индекс элемента галереи;
— offsetX, текущее смещение элемента по оси X;
— offsetZ, текущее смещение элемента по оси Z;
— angleY, текущий угол поворота элемента по оси Y;
— alignment, текущее значение атрибутаstyle
элемента галереи. -
4
Получаем значение атрибута
style
каждого элемента, т. е. текущее расположение каждого элемента. -
5
Запускается функция анимации.
-
6
Вычисляются смещение по координатам X, Z и угол поворота по оси Y в текущей итерации, а также Z-index каждого элемента.
-
7
На основании полученных текущих координат и угла поворота, формируется содержание (значение) атрибута
style
для каждого элемента галереи в текущий момент времени анимации. -
8
По окончанию анимации:
— вычисляется индекс нового текущего элемента;
— сохраняется в массив координата Z каждого элемента;
— снимается запрет на листание галереи.
Теперь начнём писать JS-код, реализующий рассмотренный алгоритм.
Начнём с того, что:
- создадим функцию
shifGallery
; - зарегистрируем в ней ряд переменных и проведём проверку входных данных;
- получим значение атрибута
style
каждого элемента галереи; - зарегистрируем внутри функции
shifGallery
функцию анимацииanimate
и сделаем её вызов; - укажем поведение функции после окончания времени, отведённого на анимацию сдвига.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
fn.shifGallery = function(dir, duration) { // функция прекращает работу, если: // - текущим является первый элемент и попытка просмотреть предыдущий // - текущим является последний элемент и попытка просмотреть следующий if (this.options.flag !== true || this.options.currIndex == 0 && dir == 1 || this.options.currIndex == this.options.count - 1 && dir == -1) return; // устанавливаем флаг, запрещающий начинать новое перемещение, // пока не закончилось текущее this.options.flag = false; var start = Date.now(), // время начала анимации zIndex = 0, // исходное значение 'z-index' для элементов галереи offsetX, // текущее смещение элемента по оси X offsetZ, // текущее смещение элемента по оси Z angleY, // текущий угол поворота элемента по оси Y alignment; // текущее значение атрибута 'style' // получаем значение атрибута 'style' каждого элемента из коллекции // элементов галереи, т.е. текущее расположение каждого элемента var styles = this.getStylesElements(); var animate = function() { // время прошедшее с момента старта var timePassed = Date.now() - start, // коэффициент выполнения анимации progress = (timePassed / duration < 1) ? timePassed / duration : 1; // перебираем коллекцию элементов, вычисляем и записываем в атрибут 'style' // координаты X и Y, угол поворота по оси Y каждого элемента галереи // при каждой рекурсии функции 'animate' ... if (progress < 1) { // рекурсивно запускаем функцию анимации листания галереи requestAnimationFrame(animate.bind(this)); } else { // получаем индекс нового текущего элемента this.options.currIndex = this.options.currIndex - dir; // сохраняем новые значения z0 всех элементов this.saveCoordinatesZ(); // разрешаем управление галереей по окончанию анимации this.options.flag = true; } }; // запускаем функцию анимации листания галереи requestAnimationFrame(animate.bind(this)); }; |
При каждом вызове функции animate
перебираются все элементы галереи и каждому элементу устанавливается значения 3D-трансформации, зависящие от текущего значения коэффициента выполнения анимации — progress
.
Прежде чем рассматривать перебор элементов и алгоритмы, по которым изменяются свойства трансформации, давайте ещё раз очень подробно разберёмся, как осуществляется анимация движения элемента. Разбирать будем на примере формирования смещения по координате X. Вот формула, которая реализует анимацию движения по данной координате:
1 2 3 |
offsetX = style.x0 + dir * this.options.shiftX * progress; |
Теперь подробно о составляющих этой формулы:
- offsetX
- Текущее значение координаты X элемента в данный момент анимации.
- style.x0
- Значение координаты X элемента до начала анимации, т. е. до нажатия на кнопки навигации «prev»/»next», или на клавишами клавиатуры со стрелочками «←»/»→», или вращения колёсика мыши. Значения всех координат каждого элемента были получены до начала анимации.
- dir
- Направление анимации. Может принимать значение 1, если галерея прокручивается в направлении «prev» и -1, если в направлении «next».
- this.options.shiftX
- Это значение указывает на расстояние между элементами галереи по оси X и зависит от типа эффекта Cover Flow. Значения задаются в функции
init
при формировании объекта настроекoptions
. Соответственно, при сдвиге галереи на один шаг, все элементы смещаются по оси X на величинуoptions.shiftX
вправо или влево.
Напомню, что для элементов, соседних с текущим, это значение в два раза больше и хранится в свойствеoptions.shiftXlong
. - progress
- Текущий коэффициент анимации, принимающий значение от 0 (при старте анимации) до 1 (по окончанию анимации) и напрямую зависящий от времени с начала анимации. Когда
progress
станет равным 1, результат произведения
this.options.shiftX * progress станет равным this.options.shiftX,
т. е. элемент к концу времени анимации сместится по оси X на расстояние расстояние равное расстоянию между двумя элементами, которое заложено в настройках галереи с эффектом Cover Flow.
По такому же принципу работает анимация по остальным координатам и углу поворота.
При переборе коллекции элементов галереи, все элементы делятся на три группы. Определим условия, по которым будет происходить это деление:
— элементы правой ветви галереи;— текущий элемент;
— элементы соседние с текущим.
К этим группам элементам применяются разные значения свойств 3D-трансформации.
У вас может возникнуть вопрос, зачем нужно делить элементы галереи Cover Flow на группы и в чём заключаются отличия в 3D-трансформации у элементов разных групп.
- Элементы правой части галереи развёрнуты по оси Y на угол с противоположным знаком, в отличии от элементов левой части. Кроме этого, накладываемое на них затенение, в виде линейного градиента, имеет другое направление.
-
У текущего элемента нет угла поворота по оси Y и линейного градиента, они появляются при анимации и зависят от направления листания галереи.
Смещение текущего элемента по оси X будет в два раза больше чем у остальных элементов галереи. -
В зависимости от направления листания, элемент справа или слева от текущего, должен занять его место, при этом, его угол поворота по оси Y и прозрачность затенения должны плавно уменьшиться до 0.
Смещение этого элемента по оси X будет в два раза больше чем у остальных элементов галереи.
теперь рассмотрим, как реализованы условия, делящие элементы галереи Cover Flow на группы:
-
Элементы правой ветви галереи.
12345if (dir == 1 && i > this.options.currIndex || dir == -1 && i > this.options.currIndex + 1) {// задаём смещение offsetZ для элементов справа от текущего} -
Текущий элемент.
1234567891011if (i == this.options.currIndex) {// рассчитывается трансформация текущего элемента// независимо от направления листания, текущий элемент становится// расположенным рядом с новым текущим элементом, поэтому// у него увеличенный размер сдвига по осям координат// устанавливается стиль линейного градиента, ориентация градиента зависит// от расположения элемента относительно нового текущего элемента} -
Элементы соседние с текущим.
123456if (i == this.options.currIndex - 1 && dir == 1 || i == this.options.currIndex + 1 && dir == -1) {// задаётся смещение элементов по оси X и Y, угол поворота по оси Y// устанавливаем затенение элемента}
Теперь выведем полный JS-код функции shifGallery
, обеспечивающий сдвиг (пролистывание) галереи на один элемент:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
fn.shifGallery = function(dir, duration) { // функция прекращает работу, если: // - текущим является первый элемент и попытка просмотреть предыдущий // - текущим является последний элемент и попытка просмотреть следующий if (this.options.flag !== true || this.options.currIndex == 0 && dir == 1 || this.options.currIndex == this.options.count - 1 && dir == -1) return; // устанавливаем флаг, запрещающий начинать новое перемещение, // пока не закончилось текущее this.options.flag = false; var start = Date.now(), // время начала анимации zIndex = 0, // исходное значение 'z-index' для элементов галереи offsetX, // текущее смещение элемента по оси X offsetZ, // текущее смещение элемента по оси Z angleY, // текущий угол поворота элемента по оси Y alignment; // текущее значение атрибута 'style' // получаем значение атрибута 'style' каждого элемента из коллекции // элементов галереи, т.е. текущее расположение каждого элемента var styles = this.getStylesElements(); var animate = function() { // время прошедшее с момента старта var timePassed = Date.now() - start, // коэффициент выполнения анимации progress = (timePassed / duration < 1) ? timePassed / duration : 1; // перебираем все элементы галереи for (var i = 0, j = this.elements.length; i < j; i++) { // значение атрибута 'style' i-го элемента var style = styles[i]; // смещение i-го элемента по оси X и Y offsetX = style.x0 + dir * this.options.shiftX * progress; offsetZ = style.z0 + dir * this.options.shiftZ * progress; // угол поворота i-го элемента по оси Y angleY = (i < this.options.currIndex) ? this.options.rotateY : (i > this.options.currIndex) ? -this.options.rotateY : 0; // z-index i-го элемента zIndex = (i < this.options.currIndex) ? style.zIndex + dir : (i > this.options.currIndex) ? style.zIndex - dir : 0; if (dir == 1 && i > this.options.currIndex || dir == -1 && i > this.options.currIndex + 1) { // задаём смещение offsetZ для элементов справа от текущего offsetZ = style.z0 - dir * this.options.shiftZ * progress; } else if (i == this.options.currIndex) { // рассчитывается трансформация текущего элемента // независимо от направления листания, текущий элемент становится // расположенным рядом с новым текущим элементом, поэтому // у него увеличенный размер сдвига по осям координат var shiftZ = this.options.shiftZlong + this.options.initShiftZ; // смещение текущего элемента по оси X и Y в текущей итерации offsetX = style.x0 + dir * this.options.shiftXlong * progress; offsetZ = -shiftZ * progress; // угол поворота текущего элемента по оси Y в текущей итерации angleY = -dir * this.options.rotateY * progress; // устанавливаем затенение элемента в зависимости от того, в // какую часть галереи он перемещается if (dir == -1) { var side = (this.options.type === 'type3') ? 'left' : 'right'; } else { var side = (this.options.type === 'type3') ? 'right' : 'left'; } // устанавливается стиль линейного градиента, ориентация градиента зависит // от расположения элемента относительно нового текущего элемента this.elementsBg[i].style.cssText = 'background:-webkit-linear-gradient(' + side + ', rgb(0, 0, 0), rgba(0, 0, 0, 0)); opacity:' + progress +';' ; } else if (i == this.options.currIndex - 1 && dir == 1 || i == this.options.currIndex + 1 && dir == -1) { // элементы рядом с текущим var angle = (dir == -1) ? -this.options.rotateY : this.options.rotateY; // смещение элементов рядом с текущим по оси X и Y в текущей итерации offsetX = style.x0 + dir * this.options.shiftXlong * progress; offsetZ = style.z0 - style.z0 * progress; // угол поворота элементов рядом с текущим по оси Y в текущей итерации angleY = (progress < 1) ? angle - dir * this.options.rotateY * progress : 0; // накладываем background this.setbg(i, 1 - progress); } // формируется содержание (значение) атрибута 'style' для i-го элемента галереи alignment = this.getAlignment(offsetX, this.options.y0, offsetZ, this.options.rotateX, angleY, zIndex); // добавляем атрибут 'style' i-му элементу галереи this.setAlignment(i, alignment); } if (progress < 1) { // рекурсивно запускаем функцию анимации листания галереи requestAnimationFrame(animate.bind(this)); } else { // получаем индекс нового текущего элемента this.options.currIndex = this.options.currIndex - dir; // сохраняем новые значения z0 все элементов this.saveCoordinatesZ(); // разрешаем управление галереей по окончанию анимации this.options.flag = true; } }; // запускаем функцию анимации листания галереи requestAnimationFrame(animate.bind(this)); }; |
Анимация поведения элемента галереи Cover Flow при наведении курсора.
Теперь рассмотрим поведение элемента, до которого мы хотим прокрутить галерею с эффектом Cover Flow, когда на него наводят курсор. Выше писалось, что при наведении курсора, элемент будет плавно выдвигаться из общего ряда элементов галереи и также плавно вставать на место, если курсор убрать.
За анимацию этого эффекта будет отвечать функция hoverShift
. Вызывается эта функция при наступлении события mouseenter
или mouseleave
. Обработчики этих событий были зарегистрированы в функции galleryControl
.
Функция hoverShift
принимает три аргумента:
— элемент галереи на котором сработал обработчик события;
— индекс элемента;
— событие, на которое сработал обработчик.
В статье неоднократно рассматривалась JS-анимация, поэтому расписывать алгоритм работы функции hoverShift
, достаточно подробных комментариев в коде функции.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
fn.hoverShift = function(el, index, action) { // если наведение на текущий элемент или предыдущая анимация ещё не закончилась, // прекращаем работу функции if (index == this.options.currIndex || this.options.flag === false) return; var start = Date.now(), // время начала анимации style = this.getStylesElement(el), // объект с текущими стилями элемента // в зависимости от наступившего события, определяем направление и // величину перемещения элемента по оси Z shiftZ = (action === 'mouseover') ? this.options.shiftZ : this.arrayZ0[index] - style.z0, // угол поворота элемента в зависимости от индекса rotateY = (index < this.options.currIndex) ? this.options.rotateY : -this.options.rotateY, progress = 0, // коэффициент выполнения анимации shift, // текущее смещение элемента по оси Z alignment; // текущее значение атрибута 'style' var fn = function() { // время прошедшее с момента старта анимации var timePassed = Date.now() - start; progress = (progress < 1) ? timePassed / this.options.durationZ : 1; // не будем выдвигать элемент из колоды при эффекте Cover Flow // равным 'type3', ограничимся при этом только изменением прозрачности // его затенения if (this.options.type !== 'type3') { shift = shiftZ * progress; // формируется содержание (значение) атрибута 'style' для элемента галереи alignment = 'transform:translate3d(' + style.x0 + 'px,' + style.y0 + 'px,' + (style.z0 + shift) + 'px) rotateX(0.1deg) rotateY(' + rotateY + 'deg); z-index:0;'; // добавляем атрибут 'style' элементу галереи this.setAlignment(index, alignment); } // текущая прозрачность затенения в зависимости от события var opacity = (action === 'mouseover') ? 1 - progress : progress, opacity = (opacity > 1) ? 1 : (opacity < 0) ? 0 : opacity; // устанавливаем прозрачность контейнера с линейным градиентом this.elementsBg[index].style.opacity = opacity; if (progress < 1) { // рекурсивно запускаем функцию анимации requestAnimationFrame(fn.bind(this)); } }; requestAnimationFrame(fn.bind(this)); }; |
Скролл галереи с эффектом Cover Flow до выбранного элемента.
Скролл галереи к выбранному элементу представляет из себя цикл сдвигов галереи на один элемент, т. е. последовательность вызовов функции shifGallery
, где количество вызовов зависит от того, насколько элементов необходимо «проскроллить» галерею.
Отвечает за это функция scroll
, вызов которой происходит при срабатывании обработчика события click
, зарегистрированного в функции galleryControl
.
1 2 3 4 |
// запускается скролл галереи к выбранному элементу el.addEventListener('click', this.scroll.bind(this, index)); |
Функция scroll
очень простая, поэтому вполне будет достаточно коментариев внутри функции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
fn.scroll = function(index) { // если клик по текущему элементу, прекращаем работу функции if (index == this.options.currIndex) return; var LIMIT = 100, RATIO = 30, // вычисляем, на сколько элементов нужно прокрутить галерею steps = Math.abs(this.options.currIndex - index), // время анимации с учётом величины прокрутки duration = this.options.duration - steps * RATIO, // направление анимации dir = (index < this.options.currIndex) ? 1 : -1; duration = (duration > LIMIT) ? duration : LIMIT; var fn = function(steps) { // скролл галереи на один элемент this.shifGallery(dir, duration); // рекурсивно вызываем функцию, если проскроллено не до конца if (steps > 0) setTimeout(fn, duration + RATIO, --steps); }.bind(this); // первоначальный запуск прокручивания галереи fn(--steps); }; |
На этом создание галереи с эффектом Cover Flow закончено. Посмотреть пример галереи и скачать HTML-вёрстку и полный JS-код, Вы можете по ссылкам, указанным в начале страницы.
Комментарии
-
Комментарии должны содержать вопросы и дополнения по статье, ответы на вопросы других пользователей.
Комментарии содержащие обсуждение политики, будут безжалостно удаляться. -
Для удобства чтения Вашего кода, не забываейте его форматировать. Вы его можете подсветить код с помощью тега
<pre>
:
—<pre class="lang:xhtml">
- HTML;
—<pre class="lang:css">
- CSS;
—<pre class="lang:javascript">
- JavaScript. - Если что-то не понятно в статье, постарайтесь указать более конкретно, что именно не понятно.
Интересный скрипт. Спасибо
Не подскажете как сделать открытие страницы картинки при клике по ней?
И как под картинкой вывести её название из alt