Перевод статьи. Англоязычный оригинал опубликован на SitePoint – "Introducing GraphicsJS, a Powerful Lightweight Graphics Library".

HTML5 – основа основ современного веба. И сегодня, когда встает задача создать интерактивную графику, выбор чаще всего падает на такие технологии, как SVG и Canvas. Flash позабыт, Silverlight – редкая птица, обитающая на задворках веба, и почти никто не помнит сторонние ActiveX и Java-плагины.

Плюсы и минусы SVG и Canvas хорошо известны – в целом все сводится к тому, что для создания интерактивных элементов и работы с ними больше подходит SVG. Это векторный формат, основанный на XML, и, когда изображение загружается на страницу с использованием тега <svg>, каждый его внутренний элемент становится доступен в SVG DOM.

В данной статье я хочу рассказать о GraphicsJS. Это мощная графическая JavaScript-библиотека с открытым исходным кодом, основанная на технологии SVG (VML для старых версий IE). Начну с краткого введения в основы GraphicsJS, а затем проиллюстрирую возможности библиотеки двумя небольшими, но наглядными примерами. Первый из них посвящен изобразительному искусству. Второй покажет, как менее чем за 50 строк кода сделать простую арт-игру в жанре таймкиллера.

Почему GraphicsJS


Библиотек, облегчающих работу с SVG, довольно много: в число лучших входят Raphael, Snap.svg и BonsaiJS. У каждой есть сильные и слабые стороны, однако их детальное сравнение будет темой одной из следующих публикаций. Данная же статья – исключительно о GraphicsJS, и сейчас я попытаюсь объяснить, чем эта библиотека хороша и выделяется среди прочих.

Во-первых, GraphicsJS весит совсем немного и обладает очень гибким JavaScript API. Она предоставляет богатые возможности для форматирования текста, а также виртуальный DOM – независимый от специфики HTML DOM в разных браузерах.

Во-вторых, код этой библиотеки был открыт только прошлой осенью. Компания AnyChart, один из лидеров в разработке решений для интерактивной визуализации данных, использует GraphicsJS как графический движок в своих коммерческих продуктах уже порядка трех лет (с момента выхода AnyChart 7.0), так что эта библиотека проверена в боевых условиях. (Дисклеймер: я руководитель R&D в AnyChart и ведущий разработчик GraphicsJS.)

В-третьих, в отличие от других продуктов AnyChart – JavaScript-библиотек для построения графиков – GraphicsJS бесплатна для использования как в коммерческих, так и в некоммерческих целях. Библиотека доступна на GitHub под лицензией Apache.

В-четвертых, GraphicsJS обладает кросс-браузерной совместимостью, включая поддержку Internet Explorer 6.0+, Safari 3.0+, Firefox 3.0+, Opera 9.5+. В старых версиях IE библиотека использует VML, во всех остальных браузерах – SVG.

Наконец, GraphicsJS позволяет эффективно комбинировать графику и анимацию. Посмотрите главную галерею библиотеки: здесь есть такие примеры, как пылающий костер, вращающаяся галактика, дождь, процедурно генерируемые листья, головоломка «Пятнашки» и так далее. В развернутой документации и детальном описании API можно найти еще больше примеров использования GraphicsJS.

Основы GraphicsJS


Для начала работы с GraphicsJS нужно подключить саму библиотеку и создать блочный элемент HTML-кода для будущего рисунка:

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>GraphicsJS Basic Example</title>    
  </head>
  <body>
    <div id="stage-container" style="width: 400px; height: 375px;"></div>

    <script src="https://cdn.anychart.com/js/latest/graphics.min.js"></script>
    <script>
      // Здесь код GraphicsJS
    </script>
  </body>
</html>

Затем надо построить рабочую область и что-нибудь в ней нарисовать, например прямоугольник, круг или какую-то другую фигуру:

// создаем рабочую область
var stage = acgraph.create('stage-container');
// рисуем прямоугольник
var stage.rect(25, 50, 350, 300);

Вот пример на CodePen, в котором мы идем чуть дальше и рисуем знак Даров Смерти.

Наш первый шедевр


Контур, заливка цветом и узором


Любую фигуру или линию можно раскрашивать, используя настройки заливки и контура. Контур (граница) есть у всего, но заливка доступна только для фигур и замкнутых линий.

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

А теперь посмотрим, что конкретно можно сделать. Я нарисую небольшую картинку (человечек, стоящий возле дома) и затем доработаю ее с помощью заливки цветом и узорами. Пусть для простоты это будет изображение в стиле наивного искусства (постараюсь не докатиться до ар брют). Поехали:

// создаем рабочую область
var stage = acgraph.create('stage-container');

// рисуем рамку
var frame = stage.rect(25, 50, 350, 300);

// рисуем дом
var walls = stage.rect(50, 250, 200, 100);
var roof  = stage.path()
  .moveTo(50, 250)
  .lineTo(150, 180)
  .lineTo(250, 250)
  .close();

// рисуем человечка
var head = stage.circle(330, 280, 10);
var neck = stage.path().moveTo(330, 290).lineTo(330, 300);
var kilt = stage.triangleUp(330, 320, 20);
var rightLeg = stage.path().moveTo(320, 330).lineTo(320, 340);
var leftLeg = stage.path().moveTo(340, 330).lineTo(340, 340);

Результат можно посмотреть на CodePen.

Как видите, здесь мы используем переменные: все методы, которые что-либо рисуют в рабочей области, возвращают ссылку на созданный объект, которую можно использовать для его изменения или удаления.

Кроме того, в GraphicsJS можно активно применять цепные вызовы (например, stage.path().moveTo(320, 330).lineTo(320, 340);), что позволяет сократить код. Использовать эту возможность нужно аккуратно, но при правильном подходе цепной вызов действительно делает код более компактным и легко читаемым.

Теперь можно дать получившуюся у нас картинку какому-нибудь ребенку и попросить раскрасить ее – ведь даже ребенок способен освоить следующий прием:

// раскрашиваем картинку
// элегантная рамка
frame.stroke(["red", "green", "blue"], 2, "2 2 2");
// кирпичные стены
walls.fill(acgraph.hatchFill('horizontalbrick'));
// соломенная крыша
roof.fill("#e4d96f");
// клетчатый килт
kilt.fill(acgraph.hatchFill('plaid'));

Теперь наш пример выглядит вот так.

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

// 169 - символьный код значка копирайта
var  text = acgraph.text().text(String.fromCharCode(169)).opacity(0.2);
var  pattern_font = stage.pattern(text.getBounds());
pattern_font.addChild(text);
// заполняем паттерном все изображение
frame.fill(pattern_font);

Как видите, это очень просто: нужно создать экземпляр текстового объекта, затем построить паттерн в рабочей области и, наконец, вставить текст в паттерн.


Рисуем арт-игру (таймкиллер) менее чем за 50 строк кода


В следующей части статьи я покажу, как с помощью GraphicsJS нарисовать игру-кликер типа Cookie Clicker меньше чем за 50 строк кода.

Название этой игры – «Дворник на ветру». В ней пользователь выступает в роли дворника, который подметает улицу ветреным осенним днем. Здесь частично используется код примера с процедурно генерируемыми листьями из галереи GraphicsJS.

Финальный вариант игры можно посмотреть на CodePen (или в конце этой статьи).

Слои, zIndex, виртуальный DOM


Начнем с создания рабочей области (как в предыдущем примере). И затем объявим несколько исходных переменных:

// создаем рабочую область
var stage = acgraph.create("stage-container");

// цветовые палитры для листьев
var palette_fill = ['#5f8c3f', '#cb9226', '#515523', '#f2ad33', '#8b0f01']; 
var palette_stroke = ['#43622c', '#8e661b', '#393b19', '#a97924', '#610b01'];

// счетчик
var leavesCounter = 0;

Для создания игры нам пригодится возможность работы со слоем – объектом, отвечающим за группировку элементов в GraphicsJS. Элементы должны быть сгруппированы, чтобы к ним было удобно применять одинаковые изменения, например трансформации. Слои можно модифицировать в режиме приостановки (о нем расскажу чуть ниже): это улучшает производительность и впечатления от использования.

В данном примере функциональность слоя помогает сгруппировать листья и не дать им закрыть собой надпись, сообщающую, сколько листьев мы вымели. Для этого сначала создадим надпись, а затем с помощью метода stage.layer создадим слой для всей рабочей области и назначим последнему более низкий zIndex, чем у надписи.

// создаем надпись для отображения счетчика
var counterLabel = stage.text(10,10, "Swiped: 0", {fontSize: 20});

// слой для листьев
var gameLayer = stage.layer().zIndex(counterLabel.zIndex()-1);

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

Трансформации


Далее добавим функцию для отрисовки листьев, воспользовавшись удобным API для трансформаций, который позволяет перемещать, масштабировать, поворачивать и обрезать как элементы, так и группы элементов. В связке с другими возможностями, которые предоставляет GraphicsJS, – слоями и виртуальным DOM – это очень мощный инструмент.

function drawLeaf(x, y) {
  // выбираем произвольный цвет из палитры
  var index = Math.floor(Math.random() * 5);
  var fill = palette_fill[index];
  var stroke = palette_stroke[index];

  // генерируем произвольные масштабирующий коэффициент и угол поворота
  var scale = Math.round(Math.random() * 30) / 10 + 1;
  var angle = Math.round(Math.random() * 360 * 100) / 100;

  // создаем новый путь (лист)
  var path = acgraph.path();

  // задаем раскраску и рисуем лист
  path.fill(fill).stroke(stroke, 1, 'none', 'round', 'round');
  var size = 18;
  path.moveTo(x, y)
    .curveTo(x + size / 2, y - size / 2, x + 3 * size / 4, y + size / 4, x + size, y)
    .curveTo(x + 3 * size / 4, y + size / 3, x + size / 3, y + size / 3, x, y);

  // применяем произвольные трансформации
  path.scale(scale, scale, x, y).rotate(angle, x, y);

  return path; 
};

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

Работа с событиями


Все объекты, рабочие области и слои в GraphicsJS умеют обрабатывать события. Список всех доступных событий есть в EventType API. При этом рабочие области поддерживают четыре специальных события для контроля рендеринга.

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

path.listen("mouseover", function(){
  path.remove();
  counterLabel.text("Swiped: " + leavesCounter++);
  if (gameLayer.numChildren() < 200) shakeTree(300); 
});

Отсюда также ясно, что для подсчета листьев используется слой.

if (gameLayer.numChildren() < 200) shakeTree(300); 

Заметьте, на самом деле мы не храним здесь количество листьев. Листья – это линии, которые добавляются в тот или иной слой или удаляются оттуда, и потому можно отследить, сколько у нас дочерних объектов (а значит, и сколько листьев остается).

Библиотека GraphicsJS дает возможность использовать виртуальный DOM, абстракцию HTML DOM, легкую и независимую от специфики применения SVG/VML в разных браузерах. Эта технология пригодится для реализации целого ряда полезных функций, таких как контроль за всеми объектами и слоями, применение трансформаций к группам и оптимизация рендеринга с помощью методов, которые позволяют отслеживать и контролировать весь его процесс.

Оптимизация производительности


Благодаря виртуальному DOM, а также обработчикам событий пользователи GraphicsJS могут контролировать рендеринг. О том, как эти вещи связаны, можно прочитать в статье "Производительность" из документации библиотеки.

На время, когда в нашей игре генерируются листья, рендеринг нужно приостановить, а затем возобновить его, как только все изменения будут произведены:

function shakeTree(n){
  stage.suspend(); // приостанавливаем рендеринг
  for (var i = 0; i < n; i++) {
    var x = Math.random() * stage.width()/2 + 50;
    var y = Math.random() * stage.height()/2 + 50;
    gameLayer.addChild(drawLeaf(x, y)); // добавляем лист
  }

  stage.resume(); // возобновляем рендеринг
}

При таком способе добавления элементов новые листья появляются практически мгновенно.

// первый раз сбрасываем все листья
shakeTree(500);

Ну и наконец, сбрасываем все листья, вызвав метод shakeTree().

Конечный результат



Заключение


Переход на HTML5 изменил Веб. Когда дело касается современных веб-приложений или даже простого сайта, мы часто сталкиваемся с задачами, которые требуют манипуляций с графикой. Хотя невозможно найти решение, которое идеально работает в абсолютно любой ситуации, я предложил бы вам обратить внимание на библиотеку GraphicsJS. У нее открытый код и open-source лицензия, она очень функциональна и производительна, а также оснащена отличной браузерной поддержкой и множеством фич. Все это делает GraphicsJS интересным, удобным и, конечно, эффективным решением.

Буду рад получить обратную связь по поводу GraphicsJS в комментариях. Уже используете эту библиотеку? Готовы ли рассмотреть возможность ее применения в новом проекте? Интересно было бы узнать, почему, равно как и почему нет. Кстати, сейчас я работаю над созданием списка лучших графических JavaScript-библиотек и статьей, где они будут сравниваться, – так что предлагаю написать в комментариях, какие библиотеки вы бы хотели там видеть.

Cсылки





Автор оригинальной статьи на SitePoint: Роман Любушкин (Roman Lubushkin).
Поделиться с друзьями
-->

Комментарии (5)


  1. vintage
    20.06.2017 13:20

    Я не очень понял, как в AnyChart эффективно обновить данные графиков.


    1. vintage
      20.06.2017 15:15

      Разобрался. Нужно создать stage и указать его в качестве контейнера, тогда можно будет применять suspend и resume.


      Ребята, а чего у вас всё такое медленное?


      http://mol.js.org/app/bench/#bench=chart%2Frope%2F/sort=fill/sample=hcharts~mol~anychart
      http://mol.js.org/app/bench/#bench=chart%2Fbar%2F/sort=fill/sample=hcharts~mol~anychart


      1. roman_lubushkin
        23.06.2017 10:50

        Да, с производительностью сейчас есть некоторые проблемы, но 80% этих проблем про AnyChart, его внутреннюю архитектуру и перегруженность фичами, а не про Graphics. Мы довольно долго не обращали на эти проблемы внимания, т.к из нашей аналитики, на графиках общего назначения редко бывает больше 200 — 300 точек. А для больших данных у нас есть AnyStock.

        Сейчас же игнорировать их уже нельзя, следующий релиз запланирован на сентябрь и будет полностью посвящен размеру JS'ки и производительности.


  1. tomgif
    22.06.2017 16:55

    Смотрю и вижу canvas


    1. roman_lubushkin
      23.06.2017 11:08

      Да, API весьма напоминает canvas, если вы про это.
      Очень надеемся что в будущем дойдут руки до сanvas рендерера и можно будет одним API работать и с вектором и с растром.