Плоский дизайн все більше стає трендом 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
).
Рис. 2. Дев'ять можливих положень анімації, використовуючи комбінації left, right, center, top та bottom.
ТАЙМІНГ
Для того, щоб підтримувати однаковий такт і ритм, ми обмежили себе у дуже конкретне інкрементування часу. Анімації тривали 2 секунди і включали в себе 10 окремих кроків. Твін (tween), кожне перетворення (переміщення, обертання і масштабування), повинен був починатися і закінчуватися на одному з цих етапів, які ми оголосили як keyframes.
Рис. 3. Приклад анімації, в якій кожен крок стає довшим на 200 мілісекунд за попередній з трьома твінами, які перекривають один одного.
СПОВІЛЬНЕННЯ
У той час як трансформацій і таймінгу вже достатньо, щоб створити візуальне сприйняття руху, сповільнення приводить все в життя. Ми виявили, що три формули сповільнення забезпечують варіації, щоб додати характеру рухам: easeOutBack
, easeInOutBack
і easeOutQuint
.
Рис. 4. Візуальне порівняння анімацій з і без сповільнення. Слід зазначити, що використання будь-якої варіації easingBack
впливатиме на трансформацію в деякій мірі.
Початок
ПІДГОТОВКА ДЕТАЛЕЙ
Хоча існує багато додатків для створення ілюстрацій, наприклад Sketch та Inkscape, які стали достатньо популярними в ці дні, ми вирішили створити наші SVG в Adobe Illustrator.
Рис. 5. Елементи, з яких складається наша анімація.
Рис. 6. Illustrator автоматично створює ID з назв елементів при експортуванні в SVG.
Перед тим, як експортувати в SVG, згрупуйте і дайте назву кожному елементу. Illustrator автоматично створить ID з цих назв в процесі експорту. Для кожного анімованого елемента, вивід повинен виглядати як XML, показаний нижче. Зверніть увагу, що навіть якщо елемент не має нащадків, він все одно повинен бути згрупований під тегом g
.
<g id="zipper">
<path d="…" fill="#272C40">
</path></g>
Рис. 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>
Рис. 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 в наш проект. Ми також включимо порт функцій сповільнення Роберта Пеннера для подальшого використання.
Рис. 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," GitHub
"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>
Створення простого плагіну
- Плагін, GitHub
Для того, щоб зробити повторно використовуваний компонент для декількох анімацій, ми створюємо "плагін", використовуючи патерн прототипу. Використання 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}
Додавання опцій
- Опції, GitHub
Розбираючи 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}
Хардкодовий Прототип
- Хардкодовий прототип, GitHub
ДОДАВАННЯ ГРУП 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: Обертання і Масштабування
- Tween: Обертання і Масштабування, GitHub
До нашої блискавки, яка рухається справа наліво, прийшов час додати обертання і масштабування. Давайте змінимо наші опції, щоб включити 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
- JSON, GitHub
Останній крок нашої розробки є вилучення хардкодових значень з 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 були в змозі програвати анімацію без будь-яких помітних відмінностей в продуктивності.
Рис. 10 Графік від Chrome DevTools, який показує використання CPU і частоту кадрів для десктопу.
Як і слід було очікувати, мобільні пристрої отримали удар по продуктивності. Використання віддаленого дебагінгу на старому пристрої Android, наша частота кадрів знизилася з 60 кадрів в секунду до 30-60. Хоч не досконально, ми відчували, що це було більш ніж задовільним для наших потреб. Однак наші останні тести на iPhone 5 з iPhone 6 показали бездоганні результати.
Рис. 11. Віддалений дебагінг на Android, який показує меншу продуктивність на мобільних девайсах.
Що далі?
На жаль, ця кампанія була припинена, тому ми не мали можливість зануритися глибше в проект. Але нам хотілося б звернулися до кількох ключових питань.
КЕРОВАНІСТЬ ПОДІЯМИ (EVENT-DRIVEN)
Наш вставлений Codepen код забезпечує кнопку "rerun", але наша реалізація не керована подіями. В ідеалі, анімація не буде відразу відтворюватися, поки вона не ініційована за допомогою деякого типу взаємодії (кліку миші, вейпойнтів і т.д.).
МОБІЛЬНІ ДЕВАЙСИ
У той час як ці анімації дійсно працюють на мобільних пристроях, як уже згадувалося, вони дуже навантажують процесор. Прийміть до уваги їх важливість в загальному дизайні вашого проекту.
FALLBACK"и
Рішення для наших анімацій працює у всіх сучасних браузерах і було протестовано в Internet Explorer 9+, Firefox і Chrome. Це в першу чергу завдяки підтримці Snap.svg. Якщо ваш проект вимагає використання старих браузерів, ви можете спробувати використовувати попередник Snap.svg, Raphael.
Вихід
Ну, тепер ви це маєте, від простої ілюстрації до складної анімації. Ви можете завантажити код на GitHub.
{full-post-img}
Ще немає коментарів