Наука про SVG анімації

37 хв. читання

Плоский дизайн все більше стає трендом 2016 року, відповідно SVG тепер використовується достатньо часто. Переваг багато: незалежність від роздільної здатності, крос-браузерність і доступні DOM ноди. У цій статті ми розглянемо, як ми можемо використовувати SVG для створення, здавалося б, складних анімацій, використовуючи прості ілюстрації.

Коротке введення

Рис. 1 Що ми створюємо? На вид важкі анімації із звичайних SVG-ілюстрацій.

Цей проект починався як звичайний експеримент: Як далеко ми можемо піти з SVG анімаціями?

У той час, дизайнер Кріс Халаска і я були колегами, ми працювали над illustration-heavy веб-сайтом. Тоді у дизайні не вистачало того необхідного "шарму", якого всі шукали. Ми знайшли відповідь у "The Camera Collection," графічні анімації, які рухаються, стали просто вірусними. Ми могли використовувати анімацію, щоб оживити ілюстрації, а SVG були ідеальним середовищем для її реалізації.

Проблемою, з якою ми зіткнулися, і яка існує до сьогодні - SVG анімація призначена або для фронтенд розробника, який розуміється на мистецтві, або для дизайнера, який розбирається у JavaScript. Звичайно, жоден із сценаріїв не є неправильним. І ми хотіли зменшити розрив між кодом та дизайном.

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

Правила анімації

У The Illusion of Life, Disney описує 12 основних принципів, щоб додати характеру анімації. Харизма та враження, сцена, сповільнення, таймінг, перебільшення тощо, всі вони існують, щоб оживити будь-який неживий предмет. Ми хотіли слідувати цим принципам у нашому проекті, віддаляючись від жорсткості DOM і намагаючись досягнути чогось більш і природного. Створюючи систему навколо трансформацій, таймінгу і пом'якшень, ми змогли створити анімації, які були стилістично однорідними, але відрізнялися один від одного своїм характером.

ТРАНСФОРМАЦІЇ

Тренд плоского дизайну сам по собі добре піддається використанню SVG через простоту ілюстрацій. Ми імітували цю характеристику в анімації, поєднуючи геометричні фігури з простими геометричними рухами. У нас було одне правило: використовувати базові перетворення (translate, rotate, scale) з базовими положеннями (left, right, top, bottom і center).

SVG

Рис. 2. Дев'ять можливих положень анімації, використовуючи комбінації left, right, center, top та bottom.

ТАЙМІНГ

Для того, щоб підтримувати однаковий такт і ритм, ми обмежили себе у дуже конкретне інкрементування часу. Анімації тривали 2 секунди і включали в себе 10 окремих кроків. Твін (tween), кожне перетворення (переміщення, обертання і масштабування), повинен був починатися і закінчуватися на одному з цих етапів, які ми оголосили як keyframes.

SVG

Рис. 3. Приклад анімації, в якій кожен крок стає довшим на 200 мілісекунд за попередній з трьома твінами, які перекривають один одного.

СПОВІЛЬНЕННЯ

У той час як трансформацій і таймінгу вже достатньо, щоб створити візуальне сприйняття руху, сповільнення приводить все в життя. Ми виявили, що три формули сповільнення забезпечують варіації, щоб додати характеру рухам: easeOutBack, easeInOutBack і easeOutQuint.

SVG

Рис. 4. Візуальне порівняння анімацій з і без сповільнення. Слід зазначити, що використання будь-якої варіації easingBack впливатиме на трансформацію в деякій мірі.

Початок

ПІДГОТОВКА ДЕТАЛЕЙ

Хоча існує багато додатків для створення ілюстрацій, наприклад Sketch та Inkscape, які стали достатньо популярними в ці дні, ми вирішили створити наші SVG в Adobe Illustrator.

SVG

Рис. 5. Елементи, з яких складається наша анімація.

SVG

Рис. 6. Illustrator автоматично створює ID з назв елементів при експортуванні в SVG.

Перед тим, як експортувати в SVG, згрупуйте і дайте назву кожному елементу. Illustrator автоматично створить ID з цих назв в процесі експорту. Для кожного анімованого елемента, вивід повинен виглядати як XML, показаний нижче. Зверніть увагу, що навіть якщо елемент не має нащадків, він все одно повинен бути згрупований під тегом g.

<g id="zipper">
<path d="…" fill="#272C40">
</path></g>

SVG

Рис. 7. Налаштування при експорті SVG. Галочка на "responsive" знята, тому що елементи анімації базуються на пікселях.

ВИКОРИСТОВУЄМО МАСКУВАННЯ

Можливо, ви помітили елемент <clip group=""> на рис. 6. Це маски, створені в Illustrator. При експорті в SVG, вони автоматично оголошуються як clipPaths, які можуть бути використані для маскування елементів таким чином.

<g>
<defs>
  <rect height="309" id="SVGID_1_" width="500" x="235" y="-106.3">
</rect></defs>

<clippath id="SVGID_2_">
  <use overflow="visible" xlink:href="#SVGID_1_">
</use></clippath>

<g clip-path="url(#SVGID_2_)" id="strap-right">
  <path d="…" fill="#93481F" stroke="#000000" stroke-miterlimit="10" stroke-width="1.5">
</path></g>
</g>

SVG

Рис. 8. Використання clipPath для приховування ременів на початку анімації.

Прототипи, прототипи, прототипи

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

CSS ТА VELOCITY.JS

Наші перші спроби використовувати CSS для створення анімації були багатообіцяючими. Ми вважали, що з апаратно-прискореними трансформаціями, анімації будуть працювати гладко і реалізація буде простою, без необхідності використання зовнішніх бібліотек. У той час як ми змогли створити працюючу версію в Chrome, в усіх інших браузерах ця ідея не мала успіху.

Firefox не приймає властивість SVG transform-origin, в той час як підтримка в Internet Explorer для SVG CSS анімації повністю відсутня. І нарешті, з CSS і JavaScript, які тісно пов'язані між собою, ми знову повернулися назад до проблеми з великою кількістю файлів для елегантного вирішення проблеми.

У тому ж дусі, ми зіткнулися з тими ж проблемами з Velocity.js. Оскільки анімаційний рушій також використовує CSS трансформації, проблеми з Firefox та Internet Explorer залишаються невирішеними.

GSAP

GSAP вважався промисловим стандартом у часи Flash, і популярність зросла ще більше, коли його перенесли на JavaScript. Завдяки його ланцюговому синтаксису, розширеній підтримці SVG і неперевершеній продуктивності, GSAP був очевидним претендентом - за винятком однієї проблеми: У ньому було занадто багато всього. Імпорт TweenMax і TimelineMax відразу вже збільшувало в два рази розмір нашого проекту.

SNAP.SVG

У нашій останній спробі, ми використовували Snap.svg, наступника Raphael. Snap пропонує широкі функціональні можливості в маніпуляції DOM, але мінімум в підтримці анімацій. Хоча ми визнали це як невдачу, недоліки привели нас до нашого власного JavaScript для заповнення деяких прогалин. Це привело до легкого вирішення проблеми, яке все ще задовольняє нашим умовам.

MO.JS, ANIME ТА WEB ANIMATIONS API

В момент написання цієї статті, три дуже перспективні бібліотеки SVG анімації набирали обертів в спільноті: Mo.js, Anime і Web Animations API. Якщо ми отримаємо шанс знову звернутися до вирішення цих проблем, ці альтернативи, безсумнівно, будуть прийняті до уваги. Проте, концепції, що лежать в цій статті, можуть бути застосовані до будь-якої бібліотеки анімацій, яку ви хочете використовувати.

Скафолдинг

Почнемо з імпорту базової таблиці стилів і бібліотеки Snap.svg в наш проект. Ми також включимо порт функцій сповільнення Роберта Пеннера для подальшого використання.

SVG

Рис. 9. Фінальна структура нашого проекту. Скафолд "Hello world" починається лише з виділених жовтим файлів.


  <meta charset="UTF-8">
  <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
  <meta content="width=device-width, initial-scale=1.0" name="viewport">
  <title>The Illusion of Life: An SVG Animation Case Study</title>

  
  <link href="css/style.css" rel="stylesheet" type="text/css">

  
  <script src="js/libs/snap.svg.min.js"></script>
  <script src="js/libs/snap.svg.easing.min.js"></script>


/* Весь екран */
html, body {
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
  overflow: hidden;
  background-color: #E6E6E6;
  font-family: sans-serif;
}

/* Відцентроване полотно */
#canvas {
  position: absolute;
  top: 50%;
  left: 50%;
  -webkit-transform: translateX(-50%) translateY(-50%);
  -ms-transform: translateX(-50%) translateY(-50%);
  transform: translateX(-50%) translateY(-50%);
  overflow: hidden;
}

Hello World

"Hello world" - невелика і проста перемога. Для нас це просто означало отримати щось виведене на екрані. Спочатку ми інстанціювали новий об'єкт Snap, з DOM ID представленим як наше полотно. Ми використовуємо функцію Snap.load, щоб вказати зовнішнє джерело SVG і анонімний зворотний callback, який буде додавати вузли в дереві DOM.


<div></div>

<script>
  (function() {
    var s = Snap('#canvas');

    Snap.load("svg/backpack.svg", function (data) {
      s.append(data);
    });
  })();
</script>

Створення простого плагіну

Для того, щоб зробити повторно використовуваний компонент для декількох анімацій, ми створюємо "плагін", використовуючи патерн прототипу. Використання immediately invoked function expression (IIFE) (функція, яка запускається після того, як була оголошена) забезпечує інкапсуляцію даних, при цьому додаючи SVGAnimation в глобальний простір імен. Якщо ми помістимо код, який ми маємо, у функцію init, ми матимемо основу для SVGAnimation.

; (function(window) {
'use strict';

var svgAnimation = function () {
  var self = this;
  self.init();
};

svgAnimation.prototype = {
  constructor: svgAnimation,

  init: function() {
    var s = Snap('#canvas');

    Snap.load("svg/backpack.svg", function (data) {
      s.append(data);
    });
  }
};

// Додати в глобальний namespace
window.svgAnimation = svgAnimation;
})(window);

{full-post-img}

Додавання опцій

Розбираючи Snap.load, ми можемо бачити два потенційні параметри, які можуть бути передані як опції, полотно і зовнішні джерела SVG. Давайте створимо окрему функцію loadSVG для цього.

/*
Завантажує SVG у DOM
@param {Object}   canvas
@param {String}   svg
*/
loadSVG: function(canvas, data) {
Snap.load(svg, function(data) {
  canvas.append(svg);
});
}

ОБ'ЄКТИ ЯК ПАРАМЕТРИ

Тепер нам потрібен спосіб, щоб передати ці опції в SVGAnimation. Є кілька способів зробити це, стандартний спосіб - передавати окремі параметри.

var backpack = new svgAnimation(Snap('#canvas'), 'svg/backpack.svg');

Але є краще рішення. Передаючи об'єкти замість цього, код стає не тільки читабельнішим, але й більш гнучким. Нам більше не потрібно стежити за порядком; ми можемо зробити параметри необов'язковими; і ми також можемо повторно використовувати об'єкт пізніше. Отже, давайте перепишемо попередній сніпет, передаючи об'єкт options.

var backpack = new svgAnimation({
canvas:       new Snap('#canvas'),
svg:          'svg/backpack.svg'
});

ЗЛИТТЯ ОБ'ЄКТІВ

Тепер, коли у нас є об'єкт options, нам потрібно зробити його значення доступними для решти плагіну. Але перш ніж ми це зробимо, давайте об'єднаємо переданий об'єкт з нашими власними дефолтами. Навіть якщо ми вирішили дати обом параметрам значення null, ми все одно включимо їх для довідки щодо типу параметрів, які ми хочемо отримати.

svgAnimation.prototype = {
constructor: svgAnimation,

options: {
  canvas:     null,
  svg:        null
}
};

З встановленими дефолтами, тепер ми будемо використовувати функцію extend, щоб об'єднати два об'єкти. По суті, функція пройде через всі властивості одного об'єкта і скопіює їх в інший об'єкт.

/*
Об'єднує два об'єкти
@param  {Object}  a
@param  {Object}  b
@return {Object}  sum
http://stackoverflow.com/questions/11197247/javascript-equivalent-of-jquerys-extend-method
*/
function extend(a, b) {
for (var key in b) {
  if (b.hasOwnProperty(key)) {
    a[key] = b[key];
  }
}

return a;
}

З оголошеною функцією extend, давайте змінимо конструктор SVGAnimation. Ви може помітите, що self присвоюється this. Ми будемо кешувати оригінальний this, щоб впевнитись, що внутрішні області мають доступ до даних і методів поточного об'єкту.

var svgAnimation = function (options) {
var self = this;
self.options = extend({}, self.options);
extend(self.options, options);
self.init();
}

І нарешті, ми оновимо init для виклику loadSVG, передаючи canvas і svg посилання, які ми встановили в момент інстанціації.

init: function() {
var self = this;

self.loadSVG(self.options.canvas, self.options.svg);
}

{full-post-img}

Хардкодовий Прототип

ДОДАВАННЯ ГРУП SVG ТРАНСФОРМАЦІЙ

Як згадувалося раніше, анімаційний рушій Snap.svg є досить примітивними і, як CSS, підтримує перетворення рядків тільки як один запит. Це означає, що якщо ви хочете анімувати більше одного типу трансформацій, то це повинно відбуватися або послідовно, або все відразу (розділяючи час та сповільнення). Хоча це і не найелегантніше рішення, додавання додаткових вузлів в дереві DOM вирішує цю проблему. З окремо згрупованим елементом для кожного зміщення, обертання і масштабування, ми можемо тепер самостійно контролювати кожен рух. Приклад, який найкращим чином ілюструє цей випадок є застібка-блискавка, яка також служить в якості нашого першого прототипу.

Ми починаємо з передачі елементу zipper функції createTransformGroup, яку ми потім оголосимо.

var $zipper = canvas.select("#zipper");
self.createTransformGroup($zipper);

Після того, як ми вибрали всі дочірні вузли, ми можемо використати функцію Snap.g, щоб розмістити контент в кожній відповідній групі трансформацій.


createTransformGroup: function(element) {
if (element.node) {
  var childNodes = element.selectAll('*');

  element.g().attr('class', 'translate')
    .g().attr('class', 'rotate')
    .g().attr('class', 'scale')
    .append(childNodes);
}
}

Це призводить до створення незалежних груп трансформацій, які ми можемо застосувати в нашій анімації.


<g id="zipper">
<path d="…" fill="#272C40">
</path></g>


<g id="zipper">
<g class="translate">
  <g class="rotate">
    <g class="scale">
      <path d="…" fill="#272C40"></path>
    </g>
  </g>
</g>
</g>

АНІМАЦІЯ SNAP.SVG

Ми нарешті готові анімувати наш перший елемент. Snap.svg надає дві функції, щоб зробити це: transform і animate. Ми будемо використовувати transform, щоб помістити анімацію в перший keyframe, а потім застосуємо animate.

Snap.svg підтримує стандартну SVG нотацію трансформацій, але ми вирішили замість цього використовувати рядки трансформації(transform strings), щоб оголосити параметри. На офіційному сайті про це написано небагато, але попередню документацію можна знайти на Raphael. Перша велика літера є абревіатурою трансформації. Параметри х, у і angle представляють значення, до яких ми анімуємо, з початковим положенням (оріджин) сх та cy.

// Масштабування
Snap.animate({transform: 'S x y cx cy'}, duration, easing, callback);

// Обертання
Snap.animate({transform: 'R angle cx cy'}, duration, callback);

// Переміщення
Snap.animate({transform: 'T x y'}, duration, callback);

РОЗРАХУНОК ОРІДЖИНІВ

Ми зіткнулися з цікавою проблемою з визначенням оріджинів. У Snap.svg, функції animate та transform приймають тільки значення пікселів в якості параметрів, що робить надзвичайно важкою роботу з вимірами. В ідеалі, ми хотіли оголосити оріджин як комбінацію top, right, bottom, left та center.

На щастя, Snap.svg має getBBox, який вимірює обмежувальну рамку будь-якого елемента, повертаючи безліч дескрипторів, в тому числі значення, які нам потрібні. Напишемо дві функції, getOriginX і getOriginY, які приймають об'єкт bBox і рядок direction в якості параметрів, та повертають значення пікселів.

/*
Переводить горизонтальний оріджин з рядка у значення пікселів
@param {Object}     Snap bBox
@param {String}     "left", "right", "center"
@return {Object}    pixel value
*/

getOriginX: function (bBox, direction) {
if (direction === 'left') {
  return bBox.x;
}

else if (direction === 'center') {
  return bBox.cx;
}

else if (direction === 'right') {
  return bBox.x2;
}
},

/*
Переводить вертикальний оріджин з рядка у значення пікселів
@param {Object}     Snap bBox
@param {String}     "top", "bottom", "center"
@return {Object}    pixel value
*/

getOriginY: function (bBox, direction) {
if (direction === 'top') {
  return bBox.y;
}

else if (direction === 'center') {
  return bBox.cy;
}

else if (direction === 'bottom') {
  return bBox.y2;
}
}

АНІМАЦІЯ НА ПРАКТИЦІ

Давайте побачимо все це на практиці з масштабуванням. Ми спочатку вибрали відповідну групу трансформацій, використовуючи її ім'я класу, зменшили, поки все не зникне, а потім збільшили до початкового розміру. Ви помітите, що ми масштабуємо з верхньої частини блискавки, з тривалістю 400 мілісекунд, і налаштовуємо сповільнення на easeOutBack.

// Твін Масштабування
var $scaleElement = $zipper.select('.scale');
var scaleBBox = $scaleElement.getBBox();
$scaleElement.transform('S' + 0 + ' ' + 0 + ' ' + self.getOriginX(scaleBBox, 'center') + ' ' + self.getOriginY(scaleBBox, 'top'));
$scaleElement.animate({transform: 'S' + 1 + ' ' + 1 + ' ' + self.getOriginX(scaleBBox, 'center') + ' ' + self.getOriginY(scaleBBox, 'top')}, 400, mina['easeOutBack']);

Обертання по тій же схемі, з деякими складнощами. У цьому випадку, у нас є три анімації, які запускаються послідовно. Коли кожна анімація закінчується, ми використовуємо її callback-функцію для запуску наступної анімації в черзі.

// Твін Обертання
var $rotateElement = $zipper.select('.rotate');
var rotateBBox = $rotateElement.getBBox();
$rotateElement.transform('R' + 45 + ' ' + rotateBBox.cx + ' ' + rotateBBox.cy);

$rotateElement.animate({ transform: 'R' + -60 + ' ' + self.getOriginX(rotateBBox, 'center') + ' ' + self.getOriginY(rotateBBox, 'top')}, 400, mina['easeOutBack'], function() {
$rotateElement.animate({ transform: 'R' + 30 + ' ' + self.getOriginX(rotateBBox, 'center') + ' ' + self.getOriginY(rotateBBox, 'top')}, 400, mina['easeOutBack'], function() {
  $rotateElement.animate({ transform: 'R' + 0 + ' ' + self.getOriginX(rotateBBox, 'center') + ' ' + self.getOriginY(rotateBBox, 'top')}, 400, mina['easeInOutBack']);
});
});

Твін переміщування повторює як масштабування, так і обертання, з однією відмінністю. Оскільки анімація переміщування не починається відразу, ми використовуємо setTimeout, щоб відкласти час запуску на 400 мілісекунд.

// Твін Переміщення
var $translateElement = $zipper.select('.translate');
$translateElement.transform('T' + 110 + ' ' + 0);

setTimeout(function() {
$translateElement.animate({ transform: 'T' + 0 + ' ' + 0 }, 600, mina['easeOutQuint']);
}, 400);

{full-post-img}

Keyframes

На даний момент, ви можете бути здивовані, "Ну, це було досить складним для такої простої анімації." Ми не згодні.

Наша мета полягала в тому, щоб відтворити процес керований даними (data-driven process), щоб швидко створювати прототипи анімацій. Створюючи окремий клас твіну і вводячи keyframe-концепції, ми можемо перейти від коду, як тут ...

// Твін Переміщення
var $translateElement = $zipper.select('.translate');
$translateElement.transform('T' + 110 + ' ' + 0);

setTimeout(function() {
$translateElement.animate({ transform: 'T' + 0 + ' ' + 0 }, 600, mina['easeOutQuint']);
}, 400);

...до коду, як тут:

// Твін Переміщення
new svgTween({
element: $zipper.select('.translate'),
keyframes: [
  {
    "step": 2,
    "x": 110,
    "y": 0
  },

  {
    "step": 5,
    "x": 0,
    "y": 0,
    "easing": "easeOutQuint"
  }
],
duration: 2000/10
});

Так як ми поділити кожну анімацію на окремі кроки, ми можемо бачити, як цей формат може зробити прототипування простіше. Давайте розберемо параметри цього твіну переміщування і пояснимо, звідки ці цифри беруться.

У нашому вихідному коді, ви могли помітити, що тривалість і затримки були поділені на коефіцієнт в 200 мілісекунд. Це не було випадковістю. Якщо вся анімація триває 2000 мілісекунд і складається з 10 кроків, нам просто потрібно розділити перше на останнє для розрахунку тривалості одного кроку. Тепер ми можемо слідувати тій самій логіці, щоб визначити, чому keyframes починаються на кроці 2 і закінчуються на кроці 5. setTimeout, яка триває 400 мілісекунд, відповідає двом крокам, початкової затримки. Крім того, тривалість анімації становить 600 мілісекунд, що розраховується на три кроки, різниця між кроками 2 і 5.

svgTween: Переміщення

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

/*
svgTween.js v1.0.0
Licensed under the MIT license.
http://www.opensource.org/licenses/mit-license.php

Copyright 2015, Smashing Magazine
http://www.smashingmagazine.com/
http://www.hellomichael.com/
*/

; (function(window) {
'use strict';

var svgTween = function (options) {
  var self = this;
  self.options = extend({}, self.options);
  extend(self.options, options);
  self.init();
};

svgTween.prototype = {
  constructor: svgTween,

  options: {
    element:    null,
    keyframes:  null,
    duration:   null
  },

  init: function () {
    var self = this;
  }
};

/*
  Об'єднує два об'єкти
  @param {Object} a
  @param {Object} b
  @return {Object} sum
  http://stackoverflow.com/questions/11197247/javascript-equivalent-of-jquerys-extend-method
*/
function extend(a, b) {
  for (var key in b) {
    if (b.hasOwnProperty(key)) {
      a[key] = b[key];
    }
  }

  return a;
}

// Додати до namespace
window.svgTween = svgTween;
})(window);

Використовуючи той самий алгоритм, як і раніше, ми поставимо анімацію в перший початковий прихований стан, а потім почнемо анімувати звідти. Замість того, щоб використовувати функції Snap.svg transform і animate, ми перепишемо їх як resetTween і playTween для натомість обробки keyframes.

resetTween прийматиме елемент і масив keyframes. Єдина відмінність полягає в тому, що замість безпосереднього оголошення значень у рядку transform , ми будемо використовувати значення в першому keyframe.

/*
Перезапускає анімацію до першого keyframe

@param {Object} element
@param {Array}  keyframes
*/
resetTween: function (element, keyframes) {
var self = this;

var translateX = keyframes[0].x;
var translateY = keyframes[0].y;

element.transform('T' + translateX + ',' + translateY);
}

Оскільки Snap.svg не надає ланцюгових методів анімації, ми повинні будемо використовувати зворотні виклики (callback) для послідовних анімацій.

Snap.animation(attr, duration, [easing], [callback]);

Проте, це миттєво стає непередбачуваним, якщо у нас є більше двох keyframes, по суті, перетворюючи код у форму callback-пекла. Для того, щоб впоратися з цією проблемою, ми імплементуємо playTween як рекурсивну функцію, що дозволяє нам перебрати анімації без всякої плутанини.

Давайте почнемо з оголошення параметрів в нашій анімації. Так само, як з resetTween, ми встановимо значення в нашому рядку transform на значення keyframe. Сповільнення робиться таким самим способом. Тривалість буде або поставлено на паузу, що поверне все до першої анімації, або буде розраховуватись як проміжок часу між кроками.

/*
Рекурсивний цикл через keyframes, щоб створити паузи або твіни

@param {Object} element
@param {Array}  keyframes
@param {Int}    duration
@param {Int}    index
*/
playTween: function(element, keyframes, duration, index) {
var self = this;

// Встановити keyframes до яких ми переходимо 
var translateX = keyframes[index].x;
var translateY = keyframes[index].y;

// Встановити сповільнюючий параметр
var easing = mina[keyframes[index].easing];

// Встановити тривалість як початкову паузу або різницю кроків між keyframes
var newDuration = index ? ((keyframes[index].step - keyframes[(index-1)].step) * duration) : (keyframes[index].step * duration);
}

З готовими параметрами, давайте напишемо умовні оператори, які ставлять на паузу, запускають або припиняють анімацію. Наш перший умовний оператор перевіряє, чи починається анімація відразу на кроці 0. Якщо так, ми будемо рухатися далі, тому що функція transform вже обробляє цей перший keyframe. Якби ми спробували анімувати до тих самих значень, як resetTween, ми би іноді бачили коротке мерехтіння, баг, який би зайняв пару років, щоб знайти його та зафіксити. Наступні два умовні оператори перевіряють, чи нам слід відкласти анімацію або запустити її. Єдине, що слід відзначити, є використання вкладених умовних операторів, які перевіряють, чи повинна рекурсивна функція знову працювати. Без них playTween буде працювати безкінечно.

// Негайно запустити перший твін якщо почався крок 0
if (index === 0 && keyframes[index].step === 0) {
self.playTween(element, keyframes, duration, (index + 1));
}

// Або поставити твін на паузу якщо початковий keyframe
else if (index === 0 && keyframes[index].step !== 0) {
setTimeout(function() {
  if (index !== (keyframes.length - 1)) {
    self.playTween(element, keyframes, duration, (index + 1));
  }
}, newDuration);
}

// Або анімувати твіни, якщо keyframes існують
else {
element.animate({
  transform: 'T' + translateX + ' ' + translateY
}, newDuration, easing, function() {
  if (index !== (keyframes.length - 1)) {
    self.playTween(element, keyframes, duration, (index + 1));
  }
});
}

Останнім кроком буде оновити нашу функцію init, щоб викликати resetTween та playTween.

init: function () {
var self = this;

self.resetTween(self.options.element, self.options.keyframes);
self.playTween(self.options.element, self.options.keyframes, self.options.duration, 0);
}

{full-post-img}

svgTween: Обертання і Масштабування

До нашої блискавки, яка рухається справа наліво, прийшов час додати обертання і масштабування. Давайте змінимо наші опції, щоб включити type, originX і originY. Так як svgTween тепер буде обробляти всі трансформації, ми включимо змінну type, щоб вказати, яку саме. Ми також будемо відстежувати originX і originY, щоб встановити правильні transform-originдля масштабування і обертання. Переміщення ніколи не впливає на transform-origin, тому воно завжди встановлюється на сenter center по дефолту.

options: {
element:    null,
type:       null,
keyframes:  null,
duration:   null,
originX:    null,
originY:    null
}

Давайте оновимо resetTween і playTween, щоб обробляти ці нові значення. Ми спочатку перевіримо тип, а потім побудуємо відповідні transform-рядки. Ми створимо окремі змінні translateX, translateY, rotationAngle, Scalex і ScaleY, щоб візуально ідентифікувати, як генеруються наші рядки трансформування.

/*
Перезапускає анімацію до першого keyframe

@param {Object} element
@param {String} type - "scale", "rotate", "translate"
@param {Array}  keyframes
@param {String} originX - "left", "right", "center"
@param {String} originY - "top", "bottom", "center"
*/
resetTween: function (element, type, keyframes, originX, originY) {
var transform, translateX, translateY, rotationAngle, scaleX, scaleY;

if (type === 'translate') {
  translateX = keyframes[0].x;
  translateY = keyframes[0].y;
  transform = 'T' + translateX + ' ' + translateY;
}

else if (type === 'rotate') {
  rotationAngle = keyframes[0].angle;
  transform = 'R' + rotationAngle + ' ' + originX + ' ' + originY;
}

else if (type === 'scale') {
  scaleX = keyframes[0].x;
  scaleY = keyframes[0].y;
  transform = 'S' + scaleX + ' ' + scaleY + ' ' + originX + ' ' + originY;
}

element.transform(transform);

Ми будемо імітувати той сами патерн в playTween, замінюючи відповідний індекс з рекурсивної функції. Ми також оновимо виклики function новими параметрами type, originX і originY.

/*
Рекурсивний цикл через keyframes, щоб створити паузи і твіни

@param {Object} element
@param {String} type - "scale", "rotate", "translate"
@param {Array}  keyframes
@param {String} originX - "left", "right", "center"
@param {String} originY - "top", "bottom", "center"
@param {Int}    duration
@param {Int}    index
*/
playTween: function(element, type, keyframes, originX, originY, duration, index) {
var self = this;

// Set keyframes we're transitioning to
var transform, translateX, translateY, rotationAngle, scaleX, scaleY;

if (type === 'translate') {
  translateX = keyframes[index].x;
  translateY = keyframes[index].y;
  transform = 'T' + translateX + ' ' + translateY;
}

else if (type === 'rotate') {
  rotationAngle = keyframes[index].angle;
  transform = 'R' + rotationAngle + ' ' + originX + ' ' + originY;
}

else if (type === 'scale') {
  scaleX = keyframes[index].x;
  scaleY = keyframes[index].y;
  transform = 'S' + scaleX + ' ' + scaleY + ' ' + originX + ' ' + originY;
}

// Встановити сповільнюючий параметр
var easing = mina[keyframes[index].easing];

// Встановити тривалість початкової паузи або різниці кроків між keyframes
var newDuration = index ? ((keyframes[index].step - keyframes[(index-1)].step) * duration) : (keyframes[index].step * duration);

// Пропустити перший твін, якщо анімація негайно починається з кроку 0 
if (index === 0 && keyframes[index].step === 0) {
  self.playTween(element, type, keyframes, originX, originY, duration, (index + 1));
}

// Або поставити твін на паузу, якщо перший keyframe
else if (index === 0 && keyframes[index].step !== 0) {
  setTimeout(function() {
    if (index !== (keyframes.length - 1)) {
      self.playTween(element, type, keyframes, originX, originY, duration, (index + 1));
    }
  }, newDuration);
}

// Або анімувати твіни, якщо keyframes існують
else {
  element.animate({
    transform: transform
  }, newDuration, easing, function() {
    if (index !== (keyframes.length - 1)) {
      self.playTween(element, type, keyframes, originX, originY, duration, (index + 1));
    }
  });
}
}

І нарешті, ми оновимо нашу функцію init, щоб оголосити type, originX і originY, перед викликом resetTween і playTween. Ми можемо оголосити type, просто прийнявши клас вхідного елемента. Так ми можемо трансферувати через getOriginX і getOriginY з SVGAnimation. Потім ми використовуємо тернарний оператор, щоб встановити наш оріджин, center по дефолту, якщо значення не оголошено.

init: function () {
var self = this;

// Встановити тип
self.options.type = self.options.element.node.getAttributeNode('class').value;

// Встановити bbox на специфічний елемент трансформації (.translate, .scale, .rotate)
var bBox = self.options.element.getBBox();

// Встановити оріджин на специфічний або на центр по дефолту
self.options.originX = self.options.keyframes[0].cx ? self.getOriginX(bBox, self.options.keyframes[0].cx) : self.getOriginX(bBox, 'center');
self.options.originY = self.options.keyframes[0].cy ? self.getOriginY(bBox, self.options.keyframes[0].cy) : self.getOriginY(bBox, 'center');

// Перезапустити твін 
self.resetTween(self.options.element, self.options.type, self.options.keyframes, self.options.originX, self.options.originY);
self.playTween(self.options.element, self.options.type, self.options.keyframes, self.options.originX, self.options.originY, self.options.duration, 0);
}

Давайте завершимо нашу анімацію блискавки інстанціюванням нових твінів для обертання і масштабування. Як і в переміщенні, ми можемо обчислити keyframe"и і тривалість за кількістю кроків і загальною тривалістю анімації. Насправді, ми оголосили всі ці параметри більш органічно: при реальній роботі анімації і знаходилися ці числа.

// Твін Обертання
new svgTween({
element: $zipper.select('.rotate'),
keyframes: [
  {
    "step": 0,
    "angle": 45,
    "cy": "top"
  },

  {
    "step": 2,
    "angle": -60,
    "easing": "easeOutBack"
  },

  {
    "step": 4,
    "angle": 30,
    "easing": "easeOutQuint"
  },

  {
    "step": 6,
    "angle": 0,
    "easing": "easeOutBack"
  }
],
duration: duration
});
// Твін Масштабування
new svgTween({
element: $zipper.select('.scale'),
keyframes: [
  {
    "step": 0,
    "x": 0,
    "y": 0,
    "cy": "top"
  },

  {
    "step": 2,
    "x": 1,
    "y": 1,
    "easing": "easeOutBack"
  }
],
duration: duration
});

{full-post-img}

JSON Config

Останній крок нашої розробки є вилучення хардкодових значень з SVGAnimation і додавання їх у наш конструктор. Давайте додамо keyframes, duration і кількість steps, в інстанціацію.

(function() {
var backpack = new svgAnimation({
  canvas:       new Snap('#canvas'),
  svg:          'svg/backpack.svg',
  data:         'json/backpack.json',
  duration:     2000,
  steps:        10
});
})();

Передавши JSON-файл, щоб оголосити keyframe"и, дизайнер може відразу створити прототип без необхідності занурюватися в документацію. Насправді, ця концепція може бути повністю бібліотечно-агностичною, якщо замінити Snap.svg на GSAP, Mo.js або Web Animations API.

JSON-файл відформатований в окремі твіни, які складаються з ID і keyframes. Ми приводимо анімацію блискавки в якості прикладу, але файл backpack.json включає в себе масиви для всіх елементів (блискавка, кишені, логотип і т.д.).

{
"animations": [
  {
    "id": "#zipper",
    "keyframes": {
      "translateKeyframes": [
        {
          "step": 6,
          "x": 110,
          "y": 0
        },

        {
          "step": 9,
          "x": 0,
          "y": 0,
          "easing": "easeOutQuint"
        }
      ],

      "rotateKeyframes": [
        {
          "step": 4,
          "angle": 45,
          "cy": "top"
        },

        {
          "step": 6,
          "angle": -60,
          "easing": "easeOutBack"
        },

        {
          "step": 8,
          "angle": 30,
          "easing": "easeOutQuint"
        },

        {
          "step": 10,
          "angle": 0,
          "easing": "easeOutBack"
        }
      ],

      "scaleKeyframes": [
        {
          "step": 4,
          "x": 0,
          "y": 0,
          "cy": "top"
        },

        {
          "step": 6,
          "x": 1,
          "y": 1,
          "easing": "easeOutBack"
        }
      ]
    }
  }
]
}
options: {
data:                 null,
canvas:               null,
svg:                  null,
duration:             null,
steps:                null
}

Подробиці про те, як завантажити JSON-файл виходить за рамки даної статті, але важливим є використання callback-функції для повернення JSON-даних для подальшого використання - в нашому випадку, передаючи масив анімацій у loadSVG.

/*
Отримати JSON-дані та заповнити опції
@param {Object}   data
@param {Function} callback
*/
loadJSON: function(data, callback) {
var self = this;

// XML request
var xobj = new XMLHttpRequest();
xobj.open('GET', data, true);

xobj.onreadystatechange = function() {
  // Success
  if (xobj.readyState === 4 && xobj.status === 200) {
    var json = JSON.parse(xobj.responseText);

    if (callback && typeof(callback) === "function") {
      callback(json);
    }
  }
};

xobj.send(null);
}

Тепер ми можемо оновити loadSVG щоб перебрати масив animations, динамічно створюючи svgTweens. Якщо оголошено щось з translateKeyframes, rotateKeyframes або scaleKeyframes, ми створюємо новий svgTween, передаючи keyframe"и і тривалість з нашого файлу options.

loadSVG: function(canvas, svg, animations, duration) {
var self = this;

Snap.load(svg, function(data) {
  // Помістити SVG в DOM
  canvas.append(data);

  // Створити твіни для кожної анімації
  animations.forEach(function(animation) {
    var element = canvas.select(animation.id);

    // Створити групи масштабування, обертання та трансформування навколо SVG-ноди
    self.createTransformGroup(element);

    // Створити твіни, які базуються на keyframes
    if (animation.keyframes.translateKeyframes) {
      self.options.tweens.push(new svgTween({
        element: element.select('.translate'),
        keyframes: animation.keyframes.translateKeyframes,
        duration: duration
      }));
    }

    if (animation.keyframes.rotateKeyframes) {
      self.options.tweens.push(new svgTween({
        element: element.select('.rotate'),
        keyframes: animation.keyframes.rotateKeyframes,
        duration: duration
      }));
    }

    if (animation.keyframes.scaleKeyframes) {
      self.options.tweens.push(new svgTween({
        element: element.select('.scale'),
        keyframes: animation.keyframes.scaleKeyframes,
        duration: duration
      }));
    }
  });
});
}

Нарешті, ми оновлюємо нашу функцію init, щоб викликати loadJSON, яка в свою чергу викликає loadSVG.

init: function() {
var self = this;

self.loadJSON(self.options.data, function (data) {
  self.loadSVG(self.options.canvas, self.options.svg, data.animations, (self.options.duration/self.options.steps));
});
}

{full-post-img}

Щодо продуктивності

Наша мета полягала в тому, щоб побачити, як далеко ми можемо піти з SVG-анімаціями; тому ми вважали анімацію важливіше, ніж продуктивність. Це дозволило нам просунути наші анімації набагато далі, ніж очікувалося. Проте, ми не повністю залишили без уваги продуктивність.

Дивлячись на графік Chrome DevTools, ми бачимо, що анімація працює з постійною швидкістю 60 кадрів в секунду, з декількома затримками всередині. Якщо ми розіб'ємо анімацію портфеля, то отримаємо 19 елементів з 3-х можливих трансформацій. Це означає, що в гіршому випадку, є 57 можливих твінів, які відбуваються одночасно. На щастя, це не так, тому що твіни розходяться протягом роботи анімації. Ми можемо візуально побачити це в графі CPU, піки, де анімації перекриваються найбільше, а потім зменшуються в міру закінчення кожного твіну. Візуально, Firefox і Internet Explorer були в змозі програвати анімацію без будь-яких помітних відмінностей в продуктивності.

SVG

Рис. 10 Графік від Chrome DevTools, який показує використання CPU і частоту кадрів для десктопу.

Як і слід було очікувати, мобільні пристрої отримали удар по продуктивності. Використання віддаленого дебагінгу на старому пристрої Android, наша частота кадрів знизилася з 60 кадрів в секунду до 30-60. Хоч не досконально, ми відчували, що це було більш ніж задовільним для наших потреб. Однак наші останні тести на iPhone 5 з iPhone 6 показали бездоганні результати.

SVG

Рис. 11. Віддалений дебагінг на Android, який показує меншу продуктивність на мобільних девайсах.

Що далі?

На жаль, ця кампанія була припинена, тому ми не мали можливість зануритися глибше в проект. Але нам хотілося б звернулися до кількох ключових питань.

КЕРОВАНІСТЬ ПОДІЯМИ (EVENT-DRIVEN)

Наш вставлений Codepen код забезпечує кнопку "rerun", але наша реалізація не керована подіями. В ідеалі, анімація не буде відразу відтворюватися, поки вона не ініційована за допомогою деякого типу взаємодії (кліку миші, вейпойнтів і т.д.).

МОБІЛЬНІ ДЕВАЙСИ

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

FALLBACK"и

Рішення для наших анімацій працює у всіх сучасних браузерах і було протестовано в Internet Explorer 9+, Firefox і Chrome. Це в першу чергу завдяки підтримці Snap.svg. Якщо ваш проект вимагає використання старих браузерів, ви можете спробувати використовувати попередник Snap.svg, Raphael.

Вихід

Ну, тепер ви це маєте, від простої ілюстрації до складної анімації. Ви можете завантажити код на GitHub.

SVG {full-post-img}

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 6.2K
Приєднався: 7 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація