Проект: эффектно переключаем картинки на странице

Проект: эффектно переключаем картинки на странице

Как в бумажном журнале

На сайтах фотографов, книгоделов и дизайнеров иногда используется такой приём: можно полистать картинки, которые как будто напечатаны в журнале. Выглядит примерно так:

Проект: эффектно переключаем картинки на странице

Сегодня сделаем то же самое и заодно посмотрим, как оно устроено изнутри.

Логика работы

Внутри у таких проектов много невидимой глазу работы:

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

Чтобы это сделать, нам понадобится HTML-страница; скрипт, который обработает переворачивание; и стили, где мы распишем всю внешнюю красоту. Начнём со страницы.

Готовим страницу

Чтобы результат выглядел одинаково во всех браузерах, кроме своих стилей мы подключим нормализатор CSS — он сгладит все различия браузеров. 

На странице сделаем два раздела — один с картинкой, второй с пояснительной надписью, как этим управлять.

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

В самом конце страницы подключим скрипт — с его помощью мы будем управлять листанием. 

<!DOCTYPE html>
<html lang="ru" >
<head>
  <meta charset="UTF-8">
  <title>Переворачиваем страницы</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- подключаем нормализатор CSS, чтобы во всех браузерах страница выглядела одинаково -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">
  <!-- подключаем свои стили -->
  <link rel="stylesheet" href="style.css">
</head>
<body>
<!-- раздел со страницами -->
  <section>
    <!-- блоки картинок -->
    <!-- левая следующая половинка -->
    <div class="left next"></div>
    <!-- правая следующая половинка -->
    <div class="right next"></div>
    <!-- левая текущая половинка -->
    <div class="left current"></div>
    <!-- правая текущая половинка -->
    <div class="right current"></div>
  </section>
  <!-- подпись снизу -->
  <h1 id="title">Нажмите на картинку, чтобы перевернуть страницу</h1>
  <!-- поключаем свой скрипт -->
  <script  src="script.js"></script>
</body>
</html>

Проект: эффектно переключаем картинки на странице
У нас пока все блоки пустые, поэтому единственное, что мы увидим, — это текст про управление листанием

Пишем скрипт

Задача такая: если это первый разворот (первая картинка), то перевернуть на второй, а если мы кликнули по второму развороту — вернуть назад первый.

Чтобы это сделать, используем приём с переключателем классов в JavaScript. Для этого используется команда classList.toggle, и работает она так:

  • если у заданного элемента нет указанного класса, то ему добавляется этот класс;
  • если у элемента этот класс есть, то он убирается.

Например, если мы напишем classList.toggle("flip"), то если у элемента не было класса flip, то он ему добавится, а если был — то он исчезнет.

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

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

// получаем доступ к разделу с картинками
const section = document.querySelector("section");
// на старте клика ещё не было
let clicked = false;
// обработчик клика по картинке
section.addEventListener("click", (e) => {
  // запускаем анимацию переворачивания, добавляя или удаляя у элемента класс flip
  section.classList.toggle("flip");
  // устанавливаем флаг клика, если хотя бы раз кликнули
  if (!clicked) {
    clicked = true;
    // скрываем пояснительную надпись внизу после первого клика
    document.getElementById("title").style.opacity = 0;
  }
});

Настраиваем стили

Прежде всего зададим глобальные переменные, чтобы можно было управлять всем из одного раздела: настройки времени анимации и адреса картинок. Для простоты мы используем картинки из бесплатного фотохостинга Unsplash, но их можно заменить на свои, указав другой путь к файлу.

/* глобальные переменные для всех стилей */
:root {
  /* длительность анимации */
  --duration: 500ms;  
  /* скорость анимации листания влево-вправо */
  --ease-in: cubic-bezier(0.85, 0, 1, 1);
  --ease-out: cubic-bezier(0, 0, 0.3, 1);
  /* устанавливаем одинаковую скорость переворачивания туда и обратно */
  --ease-in-out: ease-in-out;
  /* картинки, которые будут на разворотах */
  --image-current: url(https://images.unsplash.com/photo-1630847911146-edd8828abf14?crop=entropy&cs=srgb&fm=jpg&ixid=MnwxNDU4OXwwfDF8cmFuZG9tfHx8fHx8fHx8MTYzMjUxMjQ0Ng&ixlib=rb-1.2.1&q=85);
  --image-next: url(https://images.unsplash.com/photo-1596774468032-915cdd39ea39?crop=entropy&cs=srgb&fm=jpg&ixid=MnwxNDU4OXwwfDF8cmFuZG9tfHx8fHx8fHx8MTYzMjUxMjg1MQ&ixlib=rb-1.2.1&q=85);
}

Теперь укажем общие настройки для всей страницы и настроим внешний вид раздела с виртуальным журналом. Чтобы всё выглядело красиво, мы скруглим углы и добавим тень внизу — как будто журнал лежит на какой-то плоскости. Заодно настроим внешний вид текста внизу: добавим нужные отступы и выровняем по центру:

/* задаём высоту основных элементов на всё доступное им место */
html, body, section {
  height: 100%;
}

/* общие настройки для всего раздела с разворотами */
section {
  /* радиус скругления */
  border-radius: 1vh;
  /* тень под картинками */
  box-shadow: 0 2vh 4vh rgba(0, 0, 0, 0.2);
  /* пусть раздел занимает всё доступное ему место */
  display: flex;
  position: relative;
  /* отодвигаем точку перспективы на 2000 пикселей */
  perspective: 2000px;
  /* уменьшим размер раздела, чтобы было не впритык к границам окна */
  transform: scale(0.8);
  width: 100%;
}

/* настройки пояснительной надписи внизу */
h1 {
  /* делаем отступ снизу */
  bottom: 3vh;
  /* размер шрифта */
  font-size: 2vh;
  /* размещаем надпись по центру */
  left: 0;
  position: absolute;
  text-align: center;
  /* анимация скрывания надписи */
  transition: opacity 500ms var(--ease-out);
  /* пусть надпись занимает всю ширину доступного ей блока */
  width: 100%;
}

Проект: эффектно переключаем картинки на странице
Стало лучше, но картинок всё равно не видно. А всё потому, что мы хоть и объявили их в блоке с переменными, но сами переменные ещё нигде не использовали

Добавляем картинки

Мы выше описали переменные с картинками, теперь будем их использовать:

/* показываем текущий разворот */
.current {
background-image: var(--image-current);
}
/* сразу заряжаем для показа следующий разворот */
.next {
background-image: var(--image-next);
}

Так как каждый разворот состоит из двух половинок, настроим сначала общие параметры каждой части, а потом отдельно левую и правую часть:

/* настройки левого и правого разворота */
.left,
.right {
  /* не показываем обратную сторону картинки */
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  /* закрепляем картинку на своём месте, чтобы она переворачивалась вместе со своим блоком */
  background-attachment: fixed;
  /* делаем всё по центру */
  background-position: center center;
  /* пусть картинка занимает всё доступное ей место */
  background-size: cover;
  height: 100%;
  /* включаем абсолютное позиционирование */
  position: absolute;
  top: 0;
  /* говорим, что в анимации мы будем менять размеры */
  transition-property: transform;
  /* устанавливаем длительность анимации */
  transition-duration: var(--duration);
  /* показываем только половину картинки */
  width: 50%;
}

/* настройки левой части разворота */
.left {
  /* добавляем скругления углов */
  border-radius: 1vh 0 0 1vh;
  /* расстояние до левого края блока */
  left: 0;
  /* ось вращения — середина разворота */
  transform-origin: 100% 50%;
}

/* делаем то же самое для левой части разворота */
.right {
  border-radius: 0 1vh 1vh 0;
  right: 0;
  transform-origin: 0% 50%;
}

Проект: эффектно переключаем картинки на странице

У нас появилась первая картинка, но на самом деле на странице их уже две — мы просто не видим вторую, потому что она лежит под первой. Клик по картинке тоже пока не даёт результата: класс с переворачиванием добавляется, но он пустой — мы ещё его даже не создали. Исправим это и добавим стили с классом flip:

/* обрататываем переворачивание вперёд */
.flip .current.right {
  /* листаем справа налево, поэтому крутим правую половину разворота налево */
  transform: rotateY(-90deg);
  /* устанавливаем задержку начала и скорость анимации */
  transition-delay: 0ms;
  transition-timing-function: var(--ease-in);
}

/* обрабатываем листание назад */
.flip .next.left {
  /* возвращаем назад левую часть следующего разворота */
  transform: rotateY(0deg);
  /* устанавливаем задержку начала и скорость анимации */
  transition-delay: var(--duration);
  transition-timing-function: var(--ease-out);
}
Проект: эффектно переключаем картинки на странице

У нас заработала анимация, но наполовину: видно, что страница начинает переворачиваться туда и обратно, но всё стопорится на середине. А всё потому, что мы предусмотрели обработку разворотов с классом flip, но не сделали возврат назад, когда этот класс у блока исчезает. Исправим это и добавим два новых блока:

/* настройки левой части следующего разворота */
.next.left {
  /* сразу виртуально переворачиваем левую часть следующего разворота */
  transform: rotateY(90deg);
  /* начинаем анимацию сразу */
  transition-delay: 0ms;
  /* указываем, насколько быстрой будет анимация листания */
  transition-timing-function: var(--ease-in);
  /* отправляем следующий разворот ниже под текущий, чтобы его не было видно */
  z-index: 9;
}

/* настройки правой части текущего разворота */
.current.right {
  /* устанавливаем задержку начала и скорость анимации */
  transition-delay: var(--duration);
  transition-timing-function: var(--ease-out);
}

Теперь всё в порядке и работает так: 

  1. По клику мы добавляем класс flip к правой части первой картинки и левой части второй картинки.
  2. Это даёт нам анимацию листания до середины страницы.
  3. Как только мы добавили к ним класс flip, то тут же перестали действовать два других блока, где этого класса нет, — .next.left и .current.right.
  4. Из-за этого отменились все действия, которые были в них прописаны — а это как раз та самая вторая часть листания от середины, которой нам не хватало
  5. Так как анимация происходит по очереди, нам кажется, что страница просто переворачивается туда и обратно, а на самом деле мы управляем половинками картинок.

Посмотреть анимацию на странице проекта.

Проект: эффектно переключаем картинки на странице

<!DOCTYPE html>
<html lang="ru" >
<head>
  <meta charset="UTF-8">
  <title>Переворачиваем страницы</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- подключаем нормализатор CSS, чтобы во всех браузерах страница выглядела одинаково -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">
  <!-- подключаем свои стили -->
  <link rel="stylesheet" href="style.css">
</head>
<body>
<!-- раздел со страницами -->
  <section>
    <!-- блоки картинок -->
    <!-- левая следующая половинка -->
    <div class="left next"></div>
    <!-- правая следующая половинка -->
    <div class="right next"></div>
    <!-- левая текущая половинка -->
    <div class="left current"></div>
    <!-- правая текущая половинка -->
    <div class="right current"></div>
  </section>
  <!-- подпись снизу -->
  <h1 id="title">Нажмите на картинку, чтобы перевернуть страницу</h1>
  <!-- поключаем свой скрипт -->
  <script  src="script.js"></script>
</body>
</html>

/* глобальные переменные для всех стилей */
:root {
  /* длительность анимации */
  --duration: 500ms;  
  /* скорость анимации листания влево-вправо */
  --ease-in: cubic-bezier(0.85, 0, 1, 1);
  --ease-out: cubic-bezier(0, 0, 0.3, 1);
  /* устанавливаем одинаковую скорость переворачивания туда и обратно */
  --ease-in-out: ease-in-out;
  /* картинки, которые будут на разворотах */
  --image-current: url(https://images.unsplash.com/photo-1630847911146-edd8828abf14?crop=entropy&cs=srgb&fm=jpg&ixid=MnwxNDU4OXwwfDF8cmFuZG9tfHx8fHx8fHx8MTYzMjUxMjQ0Ng&ixlib=rb-1.2.1&q=85);
  --image-next: url(https://images.unsplash.com/photo-1596774468032-915cdd39ea39?crop=entropy&cs=srgb&fm=jpg&ixid=MnwxNDU4OXwwfDF8cmFuZG9tfHx8fHx8fHx8MTYzMjUxMjg1MQ&ixlib=rb-1.2.1&q=85);
}

/* задаём высоту основных элементов на всё доступное им место */
html, body, section {
  height: 100%;
}

/* общие настройки для всего раздела с разворотами */
section {
  /* радиус скругления */
  border-radius: 1vh;
  /* тень под картинками */
  box-shadow: 0 2vh 4vh rgba(0, 0, 0, 0.2);
  /* пусть раздел занимает всё доступное ему место */
  display: flex;
  position: relative;
  /* отодвигаем точку перспективы на 2000 пикселей */
  perspective: 2000px;
  /* уменьшим размер раздела, чтобы было не впритык к границам окна */
  transform: scale(0.8);
  width: 100%;
}


/* настройки левого и правого разворота */
.left,
.right {
  /* не показываем обратную сторону картинки */
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  /* закрепляем картинку на своём месте, чтобы она переворачивалась вместе со своим блоком */
  background-attachment: fixed;
  /* делаем всё по центру */
  background-position: center center;
  /* пусть картинка занимает всё доступное ей место */
  background-size: cover;
  height: 100%;
  /* включаем абсолютное позиционирование */
  position: absolute;
  top: 0;
  /* говорим, что в анимации мы будем менять размеры */
  transition-property: transform;
  /* устанавливаем длительность анимации */
  transition-duration: var(--duration);
  /* показываем только половину картинки */
  width: 50%;
}

/* настройки левой части разворота */
.left {
  /* добавляем скругления углов */
  border-radius: 1vh 0 0 1vh;
  /* расстояние до левого края блока */
  left: 0;
  /* ось вращения — середина разворота */
  transform-origin: 100% 50%;
}

/* делаем то же самое для левой части разворота */
.right {
  border-radius: 0 1vh 1vh 0;
  right: 0;
  transform-origin: 0% 50%;
}

/* показываем текущий разворот */
.current {
  background-image: var(--image-current);
}

/* сразу заряжаем для показа следующий разворот */
.next {
  background-image: var(--image-next);
}

/* настройки левой части следующего разворота */
.next.left {
  /* сразу виртуально переворачиваем левую часть следующего разворота */
  transform: rotateY(90deg);
  /* начинаем анимацию сразу */
  transition-delay: 0ms;
  /* указываем, насколько быстрой будет анимация листания */
  transition-timing-function: var(--ease-in);
  /* отправляем следующий разворот ниже под текущий, чтобы его не было видно */
  z-index: 9;
}

/* настройки правой части текущего разворота */
.current.right {
  /* устанавливаем задержку начала и скорость анимации */
  transition-delay: var(--duration);
  transition-timing-function: var(--ease-out);
}

/* обрататываем переворачивание вперёд */
.flip .current.right {
  /* листаем справа налево, поэтому крутим правую половину разворота налево */
  transform: rotateY(-90deg);
  /* устанавливаем задержку начала и скорость анимации */
  transition-delay: 0ms;
  transition-timing-function: var(--ease-in);
}

/* обрабатываем листание назад */
.flip .next.left {
  /* возвращаем назад левую часть следующего разворота */
  transform: rotateY(0deg);
  /* устанавливаем задержку начала и скорость анимации */
  transition-delay: var(--duration);
  transition-timing-function: var(--ease-out);
}

/* настройки пояснительной надписи внизу */
h1 {
  /* делаем отступ снизу */
  bottom: 3vh;
  /* размер шрифта */
  font-size: 2vh;
  /* размещаем надпись по центру */
  left: 0;
  position: absolute;
  text-align: center;
  /* анимация скрывания надписи */
  transition: opacity 500ms var(--ease-out);
  /* пусть надпись занимает всю ширину доступного ей блока */
  width: 100%;
}

// получаем доступ к разделу с картинками
const section = document.querySelector("section");
// на старте клика ещё не было
let clicked = false;
// обработчик клика по картинке
section.addEventListener("click", (e) => {
  // запускаем анимацию переворачивания, добавляя или удаляя у элемента класс flip
  section.classList.toggle("flip");
  // устанавливаем флаг клика, если хотя бы раз кликнули
  if (!clicked) {
    clicked = true;
    // скрываем пояснительную надпись внизу после первого клика
    document.getElementById("title").style.opacity = 0;
  }
});

Код:

Jake Albaugh

Текст:

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

Редактор:

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

Художник:

Алексей Сухов

Корректор:

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

Вёрстка:

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

Соцсети:

Виталий Вебер

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

Просто, бесплатно, но с рекламой

medium
Ещё больше полезных CSS-команд
Ещё больше полезных CSS-команд

Короткие и мощные.

hard
Задача про вёрстку баннера
Задача про вёрстку баннера

Для тех, кто любит конкурсы разработчиков.

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

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

hard
Автоматизируем новости
Автоматизируем новости

И другие нелинейные процессы принятия решений.

easy
Простая работа с исключениями
Простая работа с исключениями

Чтобы программа не падала из-за разных ошибок

easy
Бигдата и тепловые карты на примере твитов Байдена и Трампа
Бигдата и тепловые карты на примере твитов Байдена и Трампа

Сразу видно, кто постит сам, а за кого это делает команда

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

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

easy
Домашний кинотеатр на Raspberry Pi

Превращаем любой телевизор в умный гаджет.

easy
Свой тетрис на JavaScript: прокачиваем проект
Свой тетрис на JavaScript: прокачиваем проект

Доработки, чтобы получилась настоящая игра.

medium
easy