Вот набрел на интересную библиотеку для удобной работы с 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);
Разницы в размере кода пока не видно. Однако, видно что, способ работы с 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, все что надо было сделать, это добавить значение угла с 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();
Заметьте очень важную разницу. Нам не пришлось абсолютно ничего стирать перед рисованием.
Просто продолжаем работать с объектами, меняя их атрибуты, а потом перерисовываем 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);
… и вот уже на холсте красуется зеленый круг в точке 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 );
Мы выставили ‘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 не существуют сами по себе. Они формируют четкую иерархию. Большинство объектов наследуют от 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(‘…’), объекты, расположенные на нем, сразу же можно выделять, двигать, масштабировать, вращать и даже группировать вместе, управляя ими как одним целым!
Если мы хотим дать возможность пользователю управлять объектами на холсте - допустим, картинкой - нужно всего лишь создать холст и добавить на него объект. Больше не нужно никаких дополнительных настроек.
Управлять этой интерактивностью легко. Для этого есть ‘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);
Заметьте, как мы передаем *
Мы также выставляем 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).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.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(' 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.loadSVGFromString или fabric.loadSVGFromURL, загружающий целый SVG-файл. Все остальное сделает parser Fabric, пройдя по всем SVG-элементам и создавая соответствующие Path-объекты.
Кстати, что касается SVG-документов, Path в Fabric обычно представляет SVG *
PathGroup — это всего лишь группа Path-объектов. Так как fabric.PathGroup наследует от fabric.Object, такие объекты могут быть добавлены на холст как и любые другие объекты Fabric. Конечно же ими можно управлять, как и всем остальным.
Напрямую с ними работать скорее всего не придется. Если они вам попадутся во время работы с Fabric, просто имейте ввиду с чем имеете дело, и зачем они вообще нужны.
Послесловие
Мы затронули только самые базовые аспекты Fabric. Разобравшись с ними, вы с легкостью сможете создать как простые, так и сложные фигуры или картинки.
Их вы сможете показать на холсте, поменять (через атрибуты позиции, масштаба, угла, цвета, контура, прозрачности) и сделать с ними все, что душа пожелает.
В следующей части, мы поговорим о работе с группами, анимацией, текстом, SVG парсингом, рендерингом и сериализацией, управлением событиями, фильтрами картинок и остальными интересными вещами.
А пока взгляните на демки с объяснительным кодом или бенчмарки, присоединяйтесь к дискуссии в Google Group или Stackoverflow, ознакомьтесь с документацией, wiki и кодом.
Я надеюсь, вам понравится экспериментировать с Fabric!
продолжаю делать flood по javascript …