Делаем эффектную фотогалерею на сайте
hard

Делаем эффектную фотогалерею на сайте

Красивый трёхмерный виджет с несложным кодом

Это проект для тех, кто хочет показать на сайте красивую галерею с эффектом трёхмерности. Это низачем не нужно, просто симпатичный эффект. Так-то фотографии можно смотреть и без трёхмерных эффектов.

Логика проекта

Нам понадобится три компонента: HTML-страница, где будет карусель, CSS-файл, где мы настроим внешний вид карусели в целом, и скрипт на JavaScript, в котором будет вся механика работы карусели с фотографиями. Как всё будет работать: 

  1. Готовим на странице место под картинки и оформляем их как блоки.
  2. Эти блоки объединяем в общий блок с каруселью.
  3. Задаём общие параметры внешнего вида картинок, фона и блоков в целом в CSS-файле.
  4. В скрипте делаем две вещи: правильно отрисовываем карусель в любом положении и учим страницу реагировать на движение мыши.

Сделаем всё по очереди.

HTML-страница

В этом проекте нам понадобятся две библиотеки, которые мы ещё не использовали: GASP и Zepto.

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

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

Так как внутри страницы у нас будут одни блоки без содержимого, то и в браузере мы увидим только пустую белую страницу. Содержимое добавим в самом конце.

<!DOCTYPE html>
<html lang="ru" >
<head>
  <meta charset="UTF-8">
  <title>3D-фотокарусель с параллаксом</title>
<link rel="stylesheet" href="style.css">

</head>
<body>
  <!-- общий блок для всей карусели -->
  <div class="container">
    <!-- блок, который собирает фотографии в кольцо -->
    <div class="ring">
      <!-- блоки для изображений в карусели -->
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
    </div>
  </div>
  <!-- подключаем платформу анимации GSAP, чтобы создавать анимации сразу с параллаксом -->
  <script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.1/gsap.min.js'></script>
  <!-- подключаем Zepto — облегчённую библиотеку, аналог jQuery -->
  <script src='https://cdnjs.cloudflare.com/ajax/libs/zepto/1.2.0/zepto.min.js'></script>
  <!-- подключаем наш основной скрипт -->
  <script  src="script.js"></script>

</body>
</html>

CSS-файл для настройки общего вида карусели

Так как у нас 3D-карусель, то первое, что мы сделаем, — установим свойство transform-style: preserve-3d. Это значит, что теперь нужные нам элементы будут отображаться в трёхмерном пространстве, а не на плоскости экрана. В этом же блоке запретим пользователю что-то выделять мышкой, чтобы при прокрутке карусели не выделялись никакие элементы:

/* общие настройки для всей страницы, а также для картинок
и блока с кольцом */
html, body, .stage, .ring, .img {
width:100%;
height: 100%;
transform-style: preserve-3d;
user-select:none;
}

Теперь настроим правила, по которым лишние объекты будут исчезать со страницы. Смысл в том, чтобы скрыть ту часть карусели, которую мы не видим, чтобы картинки не накладывались друг на друга при прокрутке:

/* настраиваем общие правила скрытия лишних объектов на странице */
html, body{
overflow:hidden;
background:#000;
}

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

/* устанавливаем абсолютное позиционирование блоков на странице */
div {
position: absolute;
}

Последнее, что нам здесь осталось сделать, — задать отдельные настройки для самой карусели. Кроме высоты и ширины важный параметр в карусели — отдаление виртуального центра карусели от края экрана. Этот параметр влияет на перспективу: чем меньше это число, тем сильнее перспективные искажения.

/* отдельные настройки для карусели в целом */
.container {
/*на сколько центр карусели будет виртуально уедет вглубь монитора*/
perspective: 2000px;
width: 300px;
height: 400px;
left:50%;
top:50%;
/*как будут сдвигаться наши картинки*/
transform:translate(-50%,-50%);
}

Пишем скрипт

👉 Для удобства работы скрипта мы оставим оригинальные ссылки на картинки из скрипта Тома Миллера. Если вам нужно будет заменить их на свои — пронумеруйте картинки, начиная с номера 33, и положите их в одну папку.

Разделим скрипт на две части: спецэффекты с анимацией и обработку движения мыши.

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

// здесь будем хранить текущее значение сдвига карусели
let xPos = 0;

Чтобы ставить все картинки в карусели сразу по местам с самого начала, сделаем отдельную функцию getBgPos(i) — она будет брать элемент с номером i и возвращать нужное положение и значение трансформации картинки в карусели:

// получаем свойство background-position у элемента с номером i, чтобы отрисовать картинку в нужном месте в карусели
function getBgPos(i){
return ( 100-gsap.utils.wrap(0,360,gsap.getProperty(‘.ring’, ‘rotationY’)-180-i*36)/360*500 )+’px 0px’;
}

Для анимации и обработки спецэффектов будем использовать встроенный в GSAP элемент timeline и менять его свойства на нужные для нас:

// основная функция, которая управляет всеми спецэффектами в карусели
gsap.timeline()
    // устанавливаем свойство rotationY, чтобы была видна только одна половина виртуального кольца с картинками
    .set('.ring', { rotationY:180, cursor:'grab' })
    
    // поворачиваем вдоль виртуального кольца каждую картинку
    .set('.img',  { 
      rotateY: (i)=> i*-36,
      transformOrigin: '50% 50% 500px',
      z: -500,
      // берём новую картинку для карусели по её индексу
      backgroundImage:(i)=>'url(https://picsum.photos/id/'+(i+32)+'/600/400/)',
      // сразу масштабируем картинки под их размер в карусели
      backgroundPosition:(i)=>getBgPos(i),
      // не показываем обратную сторону картинок
      backfaceVisibility:'hidden'
    })    
  
    // добавляем эффекты затемнения
    .add(()=>{
      // когда мышь попадает в зону карусели
      $('.img').on('mouseenter', (e)=>{
        // находим текущий элемент, на который указывает мышь
        let current = e.currentTarget;
        // затемняем всё, кроме этого элемента
        gsap.to('.img', {opacity:(i,t)=>(t==current)? 1:0.5, ease:'power3'})
      })
      // когда мышь ушла за пределы карусели
      $('.img').on('mouseleave', (e)=>{
        // убираем затемнение и эффекты со всех картинок
        gsap.to('.img', {opacity:1, ease:'power2.inOut'})
      })
    })

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

// когда зажата левая кнопка мыши — можно крутить карусель
$(window).on(‘mousedown touchstart’, dragStart);
// отпускаем мышь — карусель останавливается
$(window).on(‘mouseup touchend’, dragEnd);

Дальше сделаем так:

  1. Если мышь нажата, то меняем курсор на зажатую руку, получаем значение сдвига карусели и следим за тем, когда начнёт двигаться мышь.
  2. Когда мышь двинется — получаем новую координату сдвига и отрисовываем карусель по новым координатам.
  3. Когда пользователь отпустит мышь, то меняем курсор на обычную руку и отрисовываем карусель с учётом того, где отпустили мышь.

Запишем это в виде кода:

// прокрутка карусели
function dragStart(e){ 
  // если было касание тач-интерфейса — получаем координаты касания
  if (e.touches) 
    e.clientX = e.touches[0].clientX;
  // устанавливаем новое значение сдвига карусели
  xPos = Math.round(e.clientX);
  // меняем форму курсора внутри карусели, чтобы было видно, что мышь ей управляет
  gsap.set('.ring', {cursor:'grabbing'})
  // когда мышь начнёт двигаться — запускаем функцию обработки захвата карусели
  $(window).on('mousemove touchmove', drag);
}

// функция обработки захвата карусели
function drag(e){
  // если было касание тач-интерфейса — получаем координаты касания
  if (e.touches) 
    e.clientX = e.touches[0].clientX;    
  // обрабатываем общий блок с кольцом
  gsap.to('.ring', {
    // высчитываем разницу между старым и новым положением и меняем значение свойства rotationY
    rotationY: '-=' +( (Math.round(e.clientX)-xPos)%360 ),
    // запускаем встроенную в GSAP функцию: она дождётся нового положения карусели и поменяет положения картинок в ней
    onUpdate:()=>{ gsap.set('.img', { backgroundPosition:(i)=>getBgPos(i) }) }
  });
  // устанавливаем новое значение сдвига карусели
  xPos = Math.round(e.clientX);
}

// обрабатываем конец вращения карусели
function dragEnd(e){
  // считаем новое положение карусели после того, как отпустили мышь
  $(window).off('mousemove touchmove', drag);
  // меняем форму курсора на карусели
  gsap.set('.ring', {cursor:'grab'});
}

Собираем всё вместе и смотрим на результат на странице:

Посмотреть готовую фотогалерею на странице проекта.

<!DOCTYPE html>
<html lang="ru" >
<head>
  <meta charset="UTF-8">
  <title>3D-фотокарусель с параллаксом</title>
<link rel="stylesheet" href="style.css">

</head>
<body>
  <!-- общий блок для всей карусели -->
  <div class="container">
    <!-- блок, который собирает фотографии в кольцо -->
    <div class="ring">
      <!-- блоки для изображений в карусели -->
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
      <div class="img"></div>
    </div>
  </div>
  <!-- подключаем платформу анимации GSAP, чтобы создавать анимации сразу с параллаксом -->
  <script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.1/gsap.min.js'></script>
  <!-- подключаем Zepto — облегчённую библиотеку, аналог jQuery -->
  <script src='https://cdnjs.cloudflare.com/ajax/libs/zepto/1.2.0/zepto.min.js'></script>
  <!-- подключаем наш основной скрипт -->
  <script  src="script.js"></script>

</body>
</html>

/* общие настройки для всей страницы, а также для картинок и блока с кольцом */
html, body, .stage, .ring, .img {
  width:100%;
  height: 100%;
  transform-style: preserve-3d;
  user-select:none;
}

/* настраиваем общие правила скрытия лишних объектов на странице */
html, body{
  overflow:hidden;
  background:#000;
  
}

/* устанавливаем абсолютное позиционирование блоков на странице */
div {
  position: absolute;
}

/* отдельные настройки для карусели в целом */
.container {
  /*на сколько центр карусели будет виртуально уедет вглубь монитора*/
  perspective: 2000px;
  width: 300px;
  height: 400px;  
  left:50%;
  top:50%;
  /*как будут сдвигаться наши картинки*/
  transform:translate(-50%,-50%);
}

// здесь будем хранить текущее значение сдвига карусели
let xPos = 0;


// получаем свойство background-position у элемента с номером i, чтобы отрисовать картинку в нужном месте в карусели
function getBgPos(i){ 
  return ( 100-gsap.utils.wrap(0,360,gsap.getProperty('.ring', 'rotationY')-180-i*36)/360*500 )+'px 0px';
}

// основная функция, которая управляет всеми спецэффектами в карусели
gsap.timeline()
    // устанавливаем свойство rotationY, чтобы была видна только одна половина виртуального кольца с картинками
    .set('.ring', { rotationY:180, cursor:'grab' })
    
    // поворачиваем вдоль виртуального кольца каждую картинку
    .set('.img',  { 
      rotateY: (i)=> i*-36,
      transformOrigin: '50% 50% 500px',
      z: -500,
      // берём новую картинку для карусели по её индексу
      backgroundImage:(i)=>'url(https://picsum.photos/id/'+(i+32)+'/600/400/)',
      // сразу масштабируем картинки под их размер в карусели
      backgroundPosition:(i)=>getBgPos(i),
      // не показываем обратную сторону картинок
      backfaceVisibility:'hidden'
    })    
  
    // добавляем эффекты затемнения
    .add(()=>{
      // когда мышь попадает в зону карусели
      $('.img').on('mouseenter', (e)=>{
        // находим текущий элемент, на который указывает мышь
        let current = e.currentTarget;
        // затемняем всё, кроме этого элемента
        gsap.to('.img', {opacity:(i,t)=>(t==current)? 1:0.5, ease:'power3'})
      })
      // когда мышь ушла за пределы карусели
      $('.img').on('mouseleave', (e)=>{
        // убираем затемнение и эффекты со всех картинок
        gsap.to('.img', {opacity:1, ease:'power2.inOut'})
      })
    })

// когда зажата левая кнопка мыши — можно крутить карусель
$(window).on('mousedown touchstart', dragStart);
// отпускаем мышь — карусель останавливается
$(window).on('mouseup touchend', dragEnd);
      
// прокрутка карусели
function dragStart(e){ 
  // если было касание тач-интерфейса — получаем координаты касания
  if (e.touches) 
    e.clientX = e.touches[0].clientX;
  // устанавливаем новое значение сдвига карусели
  xPos = Math.round(e.clientX);
  // меняем форму курсора внутри карусели, чтобы было видно, что мышь ей управляет
  gsap.set('.ring', {cursor:'grabbing'})
  // когда мышь начнёт двигаться — запускаем функцию обработки захвата карусели
  $(window).on('mousemove touchmove', drag);
}

// функция обработки захвата карусели
function drag(e){
  // если было касание тач-интерфейса — получаем координаты касания
  if (e.touches) 
    e.clientX = e.touches[0].clientX;    
  // обрабатываем общий блок с кольцом
  gsap.to('.ring', {
    // высчитываем разницу между старым и новым положением и меняем значение свойства rotationY
    rotationY: '-=' +( (Math.round(e.clientX)-xPos)%360 ),
    // запускаем встроенную в GSAP функцию: она дождётся нового положения карусели и поменяет положения картинок в ней
    onUpdate:()=>{ gsap.set('.img', { backgroundPosition:(i)=>getBgPos(i) }) }
  });
  // устанавливаем новое значение сдвига карусели
  xPos = Math.round(e.clientX);
}

// обрабатываем конец вращения карусели
function dragEnd(e){
  // считаем новое положение карусели после того, как отпустили мышь
  $(window).off('mousemove touchmove', drag);
  // меняем форму курсора на карусели
  gsap.set('.ring', {cursor:'grab'});
}

Текст и иллюстрации:

Михаил Полянин

Художник:

Даня Берковский

Корректор:

Ирина Михеева

Вёрстка:

Кирилл Климентьев

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Вам может быть интересно
Bomberman на JavaScript
Bomberman на JavaScript

Как на старой приставке из детства.

hard
Три ИТ-проекта из России от читателей «Кода»
Три ИТ-проекта из России от читателей «Кода»

Время отечественного пиара.

Как запустить стартап и не разориться
Как запустить стартап и не разориться

Всё дело в правильных расчётах на старте.

easy
Правильные цитаты: добавляем первоисточник в ворованный с вашего сайта контент
Правильные цитаты: добавляем первоисточник в ворованный с вашего сайта контент

При вставке все будут видеть, откуда скопирован текст

medium
Что означает ошибка SyntaxError: missing : after property id
Что означает ошибка SyntaxError: missing : after property id

Используйте двоеточие, если хотите обратиться к свойству объекта.

easy
Создаём  CSS-сетку нужного размера
Создаём CSS-сетку нужного размера

Рассказываем, как сделать шаблон любой страницы.

medium
Как подключить фотогалерею к сайту
Как подключить фотогалерею к сайту

Рецепт на 6 минут.

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

Берём хаос под контроль

hard
Пишем на Python квайн — программу, которая выводит свой код
Пишем на Python квайн — программу, которая выводит свой код

Красиво, остроумно, полезно

easy
hard