Введение
В 2010 году Fi.com и команда Google Chrome совместно работали над образовательным веб-приложением на базе HTML5 под названием «20 вещей, которые я узнал о браузерах и Интернете» ( www.20thingsilearned.com ). Одна из ключевых идей этого проекта заключалась в том, что его лучше всего представить в формате книги . Поскольку содержание книги во многом посвящено открытым веб-технологиям, мы сочли важным сохранить верность этому принципу, сделав сам контейнер примером того, чего эти технологии позволяют нам достичь сегодня.

Мы решили, что лучший способ добиться ощущения настоящей книги — это сымитировать все преимущества аналогового чтения, одновременно используя преимущества цифровой среды, такие как навигация. Много усилий было вложено в графическое и интерактивное оформление процесса чтения, особенно в то, как страницы книги перелистываются с одной страницы на другую.
Начиная
В этом руководстве вы узнаете, как создать собственный эффект перелистывания страниц, используя элемент Canvas и обширный код JavaScript. Часть элементарного кода, например, объявление переменных и подписка на прослушиватель событий, не вошла в фрагменты этой статьи, поэтому не забудьте обратиться к рабочему примеру.
Прежде чем начать, будет неплохо ознакомиться с демо-версией, чтобы вы понимали, что мы хотим создать.
Разметка
Всегда важно помнить, что то, что мы рисуем на холсте, не может быть проиндексировано поисковыми системами, выбрано посетителем или найдено с помощью поиска в браузере. Поэтому контент, с которым мы будем работать, помещается непосредственно в DOM и затем обрабатывается JavaScript, если он доступен. Разметка, необходимая для этого, минимальна:
<div id='book'>
<canvas id='pageflip-canvas'></canvas>
<div id='pages'>
<section>
<div> <!-- Any type of contents here --> </div>
</section>
<!-- More <section>s here -->
</div>
</div>
У нас есть один основной элемент-контейнер для книги, который, в свою очередь, содержит разные страницы книги и элемент canvas , на котором мы будем рисовать перелистываемые страницы. Внутри элемента section находится div оболочка для контента — она нужна нам для изменения ширины страницы, не влияя на её расположение. div имеет фиксированную ширину, а section настроен на скрытие переполнения. В результате ширина section служит горизонтальной маской для div .

Логика
Код, необходимый для реализации перелистывания страниц, не очень сложен, но довольно объёмен, поскольку включает в себя множество процедурно генерируемых графических элементов. Начнём с описания констант, которые мы будем использовать в коде.
var BOOK_WIDTH = 830;
var BOOK_HEIGHT = 260;
var PAGE_WIDTH = 400;
var PAGE_HEIGHT = 250;
var PAGE_Y = ( BOOK_HEIGHT - PAGE_HEIGHT ) / 2;
var CANVAS_PADDING = 60;
CANVAS_PADDING добавляется вокруг холста, чтобы при перелистывании бумага выходила за пределы книги. Обратите внимание, что некоторые константы, определённые здесь, также задаются в CSS, поэтому, если вы хотите изменить размер книги, вам также потребуется обновить значения и там.

Далее нам нужно определить объект перелистывания для каждой страницы, он будет постоянно обновляться по мере нашего взаимодействия с книгой, чтобы отражать текущий статус перелистывания.
// Create a reference to the book container element
var book = document.getElementById( 'book' );
// Grab a list of all section elements (pages) within the book
var pages = book.getElementsByTagName( 'section' );
for( var i = 0, len = pages.length; i < len; i++ ) {
pages[i].style.zIndex = len - i;
flips.push( {
progress: 1,
target: 1,
page: pages[i],
dragging: false
});
}
Сначала необходимо убедиться, что страницы правильно расположены слоями, упорядочив z-индексы элементов раздела так, чтобы первая страница находилась сверху, а последняя — снизу. Важнейшими свойствами объектов перелистывания являются значения progress и target . Они определяют, насколько далеко должна быть сложена страница в данный момент: -1 означает полное сгибание влево, 0 — мёртвую середину книги, а +1 — правый край книги.

Теперь, когда у нас есть определенный объект перелистывания для каждой страницы, нам нужно начать собирать и использовать вводимые пользователем данные для обновления состояния перелистывания.
function mouseMoveHandler( event ) {
// Offset mouse position so that the top of the book spine is 0,0
mouse.x = event.clientX - book.offsetLeft - ( BOOK_WIDTH / 2 );
mouse.y = event.clientY - book.offsetTop;
}
function mouseDownHandler( event ) {
// Make sure the mouse pointer is inside of the book
if (Math.abs(mouse.x) < PAGE_WIDTH) {
if (mouse.x < 0 && page - 1 >= 0) {
// We are on the left side, drag the previous page
flips[page - 1].dragging = true;
}
else if (mouse.x > 0 && page + 1 < flips.length) {
// We are on the right side, drag the current page
flips[page].dragging = true;
}
}
// Prevents the text selection
event.preventDefault();
}
function mouseUpHandler( event ) {
for( var i = 0; i < flips.length; i++ ) {
// If this flip was being dragged, animate to its destination
if( flips[i].dragging ) {
// Figure out which page we should navigate to
if( mouse.x < 0 ) {
flips[i].target = -1;
page = Math.min( page + 1, flips.length );
}
else {
flips[i].target = 1;
page = Math.max( page - 1, 0 );
}
}
flips[i].dragging = false;
}
}
Функция mouseMoveHandler обновляет объект mouse , чтобы мы всегда работали в направлении самого последнего положения курсора.
В mouseDownHandler мы сначала проверяем, была ли нажата кнопка мыши на левой или правой странице, чтобы определить, в каком направлении нужно начать перелистывание. Мы также проверяем, существует ли другая страница в этом направлении, поскольку мы можем находиться на первой или последней странице. Если после этих проверок доступен допустимый вариант перелистывания, мы устанавливаем флаг dragging соответствующего объекта перелистывания в true .
Достигнув mouseUpHandler , мы перебираем все flips и проверяем, были ли какие-либо из них помечены как dragging и должны быть прекращены. При прекращении переворота мы устанавливаем его целевое значение, соответствующее стороне, в которую должен перевернуться элемент, в зависимости от текущего положения мыши. Номер страницы также обновляется, отражая эту навигацию.
Рендеринг
Теперь, когда большая часть нашей логики готова, давайте разберёмся, как отрисовать сложенный лист бумаги на элементе холста. Большая часть этого происходит внутри функции render() , которая вызывается 60 раз в секунду для обновления и отображения текущего состояния всех активных переворачиваний.
function render() {
// Reset all pixels in the canvas
context.clearRect( 0, 0, canvas.width, canvas.height );
for( var i = 0, len = flips.length; i < len; i++ ) {
var flip = flips[i];
if( flip.dragging ) {
flip.target = Math.max( Math.min( mouse.x / PAGE_WIDTH, 1 ), -1 );
}
// Ease progress towards the target value
flip.progress += ( flip.target - flip.progress ) * 0.2;
// If the flip is being dragged or is somewhere in the middle
// of the book, render it
if( flip.dragging || Math.abs( flip.progress ) < 0.997 ) {
drawFlip( flip );
}
}
}
Прежде чем начать отрисовку flips , мы сбрасываем холст с помощью метода clearRect(x,y,w,h) . Очистка всего холста значительно снижает производительность, и гораздо эффективнее очищать только те области, в которых мы рисуем. Чтобы не отклоняться от темы урока, мы остановимся на очистке всего холста.
При перетаскивании объекта мы обновляем его target значение в соответствии с позицией мыши, но по шкале от -1 до 1, а не по количеству пикселей. Мы также увеличиваем значение progress на долю расстояния до target . Это обеспечит плавное и анимированное переворачивание, поскольку оно обновляется в каждом кадре.
Поскольку мы перебираем все flips в каждом кадре, нам нужно убедиться, что перерисовываются только активные. Если переворот происходит не слишком близко к краю книги (в пределах 0,3% от BOOK_WIDTH ) или если он помечен как dragging , он считается активным.
Теперь, когда вся логика на месте, нам нужно нарисовать графическое представление переворота в зависимости от его текущего состояния. Пришло время рассмотреть первую часть функции drawFlip(flip) .
// Determines the strength of the fold/bend on a 0-1 range
var strength = 1 - Math.abs( flip.progress );
// Width of the folded paper
var foldWidth = ( PAGE_WIDTH * 0.5 ) * ( 1 - flip.progress );
// X position of the folded paper
var foldX = PAGE_WIDTH * flip.progress + foldWidth;
// How far outside of the book the paper is bent due to perspective
var verticalOutdent = 20 * strength;
// The maximum widths of the three shadows used
var paperShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(1 - flip.progress, 0.5), 0);
var rightShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(strength, 0.5), 0);
var leftShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(strength, 0.5), 0);
// Mask the page by setting its width to match the foldX
flip.page.style.width = Math.max(foldX, 0) + 'px';
Этот раздел кода начинается с вычисления ряда визуальных переменных, необходимых для реалистичной отрисовки сгиба. Значение progress переворота играет здесь важную роль, поскольку именно там мы хотим, чтобы сгиб страницы появлялся. Чтобы добавить глубины эффекту переворота страницы, мы заставляем бумагу выступать за верхний и нижний края книги. Этот эффект наиболее заметен, когда сгиб находится близко к корешку книги.

Теперь, когда все значения подготовлены, остается только нарисовать рисунок на бумаге!
context.save();
context.translate( CANVAS_PADDING + ( BOOK_WIDTH / 2 ), PAGE_Y + CANVAS_PADDING );
// Draw a sharp shadow on the left side of the page
context.strokeStyle = `rgba(0,0,0,`+(0.05 * strength)+`)`;
context.lineWidth = 30 * strength;
context.beginPath();
context.moveTo(foldX - foldWidth, -verticalOutdent * 0.5);
context.lineTo(foldX - foldWidth, PAGE_HEIGHT + (verticalOutdent * 0.5));
context.stroke();
// Right side drop shadow
var rightShadowGradient = context.createLinearGradient(foldX, 0,
foldX + rightShadowWidth, 0);
rightShadowGradient.addColorStop(0, `rgba(0,0,0,`+(strength*0.2)+`)`);
rightShadowGradient.addColorStop(0.8, `rgba(0,0,0,0.0)`);
context.fillStyle = rightShadowGradient;
context.beginPath();
context.moveTo(foldX, 0);
context.lineTo(foldX + rightShadowWidth, 0);
context.lineTo(foldX + rightShadowWidth, PAGE_HEIGHT);
context.lineTo(foldX, PAGE_HEIGHT);
context.fill();
// Left side drop shadow
var leftShadowGradient = context.createLinearGradient(
foldX - foldWidth - leftShadowWidth, 0, foldX - foldWidth, 0);
leftShadowGradient.addColorStop(0, `rgba(0,0,0,0.0)`);
leftShadowGradient.addColorStop(1, `rgba(0,0,0,`+(strength*0.15)+`)`);
context.fillStyle = leftShadowGradient;
context.beginPath();
context.moveTo(foldX - foldWidth - leftShadowWidth, 0);
context.lineTo(foldX - foldWidth, 0);
context.lineTo(foldX - foldWidth, PAGE_HEIGHT);
context.lineTo(foldX - foldWidth - leftShadowWidth, PAGE_HEIGHT);
context.fill();
// Gradient applied to the folded paper (highlights & shadows)
var foldGradient = context.createLinearGradient(
foldX - paperShadowWidth, 0, foldX, 0);
foldGradient.addColorStop(0.35, `#fafafa`);
foldGradient.addColorStop(0.73, `#eeeeee`);
foldGradient.addColorStop(0.9, `#fafafa`);
foldGradient.addColorStop(1.0, `#e2e2e2`);
context.fillStyle = foldGradient;
context.strokeStyle = `rgba(0,0,0,0.06)`;
context.lineWidth = 0.5;
// Draw the folded piece of paper
context.beginPath();
context.moveTo(foldX, 0);
context.lineTo(foldX, PAGE_HEIGHT);
context.quadraticCurveTo(foldX, PAGE_HEIGHT + (verticalOutdent * 2),
foldX - foldWidth, PAGE_HEIGHT + verticalOutdent);
context.lineTo(foldX - foldWidth, -verticalOutdent);
context.quadraticCurveTo(foldX, -verticalOutdent * 2, foldX, 0);
context.fill();
context.stroke();
context.restore();
Метод translate(x,y) API холста используется для смещения системы координат, чтобы мы могли нарисовать перевёрнутую страницу, используя верхнюю часть корешка в качестве точки отсчёта 0,0. Обратите внимание, что нам также необходимо save() текущую матрицу преобразования холста и восстановить её restore() после завершения рисования.

Градиент foldGradient заполнит форму сложенного листа бумаги, чтобы создать реалистичные блики и тени. Мы также добавляем очень тонкую линию вокруг рисунка, чтобы он не терялся на светлом фоне.
Остаётся только нарисовать форму сложенного листа бумаги, используя заданные выше свойства. Левая и правая стороны листа нарисованы прямыми линиями, а верхняя и нижняя изогнуты, создавая ощущение сгиба бумаги по вертикали. Сила изгиба определяется значением параметра verticalOutdent .
Вот и всё! Теперь у вас есть полнофункциональная навигация с перелистыванием страниц.
Демонстрация перелистывания страниц
Эффект перелистывания страниц призван передать правильное интерактивное ощущение, поэтому просмотр изображений не совсем передает его суть.
Следующие шаги

Это лишь один пример того, чего можно добиться, используя такие функции HTML5, как элемент Canvas. Рекомендую вам ознакомиться с более подробным описанием этой техники на сайте www.20thingsilearned.com . Там вы увидите, как перелистывание страниц можно применить в реальном приложении и насколько эффективным оно становится в сочетании с другими функциями HTML5.
Ссылки
- Спецификация API Canvas