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

Reading time ~21 minutes

Вот набрел на интересную библиотеку для удобной работы с Canvas - Fabric.js. Не мог не пройти мимо нее, так как люблю Canvas и стараюсь узнать все о нем, по мере сил.

Ниже приведена полная версия первой части официальной документации по Fabric.js - Знакомимся с Fabric.js. Часть 1-я. Здесь я сделал ее вычитку и исправил кое-какие орфографические ошибки.

Статья понравилась еще тем, как автор объяснил чтение SVG-команд в файле.

Введение

Сегодня я хочу познакомить вас с Fabric.js — мощной Javascript-библиотекой для работы с HTML5 canvas.

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

Это полностью открытая библиотека с MIT-лицензией и многими взносами разработчиков за последние несколько лет.

Работу над Fabric я начал 3 года назад, когда понял, насколько тяжело работать с обычным API Canvas.

В тот момент я создавал интерактивный редактор на printio.ru — мой стартап, где мы даем возможность создать дизайн и напечатать его на одежде или других товарах. Редактор хотелось сделать удобным и суперинтерактивным.

В то время такой функционал можно было создать только во Flash. Но Flash использовать я не хотел. Я предпочитаю JavaScript и был уверен, что с ним можно добиться многого. Получилось довольно неплохо. :)

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

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

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

Одно дело, если нужно нарисовать несколько простых фигур или графиков и забыть о них. Другое — интерактивность, изменение картинки в какой-то момент или рисование более сложных фигур.

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

Дело в том, что обычные canvas-методы позволяют нам вызывать только очень простые графические команды, вслепую меняя целый bitmap холста (canvas).

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

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

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

// берем 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);

Fabric Image

Разницы в размере кода пока не видно. Однако, видно что, способ работы с canvas кардинально отличается.

В обычном API Canvas мы работаем с контекстом. Контекст — это объект, который по сути представляет из себя bitmap холста.

С 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 Image

Что здесь происходит? Используя Fabric, все что надо было сделать, это добавить значение угла с 45 градусами. А вот с обычными методами все не так-то просто.

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

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

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

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

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

Чтобы подвинуть фигуру, нам нужно сначала стереть предыдущий результат, а потом уже рисовать на новом месте:

// создается прямоугольник
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 и задаем ему новые значения свойств - смешаем на новое положение x/y = 20/50
rect.set({ left: 20, top: 50 });

// выполняем рендеринг снова - перерисовываем canvas
canvas.renderAll();

Fabric Image

Заметьте очень важную разницу. Нам не пришлось абсолютно ничего стирать перед рисованием.

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

Таким образом можно изменить десятки объектов и в конце одной командой обновить экран.

Объекты

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

Все они доступны из fabric объектов, соответственно: fabric.Circle, fabric.Triangle, fabric.Ellipse и т. д.

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

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

// создаем круг
var circle = new fabric.Circle({
  radius: 20, fill: 'green', left: 100, top: 100
});

// создаем треугольник
// попробуйте создать треугольник на API Canvas одной строкой
var triangle = new fabric.Triangle({
  width: 20, height: 30, fill: 'blue', left: 50, top: 50
});

// добавляем два объекта одной командой
canvas.add(circle, triangle);

Fabric Image

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

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

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

Возможно, какие-то действия пользователя должны сказываться на состоянии картинки (холста) или должна быть запущена анимация.

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

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

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

Во-первых, есть атрибуты, меняющие:

  • позицию — left, top
  • размер — width, height
  • рендеринг (отображение объекта) — fill, opacity, stroke, strokeWidth
  • масштаб и поворот — scaleX, scaleY, angle
  • переворот (flip - 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 );

Fabric Image

Мы выставили ‘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.getWidth(); // => 0
rect.getHeight(); // => 0

rect.getLeft(); // => 0
rect.getTop(); // => 0

rect.getFill(); // => rgb(0,0,0)
rect.getStroke(); // => null

rect.getOpacity(); // => 1

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

Из-за этого мы его и не видим. Как только устанавливаем позитивные width/height, черный прямоугольник появляется в левом верхнем углу.

Fabric Image

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

Объекты 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.getAngle() / 180 * Math.PI; // getAngle() - стандартный метод библиотеки Fabric
};

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 элемента , на котором основан объект типа 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 - это управление объектами, которые находятся на 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, встроенная прямо в ядро - это слой интерактивности. Он позволяет пользователю манипулировать объектной моделью, с которой мы только что ознакомились.

Объектная модель существует для програмного доступа. А что нужно, чтобы управлять объектами мышкой (или touchpad на мобильных устройствах)?

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

Fabric Image Fabric Image

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

Управлять этой интерактивностью легко. Для этого есть ‘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);

Заметьте, как мы передаем **-элемент в конструктор *fabric.Image*. Таким образом мы создаем объект типа *fabric.Image*, который представляет собой картинку из данного элемента.

Мы также выставляем left/top значения на 100/100, угол на 30 и прозрачность на 0.85. После добавления на холст картинка рендерится в позиции 100/100, повернутая на 30 градусов и слегка прозрачная! Неплохо …

Fabric Image

А что же делать, если элемента картинки в документе не существует, если есть только ее адрес? Это не страшно. В таком случае можно использовать 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).setFlipX(true);
  canvas.add(oImg);
});

Path и PathGroup

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

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

Path (дословно переводится “путь”) в Fabric представляет из себя кривую фигуру, которая может быть залита цветом, иметь контур, быть измененной любым способом.

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

Paths в Fabric имеют сходство с SVG **-элементами. Они используют одинаковый набор команд, могут быть созданы из **-элементов и сериализованы в них.

О сериализации и SVG-парсинге мы поговорим позже. Сейчас стоит сказать, что работать с Path-объектами вы вряд-ли будете вручную. Вместо этого имеет смысл использовать SVG-parser, встроенный в 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 Image

При создании объекта 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);

Fabric Image

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

...
var path = new fabric.Path(' M 121.32, 0 L 44.58,0 C36.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 Image

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

Кстати, что касается SVG-документов, Path в Fabric обычно представляет SVG **-элемент, а вот наборы таких элементов, которые очень часто можно найти в SVG документах, обычно представлены через PathGroup (*fabric.PathGroup*-объекты).

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

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

Послесловие

Мы затронули только самые базовые аспекты Fabric. Разобравшись с ними, вы с легкостью сможете создать как простые, так и сложные фигуры или картинки.

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

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

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

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


продолжаю делать flood по javascript …


Mangling Angular

Angular Builder поддерживает параметры среды:- NG_BUILD_MANGLE- NG_BUILD_MINIFY- NG_BUILD_BEAUTIFYМожно установить их при запуске скрипта...… Continue reading

Constructor parameter without access modifier

Published on February 04, 2024

RxJs and DestroyRef Provider

Published on January 24, 2024