SVG — мой трудный ребёнок

Для тех, кто ещё не успел приобщиться к прекрасному, спешу сообщить, что наш сайт пополнился новым разделом c векторной графикой, в котором мы рассказываем клиентам, как делаем сайты и как заботимся о них потом. «Прекрасное» — то есть svg-анимация — была реализована мною с помощью библиотеки Snap. js.

описание изображения

Задуманная дизайнером анимация была довольно проста и сводилась к следующим задачам: отрисовать элементы и анимировать их появление, исчезновение и трансформацию — как по отдельности, так и группами. Часть задач можно было реализовать на CSS, но, к сожалению, таким образом нельзя изменять контуры объектов. Кроме того, ограничена настройка плавности анимации: любимого нашим дизайнером эффекта bounce или elastic не добьёшься даже с помощью вариации значений cubic-bezier.

На сегодняшний день существует большое число js-библиотек для рисования. О сравнении самых известных из них (Raphaël, Paper и Processing) можно почитать в этой статье. Последние две библиотеки я сразу отклонила, поскольку они рассчитаны на работу с серьёзной, очень красивой и разнообразной графикой, а применения такой тяжёлой артиллерии на нашем сайте не требовалось. А вот Raphaël подходила больше: лёгкая, удобная, с поддержкой самых востребованных easing-методов. Кроме того, Raphaël используется, когда необходимо создать интерактивное изображение: такие события, как клики, наведения, перетаскивания и т. п., описаны в документации и весьма сходны с аналогичными событиями JavaScript.

Статья была опубликована в 2012 году, поэтому в обзор не попала более современная библиотека Snap, представленная разработчиками только в конце 2013 года. Автор библиотеки — Дмитрий Барановский, известный также по библиотеке Raphaël. Snap умеет группировать элементы, использовать градиенты, паттерны, маски и фильтры. Кроме того, эта библиотека позволяет не просто экспортировать готовые svg-файлы, но и искать элементы внутри них и с удобством работать ними.

Но во всём есть свои недостатки: по результатам теста Snap значительно проигрывает своему предшественнику в скорости. А вот по весу подключаемого файла Snap выигрывает: 299 (91 для сжатой версии) Кб Raphaël против 244 (67) Кб Snap.

Последовательный старт анимации выполнен благодаря использованию материалов этой статьи. Огромное спасибо автору! :)

Рисуем «Золотое сечение»

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

Пример работы этого слайда с подробными комментариями — здесь.

Первая часть анимации состоит из поворачивающихся и тянущихся прямоугольников, потом медленно отрисовывается внутренняя линия, затем всё, за исключением рамки, исчезает, и появляются элементы монитора со схемой.

Перед началом рисования лучше разобраться с настройками самой svg-области, иначе элементы могут выйти за пределы блока. Об этом можно прочитать в статье про область видимости и пропорции (на английском).

После этого внимательно рассматриваем изображение и планируем анимацию.

описание изображения
Конец первой части анимации

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

описание изображения
Конец второй части анимации

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

После этого подключаем библиотеки, и поехали:

var canvas = Snap('.main-svg');

Область рисования можно брать из html (как в примере) или создавать новую, указывая ее размеры.

В песочнице самого Snap объекты добавляются с подробным перечислением их атрибутов.

// Lets create big circle in the middle: var bigCircle = s.circle(150, 150, 100); // By default its black, lets change its attributes bigCircle.attr({ fill: «#bada55», stroke: «#000», strokeWidth: 5 });

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

В нашем примере точно не будут меняться: stroke-linecap (форма концов линии), stroke-linejoin (форма углов) и stroke-miterlimit (соотношение угла к толщине линии), они прописаны в css. Для кривых и прямоугольников создаём массив attr_ratio, а для кругов — attr_fill.

// так будут выглядеть кривые и прямоугольники var attr_ratio = { fill: 'none', strokeWidth: 0, stroke: '#f4394d' }; // а круги для разнообразия пусть будут с заливкой. и для удобства сразу сожмём их var attr_fill = { fill: '#f4394d', strokeWidth: 3, transform: 'matrix (0.01,0,0,0.01,226.9,155.4)', stroke: 'none' };

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

В нашем примере у кривых и прямоугольников толщина линии равна 0, чтобы они не появлялись все разом. Для этого же можно использовать opacity, масштабирование до микроразмера (как в случае с массивом attr_fil) или добавлять объекты по ходу, что, возможно, будет не очень удобно для группировки.

Если сохранить скорость анимации в отдельной переменной (или нескольких — для анимации и для пауз между ними), гораздо удобнее «рулить» ей в процессе отладки, при необходимости добавляя коэффициенты.

Для трансформации объектов нужна будет отдельная переменная-матрица, которую мы будем вращать, искривлять или масштабировать. По умолчанию её значение равно: (1,0,0,1,0,0).

var deg_ratio = new Snap.Matrix();

Пока анимация объектов не будет выходить за рамки задач вроде «поменять цвет и толщину линии» или «развернуть на 90 градусов», можно использовать типовые фигуры: прямоугольники рисуем при помощи rect, круги — circle и т. д. Если же у нарисованной фигуры в дальнейшем будут происходить серьёзные изменения контура (как, например, на этом слайде), лучше создать её с помощью кривой (path).

Почитать про базовые фигуры в svg можно здесь. Стоит помнить, что, добавляя подобные элементы с помощью Snap, можно использовать сокращённую запись, перечисляя атрибуты.

var c = paper.circle(10, 10, 10); // is the same as… var c = paper.el(«circle»).attr({ cx: 10, cy: 10, r: 10 }); // and the same as var c = paper.el(«circle», { cx: 10, cy: 10, r: 10 });

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

// сетка сечения var ratio_square_1 = canvas.rect(307.8,233,32.3,32.3).attr({class:'ratio_square_1'}), ratio_square_2 = ratio_square_1.clone().attr({class:'ratio_square_2'}); // линия сечения var ratio_line = canvas.path('M631,168.3c0,154.5-104.1,258.6-258.6,258.6l0,0c-96.6,' + '0-161.6-65.1-161.6-161.6c0-57.9,39-97,97-97c38.6,0,64.6,26,64.6,64.6 c0,19.3-13,' + '32.3-32.3,32.3c-19.3,0-32.3-13-32.3-32.3').attr({stroke: '#fff'}); //и группируем их для удобства var group_ratio = canvas.group(ratio_square_1,ratio_square_2,ratio_line);

Координаты и контуры фигур частично были скопированы из инспектора Opera, в котором открывался присланный дизайнером svg-файл, а частично вычислялись самостоятельно. В нашем примере были скопированы viewBox для svg-области, контуры для линии сечения и схемы, а также координаты первого появившегося квадрата. Остальные квадраты в файле находились уже на своих финальных местах, поэтому стартовое положение приходилось подбирать самостоятельно.

описание изображения

Задаём элементам нужный набор атрибутов. Если он уже имеет атрибуты, не описанные в массиве, новые просто добавятся, если есть — заменятся.

ratio_square_1.attr(attr_ratio); ratio_square_2.attr(attr_ratio); // у линии сечения цвет заменится на новый ratio_line.attr(attr_ratio); // для кругов — свои свойства display_circle1.attr(attr_fill); display_circle2.attr(attr_fill); display_circle3.attr(attr_fill);

Теперь анимируем появление первого квадрата. Пусть он появляется с эффектом bounce из точки, расположенной в его же центре. Для начала уменьшаем его в 100 раз и сдвигаем координатную сетку в точку с координатами центра квадрата (если не сделать последнего, изображение будет появляться из левого верхнего угла). Затем снова масштабируем матрицу до начального значения и передаём её в анимацию квадрата, добавляя эффект bounce из набора snap-овских easing-функций.

// сжимаем первый квадрат до 1/100 deg_ratio.scale(0.01,0.01,356.25,249.15); ratio_square_1.attr({strokeWidth:3, transform:deg_ratio}); // и растягиваем до начального состояния deg_ratio.scale(100,100,356.25,249.15); ratio_square_1.animate({transform:deg_ratio},speed*2,mina.bounce);

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

Второй квадрат появляется на месте первого. Его необходимо развернуть на 90° через точку, находящуюся в его правом нижнем углу. Изменяем соответствующим образом матрицу, применяем к квадрату и обязательно возвращаем матрицу в начальное состояние: нам ей ещё другие элементы вращать.

// поворачиваем матрицу, указывая угол наклона и точку, // через которую будет проходить поворот deg_ratio.rotate(90,340.1,265.3); // и поворачиваем квадрат ratio_square_2.animate({transform:deg_ratio},speed); // возвращаем матрице исходное значение deg_ratio.rotate(-90,340.1,265.3);

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

описание изображения
Пятый и шестой квадраты растянулись, но не туда

После того, как последний квадрат встал на место, должна появиться линия. Её отрисовка будет производиться с помощью изменения Stroke-dasharray (длина штриха в обводке и расстояние между двумя штрихами, по умолчанию оба значения равны 0) и Stroke-dashoffset (смещение штриха вдоль обводки). Что это такое, подробно описано здесь и здесь (в последней ссылке много красивых экспериментов).

Для анимации этих иконок используется css, что приводит к небольшим проблемам с точностью: в последнем примере (первая ссылка) заметна пауза после того, как кривая нарисована полностью. Это связано с тем, что величина штриха увеличивается до какого-то абстрактного значения, плюс-минус приблизительно равного длине контура. Стоит немного не угадать — и либо появляется пауза, либо линия не будет дорисовываться до конца.

В другом примере, реализованном на js, сначала измеряется длина контура. Это даёт более точную и скоординированную по времени анимацию, но метод getTotalLength () доступен только для path.

описание изображения
описание изображения

Наша анимация будет происходить за счёт смещения штриха, относительно начальной точки линии. Сделав начальное значение strokeDashoffset отрицательным, можно рисовать линию с другого конца. После паузы линия исчезнет с помощью того же смещения, а затем исчезнут и все сгруппированные квадраты.

// измеряем длину кривой line_lenght = ratio_line.getTotalLength(); // устанавливаем начальные значения ratio_line.attr({ strokeDasharray:line_lenght, strokeDashoffset:line_lenght, strokeWidth:3 }); // отрисовываем. анимация красивая, пусть рисуется в два раза медленнее, // чем всё остальное, а потом ещё столько же повисит перед исчезновением ratio_line.animate({strokeDashoffset:0},speed*2,mina.easeinout);

Сразу нужно сказать о том, что изменения Stroke-dasharray и Stroke-dashoffset может в дальнейшем вызывать определённые трудности. Например, в другом слайде отрисованный подобным образом вопросительный знак должен был позже растягиваться до лампочки, но вместо этого получалось вот такое ухо Ван Гога:

описание изображения
Почему вместо лампочки получается какая-то ерунда?

Собственно, всё закономерно: длина контура лампочки больше длины контура вопросительного знака, а длины штриха не хватает, поэтому перед трансформацией нужно вернуть Stroke-dasharray и Stroke-dashoffset значения, равные 0.

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

deg_ratio.y(223.7,155.4); display_circle1.animate({transform:deg_ratio,strokeWidth:3},speed,mina.bounce); deg_ratio.y(249.5,155.4); display_circle2.animate({transform:deg_ratio,strokeWidth:3},speed,mina.bounce); deg_ratio.y(275.3,155.4); display_circle3.animate({transform:deg_ratio,strokeWidth:3},speed,mina.bounce);

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

После этого увеличиваем opacity схемы до 1 и наслаждаемся результатом.

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

описание изображения
«А пусть это работает так…»

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

Ольга Бевзюк Ольга Бевзюк Младший технолог