На сайтах фотографов, книгоделов и дизайнеров иногда используется такой приём: можно полистать картинки, которые как будто напечатаны в журнале. Выглядит примерно так:
Сегодня сделаем то же самое и заодно посмотрим, как оно устроено изнутри.
Логика работы
Внутри у таких проектов много невидимой глазу работы:
- сразу загрузить все картинки;
- те развороты, которые листаем, — виртуально повернуть и временно скрыть, чтобы развернуть в нужный момент;
- настроить анимацию листания туда и обратно;
- продумать механизм листания — кнопками или по клику.
Чтобы это сделать, нам понадобится 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);
}
Теперь всё в порядке и работает так:
- По клику мы добавляем класс flip к правой части первой картинки и левой части второй картинки.
- Это даёт нам анимацию листания до середины страницы.
- Как только мы добавили к ним класс flip, то тут же перестали действовать два других блока, где этого класса нет, — .next.left и .current.right.
- Из-за этого отменились все действия, которые были в них прописаны — а это как раз та самая вторая часть листания от середины, которой нам не хватало
- Так как анимация происходит по очереди, нам кажется, что страница просто переворачивается туда и обратно, а на самом деле мы управляем половинками картинок.
Посмотреть анимацию на странице проекта.
<!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;
}
});