Знакомимся с Fabric.js. Часть 1-я.

Сегодня я хочу познакомить вас с Fabric.js — мощной Javascript библиотекой для работы с HTML5 <canvas>. Fabric включает в себя обектную модель, которой так не хватает при работе с <canvas>, а так же SVG парсер, интерактивный слой и множество других, незаменимых инструментов. Это полностью открытая библиотека с MIT лицензией и многими взносами разработчиков за последние несколько лет.

Работу над Fabric я начал 3 года назад, когда понял насколько тяжело работать с обычным canvas API. В тот момент я создавал интерактивный редактор на printio.ru — мой стартап, где мы даём возможность создать дизайн и напечатать его на одежде, или других товарах. Редактор хотелось сделать удобным и супер интерактивным. В то время, такой функционал можно было создать только во Flash. Но Flash использовать я не хотел. Я предпочитаю Javascript, и был уверен, что с ним можно добиться многого. Получилось довольно неплохо. :) Даже сейчас очень немногие визуальные редакторы могут делать то, что можно достичь с помощью Fabric.

Зачем это нужно?

В последнее время популярность Canvas растёт и люди на нём делают довольно поразительные вещи. Проблема в том, что родной canvas API ужасно низко-уровневый. Одно дело если нужно нарисовать несколько простых фигур или графиков, и забыть о них. Другое — интерактивность, изменение картинки в какой-то момент, или рисование более сложных фигур.

Вот именно для этого и нужна Fabric.js

Дело в том, что обычные canvas методы позволяют нам вызывать только очень простые графические комманды, в слепую меняя целый битмап холста (canvas). Нужно нарисовать прямоугольник? Используем fillRect(left, top, width, height). Нарисовать линию? Используем комбинацию moveTo(left, top) и lineTo(x, y). Как будто рисуем кисточкой по холсту, накладывая всё больше и больше краски, почти без какого-либо контроля.

Fabric даёт нам объектную модель поверх низко-уревневых методов canvas, хранит состояние холста, и позволяет работать с обьектами напрямую.

Давайте посмотрим на разницу между canvas и Fabric. Допустим, нужно нарисовать красный прямоугольник. Используя canvas API, это делается приблизительно так:

// берём canvas элемент (id="c")
var canvasEl = document.getElementById('c');

// берём 2d контекст, на котором рисовать ("bitmap" упомянутый ранее)
var ctx = canvasEl.getContext('2d');

// меняем fill (закраску) цвета контекста
ctx.fillStyle = 'red';

// создаём прямоугольник в точке 100,100 размером в 20x20
ctx.fillRect(100, 100, 20, 20);

А вот тоже самое с Fabric:

// создаём "оболочку" вокруг canvas элемента (id="c")
var canvas = new fabric.Canvas('c');

// создаём прямоугольник
var rect = new fabric.Rect({
  left: 100,
  top: 100,
  fill: 'red',
  width: 20,
  height: 20
});

// добавляем прямоугольник, чтобы он отобразился
canvas.add(rect);

Разницы в размере кода пока не видно. Однако, видно что способ работы с canvas кардинально отличается. В обычном canvas API, мы работаем с контекстом. Контекст — это объект, который по сути представляет из себя битмап холста. С Fabric, мы управляем именно объектами — создаём, меняем параметры, добавляем их на canvas. Как видите, эти объекты — полноценные жители в Fabric (объекты первого класса).

Рисовать красный прямоугольник — это конечно не серьёзно. Давайте хоть сделаем с ним что-нибудь интересное. Например, повернём на 45 градусов.

Сначала, используя обычные методы:

var canvasEl = document.getElementById('c');
var ctx = canvasEl.getContext('2d');
ctx.fillStyle = 'red';

ctx.translate(100, 100);
ctx.rotate(Math.PI / 180 * 45);
ctx.fillRect(-10, -10, 20, 20);

и теперь с помощью Fabric:

var canvas = new fabric.Canvas('c');

// создаём прямоугольник с углом в 45 градусов
var rect = new fabric.Rect({
  left: 100,
  top: 100,
  fill: 'red',
  width: 20,
  height: 20,
  angle: 45
});

canvas.add(rect);

Что здесь происходит?

Используя Fabric, всё что надо было сделать, это поменять значение угла на 45. А вот с обычными методами всё не так-то просто. Во первых, мы не можем управлять объектами напрямую. Вместо этого, приходится менять позицию и угол самого битмапа (ctx.translate, ctx.rotate). Потом рисуем прямоугольник, при этом не забывая отодвинуть битмап соответственно (-10, -10), так, чтобы прямоугольник появился на 100,100. Ещё надо не забыть перевести угол из градусов в радианы при повороте битмапа.

Теперь вам, наверное, становится понятно зачем существует Fabric.

Давайте посмотрим на ещё один пример — хранение состояния canvas.

Представим, что в какой-то момент нам нужно подвинуть этот красный прямоугольник в другое место. Как это сделать, не имея возможность управлять объектами? Вызывать fillRect ещё раз?

Не совсем. Вызывая ещё одну команду fillRect, прямоугольник рисуется прямо поверх всего битмапа. Именно поэтому я привёл аналог кисти с краской. Чтобы подвинуть фигуру, нам нужно сначала стереть предыдущий результат, а потом уже рисовать на новом месте.

var canvasEl = document.getElementById('c');

...
ctx.strokRect(100, 100, 20, 20);
...

// стираем весь canvas
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
ctx.fillRect(20, 50, 20, 20);

А теперь с Fabric

var canvas = new fabric.Canvas('c');
...
canvas.add(rect);
...

rect.set({ left: 20, top: 50 });
canvas.renderAll();

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

Объекты

Мы уже видели как работать с прямоугольниками, используя fabric.Rect конструктор. Но, конечно же, Fabric предоставляет многие другие простые фигуры: круги, треугольники, эллипсы и т.д. Все они доступны из fabric объектов, соответственно, fabric.Circle, fabric.Triangle, fabric.Ellipse и т.д.

7 базовых фигур доступных в Fabric:

Нужно нарисовать круг? Просто создаём соответствующий объект и добавляем его на холст. Тоже самое с другими формами:

var circle = new fabric.Circle({
  radius: 20, fill: 'green', left: 100, top: 100
});
var triangle = new fabric.Triangle({
  width: 20, height: 30, fill: 'blue', left: 50, top: 50
});

canvas.add(circle, triangle);

..и вот уже на холсте красуется зелёный круг в точке 100, 100 и синий треугольник в точке 50, 50.

Управляем объектами

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

Fabric берёт на себя заботу о состоянии холста и перерисовке. От нас требуется только менять сами объекты.

В предыдущем примере было видно, как метод set подвинул объект на новую позицию set({ left: 20, top: 50 }). Точно также можно менять любые другие атрибуты, которых доступно несколько.

Во первых, есть атрибуты, меняющие позицию — left, top; размер — width, height; сам рендеринг (отображение объекта) — fill, opacity, stroke, strokeWidth; масштаб и поворот — scaleX, scaleY, angle; и даже переворот (180 градусов) — flipX, flipY.

Да, отобразить зеркально повёрнутую картинку в Fabric на удивление легко — просто присваеваем true в атрибут flip*.

Чтение атрибутов происходит с помощью метода get, присваивание — с помощью set. Давайте как-нибудь поменяем наш прямоугольник.

var canvas = new fabric.Canvas('c');
...
canvas.add(rect);

rect.set('fill', 'red');
rect.set({ strokeWidth: 5, stroke: 'rgba(100,200,200,0.5)' });
rect.set('angle', 15).set('flipY', true);

Мы выставили “fill” значением “red”, меняя цвет объекта на красный. Затем поменяли “strokeWidth” и “stroke”, что добавляет прямоугольнику 5и-пиксельную рамку светло-зелёного цвета. И наконец, меняем атрибуты “angle” и “flipY”. Заметьте, как три выражения используют слегка разный синтакс.

Отсюда видно, что set() — довольно универсальный метод. Он предназначен для частого использования, поэтому заточен под удобство.

Ну, а как насчёт чтения? Я уже упомянул, что есть общий get(), а также набор конкретных get*() методов. Например, для получения “width” объекта можно использовать get('width') или getWidth(). Для “scaleX” — get('scaleX') или getScaleX(), и т.д. Такие специальные методы, как getWidth() и getScaleX() существуют для всех “публичных” атрибутов объекта (“stroke”, “strokeWidth”, “angle”, и т.д.)

Вы наверное заметили, что в предыдущих примерах были использованы конфигурационные хэши, которые выглядели точно также, как и те, которые мы только что использовали в методе set. Это потому, что они действительно одинаковые. Объект может быть "сконфигурирован" в момент создания, или позже, с помощью метода set. Синтакс, при этом, абсолютно одинаковый:

var rect = new fabric.Rect({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });

// полностью идентичен

var rect = new fabric.Rect();
rect.set({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });

Атрибуты по умолчанию

У всех объектов в Fabric есть набор значений по умолчанию. Они используются, когда во время создания мы не задаём другие значения. Привожу пример.

var rect = new fabric.Rect(); // не передаём никаких параметров

rect.get('width'); // 0
rect.get('height'); // 0

rect.get('left'); // 0
rect.get('top'); // 0

rect.get('fill'); // rgb(0,0,0)
rect.get('stroke'); // null

rect.get('opacity'); // 1

Прямоугольник получил значения по умолчанию. Он находится в позиции 0,0, чёрного цвета, непрозрачный, не имеет ни рамок, ни габаритов (ширина и высота равны нулю). Из-за этого мы его и не видим. Как только устанавливаем позитивные width/height, чёрный прямоугольник появляется в левом верхнем углу.

Иерархия и Наследование

Объекты Fabric не существуют сами по себе. Они формируют чёткую иерархию.

Большинство объектов наследуют от fabric.Object. fabric.Object — это абстрактная 2-х мерная фигура на плоскости. Она имеет left/top и width/height атрибуты, а также набор других визуальных параметров. Те атрибуты, которые мы видели ранее (fill, stroke, angle, opacity, flip* и т.д.) принадлежат всем Fabric объектам, которые наследуют от fabric.Object.

Такое наследование очень удобно. Оно позволяет нам определить методы на fabric.Object, таким образом делая его доступным во всех "классах"-потомках. Например, если нужен метод getAngleInRadians на всех объектах, просто создаём его на fabric.Object.prototype:

fabric.Object.prototype.getAngleInRadians = function() {
  return this.get('angle') / 180 * Math.PI;
};

var rect = new fabric.Rect({ angle: 45 });
rect.getAngleInRadians(); // 0.785...

var circle = new fabric.Circle({ angle: 30, radius: 10 });
circle.getAngleInRadians(); // 0.523...

circle instanceof fabric.Circle; // true
circle instanceof fabric.Object; // true

Как видите, метод теперь доступен всем объектам.

Разумеется, классы потомки могут не только наследовать от fabric.Object, но и определять свои собственные методы, и параметры. Например, в fabric.Circle существует дополнительный атрибут “radius”. Или возьмём к примеру fabric.Image, с которым мы познакомимся подробнее чуть позже. В нём имеются методы getElement/setElement, предназначенные для чтения/записи HTML элемента <img>, на котором основан объект типа fabric.Image.

Canvas (холст)

Мы рассмотрели в подробности объекты; давайте опять вернёмся к canvas.

Как видно из примеров, первое - это создание самого "холста" для рисования — new fabric.Canvas('...'). fabric.Canvas - это, по сути, оболочка вокруг <canvas> елемента, ответственная за управление всеми содержащимися на нём объектами. Конструктор берёт id элемента, и возвращает объект типа fabric.Canvas.

Теперь в него можно добавлять объекты (add()), а также их читать (item(), getObjects()), или удалять (remove()):

var canvas = new fabric.Canvas('c');
var rect = new fabric.Rect();

canvas.add(rect); // добавляем

canvas.item(0); // получаем fabric.Rect, добавленный ранее (первый объект)
canvas.getObjects(); // поучаем все объекты (прямоугольник будет первым и единственным)

canvas.remove(rect); // удаляем прямоугольник

Как мы уже выяснили, главная задача fabric.Canvas - это управление объектами, которые на нём находятся. Также, его можно сконфигурировать через набор параметров. Такие настройки, как изменение фона холста, скрывание объектов по маске, изменение общей длины/ширины, включение/выключение интерактивности - эти, и другие опции можно выставить прямо на fabric.Canvas как во время создания, так и позже:

var canvas = new fabric.Canvas('c', {
  backgroundColor: 'rgb(100,100,200)',
  selectionColor: 'blue',
  selectionLineWidth: 2
  // ...
});

// или

var canvas = new fabric.Canvas('c');
canvas.setBackgroundImage(http://...');
canvas.onFpsUpdate = function(){ /* ... */ };
// ...

Интерактивность

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

Объектная модель существует для програмного доступа. А что нужно, чтобы управлять объектами мышкой (или тачпадом, на мобильных устройствах)? Для этого в Fabric заложен функционал пользовательского доступа. Как только мы создаём холст через new fabric.Canvas('...'), объекты, расположенные на нём, сразу же можно выделять, двигать, масштабировать, вращать и даже групировать вместе, управляя ими как одним целым!

Если мы хотим дать возможность пользователю управлять объектами на холсте - допустим, картинкой - нужно всего лишь создать холст, и добавить на него объект. Больше не нужно никаких дополнительных настроек.

Управлять этой интерактивностью легко. Для этого есть “selection” флаг на холсте, а также “selectable” флаг на индивидуальных объектах.

var canvas = new fabric.Canvas('c');
...
canvas.selection = false; // выключаем выделение
rect.set('selectable', false); // делаем объект невыделяемым

А что делать, если интерактивность вовсе не нужна? Тогда просто меняем fabric.Canvas на fabric.StaticCanvas. Синтакс (конфигурация, методы) абсолютно идентичный, просто используем слово StaticCanvas вместо Canvas.

var staticCanvas = new fabric.StaticCanvas('c');

staticCanvas.add(
  new fabric.Rect({
    width: 10, height: 20,
    left: 100, top: 100,
    fill: 'yellow',
    angle: 30
  }));

Это создаёт облегчённую версию холста, без лишней логики для интерактивности и управления event'ами. Всё остальное остаётся таким же. Мы получаем полную объектную модель, можем добавлять удалять и менять объекты, ну и конечно же менять опции самого холста. Исчезает только управление внешними event'ами.

В дальнейшем, когда мы ознакомимся с возможностью кастомной сборки Fabric (custom build), вы увидете, что можно создать более лёгкую версию библиотеки под ваши нужды. Это может быть полезно если, например, нужно просто отобразить статичный график, SVG фигуру, или картинки с фильтрами.

Картинки

Кстати, насчёт картинок…

Всё-таки работа с простыми фигурами не так интересна как с более графически-насыщенными картинками. Как вы наверное уже догадываетесь, в Fabric это очень просто. Создаём fabric.Image объект, добавляем его на холст:

(html)

<canvas id="c"></canvas>
<img src="my_image.png" id="my-img">

(js)

var canvas = new fabric.Canvas('c');
var imgElement = document.getElementById('my-img');
var imgInstance = new fabric.Image(imgElement, {
  left: 100,
  top: 100,
  angle: 30,
  opacity: 0.85
});
canvas.add(imgInstance);

Заметьте, как мы передаём <image> елемент в конструктор fabric.Image. Таким образом мы создаём объект типа fabric.Image, который представляет собой картинку из данного элемента. Мы также выставляем left/top значения на 100/100, угол на 30, и прозрачность на 0.85. После добавления на холст, картинка рендерится в позиции 100,100, повёрнутая на 30 градусов, и слегка прозрачная! Неплохо...

А что же делать, если элемента картинки в документе не существует, если есть только её адрес? Это не страшно. В таком случае можно использовать fabric.Image.fromURL:

fabric.Image.fromURL('my_image.png', function(oImg) {
  canvas.add(oImg);
});

Здесь никаких сюрпризов. Вызываем fabric.Image.fromURL передавая адрес картинки, а также функцию (callback), которую надо вызвать когда картинка загрузится. Callback получает первым аргументом объект fabric.Image. В момент вызова, с ней можно делать что угодно - изменить, или сразу добавить на холст для показа.

fabric.Image.fromURL('my_image.png', function(oImg) {
  // уменьшаем картинку и переварачиваем перед добавлением
  oImg.scale(0.5).set('flipX', true);
  canvas.add(oImg);
});

Path и PathGroup

Мы ознакомились с простыми фигурами и картинками. Теперь перейдём к более сложному контенту.

Встречайте мощную и незаменимую пару: Path и PathGroup.

Path (дословно переводится "путь") в Fabric представляет из себя кривую фигуру, которая может быть залита цветом, иметь контур, быть изменённой любым способом. Она изображается набором команд, которые можно сравнить с рисованием ручкой от одной точки до другой. При помощи таких команд как “move” (двинуть), “line” (линия), “curve” (кривая), или “arc” (арка), Path могут воспроизводить удивительно сложные фигуры. А с помощью Path групп (PathGroup), всё становится возможным.

Paths в Fabric имеют сходство с SVG <path> элементами. Они используют одинаковый набор комманд, могут быть созданы из <path> элементов и сериализованы в них. О сериализации и SVG парсинге мы поговорим позже. Сейчас стоит сказать, что работать с Path объектами вы врядли будете вручную. Вместо этого имеет смысл использовать SVG парсер, встроенный в Fabric. Чтобы понять, что же из себя представляют эти Path объекты, давайте создадим один из них.

var canvas = new fabric.Canvas('c');
var path = new fabric.Path('M 0 0 L 200 100 L 170 200 z');
path.set({ left: 120, top: 120 });
canvas.add(path);

При создании объекта fabric.Path, мы передаём строку с инструкциями "черчения" кривой. Выглядит эта инструкция, конечно, очень загадочно, но понять её на самом деле довольно легко. “M” означает “move” (двинуть), и говорит невидимой ручке подвинуться в точку 0, 0. “L” означает “line” (линия) и рисует линию до точки 200, 100. Затем команда “L” рисует линию до 170, 200. И наконец, “z” заставляет невидимую ручку замкнуть текущий контур и завершить фигуру. Как результат, получается вот такая треугольная форма.

Объект fabric.Path такой же, как и остальные объекты в Fabric, поэтому мы легко изменили его параметры (left, top). Но можно изменить и большее:

...
var path = new fabric.Path('M 0 0 L 300 100 L 200 300 z');
...
path.set({ fill: 'red', stroke: 'green', opacity: 0.5 });
canvas.add(path);

Ради интереса, давайте посмотрим на ещё один контур, на этот раз более сложный. Вы поймёте почему создание контуров вручную — не самое весёлое занятие.

...
var path = new fabric.Path('M121.32,0L44.58,0C36.67,0,29.5,3.22,24.31,8.41\
c-5.19,5.19-8.41,12.37-8.41,20.28c0,15.82,12.87,28.69,28.69,28.69c0,0,4.4,\
0,7.48,0C36.66,72.78,8.4,101.04,8.4,101.04C2.98,106.45,0,113.66,0,121.32\
c0,7.66,2.98,14.87,8.4,20.29l0,0c5.42,5.42,12.62,8.4,20.28,8.4c7.66,0,14.87\
-2.98,20.29-8.4c0,0,28.26-28.25,43.66-43.66c0,3.08,0,7.48,0,7.48c0,15.82,\
12.87,28.69,28.69,28.69c7.66,0,14.87-2.99,20.29-8.4c5.42-5.42,8.4-12.62,8.4\
-20.28l0-76.74c0-7.66-2.98-14.87-8.4-20.29C136.19,2.98,128.98,0,121.32,0z');

canvas.add(path.set({ left: 100, top: 200 }));

Огого, что же здесь происходит?! Давайте разбираться.

“M” всё ещё означает “move” (двинуть) команду, и вот невидимая ручка начинает своё путешествие от точки “121.32, 0”. Затем идёт команда “L”, которая приводит её к точке “44.58, 0”. Пока всё просто. А что следующее? Команда “C” означает “cubic bezier” (кривая безье). Она принуждает ручку рисовать кривую в точку “36.67, 0”. Кривая использует “29.5, 3.22” как точку контроля в начале линии и “24.31, 8.41” как точку контроля в конце линии. Далее следует целая мириада остальных кривых безье, что в итоге и создаёт финальную фигуру.

С такими "монстрами" вручную работать вы наверняка не будете. Вместо этого, можно использовать очень удобный метод fabric.loadSVGFromString или fabric.loadSVGFromURL, загружающий целый SVG файл. Всё остальное сделает парсер Fabric, пройдя по всем SVG элементам и создавая соответствующие Path объекты.

Кстати что касается SVG документов, Path в Fabric обычно представляет SVG <path> элемент, а вот наборы таких элементов, которые очень часто можно найти в SVG документах, обычно представлены через PathGroup (fabric.PathGroup объекты). PathGroup — это всего лишь група Path объектов. Так как fabric.PathGroup наследует от fabric.Object, такие объекты могут быть добавлены на холст как и любые другие объекты Fabric. Конечно же ими можно управлять, как и всем остальным.

Напрямую с ними работать скорее всего не придётся. Если они вам попадутся во время работы с Fabric, просто имейте ввиду с чем имеете дело, и зачем они вообще нужны.

Послесловие

Мы затронули только самые базовые аспекты Fabric. Разбравшись с ними, вы с лёгкостью сможете создать как простые, так и сложные фигуры, или картинки. Их вы сможете показать на холсте, поменять (через атрибуты позиции, масштаба, угла, цвета, контура, прозрачности), и сделать с ними всё, что душа пожелает.

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

А пока взгляните на демки с объяснительным кодом или бенчмарки, присоединяйтесь к дискусии в google group или Stackoverflow, ознакомтесь с документацией, wiki, и кодом.

Я надеюсь вам понравится экспериментировать с Fabric!

Читайте Часть 2.