Применение HTML5-элемента canvas для визуализации данных

OSzone.net » Microsoft » Разработка приложений » HTML5 » Применение HTML5-элемента canvas для визуализации данных
Автор: Брэндон Сэтром
Иcточник: MSDN Magazine
Опубликована: 22.05.2012

На заре Web, когда она представляла собой чуть большее набора статического текста и ссылок, рос интерес к поддержке других типов контента. В 1993 году Марк Андреессен (Marc Andreessen) — создатель браузера Mosaic, который впоследствии развился в Netscape Navigator, — предложил тег IMG в качестве стандарта для встраивания изображений в текст на странице. Вскоре после этого тег IMG стал стандартом де-факто для добавления графических ресурсов в веб-страницы — этот стандарт используется и сегодня. Вы могли бы даже выдвинуть аргументы в пользу того, что по мере эволюции Web документов в Web приложений тег IMG становится еще важнее, чем раньше.

В целом, медийная информация определенно приобрела более важное значение, и, хотя требования к ней в Web претерпели изменения за прошедшие 18 лет, изображения остались статическими. Веб-авторы стали все чаще использовать динамическую медийную информацию, такую как аудио/видео и интерактивные анимации, на своих сайтах и в приложениях, и до недавних пор основным решением было применение плагина вроде Flash или Silverlight.

Теперь с появлением HTML5 медийные элементы в браузере получают новый импульс для развития. Вероятно, вы слышали о новых тегах Audio и Video, которые позволяют оперировать соответствующим контентом в браузере безо всяких плагинов. (Подробно об этих элементах и их API я расскажу в следующей статье.) Возможно, вы также слышали об элементе canvas — поверхности для рисования с богатым набором JavaScript API-функций, позволяющих «на лету» создавать изображения и анимации и манипулировать ими. То, что делал IMG для статического графического контента, canvas потенциально может делать для динамического контента, управляемого скриптами.

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

В сегодняшней статье я бы хотел отступить от гламурных возможностей canvas и показать некоторые простые, базовые варианты его применения. Моя главная цель — позиционировать canvas как эффективный вариант для визуализации данных в веб-приложениях. Учитывая это, я сосредоточусь на том, как приступить к работе с canvas и как рисовать простые линии, фигуры и текст. Потом мы поговорим о том, как работать с градиентами, а также добавлять внешние изображения на canvas. Наконец, под занавес мы кратко обсудим поли-заполнение поддержки canvas для более старых браузеров.

Знакомство с HTML5-элементом canvas

Согласно W3C-спецификации HTML5 (w3.org/TR/html5/the-canvas-element.html), элемент canvas «предоставляет скрипты с растровым холстом, зависимым от разрешения, который можно использовать для рендеринга графов, игровой графики или других изображений на лету». Элемент canvas на самом деле определен в двух спецификациях W3C. Первое определение содержится в базовой спецификации HTML5, где сам элемент определен очень детально. Эта спецификация описывает, как использовать canvas, как получить его контекст рисования, API для экспорта содержимого canvas и соображения по безопасности для поставщиков браузеров. Второе определение — HTML Canvas 2D Context (w3.org/TR/2dcontext), о котором мы поговорим чуть позже.

Приступить к работе с canvas не сложнее, чем добавить элемент <canvas> в HTML5-разметку, например:

<!DOCTYPE html>

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>My Canvas Demo </title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <canvas id="chart" width="600" height="450"></canvas>
  </body>
</html>

Хотя теперь у меня есть элемент canvas в DOM, включение этой разметки в страницу ничего не даст, потому что в элементе canvas нет никакого контента, пока вы сами не добавите его. Для этого как раз и нужен контекст рисования (drawing context). Чтобы показать вам, где находится мой пустой canvas, я могу воспользоваться CSS и применить к нему стиль; я добавлю пунктирную синюю линию вокруг этого пустого элемента:

canvas {
    border-width: 5px;
    border-style: dashed;
    border-color: rgba(20, 126, 239, 0.50)
}

Когда эта страница будет открыта в Internet Explorer 9+, Chrome, Firefox, Opera или Safari, вы увидите результат, показанный на рис. 1.

{Рисунок}

*
Рис. 1. Пустой элемент canvas с примененным к нему стилем

При использовании canvas большая часть работы выполняется на JavaScript, где API, предоставляемые контекстом рисования canvas, можно использовать для манипуляций над каждым пикселем поверхности. Чтобы получить контекст рисования canvas, нужно получить элемент canvas из DOM, а затем вызвать метод getContext этого элемента.

var _canvas = document.getElementById('chart');
var _ctx = _canvas.getContext("2d");

GetContext возвращает объект с API, посредством которого можно рисовать на данном canvas. Первый аргумент этого метода (здесь «2d») указывает нужный нам API рисования для canvas. Значение «2d» относится к HTML Canvas 2D Context, о котором я уже упоминал. Как вы, вероятно, догадались, 2D означает, что это двухмерный контекст рисования. На момент написания этой статьи, 2D Context является единственным широко поддерживаемым контекстом рисования, и именно его мы будем использовать в этой статье. С трехмерных контекстом рисования работы пока не закончены, но в будущем canvas даст нашим приложениям еще более широкие возможности.

Рисование линий, фигур и текста

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

Начнем с линий для осей x и y. Рисование линий (или траекторий) с помощью контекста canvas является двухступенчатым процессом. Сначала вы «размечаете» (trace) линии на поверхности, используя серии вызовов lineTo(x, y) и moveTo(x, y). Каждый метод принимает x- и y-координаты в объекте canvas (начиная с верхнего левого угла), которые используются при выполнении операции (в противоположность координатам на самом экране). Метод moveTo перемещает к указанным координатам, а lineTo размечает линию из текущих координат до заданных вами. Например, следующий код разметит на поверхности canvas ось y:

_ctx.moveTo(110, 5);
_ctx.lineTo(110, 375);

При использовании canvas большая часть работы выполняется на JavaScript.

Если вы добавите этот код в свой скрипт и запустите его в браузере, то обнаружите, что ничего не происходит. На экране ничего не рисуется потому, что первая стадия — простая разметка линий. Эта разметка инструктирует браузер принять во внимание операцию черчения траектории (path operation), результат которой должен быть выведен на экран в некий момент в будущем. Когда я готов к прорисовке траекторий на экране, я дополнительно задаю свойство strokeStyle своего контекста, а затем вызываю метод stroke, который и заполняет невидимые линии. Результат приведен на рис. 2.

// Определяем стиль и заполняем намеченные линии
_ctx.strokeStyle = "#000";
_ctx.stroke();
{Рисунок}

*
Рис. 2. Единственная линия на холсте

Поскольку определение линий (lineTo, moveTo) и их рисование (stroke) — отдельные операции, вы можете собрать множество операций lineTo и moveTo в пакет, а затем выводить на экран сразу весь результат. Я сделаю это для осей x и y и для операций, которые рисуют стрелки в конце каждой оси. Вся функция для рисования осей показана на рис. 3, а результат — на рис. 4.

Рис. 3. Функция drawAxes

function drawAxes(baseX, baseY, chartWidth) {
   var leftY, rightX;
   leftY = 5;
   rightX = baseX + chartWidth;

   // Рисуем ось y
   _ctx.moveTo(baseX, leftY);
   _ctx.lineTo(baseX, baseY);

   // Рисуем стрелку для оси y
   _ctx.moveTo(baseX, leftY);
   _ctx.lineTo(baseX + 5, leftY + 5);
   _ctx.moveTo(baseX, leftY);
   _ctx.lineTo(baseX - 5, leftY + 5);

   // Рисуем ось x
   _ctx.moveTo(baseX, baseY);
   _ctx.lineTo(rightX, baseY);

   // Рисуем стрелку для оси x
   _ctx.moveTo(rightX, baseY);
   _ctx.lineTo(rightX - 5, baseY + 5);
   _ctx.moveTo(rightX, baseY);
   _ctx.lineTo(rightX - 5, baseY - 5);

   // Определяем стиль и заполняем намеченные линии
   _ctx.strokeStyle = "#000";
   _ctx.stroke();
}
{Рисунок}

*
Рис. 4. Законченные оси x и y

Теперь оси у нас есть, но, вероятно, мы должны как-то подписать их, чтобы было понятно, что они представляют. У двухмерного контекста canvas есть API для добавления текста к элементам canvas, поэтому вам не придется выдумывать всякие фокусы вроде размещения текста поверх canvas. Однако вы должны понимать, что текст на холсте не поддерживает рамочную модель (box model), не принимает CSS-стили, определенные для текста на уровне страницы, и др. В то же время этот API предоставляет атрибут font, который работает аналогично правилу шрифта в CSS (font rule), а также свойства textAlign и textBaseline, дающие некоторый контроль над позицией относительно переданных координат; в остальном рисование текста в canvas сводится к выбору точной позиции нужного вам текста на холсте.

По оси x мы указываем спортивные товары, которыми торгует наш вымышленный магазин, поэтому мы должны соответственно обозначить эту ось:

var height, widthOffset;
height = _ctx.canvas.height;
widthOffset = _ctx.canvas.width/2;

_ctx.font = "bold 18px sans-serif";
_ctx.fillText("Product", widthOffset, height - 20);

В этом фрагменте кода я использую необязательное свойство font и указываю строку, которую нужно нарисовать на поверхности, а также начальные координаты x и y для размещения строки. В этом примере я рисую слово «Product» с выравниванием по центру холста, на 20 пикселей выше нижней границы, что оставляет место для обозначений всех продуктов на этой линейчатой диаграмме. Примерно то же самое я сделаю с осью y, по которой откладываются данные объемов продаж каждого товара. Результат приведен на рис. 5.

{Рисунок}

*
Рис. 5. Холст с текстом

Теперь, когда у нас есть инфраструктура для диаграммы, можно добавлять полоски (столбики). Давайте создадим фиктивные данные по объемам продаж для этой диаграммы, которую я определю как JavaScript-массив литералов объектов:

var salesData = [{
   category: "Basketballs",
   sales: 150
}, {
   category: "Baseballs",
   sales: 125
}, {
   category: "Footballs",
   sales: 300
}];

Располагая этими данными, можно нарисовать полоски на диаграмме с помощью fillRect и fillStyle.

Метод fillRect(x, y, width, height) будет рисовать на холсте прямоугольник заданной высоты и ширины в указанных координатах x и y. Важно отметить, что fillRect рисует фигуры, начиная от точки в верхнем левом углу, если только вы не укажете отрицательные значения ширины и высоты (в этом случае направление поменяется на противоположное). Применительно к диаграммам это означает, что мы будем рисовать полоски сверху вниз, а не снизу вверх.

Для вывода полосок можно перебирать массив данных по продажам в цикле и вызывать fillRect с соответствующими координатами:

var i, length, category, sales;
var barWidth = 80;
var xPos = baseX + 30;
var baseY = 375;

for (i = 0, length = salesData.length; i < length; i++) {
   category = salesData[i].category;
   sales = salesData[i].sales;

   _ctx.fillRect(xPos, baseY - sales-1, barWidth, sales);
   xPos += 125;
}

В этом коде ширина каждой полоски стандартна, а высота зависит от свойства sales для каждого товара в массиве. Результат работы этого кода показан на рис. 6.

{Рисунок, без перевода}

*
Рис. 6. Прямоугольники как полоски на линейчатой диаграмме

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

Работа с цветами и градиентами

Когда вызывается метод fillRect контекста рисования, последний будет использовать текущее значение свойства fillStyle для применения стилей к выводимому прямоугольнику. Стиль по умолчанию — заливка сплошным черным цветом, и вот почему наша диаграмма выглядит так, как на рис. 6. Свойство fillStyle принимает именованные шестнадцатеричные и RGB-цвета:

// Цвета можно именовать как шестнадцатеричные или RGB
colors = ["orange", "#0092bf", "rgba(240, 101, 41, 0.90)"];
...
_ctx.fillStyle = colors[i % length];
_ctx.fillRect(xPos, baseY - sales-1, barWidth, sales);

Сначала мы создаем массив цветов. Затем, перебирая в цикле данные по каждому товару, мы используем один из этих цветов в качестве стиля заполнения для текущего элемента. Результат показан на рис. 7.

{Рисунок}

*
Рис. 7. С помощью fillStyle к фигурам применяется стиль

Это выглядит уже лучше, но fillStyle — очень гибкое свойство и позволяет использовать линейные и радиальные градиенты вместо простых сплошных цветов. В двухмерном контексте рисования определены две функции градиентов, createLinerGradient и createRadialGradient, обе из которых могут расширить стиль наших фигур за счет применения плавных цветовых переходов.

В этом примере я определю функцию createGradient, которая принимает координаты x и y для градиента, ширину и основной цвет:

function createGradient(x, y, width, color) {
   var gradient;

   gradient = _ctx.createLinearGradient(x, y, x+width, y);
   gradient.addColorStop(0, color);
   gradient.addColorStop(1, "#efe3e3");

   return gradient;
}

После вызова createLinearGradient с моими начальными и конечными координатами я добавлю два фиксатора цвета (color stops) в объект градиента, возвращаемый контекстом рисования. Метод addColorStop добавит цветовые переходы вдоль градиента; его можно вызывать сколько угодно раз, причем значение первого параметра должно варьироваться от 0 до 1. После настройки градиента я возвращаю его из функции.

Теперь объект градиента можно поместить в свойство fillStyle в моем контексте вместо шестнадцатеричных и RGB-строк, которые я указывал в предыдущем примере. Я использую те же цвета в качестве отправной точки, а затем плавно меняю их до светло-серого цвета:

colors = ["orange", "#0092bf", "rgba(240, 101, 41, 0.90)"];
_ctx.fillStyle = createGradient(xPos, baseY - sales-1,
  barWidth, colors[i % length]);
_ctx.fillRect(xPos, baseY - sales-1, barWidth, sales);

Результат показан на рис. 8.

{Рисунок}

*
Рис. 8. Использование градиентов в элементе canvas

Работа с изображениями

К этому моменту мы имеем весьма симпатичную диаграмму, которая визуализируется в браузере несколькими десятками строк кода на JavaScript. На этом можно было бы и остановиться, но мы не рассмотрели один базовый API элемента canvas, относящийся к работе с изображениями. Статические изображения можно не только заменять скриптовым интерактивным контентом, но и использовать для более эффектных визуализаций.

В этом примере я воспользуюсь изображениями в качестве полосок на линейчатой диаграмме. И не просто изображениями, а картинками с конкретными товарами. Учитывая эту цель, на своем веб-сайте я создаю папку с JPG-изображениями каждого товара — в данном случае это basketballs.jpg, baseballs.jpg и footballs.jpg. Мне нужно лишь соответственно позиционировать и масштабировать каждое изображение.

В двухмерном контексте рисования определен метод drawImage с тремя перегруженными версиями, которые соответственно принимают три, пять и девять параметров. Первый параметр — это всегда DOM-элемент изображения, которое нужно нарисовать. Простейшая версия drawImage также принимает x- и y-координаты на холсте и рисует изображение в том виде, как есть, в указанном месте. Кроме того, вы можете передать ширину и высоту как два последних параметра, что приведет к изменению размеров изображения до его рисования на поверхности. Наконец, самая сложная версия drawImage позволяет обрезать изображение по границам определенного прямоугольника, изменять его в соответствии с заданным набором размеров и, наконец, рисовать на холсте в указанных координатах.

Простейшая стратегия, которую вы можете применять для браузеров, не поддерживающих элемент canvas, — использовать элемент-заменитель вроде изображения или текста.

Поскольку исходные изображения имеют большие размеры и используются в других местах сайта, я прибегну ко второму варианту. В этом примере вместо вызова fillRect для каждого товара при переборе массива salesData в цикле я буду создавать DOM-элемент Image, указывать в качестве его источника изображение одного из товаров и визуализировать его уменьшенную копию на диаграмме, как показано на рис. 9.

Рис. 9. Рисование изображений на холсте

// Задается вне цикла
xPos = 110 + 30;
// Создаем DOM-элемент Image
img = new Image();
img.onload = (function(height, base, currentImage,
currentCategory) {
  return function() {
    var yPos, barWidth, xPos;
    barWidth = 80;
      yPos = base - height - 1;

    _ctx.drawImage(currentImage, 30, 30, barWidth, height,
      xPos, yPos, barWidth, height);
      xPos += 125;
  }
})(salesData[i].sales, baseY, img, salesData[i].category);
img.src = "images/" + salesData[i].category + ".jpg";

Поскольку я создаю эти изображения динамически, не добавляя их вручную в разметку на этапе разработки, мне нельзя полагать, что я смогу задать источник изображения и тут же нарисовать это изображение на своем холсте. Чтобы рисовать каждое изображение только после его полной загрузки, я добавлю свою логику рисования в событие onload для изображения, затем оберну этот код в самовызываемую функцию (self-invoking function), которая создает замыкание (closure) с переменными, указывающими на правильную категорию товаров, а также переменными, содержащими данные по продажам, и переменными, используемыми для позиционирования. Результат показан на рис. 10.

{Рисунок}

*
Рис. 10. Использование изображений в элементе canvas

Применение поли-заполнения canvas

Как вы, вероятно, знаете, версии Internet Explorer ниже 9, а также старые версии других браузеров не поддерживают элемент canvas. Вы можете сами убедиться в этом, открыв демонстрационный проект в Internet Explorer и нажав F12 для доступа к инструментарию разработчика. Этот инструментарий позволяет сменить Browser Mode на Internet Explorer 8 или Internet Explorer 7 и обновить страницу. После этого вы скорее всего увидите JavaScript-исключение с сообщением «Object doesn’t support property of method» («объект не поддерживает свойство метода getContext»). Двухмерный контекст рисования недоступен, равно как и сам элемент canvas. Также важно понимать, что даже в Internet Explorer 9 элемент canvas недоступен, пока вы не укажете DOCTYPE. Как уже упоминалось в первой статье этого цикла (msdn.microsoft.com/magazine/hh335062), следует всегда указывать <!DOCTYPE html> в начале всех ваших HTML-страниц, чтобы гарантировать доступность новейших возможностей в браузере.

Простейшая стратегия, которую вы можете применять для браузеров, не поддерживающих элемент canvas, — использовать элемент-заменитель (fallback element) вроде изображения или текста. Например, чтобы вывести изображение-заменитель, можно использовать такую разметку:

<canvas id="chart">
  <img id="chartIMG" src="images/fallback.png"/>
</canvas>

Если браузер пользователя не поддерживает тег <canvas>, то все его содержимое визуализируется. То есть вы можете помещать изображения или текст в этот тег и использовать его как простой элемент-заменитель.

Если же вы хотите расширить поддержку заменяющей функциональности, тогда применяйте одно из множества уже существующих решений поли-заполнения для canvas; они более удобны в использовании с устаревшими браузерами при условии, что вы тщательно отбираете потенциальные решения и осознаете ограничения конкретного поли-заполнения. Как я говорил в других статьях этого цикла, вашей отправной точкой в поисках поли-заполнения для любой технологии HTML5 должна быть страница HTML5 Cross Browser Polyfills в википедии Modernizr на GitHub (bit.ly/nZW85d). На момент написания этой статьи было доступно несколько поли-заполнений canvas, в том числе два из них использовали переключение на Flash и Silverlight.

В демонстрационном проекте для этой статьи я использую explorercanvas (code.google.com/p/explorercanvas), где применяется Vector Markup Language (VML), поддерживаемый в Internet Explorer (с его помощью создаются реализации функциональности, весьма близкие к исходной canvas), и canvas-text (code.google.com/p/canvas-text), который включает дополнительную поддержку для визуализации текста в устаревших браузерах.

Как было показано в предыдущих статьях, вы можете задействовать Modernizr для распознавания поддержки canvas (и canvastext) в браузере, вызывая Modernizr.canvas, а затем (при необходимости) используя Modernizr.load для асинхронной загрузки explorercanvas. Все подробности см. на сайте modernizr.com.

Если вы не хотите применять Modenrizr, есть другой способ условного добавления explorercanvas для более старых версий IE: условные комментарии (conditional comments):

<!--[if lt IE 9]>
  <script src="js/excanvas.js"></script>
  <script src="js/canvas.text.js"></script>
<![endif]-->

Когда Internet Explorer 8 или ниже встречает комментарий с таким форматированием, он выполняет этот блок как выражение if и включает файлы скриптов explorercanvas и canvas-text. Другие браузеры, в том числе Internet Explorer 10, будут интерпретировать весь этот блок как комментарий и напрочь его игнорировать.

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

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

Исходный код можно скачать по ссылке code.msdn.microsoft.com/mag201201HTML5.


Ссылка: http://www.oszone.net/18006/HTML5-canvas