Что такое m4b

Формат m4b — не предусмотренное стандартом расширение для медиаконтейнера mp4. Это аудио-файл формата AAC ( Advanced Audio Coding ) поддерживающий закладки. В основном этот формат используется для аудиокниг и подкастов онлайн-магазинов компании Apple ( iTunes Store ).

Какие программы есть

Под macOS существует хорошее приложение для создания \ конвертирования книг в формате m4b. Это приложение называется Audiobook Builder.

Приложение отличное - простое, интуитивно понятное и имеет в себе все, что необходимо, ничего лишнего.

Но есть одна загвоздка - чтобы пользоваться данным приложением, нужно его купить ( во-первых ); а во-вторых - нужно приобрести технику Apple ( iMac, macBook ).

Для пользователей Linux вариантов в плане приложений для создания формата m4b не так уж много. Есть много гиковских - консоль, консольные утилиты. Можете сами погуглить - вариантов выпадет много, но все они на любителя, что называется.

Единственным ( на момент написания статьи ) “человеческим” вариантом является приложение под названием m4Baker.

m4Baker — небольшое Python / QT ( PyQT ) графическое приложение для создания iPod-совместимых аудиокниг формата m4b, с главами и обложками. Приложение бесплатное. Лицензия - GNU General Public License v3 ( GPLv3 ).

Установка m4Baker

Приложения m4Baker нет в официальном репозитории дистрибутива Linux Mint. Программу нужно устанавливать вручную, набором нескольких команд.

Итак, открываем терминал и последовательно запускаем на выполнение команды:

sudo apt-get install python-qt4
sudo apt-get install libcanberra-gtk-module
sudo apt-get install faac
sudo apt-get install libmp4v2-2
sudo apt-get install mp4v2-utils
sudo apt-get install sox
sudo apt-get install libsox-fmt-mp3

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

Установку пакетов я производил под Linux Mint 18 Cinnamon. Это аналог Ubuntu 16.04.1 LTS (Xenial Xerus).

Все пакеты нашлись в репозитории, кроме последнего - libsox-fmt-mp3. Его пришлось скачивать вручную по ссылке - Xenial/libsox-fmt-mp3. И устанавливать пакет libsox-fmt-mp3_14.4.1-5_amd64.deb также вручную, локально.

После того, как все вышеперечисленные пакеты успешно установлены, необходимо установить пакет программы m4Baker:

  • скачиваем дистрибутив программы m4Baker из GitHub-репозитория по ссылке - GitHub m4Baker
  • распаковываем полученный архив m4baker-master.zip, получаем разархивированную директорию m4baker-master
  • открываем директорию m4baker-master в терминале и запускаем команду установки программы m4Baker:
sudo python setup.py install --optimize=1

Эта команда установит программу m4Baker со всеми ее зависимостями.

Теперь можно запустить m4Baker любым удобным способом - через Start Menu, из командной строки консоли. У меня m4Baker успешно запустился через лаунчер Synapse.

Обзор m4Baker

m4Baker благодаря использованию SoX, faac и mp4v2 поддерживает большинство популярных аудио-форматов, есть возможность изменения и добавления метаданных, добавления обложки.

Первый запуск m4Baker выглядит таким образом:

m4Baker - First Launch

Справка, представленная в самой программе m4Baker, является самодостаточной - ее полностью хватает для пользования.

Для загрузки mp3-файлов нужно нажать кнопку “Add Audiobook” ( иконка книги ), откроется диалоговое окно выбора файлов.

После загрузки файлы добавятся в список обработки:

m4Baker - Add Files

Стоит обратить внимание, что выборка должна быть “чистой” - только mp3-файлы и никакого другого “мусора”, иначе программа m4Baker не запустится для обработки файлов.

В создаваемой m4b-аудиокниге возможно автоматическое разделение на части, возможна сортировка исходных файлов по имени или номеру трека (ID3-тегу), создание маркеров глав, перемещение глав между аудиокнигами:

m4Baker - Chapters

m4Baker - Sort Chapters

m4Baker - Split Book

Нажатие на кнопку “Procell all” ( иконка шестеренки ) запустит процесс создания файла формата m4b:

m4Baker - Process

Замечания

OS Linux

Я протестировал приложение m4Baker на рабочей машине Linux Mint 18 Cinnamon. Файлы mp3-формата аудиокниги были успешно сконвертированы в файл формата m4b. На Linux Desktop данный файл успешно прослушивался.

m4Baker - Play m4b File

OS Android

На Android ( v4.0.3 ) файл запускался с некоторой задержкой, но прослушивался ( Smart AudioBook Player ).

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

macOS

Самый главный минус и разочарование. Файл m4b, созданный в программе m4Baker, у меня не открылся вообще под macOS Sierra ( 10.12 ).

Плейер iTunes не открыл файл и скромно промолчал о данном факте. QuickTime Player также не смог открыть файл, но сообщил мне о данном факте.

OS Windows

О системе Windows ничего сказать не могу, так как не пользуюсь данной операционной системой.

Заключение

Приложение m4Baker можно использовать для создания аудиокниг формата m4b под операционной системой Linux. Приложение удобно и просто в использовании.

Однако, данное приложение несколько сложно в установке, имеет не полный функционал. Созданные в этой программе аудиокниги проиигрываются не под всеми операционными системами и плейерами.

В программе отсутсвуют возможности для добавления дополнительных метаданных, таких как “Narrator”, “Year”, “Description”, “Copyright”. К минусам можно отнести и отсутсвие программы такого класса в безграничных репозиториях Ubuntu.

Увы, как мне кажется, пока у программы Audiobook Builder нет достойной альтернативы.

В написании данного материала мне помог ресурс - Baking audiobooks with m4Baker.

На этом все. Всем удачного Нового Года!


Вторая часть официальной документации по Fabric. Речь идет об анимации объектов на Canvas, применении стандартных фильтров к изображениям или создании пользовательских фильтров.

Вычитал и поправил некоторые орфографические ошибки. Продолжает радовать факт, как автор детально и внятно описывает работу каждой строки кода в сниппетах. Эту статью можно продолжать читать просто как общий легкий курс по JavaScript. )


Это вторая часть серии статей об открытой Javascript Canvas библиотеке Fabric.js, которую мы используем на printio.ru для редактора дизайнов.

В первой части этой серии мы ознакомились с самыми базовыми аспектами canvas библиотеки Fabric.js. Мы узнали, чем может быть полезна Fabric, рассмотрели ее объектную модель и иерархию объектов; увидели, что существуют как простые фигуры (прямоугольник, треугольник, круг), так и сложные (SVG). Научились выполнять простые операции над этими объектами.

Ну вот, разобрались с азами, давайте приступать к более интересным вещам!

Анимация

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

Вы наверное помните, как менять атрибут у объекта. Просто вызываем метод

1
set
, передавая соответствующее значение:

rect.set('angle', 45);

Анимировать объект можно по такому же принципу и с такой же легкостью. Каждый объект в Fabric имеет метод

1
animate
(наследуя от
1
fabric.Object
) который … анимирует этот объект:

rect.animate( 'angle', 45, {
  onChange: canvas.renderAll.bind(canvas)
});

Первый аргумент - это атрибут, который хотим менять. Второй аргумент — финальное значение этого атрибута.

Например, если прямоугольник находится под углом

1
-15°
и мы указываем
1
45°
, то угол постепенно изменится с
1
-15°
до
1
45°
.

Ну, а последний аргумент — опциональный объект для более детальных настроек (длительность, вызовы, easing и т. д. )

1
animate
кстати имеет очень полезную функциональность — поддержку относительных значений. Например, если нужно подвинуть объект на
1
100px
вправо, то сделать это очень просто:

rect.animate('left', '+=100', { onChange: canvas.renderAll.bind(canvas) });

По такому же принципу, для поворота объекта на 5 градусов против часовой стрелки:

rect.animate('angle', '-=5', { onChange: canvas.renderAll.bind(canvas) });

Вы наверняка заметили, что мы постоянно указываем вызов

1
onChange
. Разве 3-й аргумент не опциональный? Да, именно так.

Дело в том, что как раз это вызывание

1
canvas.renderAll
на каждый кадр анимации позволяет видеть саму анимацию!

Mетод

1
animate
всего лишь изменяет значение атрибута в течении указанного времени и по определенному алгоритму (
1
easing
).

1
rect.animate('angle', 45)
изменяет значение угла, при этом не перерисовывая экран после каждого изменения. А перерисовка экрана нужна для того, чтобы увидеть анимацию.

Ну, а почему же

1
animate
не перерисовывает экран автоматически? Из-за производительности. Ведь на холсте могут находиться сотни или даже тысячи объектов.

Было бы довольно ужасно, если каждый из объектов перерисовывал экран при изменении. В таком случае лучше использовать, например,

1
requestAnimationFrame
для постоянной отрисовки холста, не вызывая
1
renderAll
для каждого объекта.

Однако, в большинстве случаев вы скорее всего будете использовать

1
canvas.renderAll
как
1
onChange
-вызов.

Возвращаясь к опциям для анимации - что же именно мы можем менять?

  • 1
    from:
    
    - позволяет менять начальное значение атрибута для анимации (если не хотим использовать текущее)
  • 1
    duration:
    
    - длительность анимации; по умолчанию - 500 ms
  • 1
    onComplete:
    
    - функция для вызова в конце анимации (callback)
  • 1
    easing:
    
    - функция easing (смягчение)

Все эти опции более менее очевидны, кроме наверное

1
easing
. Давайте посмотрим поближе.

По умолчанию,

1
animate
используют
1
easeInSine
-функцию для смягчения анимации. Если такой вариант не подходит, в Fabric имеется большой набор популярных easing-функций (доступных через объект
1
fabric.util.ease
).

Например, вот так можно подвинуть объект вправо, при этом “отпружинивая”” в конце:

rect.animate('left', 500, {
  onChange: canvas.renderAll.bind(canvas),
  duration: 1000,
  easing: fabric.util.ease.easeOutBounce
});

Заметьте, что мы используем

1
fabric.util.ease.easeOutBounce
как опцию смягчения. Есть и другие популярные функции —
1
easeInCubic
,
1
easeOutCubic
,
1
easeInElastic
,
1
easeOutElastic
,
1
easeInBounce
,
1
easeOutExpo
и т. д.

Вот в принципе и все, что нужно знать о анимации. Теперь можно с легкостью делать интересные вещи — менять угол объекта, чтобы сделать его вращающимся; анимировать

1
left/top
чтобы его двигать; анимировать
1
width/height
для увеличения/уменьшения; анимировать
1
opacity
для появления/исчезания и т. д.

Фильтры изображений

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

1
fabric.Image
-конструктор, передавая в него
1
<img>
-элемент.

Также есть метод

1
fabric.Image.fromURL
, с помощью которого можно создать объект прямо из строки URL. И конечно же эти
1
fabric.Image
-объекты можно кинуть на холст, где они отобразятся как и все остальное.

Работать с изображениями прикольно, а с фильтрами изображений — еще веселей! Fabric уже имеет несколько фильтров, а также позволяет легко определять свои.

Некоторые фильтры из Fabric вам наверное знакомы — удаление белого фона, перевод в черно-белый, негатив или яркость. А некоторые менее популярны — градиентная прозрачность, сепия, шум.

Так как же применить фильтр к изображению? Каждый

1
fabric.Image
-объект имеет
1
filters
-атрибут, который просто является массивом фильтров. Каждый элемент в этом массиве — или один из существующих в Fabric или собственный фильтр.

Ну вот, к примеру, сделаем картинку черно-белой:

fabric.Image.fromURL( 'pug.jpg', function ( img ) {
  
  // добавляем фильтр
  img.filters.push( new fabric.Image.filters.Grayscale() );
  
  // применяем фильтры и перерисовываем canvas после применения
  img.applyFilters( canvas.renderAll.bind( canvas ) );
  
  // добавляем изображения на холст
  canvas.add( img );

});

Fabric Image

А вот так можно сделать сепию:

fabric.Image.fromURL( 'pug.jpg', function ( img ) {
  
  img.filters.push( new fabric.Image.filters.Sepia() );
  
  img.applyFilters( canvas.renderAll.bind( canvas ) );
  
  canvas.add( img );

});

Fabric Image

С атрибутом

1
filters
можно делать все тоже что и с обычным массивом — удалить фильтр (с помощью
1
pop
,
1
splice
, или
1
shift
), добавить фильтр (с помощью
1
push
,
1
splice
,
1
unshift
) или даже соединить несколько фильтров.

Когда вызывается

1
applyFilters
, все фильтры в массиве применяются к картинке по очереди. Вот, например, давайте создадим картинку с увеличенной яркостью и с эффектом сепии:

fabric.Image.fromURL( 'pug.jpg', function ( img ) {

  img.filters.push(
    new fabric.Image.filters.Sepia(),
    new fabric.Image.filters.Brightness({ brightness: 100 }));

  img.applyFilters(canvas.renderAll.bind( canvas ));
  
  canvas.add( img );

});

Fabric Image

Заметьте, что мы передали

1
{ brightness: 100 }
-объект в Brightness-фильтр. Это потому, что некоторым фильтрам ничего дополнительного не нужно; а некоторым (например -
1
grayscale
,
1
invert
,
1
sepia
) надо указать определенные параметры.

Для фильтра яркости это собственно само значение яркости (

1
0-255
). У фильтра шума, это значение шума (
1
0-1000
). А у фильтра удаления белого фона (‘remove white’) есть порог (‘threshold’) и расстояние (‘distance’).

Ну вот разобрались с фильтрами; пора создать свой!

Образец для создания фильтров будет довольно прост. Нам нужно создать ‘класс’’ и написать метод

1
applyTo
. Опционально мы можем дать фильтру
1
toJSON
-метод (поддержка JSON-сериализации) и/или
1
initialize
(если фильтр имеет дополнительные параметры):

fabric.Image.filters.Redify = fabric.util.createClass({

  type: 'Redify',

  applyTo: function ( canvasEl ) {
    var context = canvasEl.getContext( '2d' ),
        imageData = context.getImageData( 0, 0, canvasEl.width, canvasEl.height ),
        data = imageData.data;

    for ( var i = 0, len = data.length; i < len; i += 4 ) {
      data[ i + 1 ] = 0;
      data[ i + 2 ] = 0;
    }

    context.putImageData( imageData, 0, 0 );
  }
});

fabric.Image.filters.Redify.fromObject = function ( object ) {
  return new fabric.Image.filters.Redify( object );
};

Fabric Image

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

1
data[i+1]
) и голубую (
1
data[i+2]
) компоненты каждого пикселя на
1
0
, по сути дела удаляя их.

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

Примечание чтеца: для желающих - более подробно аналог представленного выше кода описан в моей статье Canvas - Raw Pixel.

Как видите,

1
applyTo
-метод получает в себя canvas-элемент, который представляет собой изображение. Имея такой canvas, мы можем пройтись по всем пикселям изображения (
1
getImageData().data
) изменяя их как нам угодно.

Цвета

Независимо от того, с чем вам удобней работать — hex, RGB, или RGBA-форматами цвета — Fabric упрощает утомительные операции и переводы из одного формата в другой.

Давайте посмотрим на несколько способов определить цвет в Fabric:

new fabric.Color( '#f55' );
new fabric.Color( '#123123' );
new fabric.Color( '356735' );
new fabric.Color( 'rgb( 100, 0, 100 )' );
new fabric.Color( 'rgba( 10, 20, 30, 0.5 )' );

Перевод формата происходит очень просто. Метод

1
toHex()
переводит цвет в
1
hex
. Метод
1
toRgb()
— в
1
RGB
, а метод
1
toRgba()
— в
1
RGB
с альфа каналом (прозрачностью):

new fabric.Color( '#f55' ).toRgb(); // => "rgb( 255, 85, 85 )"
new fabric.Color( 'rgb( 100, 100, 100 )' ).toHex(); // => "646464"
new fabric.Color( 'fff' ).toHex(); // => "FFFFFF"

Кстати, можно делать не только перевод. Можно “накладывать” цвета один на другой или делать из них черно-белый вариант:

var redish = new fabric.Color('#f55');
var greenish = new fabric.Color('#5f5');

redish.overlayWith(greenish).toHex(); // => "AAAA55"
redish.toGrayscale().toHex(); // => "A1A1A1"

Градиенты

Еще более экспрессивный способ работы с цветами — используя градиенты. Градиенты позволяет плавно смешать один цвет с другим, открывая возможность довольно изумительным эффектам.

Fabric поддерживает градиенты с помощью метода setGradient (setGradientFill до 1.1.0 версии), который присутствует на всех объектах.

Вызывание

1
setGradient( 'fill', { ... } )
- это почти как выставление значения ‘fill’ у объекта, только вместо цвета используется градиент:

var circle = new fabric.Circle({
  left: 100,
  top: 100,
  radius: 50
});

circle.setGradient( 'fill', {
  x1: 0,
  y1: -circle.height / 2,
  x2: 0,
  y2: circle.height / 2,
  colorStops: {
    0: '#000',
    1: '#fff'
  }
});

Fabric Image

В этом примере мы создаем круг в точке

1
100/100
с радиусом в
1
50px
. Потом выставляем ему градиент, идущий по всей высоте объекта, от черного к белому.

Как видите, метод получает в себя конфигурационный объект, в котором могут присутствовать 2 пары координат (

1
x1/y1
и
1
x2/y2
) и объект ‘colorStops’.

Координаты указывают, где градиент начинается и где он заканчивается.

1
colorStops
указывают - из каких цветов он состоит.

Вы можете определить сколько угодно цветов; главное, чтобы их позиции находились в интервале от 0 до 1 (например 0, 0.1, 0.3, 0.5, 0.75, 1). 0 представляет начало градиента, 1 — его конец.

Чтобы понять, как мы определили координаты, посмотрим на изображение ниже:

Fabric Image

Так как координаты относительны центру объекта, то верхней точкой является

1
-circle.height / 2
, а нижней -
1
circle.height / 2
. Координаты по ширине (
1
x1/x2
) определяем точно так же.

Вот пример красно-голубого градиента, идущего слева направо:

circle.setGradient( 'fill', {
  x1: -circle.width / 2,
  y1: 0,
  x2: circle.width / 2,
  y2: 0,
  colorStops: {
    0: "red",
    1: "blue"
  }
});

Fabric Image

А вот 5-ти шаговый градиент-радуга, с цветами, занимающими по 20% всей длины:

circle.setGradient( 'fill', {
  x1: -circle.width / 2,
  y1: 0,
  x2: circle.width / 2,
  y2: 0,
  colorStops: {
    0: "red",
    0.2: "orange",
    0.4: "yellow",
    0.6: "green",
    0.8: "blue",
    1: "purple"
  }
});

Fabric Image

А вы можете придумать что-нибудь интересное?

Текст

Что если нужно отобразить не только картинки и векторные формы на холсте, а еще и текст? Fabric умеет и это! Встречайте

1
fabric.Text
.

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

Ведь canvas имеет встроенные методы

1
fillText
и
1
strokeText
.

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

А вот создав объект типа

1
fabric.Text
, мы можем работать с ним как и с любым другим объектом на холсте — двигать его, масштабировать, менять атрибуты, и т. д.

Вторая причина — чтобы иметь более богатый функционал, чем то что дает нам canvas. Некоторые вещи, которые есть в Fabric, но нет в родных методах:

  • Многострочность - Родные методы позволяют написать только одну линию, игнорируя переходы строки
  • Выравнивание текста - Влево, по центру, вправо. Полезно во время работы с многострочным текстом
  • Фон текста - Фон выводится только под самим текстом, в зависимости от выравнивания
  • Декорация текста - Подчеркивание, надчеркивание, перечеркивание
  • Высота строки - Полезно во время работы с многострочным текстом

Ну что ж, давайте посмотрим на вездесущий “hello world”?

var text = new fabric.Text( 'hello world', { left: 100, top: 100 });
canvas.add( text );

Вот и все! Для показа текста необходимо всего лишь добавить объект типа

1
fabric.Text
на холст, указывая нужную позицию.

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

1
left
,
1
top
,
1
fill
,
1
opacity
, и т. д.

Помимо обычных атрибутов, у текстовых объектов конечно же есть и свои, относящиеся к тексту. Вкратце, об этих атрибутах:

fontFamily

По умолчанию - “Times New Roman”. Позволяет менять семейство шрифта для текста:

var comicSansText = new fabric.Text( "I'm in Comic Sans", {
  fontFamily: 'Comic Sans'
});

Fabric Image

fontSize

Контролирует размер текста. Заметьте, что в отличие от других объектов в Fabric, мы не можем менять размер текста с помощью width/height. Вместо этого как раз и используется fontSize и конечно же

1
scaleX/scaleY
:

var text40 = new fabric.Text( "I'm at fontSize 40", {
  fontSize: 40
});
var text20 = new fabric.Text( "I'm at fontSize 20", {
  fontSize: 20
});

Fabric Image

fontWeight

Позволяет сделать текст жирнее или тоньше. Точно также как в CSS, можно использовать или слова (“normal”, “bold”) или номерные значения (100, 200, 400, 600, 800).

Важно понимать, что для определенной толщины нужно иметь соответствующий шрифт. Если в шрифте не присутствует “bold” (жирный) вариант, например, то жирный текст может не отобразиться:

var normalText = new fabric.Text( "I'm a normal text", {
  fontWeight: 'normal'
});
var boldText = new fabric.Text( "I'm a bold text", {
  fontWeight: 'bold'
});

Fabric Image

textDecoration

Позволяет добавить тексту перечеркивание, надчеркивание или подчеркивание. Опять же, эта декларация работает также, как в CSS.

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

var underlineText = new fabric.Text( "I'm an underlined text", {
  textDecoration: 'underline'
});
var strokeThroughText = new fabric.Text( "I'm a stroke-through text", {
  textDecoration: 'line-through'
});
var overlineText = new fabric.Text( "I'm an overline text", {
  textDecoration: 'overline'
});

Fabric Image

shadow

До версии 1.3.0 этот атрибут назывался “textShadow”.

Тень для текста. Состоит из 4-х компонент: цвет, горизонтальный отступ, вертикальный отступ, и размер размытия.

Это все должно быть знакомо, если вы до этого работали с тенями в CSS. Меняя эти 4 опции, можно добиться многих интересных эффектов:

var shadowText1 = new fabric.Text( "I'm a text with shadow", {
  shadow: 'rgba(0,0,0,0.3) 5px 5px 5px'
});

var shadowText2 = new fabric.Text( "And another shadow", {
  shadow: 'rgba(0,0,0,0.2) 0 0 5px'
});

var shadowText3 = new fabric.Text( "Lorem ipsum dolor sit", {
  shadow: 'green -5px -5px 3px'
});

Fabric Image

fontStyle

Стиль текста. Может быть только один из двух:

1
normal
или
1
italic
. Опять же, работает так же, как и в CSS:

var italicText = new fabric.Text( "A very fancy italic text", {
  fontStyle: 'italic',
  fontFamily: 'Delicious'
});

var anotherItalicText = new fabric.Text( "another italic text", {
  fontStyle: 'italic',
  fontFamily: 'Hoefler Text'
});

Fabric Image

stroke и strokeWidth

Соединяя

1
stroke
(цвет наружнего штриха) и
1
strokeWidth
(ширину наружнего штриха), можно достичь довольно интересных эффектов.

Вот пара примеров:

var textWithStroke = new fabric.Text( "Text with a stroke", {
  stroke: '#ff1318',
  strokeWidth: 1
});

var loremIpsumDolor = new fabric.Text( "Lorem ipsum dolor", {
  fontFamily: 'Impact',
  stroke: '#c3bfbf',
  strokeWidth: 3
});

Fabric Image

Стоит отметить, что

1
stroke
был назван
1
strokeStyle
до версии 1.1.6

textAlign

Выравнивание полезно при работе с многострочным текстом. В однострочном тексте, выравнивание не видно, потому как ширина самого текстового объекта такая же как и длина строки.

Возможные значения:

1
left
,
1
center
,
1
right
и
1
justify
:

var text = 'this is\na multiline\ntext\naligned right!';
var alignedRightText = new fabric.Text( text, {
  textAlign: 'right'
});

Fabric Image

lineHeight

Еще один атрибут, скорее всего знакомый из CSS —

1
lineHeight
(высота строки).

Позволяет менять расстояние между строк в многострочном тексте. Вот пример текста с ‘lineHeight 3’ и второй с ‘lineHeight 1’:

var lineHeight3 = new fabric.Text( 'Lorem ipsum ...', {
  lineHeight: 3
});
var lineHeight1 = new fabric.Text( 'Lorem ipsum ...', {
  lineHeight: 1
});

Fabric Image

textBackgroundColor

И наконец, дать тексту фон можно с помощью

1
textBackgroundColor
. Заметьте, что фон заполняется только под самим текстом, а не на всю “коробку”.

Чтобы закрасить весь текстовый объект, можно использовать атрибут

1
backgroundColor
. Также видно, что фон зависит от выравнивания текста и
1
lineHeight
.

Если

1
lineHeight
очень большой, фон будет видно только под текстом:

var text = 'this is\na multiline\ntext\nwith\ncustom lineheight\n&background';
var textWithBackground = new fabric.Text( text, {
  textBackgroundColor: 'rgb(0,200,0)'
});

Fabric Image

События

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

События позволяют нам “поймать” различные моменты, когда что-то происходит на холсте.

Хотим узнать, когда была нажата мышка? Следим за событием

1
mouse:down
.

Как насчет, когда объект был добавлен на холст? Для этого есть

1
object:added
.

Ну, а что насчет перерисовки холста? Используем

1
after:render
.

API событий очень прост и похож на то, к чему вы скорее всего привыкли в jQuery, Underscore.js или других популярных JS-библиотеках.

Есть метод on для инициализации слушателя событий и есть метод off для его удаления.

Давайте посмотрим на пример:

var canvas = new fabric.Canvas('...');
canvas.on( 'mouse:down', function( options ) {
  console.log( options.e.clientX, options.e.clientY );
});

Мы добавили слушатель события

1
mouse:down
на canvas-объекте и указали обработчика, который будет записывать координаты, где произошло это событие.

Таким образом, мы можем видеть, где именно произошел клик на холсте. Обработчик событий получает options-объект с двумя параметрами:

1
e
— оригинальное событие и
1
target
— Fabric-объект на холсте, если он найден.

Первый параметр присутствует всегда, а вот

1
target
- только если клик произошел на объекте. Ну и конечно же,
1
target
передается только обработчикам тех событий, где это имеет смысл.

Например, для

1
mouse:down
но не для
1
after:render
(так как это событие не “имеет” никаких объектов, а просто обозначает что холст был перерисован):

canvas.on( 'mouse:down', function( options ) {
  if (options.target) {
    console.log('an object was clicked! ', options.target.type);
  }
});

Этот пример выведет ‘an object was clicked!’ если мы нажмем на объект. Также покажется тип этого объекта.

Какие еще события доступны в Fabric?

На уровне мышки, у нас есть

1
mouse:down
,
1
mouse:move
и
1
mouse:up
.

Из общих есть

1
after:render
.

Есть события, касающиеся выбора объектов:

1
before:selection:cleared
,
1
selection:created
,
1
selection:cleared
.

Ну и конечно же, события объектов:

1
object:modified
,
1
object:selected
,
1
object:moving
,
1
object:scaling
,
1
object:rotating
,
1
object:added
и
1
object:removed
.

Стоит заметить, что события типа

1
object:moving
(или
1
object:scaling
) происходят постоянно, во время движения или масштабирования объекта, даже если на один пиксель.

В то же время, события типа

1
object:modified
или
1
selection:created
происходят только в конце действия (изменение объекта, создание группы объектов, и т. д.).

В предыдущих примерах мы присоединяли слушателя на canvas объект (

1
canvas.on( 'mouse:down', ... )
).

Как вы наверное догадываетесь, это означает, что события распространяются только на тот холст, к которому мы их присоединили. Если у вас несколько холстов на странице, вы можете дать им разные слушатели. События на одном холсте не распространяются на другие холсты.

Для удобства, Fabric позволяет добавлять слушатели прямо на Fabric объекты!

var rect = new fabric.Rect({ width: 100, height: 50, fill: 'green' });
rect.on( 'selected', function () {
  console.log('selected a rectangle');
});

var circle = new fabric.Circle({ radius: 75, fill: 'blue' });
circle.on( 'selected', function () {
  console.log('selected a circle');
});

В этом примере слушатели “присоединяются” прямо к прямоугольнику и кругу. Вместо

1
object:selected
мы используем событие
1
selected
.

По такому же принципу, можно использовать событие

1
modified
(
1
object:modified
когда “вешаем” на холст),
1
rotating
(аналог
1
object:rotating
) и т. д.

Вы можете ознакомиться с событиями поближе и прямо в реальном времени вот в этой демо.

На этом 2-я часть подошла к концу. Столько всего нового, но это еще не все! В 3-й части мы рассмотрим группы объектов, сериализацию/десериализацию холста и формат JSON, SVG-парсер, а также создание подклассов.


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

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

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

Введение

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

1
<canvas>
.

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

1
<canvas>
, а так же SVG-парсер, интерактивный слой и множество других незаменимых инструментов.

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

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

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

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

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

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

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

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

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

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

Нужно нарисовать прямоугольник? Используем

1
fillRect(left, top, width, height)
. Нарисовать линию? Используем комбинацию
1
moveTo(left, top)
и
1
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 (

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

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

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

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

1
fillRect
еще раз?

Не совсем. Вызывая еще одну команду

1
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, чтобы увидеть изменения.

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

Объекты

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

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

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

1
fabric.Circle
,
1
fabric.Triangle
,
1
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 берет на себя заботу о состоянии холста и перерисовке. От нас требуется менять только сами объекты.

В предыдущем примере было видно, как метод

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

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

  • позицию —
    1
    left, top
    
  • размер —
    1
    width, height
    
  • рендеринг (отображение объекта) —
    1
    fill, opacity, stroke, strokeWidth
    
  • масштаб и поворот —
    1
    scaleX, scaleY, angle
    
  • переворот (flip - 180 градусов) —
    1
    flipX, flipY
    
    .

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

1
true
в атрибут
1
flip*
.

Чтение атрибутов происходит с помощью метода

1
get
, присваивание — с помощью метода
1
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’. Заметьте, как три выражения используют слегка разный синтаксис.

Отсюда видно, что

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

Ну, а как насчет чтения? Я уже упомянул, что есть общий

1
get()
, а также набор конкретных
1
get*()
методов.

Например, для получения ‘width’ объекта можно использовать

1
get('width')
или
1
getWidth()
. Для ‘scaleX’ —
1
get('scaleX')
или
1
getScaleX()
, и т. д.

Такие специальные методы, как

1
getWidth()
и
1
getScaleX()
существуют для всех ‘публичных’ атрибутов объекта (‘stroke’, ‘strokeWidth’, ‘angle’, и т. д.).

Вы наверное заметили, что в предыдущих примерах были использованы конфигурационные хэши, которые выглядели точно также, как и те, которые мы только что использовали в методе

1
set
. Это потому, что они действительно одинаковые. Объект может быть “сконфигурирован” в момент создания или позже, с помощью метода
1
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

Прямоугольник получил значения по умолчанию. Он находится в позиции

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

Из-за этого мы его и не видим. Как только устанавливаем позитивные

1
width/height
, черный прямоугольник появляется в левом верхнем углу.

Fabric Image

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

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

1
fabric.Object
.

1
fabric.Object
— это абстрактная 2-мерная фигура на плоскости. Она имеет left/top и width/height атрибуты, а также набор других визуальных параметров.

Те атрибуты, которые мы видели ранее (

1
fill
,
1
stroke
,
1
angle
,
1
opacity
,
1
flip*
и т. д.) принадлежат всем Fabric-объектам, которые наследуют от
1
fabric.Object
.

Такое наследование очень удобно. Оно позволяет нам определить методы на

1
fabric.Object
, таким образом делая его доступным во всех “классах”-потомках.

Например, если нужен метод

1
getAngleInRadians
на всех объектах, просто создаем его на
1
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

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

Разумеется, классы-потомки могут не только наследовать от

1
fabric.Object
, но и определять свои собственные методы и параметры.

Например, в

1
fabric.Circle
существует дополнительный атрибут ‘radius’. Или возьмем к примеру
1
fabric.Image
, с которым мы познакомимся подробнее чуть позже.

В нем имеются методы

1
getElement/setElement
, предназначенные для чтения\записи HTML элемента
1
<img>
, на котором основан объект типа
1
fabric.Image
.

Canvas (холст)

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

Как видно из примеров, первый шаг - это создание самого “холста” для рисования —

1
new fabric.Canvas('...')
.

1
fabric.Canvas
- это, по сути, оболочка вокруг
1
<canvas>
-элемента, ответственная за управление всеми содержащимися на нем объектами. Конструктор берет id элемента и возвращает объект типа
1
fabric.Canvas
.

Теперь в него можно добавлять объекты (

1
add()
), а также их читать (
1
item()
,
1
getObjects()
) или удалять (
1
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); // удаляем прямоугольник

Как мы уже выяснили, главная задача объекта

1
fabric.Canvas
- это управление объектами, которые находятся на canvas.

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

1
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 заложен функционал пользовательского доступа. Как только мы создаем холст через

1
new fabric.Canvas('...')
, объекты, расположенные на нем, сразу же можно выделять, двигать, масштабировать, вращать и даже группировать вместе, управляя ими как одним целым!

Fabric Image Fabric Image

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

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

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

А что делать, если интерактивность вовсе не нужна? Тогда просто меняем

1
fabric.Canvas
на
1
fabric.StaticCanvas
.

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

1
StaticCanvas
вместо
1
Canvas
.

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

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

Это создает облегченную версию холста, без лишней логики для интерактивности и управления

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

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

Это может быть полезно если, например, нужно просто отобразить статичный график, SVG-фигуру или картинки с фильтрами.

Картинки

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

Все-таки работа с простыми фигурами не так интересна, как с более графически насыщенными картинками. Как вы наверное уже догадываетесь, в Fabric это очень просто.

Создаем

1
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);

Заметьте, как мы передаем

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

Мы также выставляем

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

Fabric Image

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

1
fabric.Image.fromURL
:

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

Здесь никаких сюрпризов. Вызываем

1
fabric.Image.fromURL
передавая адрес картинки, а также функцию (callback), которую надо вызвать когда картинка загрузится.

Callback получает первым аргументом объект

1
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

1
<path>
-элементами. Они используют одинаковый набор команд, могут быть созданы из
1
<path>
-элементов и сериализованы в них.

О сериализации и 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

При создании объекта

1
fabric.Path
мы передаем строку с инструкциями “черчения” кривой. Выглядит эта инструкция, конечно, очень загадочно, но понять ее на самом деле довольно легко.

‘M’ означает ‘move’ (двинуть) и говорит невидимой ручке подвинуться в точку 0, 0. ‘L’ означает ‘line’ (линия) и рисует линию до точки 200/100. Затем команда ‘L’ рисует линию до 170/200. И наконец, ‘z’ заставляет невидимую ручку замкнуть текущий контур и завершить фигуру. Как результат, получается вот такая треугольная форма.

Объект

1
fabric.Path
такой же, как и остальные объекты в Fabric, поэтому мы легко изменили его параметры (
1
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

С такими “монстрами” вручную работать вы наверняка не будете. Вместо этого, можно использовать очень удобный метод

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

Кстати, что касается SVG-документов, Path в Fabric обычно представляет SVG

1
<path>
-элемент, а вот наборы таких элементов, которые очень часто можно найти в SVG документах, обычно представлены через PathGroup (
1
fabric.PathGroup
-объекты).

PathGroup — это всего лишь группа Path-объектов. Так как

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

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

Послесловие

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

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

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

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

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


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


Пример создания часов на Canvas. Снова я “подсмотрел” пример из зарубежного источника.

И снова мне захотелось его разобрать “по косточкам”. Поэтому на оригинальность не претендую (к моему сожалению).

HTML-разметка

Для HTML-разметки создаем элемент canvas:

<canvas id="clock" class="clock"></canvas>

Затем в JavaScript-скрипте “достаем” элемент canvas и динамически задаем для него размеры

1
500 x 500
(мне нравится задавать их динамически):

window.addEventListener( 'DOMContentLoaded', function () {

    var clock = document.querySelector('#clock').getContext('2d');
    clock.canvas.width = 500;
    clock.canvas.height = 500;

}, false);

Время - работа с ним

Создаем новый экземпляр объекта Date для того, чтобы получить текущее время:

var now = new Date();

Затем из полученного объекта получаем значения времени - часов, минут, секунд и миллисекунд:

var hours = now.getHours();
var minutes = now.getMinutes();
var seconds = now.getSeconds();
var miliSeconds = now.getMilliseconds();

В дополнение к этому, получим также текущую дату с учетом local time и locale, в удобном для чтения человеком формате - метод toDateString:

var today = now.toDateString();
// пример вывода
"Fri Jun 17 2016"

… и точно также - локальное время (метод toLocaleTimeString):

var time = now.toLocaleTimeString();
// пример вывода
"3:34:31 PM"

Часы, минуты, секунды

В canvas создаем круговую индикацию часов, минут и секунд.

Делает это будем при помощи canvas-метода

1
arc()
:

function degreeToRadian (degree) {
    return degree * Math.PI / 180;
}

function renderTime () {

    // HOURS
    clock.beginPath();
    clock.arc( 250, 250, 200, degreeToRadian(270), degreeToRadian( hours * 15 - 90 ) );
    clock.stroke();

    // MINUTES
    clock.beginPath();
    clock.arc( 250, 250, 180, degreeToRadian(270), degreeToRadian( minutes * 6 - 90 ) );
    clock.stroke();

    // SECONDS
    clock.beginPath();
    clock.arc( 250, 250, 160, degreeToRadian(270), degreeToRadian( newSeconds * 6 - 90 ) );
    clock.stroke();

}

setInterval( renderTime, 40 );

В приведенном выше коде остановимся на некоторых моментах.

Первый момент - метод

1
arc()
принимает аргументы для угловых значений в радианах. Для человека этот формат непривычен, так как мы пользуемся градусами.

Поэтому для удобства создадим функцию degreeToRadian, которая преобразовывает градусы в радианы.

Второй момент - начальный угол (точка отсчета) по стандарту canvas (я не ошибся?) - это 0. Это соответствует трем часам (15:00) на часовом циферблате.

Конечно, нас это не устраивает, поэтому смешаем начальную точку на 270 градусов (по часовой стрелке) так, чтобы эта точка находилась в положении 12 часов (12:00) -

1
degreeToRadian(270)
.

Второй аргумент - конечный угол - будет принимать значение угла динамически, из объекта

1
now
:

var hours = now.getHours();
var minutes = now.getMinutes();
var seconds = now.getSeconds();

Из полученного значения нам необходимо вычесть 90 градусов потому, что хоть начальную точку мы и сместили на 270 градусов, отсчет продолжает вестись из начальной точки 0 (которая на 15:00). Почему так происходит, я так и не понял - но код работает.

Результирующее значение умножаем на 15 или 6 для кратности - эти величины взяты чисто эмпирически. В итоге получится красивые и “смотрибельные” окружности\стрелки.

Последний момент - сделаем для всего этого “дела” обертку-функцию

1
renderTime
и “запихнем” ее в тайминговую функцию setInterval, с частотой выполнения 40 миллисекунд.

Таким образом мы оформим анимацию отрисовки окружностей\стрелок и наши часы станут настоящими (почти) часами.

Пора взглянуть по получившийся результат, ибо иначе теряется “связь с реальностью”. Готовый код можно посмотреть здесь - JavaScript Canvas Clock - Start.

Небольшое примечание - для большей наглядности примера в код были добавлены толщина линий (lineWidth), цвет линий (strokeStyle), скругление концов линий (lineCap), а также фоновая заливка (fillStyle, fillRect) всего canvas:

clock.strokeStyle = '#28d1fa';
clock.lineWidth = 14;
clock.lineCap = 'round';
...
// BACKGROUND
clock.fillStyle = '#000';
clock.fillRect( 0, 0, 500, 500 );

И еще один момент - нужно обратить внимание на новую строку:

var newSeconds = seconds + ( miliSeconds / 1000 );

Что она делает? Она просто получает текущее значение миллисекунд, дробит это значение на еще меньшее значение ( miliSeconds / 1000 ) и прибавляет полученный результат к значению секунд (now.getSeconds).

Зачем это делается? С одной лишь целью - сделать отрисовку секундной окружности более плавной; иначе шаг прибавления будет слишком большим и резким - получится некрасиво.

В результате код для отрисовки секундной окружности будет таким:

// SECONDS
clock.beginPath();
clock.arc( 250, 250, 160, degreeToRadian(270), degreeToRadian( newSeconds * 6 - 90 ) );
clock.stroke();

Часы - дата и время

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

1
fillText
, а также воспользоваться методом
1
font
для настройки отображения шрифта:

// DATE
clock.font = '700 24px Arial, sans-serif';
clock.fillStyle = '#28d1fa';
clock.fillText(today, 175, 250);

// TIME
clock.font = '14px Arial, sans-serif';
clock.fillText(time, 185, 270);

Не забудем спозиционировать текст так, чтобы он нормально смотрелся и посмотрим на полученный результат - JavaScript Canvas Clock - Date and Time.

Добавляем градиент

Пример с часами можно существенно украсить, заменив простую фоновую заливку цветом на градиентную заливку.

В canvas существует два типа градиента - линейный и радиальный. В данном случае будет применяться радиальный градиент.

Градиенты в canvas добавляются совсем не так, как в CSS. Здесь это объект, у которого есть свой метод

1
addColorStop()
; создается градиент при помощи функции-конструктора
1
createLinearGradient(x,y,x1,y1)
или
1
createRadialGradient(x,y,r,x1,y1,r1)
.

Изменим код для создания фоновой заливки из предыдущего примера таким образом:

// BACKGROUND
var gradient = clock.createRadialGradient( 250, 250, 5, 250, 250, 300 );
gradient.addColorStop( 0, '#09303a' );
gradient.addColorStop( 1, '#000' );
clock.fillStyle = gradient;
clock.fillRect( 0, 0, 500, 500 );

Здесь цвет, идущий (позиция 0) из центра радиального градиента - это:

// BACKGROUND
...
gradient.addColorStop( 0, '#09303a' );

… а цвет, на котором радиальный градиент останавливается (позиция 1), это:

// BACKGROUND
...
gradient.addColorStop( 1, '#000' );
...

Не забываем изменить строку

1
clock.fillStyle = '#000';
на строку
1
clock.fillStyle = gradient;
и посмотрим на готовый результат - JavaScript Canvas Clock - Radial Gradient.

В коде выше были использованы еще два метода canvas для создания тени -

1
shadowBlur
(размытие тени) и
1
shadowColor
(цвет тени). Так результат смотрится лучше.

canvas в image

Напоследок можно воспользоваться методом canvas под названием

1
toDataURL
. Этот метод может брать текущий отрисованный canvas и конвертировать его в картинку (в формате data URI).

Эту картинку можно использовать как угодно, но в том числе - вставлять на HTML-страницу. Этот подход можно применить и заменить картинкой canvas.

В результате canvas будет генерировать картинку с частотой 40 миллисекунд и с такой же частотой вставлять ее на страницу. А canvas убрать. Визуально подмены не будет заметно.

Можно проверить, правда ли на странице картинка, а не canvas. Элементарно - правый клик мыши на изображении и смотрим - в контекстном меню есть пункт “Save image as…”. Будь на этом месте canvas, такого “фокуса” бы не получилось.

Делается это в угоду браузеров, которые не понимают canvas. Правда, такой способ “подвешивает” браузер со страшной силой (по крайней мере, так дело обстоит у меня). Оно и понятно (если я не ошибаюсь) - попробуйте рисовать каждые 40 миллисекунд новую картинку )

Ниже - код:

<canvas id="clock" class="clock"></canvas>
<img src="" id="image" alt="Clock"/>
.clock {
    display: none;
}
document.querySelector('#image').src = clock.canvas.toDataURL();

Тут стоит заметить один важный момент - метод

1
toDataURL()
относится к canvas, но не к ‘2d’-контексту canvas’а.

Смотрим окончательный готовый результат (слегка измененный) - JavaScript Canvas Clock - Image.

P.S.

Тут вышла заминка - даже CodePen с трудом справляется с такой задачей (но если подождать, то можно увидеть).

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

JavaScript Canvas Clock - Image

На этом все.


Что такое модульная система в Node.js и как ею пользоваться. Статья мне очень понравилась - в ней автор подробно и внятно доносит тему.

Кроме того, это тот самый случай, когда я наконец нашел ответ на свой вопрос.

Итак - первоисточник находится здесь - Руководство для начинающих по Node.js от Felix’a.


Для организации программ в виде отдельных файлов Node.js предлагает к использованию модульную систему.

Для демонстрации подхода создадим файл

1
main.js
со следующим содержимым:

var hello = require('./hello');
hello.world();

Нетрудно догадаться, что функция

1
require('./hello')
используется для импорта данных из отдельного JavaScript-файла.

1
./
- означает, что файл находится в той же директории, что и сам файл
1
main.js
. Также следует обратить внимание на то, что не требуется указывать расширение файла, так как
1
.js
подставляется по умолчанию.

Далее необходимо создать файл (модуль)

1
hello.js
со следующим содержимым:

exports.world = function () {
  console.log('Hello World');
}

Тут следует обратить внимание на то, что происходит присваивание свойству

1
world
объекта
1
exports
.

Таким образом объявляется, что модуль

1
hello.js
экспортирует во вне функцию
1
world
.

Объект

1
exports
доступен в любом модуле и возвращается при каждом вызове функции
1
require
при подключении модуля.

При запуске

1
main.js
вывод будет следующим:

$ node main.js
Hello World

Будет не лишним упомянуть, что зачастую объект

1
exports
переопределяется следующим образом:

module.exports = function () {
  // ...
}

Как и ожидается, такое переопределение будет причиной того, что

1
require
будет возвращать функцию.

Такой подход полезен при объекто-ориентированном программировании (ООП), где каждый файл экспортирует конструктор одного класса.

Следующая вещь, которую надо понимать при работе с модульной системой, это то, как система работает с вызовами

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

Например:

var http = require('http');

Первое, что сделает Node.js - это определит, есть ли модуль ядра с именем

1
http
. Если есть, то вернет соответствующую директорию.

Но что будет при работе с модулями, не относящимися к ядру, как, например,

1
mysql
?

var mysql = require('mysql');

В этом случае Node.js будет искать модуль в каждой директории, начиная с одного из текущих файлов, и проверять есть ли директория с именем

1
node_modules
. Если такая директория найдена, Node.js начнет поиск в ней файла с именем
1
mysql.js
.

Если подходящих файлов не найдено и достигнут корень файловой системы (

1
/
), Node.js сдается и выбрасывает исключение.

В настоящий момент Node.js учитывает дополнительный изменяемый список альтернативных директорий для импорта. Список доступен через массив

1
require.paths
. Однако идет активное обсуждение на тему удаления этой возможности, так что лучше пока не использовать ее.

И наконец, Node.js учитывает файл

1
index.js
, в котором описывается главный файл импорта для директории.

Таким образом, если встретится вызов

1
require('./foo')
, то Node.js будет пробовать искать файлы и
1
foo.js
и
1
foo/index.js
.

JavaScript-flood продолжается … )