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



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

    Работу над Fabric я начал 3 года назад, когда понял насколько тяжело работать с обычным canvas API. В тот момент я создавал интерактивный редактор — мой стартап, где мы даём возможность создать дизайн и напечатать его на одежде, или других товарах. Редактор хотелось сделать удобным и супер интерактивным. В то время, такой функционал можно было создать только во 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';
    
    <b>ctx.translate(100, 100);
    ctx.rotate(Math.PI / 180 * 45);
    ctx.fillRect(-10, -10, 20, 20);</b>
    

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

    var canvas = new fabric.Canvas('c');
    
    // создаём прямоугольник с углом в 45 градусов
    var rect = new fabric.Rect({
      left: 100,
      top: 100,
      fill: 'red',
      width: 20,
      height: 20,
      <b>angle: 45</b>
    });
    
    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
    <b>ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
    ctx.fillRect(20, 50, 20, 20);</b>
    

    А теперь с Fabric

    var canvas = new fabric.Canvas('c');
    ...
    canvas.add(rect);
    ...
    
    <b>rect.set({ left: 20, top: 50 });
    canvas.renderAll();</b>
    



    Заметьте очень важную разницу. Нам не пришлось абсолютно ничего стирать перед рисованием. Просто продолжаем работать с объектами, меняя их атрибуты, а потом перерисовываем 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.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;
    };
    
    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.backgroundImage = '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-image">
    

    (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).setFlipX(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!

    Огромное спасибо Максиму Черняку за помощь с написанием и переводом статьи.
    Поделиться публикацией

    Похожие публикации

    Комментарии 53

      0
      Спасибо за замечательную либу. Попробую как будет возможность
        0
        Чем она лучше/хуже существующих библиотек? Было бы очень интересно увидеть сравнение производительности.

        Не заметил что это перевод, извиняюсь — пошел смотреть на офф.сайт.
          0
          Хороший вопрос. Я как раз для этого сделал вот такую табличку сравнения популярных canvas библиотек — docs.google.com/spreadsheet/ccc?key=0Aqj_mVmuz3Y8dHNhUVFDYlRaaXlyX0xYSTVnalV5ZlE#gid=0

          Мы немного уступаем по документации и демкам Kinetic, Paper, Easel. Но зато строго следим за качеством кода, модулярностью, простотой, и конечно же тестами. А ещё широкая поддержка браузеров из-за того, что используем базовые фичи и избегаем вынюхивания браузеров (browser sniffing).
            0
            Есть ли что-то подоное paperjs.org/examples/path-simplification/? И есть ли возможность у любого объекта получить данные как-он нарисован, что б сохранить/загрузить его?
              0
              Да, есть поддержка свободного рисования. Если откроете fabricjs.com/kitchensink/ и нажмёте «Enter drawing mode» внизу справа, можно начинать рисовать — цвет и диаметр кисти меняется. После завершения, кривые автоматически конвертируются в Path объекты, с которыми можно делать всё тоже что и с другими объектами. В будущем планирую добавить дополнительные кисти, и наверное так чтобы легко можно было свои добавлять.

              Сохранение/загрузка — это вообще отдельная тема :) У нас довольно серьёзный фокус именно на это; можно сохранять в разные форматы — SVG, object, JSON, data URL. И соответственно подгружать обратно. Об этом — в следующих частях.

              Все объекты имеют «type» аттрибут: «rect», «circle», «path», «path-group», и т.д.

              А если нужна именно информация о кривых, то конечно её легко можно вытащить из одного из аттрибутов объекта.
                0
                path-simplification — это не просто свободное рисование, это алгоритм упрощения пути (удаления лишних точек). В paperjs он в среднем удаляет 80% точек с пути, что сильно снижает объём хранимых данных. В fabric я этого алгоритма не нашёл так что видимо его нужно реализовывать самостоятельно.
                  0
                  Да, в Fabric такого алгоритма нет. Хотя интересная оптимизация; возможно добавим в будущем.
            +1
            kangax перевел свою же статью :)
              0
              на родной язык грех не перевести :)
            0
            Сейчас выбираем между Kinetic JS и вашей библиотекой. Fabric явно пошустрее будет, на мой взгляд.
            Только несколько смущает позиционирование объектов: вот пример
            Я так понимаю, отсчет координат идет о центра объекта? Если да, то с какой целью так сделано и можно ли это поведение изменить?
              +1
              Как раз только что закончил работу над тем чтобы можно было указать точку трансформации. До этого она всегда была в центре объекта. Вот подключил branch с гитхаба, посмотрите как работает — jsfiddle.net/PNb8Y/27/
                0
                То, что надо! Спасибо
              +1
              А раз нужна объектная модель и редактирование, то чем плох собственно svg?
                +1
                Если SVG элементов много они сильно тормозят. SVG-нода почти DOM-нода.
                  0
                  «Ручная» перерисовка большого количества объектов на канвасе тормозить не будет?
                  Есть где-нибудь сравнительные тесты?
                    0
                    Есть секция бенчмарков — fabricjs.com/benchmarks/

                    Вот посмотрите как мы рендерим знаменитую фигуру тигра состоящую из ~2500 кривых — fabricjs.com/raphael-vs-fabric-complex/

                    Когда таскаешь по холсту, всё это дело перерисовывается на каждое движение мыши. На десктопных современных браузерах даже такая сложная фигура «работает» довольно быстро. Сам движок я стараюсь писать как можно эффективней; производительность штука важная :)

                    Есть ещё идеи по некоторым оптимизациям на будущее.
                      0
                      fabricjs.com/raphael-vs-fabric-simple/?100
                      Здесь канвас заметно подтормаживает, кружочек за мышкой не успевает. SVG же не тормозит вовсе.

                      fabricjs.com/raphael-vs-fabric-simple/?5000
                      Здесь SVG начинает подтормаживать, но не так сильно, как канвас в предыдущем примере. Но на канвасе кружочки здесь не двигаются вовсе. Точнее через пару секунд передвигаются.

                      Браузер: Firefox 17.0.1
                        0
                        Да, зависит от браузера. SVG в последнее время становится всё быстрее и быстрее. Но у меня в Chrome 25, например, 1000 кружков работают с одинаковой скоростью как на канвасе так и в SVG. Только с 5000 канвас начинает подтармаживать.

                        Заметьте что в изначальной (статичной) отрисовке канвас обычно всегда выигрывает у SVG.
                          0
                          Заметьте что в изначальной (статичной) отрисовке канвас обычно всегда выигрывает у SVG.

                          Видимо, тоже от браузера зависит. У меня с 5000 объектов
                          Raphael: 897 ms
                          fabric: 1373 ms
                  +1
                  Когда начинал работать над Fabric 3 года назад, производительность SVG оставляла желать лучшего. Особенно когда имеешь дело со сложными фигурами, их анимацией, и т.д. В SVG всё-таки каждый элемент — это node в документе. DOM всегда был медленным. Сейчас ситуация получше но не намного.

                  А в Fabric фигуры — это просто Javascript объекты в памяти. Очень лёгкие, не сравнить с элементами. К тому же объекты используют prototypal inheritance; большинство методов вообще только на классах-потомках. Это делает их супер лёгкими — только, по сути дела, набор простых параметров как цвет, ширина, высота, координаты, и т.д.
                    0
                    Android 2
                    0
                    Как-то давно пользовался jcscript.com/. К сожалению не в курсе как развивался проект с тех пор.
                    +1
                    Если кого интересуют все доступные альтернативы, то они есть:
                    jster.net/category/canvas-wrappers

                    А Fabric.Js одна из самых популярных либ для канваса.
                      0
                      Люди сведущие, скажите: отлавливаются ли события мыши, когда нужный объект перекрывается другим с областью прозрачности?
                      Посмотрел демки Fabric, там достаточно ткунуть в прозрачный ограничивающий прямоугольник верхнего объекта, чтобы она захватил на себя событие мыши. Хотя визуально событие возникло над вторым объектом. Что делать?
                      +1
                      спасибо за работу, очень круто и интуитивно!
                      А есть ли возможность смены z-index, т.е. перемещать объекты выше/ниже других объектов?
                        0
                        Спасибо!

                        Да, конечно. Есть 4 метода для «перемещения» объектов по z-index — bringForward(), sendBackwards(), bringToFront(), sendToBack().

                        А вообще canvas.getObjects() массив собственно и является стэком видимости объектов. То есть первый добавленный объект будет перекрываться вторым, второй — третьим, и т.д. Так что можно даже вручную поменять порядок как надо.
                          0
                          отличненько, я просто здесь fabricjs.com/kitchensink поигрался с объектами и искал возможность переместить объект наверх, но не нашел
                        +1
                        Ну во-первых, внутри тега source другие теги не работают, поправьте пожалуйста.

                        Во-вторых, вы так говорите, как будто Fabric.js очень уникален. На самом деле canvas-библиотек с объектной моделью куча. Да хотя бы LibCanvas, JCScript, PaperJS. Первые две кстати от хабраюзеров TheShock и asavin. Что касается уникальности — уникален Fabric своей интеграцией с SVG и интерактивом, хотя конечно насчёт последнего можно поспорить. У PaperJS интерактив вполне можно реализовать: да хотя бы paperjs.org/examples/path-simplification/. LibCanvas тоже: libcanvas.github.com/ui/projective-image.html.

                          +1
                          Ну во-первых, внутри тега source другие теги не работают, поправьте пожалуйста.


                          Ага, не заметил. Исправлю.

                          Насчёт уникальности — я в принципе на неё не претендую :) Хотя не забывайте что начинал я это всё почти 4 года назад, когда ни Easel.js, ни Kinetic.js, и тем более ни Paper.js ещё не было даже в задумке.

                          Помню в то время был Cake.js (https://code.google.com/p/cakejs/source/detail?r=1), который сейчас уже умер.

                          Но это не важно. Важно качество. Именно на этом у нас фокус. А также, такие вещи как поддержка большого количества сред (cross-browser), тесты, высокая производительность, чистый код, и следование стандартам (как разработчик Prototype.js я с этим хорошо знаком). К сожелению всё ещё до сих пор сложно найти библиотеку сочетающие в себе _все_ эти качества. То одно, то другое, да и пропущено.

                          Даже взять, к примеру, упомянытые вами LibCanvas и JCScript (о которых я до этого не слышал). Посмотрел, ради интереса. Добавил в таблицу сравнения — docs.google.com/spreadsheet/ccc?key=0Aqj_mVmuz3Y8dHNhUVFDYlRaaXlyX0xYSTVnalV5ZlE#gid=0

                          JCScript выглядит интересно. Видно влияние jQuery. Документация хорошая, но тесты полностью отсутствуют. Не модулярный, хотя с 58КБ (minified) это не страшно. SVG парсера нет, и интерактивный слой самый базовый (таскать можно а вращать/масштабировать нет). Перетаскивание у меня в Chrome кстати как-то медленно. В коде вижу что методы присваеваются напрямую объектам во время создания (Зачем? Это же абсолютно не эффективно. Возможно из за этого?).

                          LibCanvas выглядит действительно впечатляюще. Ну и размер впечатляющий (106KB плюс AtomJS как dependency). Много функционала, похоже что в основном ориентировано на игры. Опять же, тестов нет. Документация минимальная. Модулярность вроде как отсутствует. И к тому же ещё использует ES5 аксессоры, что полностью исключает IE<9 и другие пожилые браузеры. Смотря на код, вижу ещё и навешивание методов на хост-объекты. Проблемы с этим мы уже проходили в Prototype.js — я даже пост на эту тему когда-то писал :) perfectionkills.com/whats-wrong-with-extending-the-dom/ Зато в LibCanvas интерактив хороший; действительно почти как в Fabric.
                            0
                            > Хотя не забывайте что начинал я это всё почти 4 года назад, когда ни Easel.js, ни Kinetic.js, и тем более ни Paper.js ещё не было даже в задумке.
                            Ну тогда да, вы имеете право на неё претендовать.

                            > Важно качество
                            Согласен полностью.

                            Что касается JCScript, он выглядит интересно, но довольно-таки сыроват… и не очень, в общем. Что касается SVG-парсера — это хорошо, но всё-таки зачем он так нужен?..

                            LibCanvas — да, кстати сам TheShock, если не ошибаюсь, работает в области игр. ES5-аксессоры — это не баг, а фича :). Ведь в IE<9 canvas-а и так нету, а юзать разные excanvas — те ещё костыли. Если уж так, можно взять dojox.gfx, который для рисования юзает кроме canvas ещё и vml, svg и silverlight :).

                            Ну в общем да, Fabric тоже выглядит здорово.

                            P.S. Кстати я тоже занимаюсь своим фреймворком для canvas :).
                              +1
                              LibCanvas — да, кстати сам TheShock, если не ошибаюсь, работает в области игр.

                              Не ошибаетесь) Работаю в Wargaming.net, а если быть точным, то в киевском подразделении — Persha студия

                              P.S. Кстати я тоже занимаюсь своим фреймворком для canvas :).

                              как прогресс, кстати?)
                                0
                                Делаю :). Часто пишу г-код и, не зная как с ним справиться, выкидываю нафиг и переписываю. Из-за этого часто задерживаюсь(.
                                  0
                                  А где посмотреть?)
                                    0
                                    Сейчас текст малость доделаю, скину)
                              +2
                              Сначала обращу внимание на ошибки.

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

                              Так что дату «Last updated» можно смело ставить 21.12.2012 ;)

                              Docs в LibCanvas таки присутствуют, хоть и только на русском. В репозитории папка Docs. Из недостатков — не полная, да, но я в работе) А вот AtomJS полностью покрыт английской документацией и тестами.

                              Модулярность вроде как отсутствует

                              Если под модулярностью имеете ввиду подключить только необходимые модули, то она вполне есть в билдере:
                              shock@shock-notebook:~/dev/libcanvas/Builder$ ./build Point Point3D > result.js
                              /**
                               * LibCanvas
                               * 
                               * Included Components:
                               *   LibCanvas/LibCanvas
                               *   LibCanvas/Geometry
                               *   LibCanvas/Point
                               *   LibCanvas/Point3D
                               */
                              


                              Опять же, тестов нет

                              Все вещи, мало связанные с отрисовкой я стараюсь выносить в AtomJS. Например, atom.Color или atom.Events, где они уже покрываются тестами. Я к сожалению не могу открыть ваши тесты, где-то в районе 260-ого зависает, но у вас в основном тестируется именно такое — fabric.Color, fabric.Observable, установка свойств и тому подобное, но не отрисовка. Не отрицая полезности unit тестов для LibCanvas я использую подход функционального тестирования. AtomJS, как базовый фреймворк, обязательно покрывается Unit тестами)

                              Ну и размер впечатляющий (106KB плюс AtomJS как dependency).

                              Последняя версия 164Kb + 108Kb AtomJS)

                              И к тому же ещё использует ES5 аксессоры, что полностью исключает IE<9 и другие пожилые браузеры.

                              Это фича. Я специально отказался от IE<9, т.к. там нереально реализовать всё необходимое для обратной совместимости с Canvas. Например, отрисовку одного Canvas в другой)

                              Смотря на код, вижу ещё и навешивание методов на хост-объекты.

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

                              На счёт nodeJS скажу, что лично я не юзал, но ребята на сервере его юзают)

                              this.callSuper('initialize', options);

                              Кстати, я нашёл отличную альтернативу этому)

                              Зато в LibCanvas интерактив хороший; действительно почти как в Fabric.

                              Нескромно замечу, что интерактив в LibCanvas намного лучше, чем в Fabric. Особенно в плане оптимизации и разнообразии возможностей) Не зря у LibCanvas в качестве примеров игры, а у "обычных" фреймворков — всякая банальщина вроде изменения векторных фигур.

                              Ну вот к примеру Various: Static Canvas. Что я вижу? 3 статичных объекта и 1 движущийся. Двигается один, а перерисовывается все 4 из-за того, что вызывается canvas.renderAll(). В LibCanvas мы при изменении дёргаем element.redraw(), а уже фреймворк считает, что надо перерисовать, смотрит, не затрём ли мы что-то другое и так далее. В итоге, на поле может быть сотни объектов, а проц чувствует себя спокойно.

                              Хитрая работа со слоями и подобная перерисовка вообще позволяют держать десятки тысяч объектов на карте и проц будет чувствовать себя прохладно)

                              Ну и мне всегда было непонятно, почему во всех либах круто раскрыта всякая банальщина вроде работы с вектором, но никаких интересностей типа графических редакторов и игр) Единственный интересный экземпляр — impactjs.com/, но там исходники закрыты и она платная. Посмотреть бы хоть на какую-то простотень а-ля Пятнашек, чтобы знать, как библиотека показывает себя в реальном проекте.
                                +1
                                На самом деле стоило смотреть на новую версию, которую я разрабатываю и как раз перелил в основной бранч. Там и коммиты регулярные и вес побольше.


                                Отлично, подправил.

                                Насчёт тестов — да, в unit tests проверяю только логику. Функциональные есть, но не настолько хорошие и достаточные насколько хотелось бы. Вот например когда-то пытался использовать оффициальный SVG test suite — fabricjs.com/test/functional/ Также есть базовая проверка для Node и SVG экспорта. Будем всё это дополнять в будущем. Тестирование прорисовки через unit tests — это вообще отдельная (интересная, но не без проблем) тема.

                                Это фича. Я специально отказался от IE<9, т.к. там нереально реализовать всё необходимое для обратной совместимости с Canvas.


                                Это понятно. Но для моих целей не подходит. Даже просто показать картинку человеку и позволить её двигать и вращать по канвасу — уже хорошо. Не обязательно поддерживать полный функционал. Получается что-то вроде «progressive enhancement».

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


                                Это хорошо :)

                                На счёт nodeJS скажу, что лично я не юзал, но ребята на сервере его юзают)


                                Мы на printio.ru запускаем Fabric прямо на сервере, на Node. Всё просто летает (когда-то давно мучались с Jaxer'ом, если слышали о таком). Канвас сериализуется на клиенте, посылаем JSON на сервер, загружаем через Node и получаем картинку в любом разрешении (обычно здоровенном) через секунду.

                                Кстати, я нашёл отличную альтернативу этому)


                                Ну там в принципе и искать не надо :) Все альтернативы уже известны. Можно конечно сократить буквально до `$super()`. Но у всего как плюсы так и минусы.

                                Ну вот к примеру Various: Static Canvas. Что я вижу? 3 статичных объекта и 1 движущийся. Двигается один, а перерисовывается все 4 из-за того, что вызывается canvas.renderAll(). В LibCanvas мы при изменении дёргаем element.redraw(), а уже фреймворк считает, что надо перерисовать, смотрит, не затрём ли мы что-то другое и так далее. В итоге, на поле может быть сотни объектов, а проц чувствует себя спокойно.


                                Да, есть такое дело. Хочу заметить что в Fabric `renderAll` тоже проходит по всем объектам и говорит каждому отрисовать себя. Однако оптимизацию типа «dirty rectangles» пока не делали (вот открытый тикет кстати — github.com/kangax/fabric.js/issues/318).

                                Важно понимать что Fabric не позиционирует себя как framework для создания игр — у нас нет абстракций для работы со спрайтами, звуком, collision detection, и т.д. Поэтому, например, не было особого приоритета оптимизировать тысячи объектов на канвасе. Однако для удобной работы с коллажами Fabric по-моему идеален. Именно для этого я и создал его; и если посмотрите на наш редактор дизайнов — printio.ru/tees/new — я например редко вижу что-то более удобное чем у нас (даже у гигантских западных аналогов — www.zazzle.com/cr/design/pt-zazzle_shirt?style=value_tshirt)

                                Хитрая работа со слоями и подобная перерисовка вообще позволяют держать десятки тысяч объектов на карте и проц будет чувствовать себя прохладно)


                                Да конечно. Kinetic.js этим постоянно хвастается :) Возможно добавим поддержку слоёв в будущем. Пока только два статичныx — для всех объектов и для прямоугольника выбора объектов. Добавить поддержку не сложно, но надо продумать как иметь дело с групами.

                                Ну и мне всегда было непонятно, почему во всех либах круто раскрыта всякая банальщина вроде работы с вектором, но никаких интересностей типа графических редакторов и игр)


                                Опять же, зависит от назначения. Для нас SVG парсер просто необходим. Для игр он скорей всего нафиг не нужен (хотя… кто его знает) :)

                                Вот кстати есть такой документик о том когда стоит использовать Fabric, а когда нет — github.com/kangax/fabric.js/wiki/When-to-use-Fabric
                                  0
                                  object.left = 100; // new value
                                  canvas.renderAll(); // need to mark object dirty before rendering
                                  although this could be "fixed" via set:
                                  
                                  object.setLeft(100); // object is marked as dirty
                                  canvas.renderAll(); // object is rendered at a new location
                                  


                                  Почему бы не так?
                                  object.left = 100; // new value
                                  object.top  = 100; // new value
                                  object.redraw();
                                  


                                  Ну там в принципе и искать не надо :) Все альтернативы уже известны. Можно конечно сократить буквально до `$super()`. Но у всего как плюсы так и минусы.

                                  Ни в коем случае. У всех этих подходов есть два существенных недостатка:
                                  1. Они оборачивают все методы во враппер
                                  2. Они вызываются через другой метод

                                  В итоге ухудшается производительность и осложняется дебаг (тяжело лазить по стеку вызовов). Мой же подход слегка иной. Я думаю вы сразу поймёте, когда я покажу пример:

                                  atom.declare('MyClass', ParentClass, {
                                  
                                    initialize: function initialize () {
                                      initialize.previous.apply( this, arguments );
                                      // our code here
                                    }
                                    
                                  });
                                  


                                  Такого решения я нигде не видел.
                                  Не так изящно, как в мутулз, конечно, но зато без его недостатков:

                                  MyClass = new Class({
                                  
                                    Extends: ParentClass,
                                  
                                    initialize: function initialize (a, b, c) {
                                      this.parent( a, b, c );
                                      // our code here
                                    }
                                    
                                  });
                                  


                                  Важно понимать что Fabric не позиционирует себя как framework для создания игр — у нас нет абстракций для работы со спрайтами, звуком, collision detection, и т.д. Поэтому, например, не было особого приоритета оптимизировать тысячи объектов на канвасе. Однако для удобной работы с коллажами Fabric по-моему идеален

                                  Вообще согласен, всегда необходимо помнить цели и задачи. Но collision detection вам таки понадобится для учёта отрисовок)
                                    0
                                    initialize.previous.apply( this, arguments );


                                    Да, интерeсный подход. В принципе всегда можно проверить если вызов супер метода присутствует в теле функции. Если да — заворачиваем, нет — нет. Я в Fabric именно так и делаю.

                                    Для этого используем так называемое function decompilation (function(){...} + '') и просто смотрим если присутствует наш keyword ($super, callSuper, и т.д.).

                                    Это де факто стандарт, и присутстует практически во всех существующих движках. В идеале можно перестраховаться и добавить feature test проверяющий что действительно работает. Если нет, то заворачиваем все функции подряд (что на практике не должно произойти вообще).
                                      0
                                      Тем не менее стек вызовов всё-равно смотрится отвратительно)
                                +1
                                Хотя, скажу откровенно, подача фреймворка у вас отличная. Сайт, группа, хорошо оформленные доки. LibCanvas это откровенно не хватает.
                              +1
                              Проблема в том, что родной canvas API ужасно низко-уровневый

                              Вы так шутите что ли? ))
                              А вообще (как-то мысль нагрянула) что разных фреймворков для канваса уже больше чем игр. Кстати на вашей фабрике есть готовые игры на которые бы приятно было бы глянуть?
                                +1
                                Вы так шутите что ли? ))


                                Ну если сравнивать с попиксельным рисованием, то может быть и шучу :)

                                На самом деле, очень много повторяющихся конструкций и абстракция действительно помогает.

                                Из игр, я видел только тетрис — end3r.com/games/fabrictetris/
                                  –1
                                  Для меня ужасно низко-уровневый это 2д в опенГЛ. А канва это так… душой отдохнуть))
                                0
                                Спасибо, хорошая статья. Когда будет следующая? Очень хотелось бы услышать про события, т. к. намерен использовать эту штуку для создания редактора карт.
                                  0
                                  Постараюсь через недельку-две подготовить. Можете ознакомиться с английской версией — fabricjs.com/fabric-intro-part-2/
                                    0
                                    Спасибо, жду с нетерпением новых статей. А третья часть есть? Хотя бы на английском, а то на сайте не нашел.
                                      0
                                      Да, они все здесь — fabricjs.com/articles/ Скоро будет последняя, 4-я.
                                        0
                                        О, спасибо. Вопрос, на который не нашел ответа в доках — как скроллить канву? Нет, можно конечно вручную сдвигать координаты, но… Сами понимаете, хочется красоты и порядка:)
                                          0
                                          Встроенной поддержки для таких вещей как прокрутка всего канваса, увеличение или поворот — пока нет. Но в форуме можно найти несколько решений — groups.google.com/forum/#!forum/fabricjs
                                • НЛО прилетело и опубликовало эту надпись здесь
                                    0
                                    все трансформации проходят только относительно центра объекта?
                                    я бы хотел трансформации по выбранной точке делать — есть возможность?
                                    ну вот скажем я беру кубик с левой стороны и тяну вбок — чтобы правая сторона оставалась на месте, а левая двигалась только… вытягивать в нужную сторону как бы…


                                    Да, это был один из самых популярных запросов :) Начиная с 1.0 версии, теперь именно так всё и работает. Вот посмотрите fabricjs.com/kitchensink/

                                    еще интересно что манипулятор трансформации именно скейлит объект, в итоге обводка объекта тоже скейлится и вытягивая квадратик получаешь по бокам другую толщину контура.
                                    есть ли возможность при таскании за манипулятор менять не скейл объекта, а его размер? то есть width и height??


                                    Пока нет. Но изменение габаритов углов и краёв — это не хорошо, будем исправлять.

                                    и есть ли кривые безье? а то квадратики да кругляки как то уже не радуют, хочется гладких кривых управляемых…


                                    Конечно! Я же даже в конце статьи упомянул.

                                    и все сказанное можно еще к группам применительно спросить — выделяем 3 объекта и тягяем их сбоку — чтобы все не скейлились, а изменяли свои размерности и все трансформации шли по некому пайвоту. лучше всего чтобы именно как вытягивание работало все — то есть двигаем левую границу, все остальные на места остаются…


                                    Ну да. Посмотрите, именно так сейчас и работает.

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

                                  Самое читаемое