Пример использования: эффект отражения страницы от 20thingsilearned.com

HTML5 Rocks

Введение

В 2010 году команды F-i.com и Google Chrome вместе создали учебное веб-приложение на базе HTML5, которое называется "20 занятных фактов о браузерах и Интернете" (http://www.20thingsilearned.com/ru-RU). Одной из основных идей этого проекта стало представление материала в виде книги. Так как главная тема книги – открытые веб-технологии, мы хотели продемонстрировать их в действии и поэтому создали на их основе саму книгу.

Обложка книги (главная страница сайта)
Обложка книги (главная страница сайта) "20 занятных фактов о браузерах и Интернете": www.20thingsilearned.com/ru-RU

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

Начало работы

В руководстве вы найдете инструкции по созданию эффекта перелистывания с помощью элемента canvas (т. н. холста) и кода JavaScript. Имейте в виду, что в примерах кода, представленных в этой статье, опущены некоторые вспомогательные элементы, такие как объявления переменных и подписка слушателей событий.

Прежде чем начать, посмотрите демонстрационное видео, в котором показано, что именно мы будем создавать.

Разметка

Помните, что содержание элемента canvas не индексируется поисковыми системами, его невозможно выделить на странице или найти с помощью поиска в браузере. Так что содержание, с которым мы планируем работать, будет помещаться непосредственно в структуру 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-файл, поэтому при изменении размера книги потребуется обновить и значения в этом файле.

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

Теперь необходимо задать для всех страниц объекты flip: при работе с книгой они будут постоянно обновляться и отображать текущее положение страницы.

// 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 для элементов section, чтобы первая страница оказалась наверху, а последняя – внизу. Ключевые свойства объекта flip – это progress и target. Они определяют положение страницы в книге: "–1" соответствует положению у левого края, "0" – в центре (у корешка) книги, а "+1" – у правого края.

Место в книге
Значения progress и target объектов flip определяют положение переворачиваемой страницы по шкале от –1 до +1.

Итак, мы задали объекты flip для всех страниц и можем переходить к считыванию действий пользователя, на основе которых будет формироваться изображение переворачиваемой страницы.

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 соответствующего объекта flip устанавливается значение true.

Функция mouseUpHandler проверяет значение флага dragging во всех объектах flip – это переворачиваемые страницы, которые необходимо отпустить. Теперь необходимо установить значение целевого положения страницы: оно определяется положением указателя мыши. Номер страницы, на которой находится читатель, также обновляется.

Отображение

Мы разобрались с логикой и можем переходить к отображению переворачиваемых страниц в элементе canvas. Основную часть этой задачи выполняет функция 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 );
    }

  }
}

Прежде чем обрабатывать объекты flip, необходимо очистить элемент canvas с помощью метода clearRect(x,y,w,h). Очистка всего фона требует значительных ресурсов, поэтому на практике имеет смысл обновлять только те области, в которых что-то меняется. Однако чтобы не отклоняться от темы руководства, мы не будем подробно разбирать этот вопрос и очистим весь элемент canvas.

При перетаскивании объекта flip значение его свойства target меняется в соответствии с положением указателя мыши, но используются не пиксели, а значение по шкале от -1 до +1. Также увеличивается значение свойства progress, к которому добавляется часть расстояния до целевого положения (target). Таким образом, анимация выполняется в каждом кадре, и перелистывание выглядит более естественно.

Так как в каждом кадре проверяются все объекты flip, нам необходимо вычленить активные и обновить только их. Объект flip считается активным, если он находится на достаточном расстоянии от края страницы (в пределах 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 отображаемого объекта flip, так как оно определяет расположение загиба страницы. Чтобы изображение было объемным, мы выводим верхний и нижний края страницы за пределы книги: этот эффект особенно заметен, когда загиб приближается к корешку.

Перелистывание
Загиб страницы при перелистывании (перетаскивании мышью).

Теперь, когда все значения подготовлены, осталось нарисовать бумагу.

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 элемента canvas, позволяет сместить систему координат так, чтобы ее начало (0,0) совпадало с верхним краем корешка книги. Обратите внимание на то, что для сохранения текущей матрицы трансформации используется функция save(), а для ее восстановления после окончания анимации – функция restore().

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

Свойство foldGradient отвечает за заливку загнутой страницы, благодаря которой светотень выглядит более естественно. Кроме того, чтобы край страницы не терялся на светлом фоне, он обводится тонкой линией.

Теперь остается только придать загнутой странице нужную форму с помощью описанных выше свойств. Левый и правый края разворота представлены прямыми линиями, а верхний и нижний – изогнутыми, которые создают реалистичное ощущение перелистывания. Глубина загиба определяется значением свойства verticalOutdent.

Вот и все! Мы только что создали книгу, в которой навигация реализована в форме перелистывания.

Демонстрация эффекта перелистывания

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

Просмотреть книгу в действии

Загрузить исходный код (ZIP-файл, 75 КБ)

Что дальше

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

Это лишь один из примеров использования возможностей HTML5, таких как элемент canvas. Рекомендуем ознакомиться с более совершенной книгой, созданной с помощью этой технологии, на сайте www.20thingsilearned.com/ru-RU. Вы сами увидите, как перелистывание используется в реальном веб-приложении и как естественно оно выглядит в сочетании с другими возможностями HTML5.

Ссылки

Comments

0