Практический пример — эффект переворота страницы от 20thingsilearned.com

Введение

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

Обложка книги и домашняя страница книги «20 вещей, которые я узнал о браузерах и Интернете»
Обложка книги и домашняя страница книги «20 вещей, которые я узнал о браузерах и Интернете» ( www.20thingsilearned.com )

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

Начиная

В этом уроке вы познакомитесь с процессом создания собственного эффекта переворота страницы с использованием элемента холста и большого количества кода 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 означает самый правый край книги.

Прогресс.
Значения прогресса и целевые значения переворотов используются для определения места, где должна быть нарисована складная страница, по шкале от -1 до +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 &amp;&amp; page - 1 >= 0) {
    // We are on the left side, drag the previous page
    flips[page - 1].dragging = true;
}
else if (mouse.x > 0 &amp;&amp; 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 &amp; 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 Canvas используется для смещения системы координат, чтобы мы могли нарисовать перевернутую страницу так, чтобы верхняя часть корешка выступала в качестве позиции 0,0. Обратите внимание, что нам также необходимо save() текущую матрицу преобразования холста и restore() , когда мы закончим рисование.

Переводить
Это точка, из которой мы рисуем переворот страницы. Исходная точка 0,0 находится в левом верхнем углу изображения, но, изменив ее с помощью перевода (x,y), мы упростим логику рисования.

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

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

Вот и все! Теперь у вас есть полнофункциональная навигация с перелистыванием страниц.

Демонстрация переворота страницы

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

Следующие шаги

Хард-флип
Мягкое переворачивание страниц в этом уроке становится еще более эффективным в сочетании с другими функциями, подобными книгам, такими как интерактивная твердая обложка.

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

Ссылки