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

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

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

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

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

Нам понадобится три компонента: 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

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

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

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

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

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

Своя игра: создаём собственную «Змейку»

Работы на 10 минут, а удовольствия на целый день.

Что означает ошибка TypeError: 'list' object cannot be interpreted as an integer
Что означает ошибка TypeError: 'list' object cannot be interpreted as an integer

Неочевидная ошибка при организации цикла в Python.

Uncaught SyntaxError: missing ) after argument list — что это значит
Uncaught SyntaxError: missing ) after argument list — что это значит

Потрясающе хитрая ошибка.

Автоматическое оглавление на странице
Автоматическое оглавление на странице

Поручите это машине.

Делаем форму обратной связи на сайте

Говорят, что если программист может написать форму обратной связи, он может написать всё.

Делаем сами: адаптивный сайт

С котиками!

Запускаем телеграм-бота на сервере

Тогда он будет работать круглые сутки, а вы — отдыхать.

Блокировщик соцсетей, который спасёт вашу продуктивность

И поднимет осознанность.

Bomberman на JavaScript

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

Пишем программу, которая одобрит вам кредит

Или не одобрит, хехех.

hard