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

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

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

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

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

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

index.html

<!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>

style.css

/* общие настройки для всей страницы, а также для картинок и блока с кольцом */
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%);
}

script.js

// здесь будем хранить текущее значение сдвига карусели
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'});
}

Текст:

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

Редак­тор:

Мак­сим Ильяхов

Худож­ник:

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

Кор­рек­тор:

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

Вёрст­ка:

Ники­та Кучеров

Соц­се­ти:

Олег Веш­кур­цев