#фреймер_кейс №1: Бесшовный переход между экранами

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

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

Он о сложной анимации в Framer Studio. Написан доступным языком для начинающих изучать Фреймер. Я комментирую каждую строку кода и объясняю базовые понятия: импорт, массивы, цикл, свойства, ивенты, состояния, дилей. Много примеров кода, которые помогут тебе перестать бояться писать самостоятельно.

Сама задача весьма трудоёмкая. Прежде чем за неё всерьёз браться, я рекомендую разобрать изолированные примеры и пошевелить квадратики в пустом фреймер-проекте.

Этот пост на много вечеров. Он содержит немало решений, на поиск которых у меня уходило много часов. Моя цель здесь — продемонстрировать фреймер-подход к анимации на конкретном законченном кейсе.

Вот получившийся в итоге проект:

Фреймер-проект

Ссылка на Фреймер Клауд, смотреть в Safari или Сhrome

Ты можешь скачать исходник и открыть его в Фреймере. Там много полезных комментариев к коду.

Мы будем дробить сложную задачу на простые и из них постепенно вырастить решение сложной пошагово. Наибольший эффект от поста будет, если ты выделишь время и повторишь всё описанное сам, а не просто пробежишь по заголовкам. Не стоит бездумно копировать код примеров через Copy-Paste, потому что руки должны привыкнуть печатать неудобные символы вроде >, (), [], и “”. Без этого не достичь беглости в Фреймере и в любом другом кодинге.

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

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

Фреймер-чат — твоя группа поддержки

Если у тебя не получается воспроизвести этот урок, задавай вопросы в Фреймер-чате.

Если из этого поста ты поймёшь, что Фреймер вообще не для тебя, это тоже здорово: сэкономишь себе время. Каждому свой инструмент. Не все дизайнеры могут научиться кодить.


Терминология: Слои или группы?

Группы (groups) структурируют макет в Скетче. Термина слои в Скетче нет в принципе. После импорта проекта в Фреймер, группы правильнее называть слоями (layers), поскольку это термин Фреймера. Слои из Скетча ничем не отличаются от слоёв, которые можно создать в самом Фреймере. Это просто прямоугольники с png-изображениями внутри.

План действий

Чтобы создать такой прототип, мы будем действовать пошагово:

  1. Импортируем макет
  2. Зададим скорость анимации и научимся использовать складки
  3. Скроем всё лишнее, что мешает работать с нужными слоями, а заодно научимся использовать массивы и циклы
  4. Реализуем модульность кода, используя очередь анимаций
  5. Сделаем шаблон складки, он поможет в анимации всех блоков
  6. Увеличим экран вчетверо
  7. Сместим блок Проекты
  8. Сделаем зум аватара
  9. Уведём зелёный индикатор у аватара в нулевой размер
  10. Выдвинем серый блок Open Tasks
  11. Проявим и подвинем заголовок Beatrice Harris
  12. Проявим зелёный индикатор у заголовка
  13. Проявим и выдвинем подзаголовок Sales Manager
  14. Сформируем иконку More из трёх точек
  15. Проявим контейнер hours с одометром и графиком
  16. Заанимируем одометр в блоке hours из предыдущего поста
  17. Проявим график, выдвинув его из-под маски
  18. Проявим и выдвинем слово «hours»
  19. Проявим и выдвинем из-за левого края экрана заголовок STATISTICS
  20. А из правого — заголовок Current Week
  21. Проявим блок с пайчартом
  22. Заанимируем одометр в пайчарте
  23. Заанимируем пайчарт, интегрировав его из отдельного проекта
  24. Проявим и выдвинем снизу таблицу статистики.

24 шага — цена красоты, которая будет длиться ровно одну секунду.


Готовимся импортировать

Перед импортом контент в скетч-проекте должен быть особым образом подготовлен, и это самая занудная часть работы. Если названия слоёв в Фреймере или их структуру надо будет менять, удобнее это делать в интерфейсе Скетча. Если возникает потребность изменить дизайн или иерархию слоёв, на любом этапе работы ты всегда можешь вернуться в Скетч, внести изменения и сделать импорт заново. На протяжении работы с этим проектом я импортировал из Скетча обновлённый проект десятки раз, и это нормально. Дизайн остаётся в одной программе, а интерактивность в другой. На выходе ты получаешь колоссальную мощь обеих. В такой гибкости большая прелесть связки Скетч+Фреймер.

Если ты хочешь попрактиковаться и провести эту подготовку слоёв самостоятельно, скачай макет из предыдущего поста (zip, 7,3 mb).

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

Скачать исходники

Ссылка на файл в Скетч-дизайнере

Требования к скетч-файлу:

  1. Все объекты в Скетче складываем в один артборд, разделив содержимое экранов на две группы — profile и projects. Эти группы будут выполнять функцию экранов. В Фреймер без проблем можно импортировать и несколько артбордов, если между ними происходит более простой переход, но это не наш случай. Группа profile должна быть сверху, словно верхняя карта в колоде. В процессе анимации её содержимое станет видимым поверх группы projects.
  2. Оборачиваем в группы все текстовые слои и растровые картинки, которые будем шевелить. Группы после импорта превращаются в слои Фреймера.
  3. Все символы детачим, удаляя их мастер-артборды. Чем больше символов, тем дольше импортируется проект. Если в скетч-проекте много символов и их вообще не удалять, на слабой машине импорт может занять до 40 секунд, что полностью парализует работу. Нормальное время импорта — 3–5 секунд. После детача символы превращаются в группы, которые станут слоями в Фреймере. Если ты непременно хочешь сохранить какие-то символы, их нужно обернуть в группы и убедиться в том что они находятся на странице Symbols, а не на странице, артборды из которой ты импортируешь. Я использовал символы, чтобы задать шкалы одометров и пайчарта. Если символ не обёрнут, мы не сможем к нему обратиться из Фреймера.
  4. Избегаем одинаковых названий в группах. Хотя Фреймер при импорте не позволит создать два слоя с одинаковым названием, желательно контролировать названия. При совпадении Фреймер добавляет одной из групп единицу: testGroup, testGroup1.
  5. В названиях групп используем camelCase, поскольку такова конвенция названий переменных в JavaScript. Не используем пробелы, русские буквы и не начинаем с цифр. 
    Если пробелы оставить, они сконвертируются в нижние подчёркивания: test_group. В принципе, в этом нет криминала. Со временем ты придёшь к тому, что называть слои в стиле camelCase удобнее.
  6. Маскируем тени. Во время импорта объектов, в которых есть тени, тебя ждёт неприятный подводный камень, на который я в своё время потратил немало времени:

Тени могут влиять на размер и координаты слоя

Допустим, у тебя есть группа с названием Group, в которую обёрнут красный квадрат с тенью. В Скетче группа имеет понятные координаты 32, 32. Тень на них не влияет:

Во время импорта в Фреймер группа с тенью будет сохранена как png-файл. Фреймер включит в кадр эту тень, а значит, высота и ширина файла будет больше размера квадрата. Тень создаст нежелательный отступ. Форма сохранённого png-изображения показана синим контуром. Слой Group будет его размера. Поэтому, Фреймер позиционирует Group в области отрицательных значений. А нам остаётся удивляться, какого чёрта х не равен 32, как это было в Скетче. x равен -61.

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

Так мы получим предсказуемый размер группы profile и projects. В нашем случае это координаты 0, 0 и размер 320х568. В Фреймере после импорта в 2х он станет равен 640 х 1136.

Фрейм слоя

У любого слоя в Фреймере можно запросить свойство frame, в котором хранятся сразу четыре свойства: координаты по х, y, ширина и высота. Это свойство для простоты я называю фреймом слоя и часто использую, чтобы сохранять изначальное положение слоя. Чтобы увидеть значения фрейма profile в консоли, пишем команду print:

print profile.frame
Этот белый блок называется консолью Фреймера

Тест пройден, консоль сообщает, что profile имеет правильный размер.


Из-за этих довольно строгих требований для фреймер-прототипа нужно делать отдельный макет. Скетч-файл для Фреймера я традиционно называю prototype.sketch и кладу рядом с папкой фреймер-проекта.

Иерархия групп

В группах Скетча я выстраиваю иерархию, с которой будет удобно работать в Фреймере. Я анализирую каждый объект макета и вспоминаю, будет ли он шевелится. Например, напротив имени Beatrice Harris справа вылетает иконка moreIcon, которая формируется из трёх точек. Поэтому, я каждую точку оборачиваю в отдельную группу. Получившиеся группы кладу в общую группу moreIcon, чтобы в будущем пройти по всем точкам циклом. Если в каком-то блоке не будет анимации, к нему не нужно обращаться, а значит, детализировать названия групп в нём нет смысла.

Как выглядят группы в скетч-проекте:

На экране profile значительно больше движущихся объектов, поэтому в нём больше групп, чем projects.

Итог: файл prototype.sketch готов к импорту.

Шаг 1: Импортируем

Открываем подготовленный скетч-проект. Открываем Фреймер и создаём новый проект.

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

Canvas → Apple iPhone → iPhone 5

Нажимаем Cmd + I и импортируем дизайн из Скетча в размере 2x. Все дальнейшие импорты будем делать клавишей Shift + Cmd + I.

При первом импорте Фреймер автоматом пишет строку:

sketch = Framer.Importer.load(“imported/prototype@2x”)

Это значит, что теперь в Фреймере есть объект sketch, к которому можно обращаться для манипуляций со слоями из Скетча.

Определяем переменную sketch объектом по умолчанию:

Utils.globalLayers(sketch)

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

sketch.projects

Теперь мы сможем обращаться к объекту projects, не дописывая переменную sketch.

Итог: мы успешно импортировали проект в Фреймер.

Организуем код правильно

Комментарии

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

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

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

# после решётки можно писать по-русски

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

Складки кода (folding)

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

Выделить код, затем нажать Cmd + Enter — создать новую складку.

Cmd + Enter — войти в складку, Cmd + Esc — выйти.

Повторный Cmd + Esc — разгладить.

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

Как использовать складки

Без них я бы тратил уйму времени на чтение комментариев и поиск нужных мест. Складки позволяют сфокусироваться на решаемой задаче. Я стараюсь в рамках одной складки писать законченное решение так чтобы на верхнем уровне код состоял только из заголовков.

Комментарии и складки — мощное оружие в борьбе с бардаком и багами. Пиши внятные и короткие комментарии и складывай крупные логические шаги в складки.


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

Шаг 2: задаём глобальную скорость анимации

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

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

animationTime = 2

Её имя может быть любым. Теперь в любое место кода можно будет вставлять animationTime и код будет вести себя так, будто там написано значение 2.

Чтобы задать настройки анимации всех объектов по умолчанию, используем объект Framer, в котором хранятся эти настройки:

Framer.Defaults.Animation =

За длительность анимации в секундах отвечает свойство time. Задаём его на одном таб-отступе, так оно будет относиться к верхней строке:

Framer.Defaults.Animation =
time: animationTime

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

Итог:

  • любая анимация в проекте теперь будет длиться две секунды
  • Мы сделали переменную animationTime, которую будем хитро использовать позже

Шаг 3: Скрываем ненужное

Слои внутри profile загораживают слой projects

Если импорт прошёл успешно, в списке слоёв появился слои profile, projects и все их дети (вложенные слои). Всё выглядит перемешанным. чтобы дальше эффективно работать, нужно скрыть слои, которые сейчас не нужны.

Свойство opacity у каждого слоя содержит его опасность (непрозрачность). Значение может быть от 0 до 1. Чтобы полностью скрыть слой profile, можно написать:

profile.props =
opacity: 0

Другой способ с тем же смыслом:

profile.opacity = 0

Однако, мы хотим отдельно двигать и проявлять детей внутри profile. Скрывать его целиком нет смысла, иначе дети будут скрыты все разом, хотя их opacity останется в значении 1, и мы не сможем их контролировать.

Нам нужно скрыть всех детей profile индивидуально. Мы обратимся к ним и установим каждому слою опасность в 0. Самый тупой и лобовой способ сделать это — написать список всех детей вручную, задавая им опасность.

Так делать не надо, потому что это плохой код:

avatarBig.opacity = 0
openTasks.opacity = 0
moreIcon.opacity = 0
...

Это приемлемо, если детей не более трёх и не хочется включать мозг. Если больше, эту задачу надо решать, используя массив и цикл for-in.

Ключевой момент: Массив и цикл

Здесь я приведу простой, но очень важный пример на Фреймере. Он о том как цикл и массив работают в паре. Поймёшь его — поймёшь Фреймер.

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

Так мы выводим в консоль все значения, которые есть в массиве:

Смотреть в Фреймер Клауде

Складываем все деньги в мешок:

currency = ["рубли", "доллары", "гривны", "евро", "йены", "юани", "лиры", "тенге", "сомони", "сумы"]

Проходим по всем элементам мешка:

for i in currency

Сорим деньгами: выводим все значения в консоль при помощи команды print.

  print "Вот вам " + i

Такой тандем массива и цикла используется в Фреймере постоянно. Ещё одна концепция, которую нужно понять — children.

Свойство children

Если у слоя есть дети, их можно получить в виде массива. Для этого используем свойство children.

profile.children — это массив всех слоёв внутри profile.

По списку слоёв, как и по любому другому массиву, можно пробежать циклом for-in. Цикл сработает ровно столько раз, сколько дочерних слоёв в profile.

Зная, что такое массив children, for-in и opacity, мы обращаемся к каждому элементу внутри profile и скрываем его:

for layer in profile.children
layer.opacity = 0

Итог: все слои-дети у profile стали невидимы.

Шаг 4. Реализуем модульность кода

На этом моменте те, кто знает Фреймер, должны проснуться, потому что начинает происходить что-то интересное. Если ты не очень умеешь в программирование и не хочешь вникать в детали, можно просто пропустить этот шаг и скопировать складку 4.2 из проекта.

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

Стандартная простая фреймер-анимация обычно пишется так:

#слой 1. Подготовка и состояния
#слой 2. --//--
#слой 3. --//--
слой-триггер.кликнуть ->
# поведение слоя 1 при клике
# поведение слоя 2
# поведение слоя 3

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

# складка 1
# слой 1. Подготовка и состояния
# поведение слоя 1
# складка 2
# слой 2. Подготовка и состояния
# поведение слоя 2
# складка 3
# слой 3. Подготовка и состояния
# поведение слоя 3

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

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

В складке 4.1 Создаём два пустых массива.

nextArray = []
backArray = []

В первом мы будем собирать поведение слоёв при переходе на экран Профиль. Во втором — поведение возврата на экран Проекты.

Каждому слою мы будем создавать функции next и back, в которые будем складывать код их поведения. В самом конце проекта будет складка 4.2, в которой все эти функции будут разом запускаться.

Чтобы наполнять массив очереди анимаций, будем использовать метод.push(), в который передадим функцию next текущей складки. После этого действия массив nextArray получит первую ячейку, в которой будет храниться первый next.

Пример простейшего поведения. Делаем функцию, которая выводит команду print:

# создаём функцию next
next = ->
print "функция next сработала"
# кладём её в очередь
nextArray.push(next)

Стрелочка обозначает открытие функции. Код, написанный с одним табом, относится к этой функции и сработает, когда она будет вызвана. Чтобы вызвать её в нужный момент, можно написать так:

next()

Но мы так делать не будем, а сделаем умнее: вызовем её в массиве.

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

nextArray[0]()

Если в ячейке массива действительно хранится функция next, в консоль выведется:

"функция next сработала"

Делаем аватар кликабельным

На этом шаге мы создаём триггер, запускающий смену состояний.

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

Ивент onTap

Мы будем слушать ивент onTap у слоя avatarMin. Это самый часто используемый ивент «по тапу».

avatarMin.onTap ->
print "аватар кликнули"

Запускаем очередь анимации

В ивент .onTap мы положим цикл, который проходит по массиву nextArray и запускает из него все функции next. Если бы в массиве было 4 ячейки, можно было бы написать так:

avatarMin.onTap ->
  for i in [0..3]
nextArray[i]()

На каждой итерации i меняется от 0 до 3. Всего в таком диапазоне будет 4 итерации. i на них будет равен: 0, 1, 2, 3.

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

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

avatarMin.onTap ->
for i in [0..nextArray.length-1]
nextArray[i]()

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

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

Складка 5 состоит из трёх частей: подготовки слоя к появлению на экране, функции next и функции back:

# 5. Шаблон складки
# ==================================================
# 1. Подготовка
# layer.props =
# opacity: 1
# layer.states.profileState =
# opacity: 1
# ================================================== 
# 2. Анимация на экран profile
# next = ->
# print “едем туда”
# кладём анимацию next в очередь
# nextArray.push(next)
# ================================================== 
# 3. Обратная анимация на экран projects
# back = ->
# print “едем обратно”
# кладём анимацию back в очередь на обратный ход
# backArray.push(back)

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

Используй автозамену!

Лайфхак: можно копировать нужную складку в новый фреймер-проект, нажимать Alt + Cmd + F, заменять layer на название реального слоя. Затем, копировать получившийся код в новую складку.


Шаг 6: Учим слой projects увеличиваться от верхнего левого угла

Это первая вкладка, в которой мы анимируем слой.

Копируем весь код из складки 5 и создаём складку 6.

По нажатию на аватар слой projects должен увеличиваться вчетверо.

Подготовка слоя projects: чиним origin

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

Хорошая иллюстрация того чем отличаются разные значения origin:

Отсюда

Это можно сделать через установщик свойств: .props.

В складке 6 снимаем комментарий (Cmd + /) со строки layer.props и дописываем свойства:

layer.props =
originX: 0
originY: 0

Меняем layer на название нужного нам слоя projects. Получается:

projects.props =
originX: 0
originY: 0

Итог: теперь когда мы будем увеличивать слой projects, он будет вести себя прилично, а не разъезжаться во все стороны.

Делаем состояние для слоя projects

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

Пример: допустим, у слоя есть состояния default и long. В первом слой квадратный и синий, а во втором прямоугольный и зелёный. По нажатию они переключаются по кругу, а свойства анимируются:

Смотреть в Фреймер Клауде

По этому же принципу мы делаем слою projects состояние profileState, в котором у него размер в 4 раза больше. Анимироваться будет свойство scale.

Снимаем комментарий со строки:

layer.states.profileState =
opacity: 1

меняем название слоя на projects, удаляем свойство opacity, добавляем scale.

projects.states.profileState =
scale: 4

Переключаем состояния: stateCycle()

Состояния можно переключать по кругу. Чтобы сделать это, внутри функций next или back используем функцию stateCycle(). Она будет срабатывать каждый раз когда мы кликаем на слой avatarMin.

Традиционно смену состояния вешают на ивент:

avatarMin.onTap ->
projects.stateCycle()

В моём варианте код будет выглядеть так:

next = ->
projects.stateCycle()
nextArray.push(next)

Первое нажатие — projects переходит в состояние profileState, второе — возвращается в default.

Пишем предохранитель: проверку на состояния

Чтобы избегать ошибок и быть точно уверенным, что анимация начнётся только если avatarMin будет в состоянии default, во вкладке 4.2. пишем дополнительную проверку. Объявляем переменную state, в которую кладём текущее название состояния. Если оно равно default, запускаем очередь анимаций. Важно, что state объявляется внутри ивента.

avatarMin.onTap ->
  state = avatarMin.states.current.name
  if state == "default"
    for i in [0..nextArray.length-1]
nextArray[i]()

Такую же проверку сделаем на ивент avatarMax.onTap. Этот слой тоже будет триггером:

avatarMax.onTap ->
  state = avatarMin.states.current.name
  if state == "profileState"
    for i in [0..backArray.length-1]
backArray[i]()

Итог: теперь можно кликать по аватару и будет меняться состояние у projects:

Шаг 7: Смещаем слой projectList

Содержимое блока Projects c белым и синим квадратами находится в слое projectList. Можно задать ему состояние, в котором координаты смещены за нижний край экрана и он гарантированно не мешает.

Простой пример состояния смещения:

Смотреть в Фреймер Клауде

Лайфхак, как получить координаты смещения блока без регистрации и СМС

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

Копируем группу projectList в новый проект и мышью смещаем относительно левого верхнего края артборда на сколько необходимо. Координаты записываем.

Ширина экрана в нашем проекте — 320. Красным квадратом показано значение 80, что в 4 раза меньше 320. Напомню, что мы увеличиваем слой projects на 4. Мне нужно убедиться, что слой projectList окажется за экраном, то есть за пределами красного квадрата, поэтому, полезно представлять примерный масштаб.

Значение х: 20, y: 100 выглядит подходящим.

7.1. Сохраняем фрейм слоя и пишем состояние

Нам нужны две пары координат: начальное положение блока projectList и конечное, в которое он сместится.

У блока projectList в состоянии default есть начальные координаты. Точные их значения нам не важны.

Вторая пара координат должна отсчитываться относительно первой. Разница между ними — то самое смещение, которое мы сделали в Скетче.

Берём фрейм слоя projectList и помещаем его в переменную:

projectListFrame = projectList.frame

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

projectListFrame.x

Создаём состояние, в котором блок смещён.

В x и y пишем исходный x и y блока, затем добавляем к нему координаты из Скетча.

  x: projectListFrame.x + 20
y: projectListFrame.y + 100

Не забываем, что нужно умножать все значения из Скетча на 2, поскольку мы импортировали макет в 2х:

projectList.states.profileState =
x: projectListFrame.x + 20 * 2
y: projectListFrame.y + 100 * 2

Пишем поведение в next и back

В ивенте avatarMin.onTap дописываем команду смены состояния:

next = ->
projectList.stateCycle()
nextArray.push(next)

Ту же самую команду пишем в back.

Итог: Нажимаем аватар — экран Projects увеличивается вчетверо, блок с проектами отъезжает вниз по диагонали. Нажимаем снова — анимация даёт задний ход.


Шаг 8: Делаем зум-анимацию аватара

Аватар при нажатии увеличивается и меняет разрешение картинки.

Сложное место. В проекте есть два похожих объекта аватара: avatarMin и avatarMax. Фокус в том, что в самом начале анимации мы подменяем один другим.

Изолированный пример на Фреймере с комментариями.

План действий:

  1. Сохранить исходный фрейм маленького аватара.
  2. Подготовить avatarMax к анимации. Сдвинуть его на координаты avatarMin, чтобы не было стыка. Скукожить avatarMax до размера avatarMin.
  3. При клике на avatarMin мгновенно скрывать его и переводить avatarMax в видимое состояние, из которого будет начинаться его увеличение.
  4. Начинать увеличивать avatarMax до 100%.
  5. При клике запускать анимацию.
  6. При клике на avatarMax (функция back) запускать переход в уменьшенное состояние.
  7. Когда анимация уменьшения завершилась, скрывать avatarMax и проявлять avatarMin.
  8. Исключить коллизию. Предусмотреть, чтобы нельзя было запустить анимацию, когда она уже выполняется.

Копируем вкладку 5.

8.1. Сохраняем фрейм аватара

avatarMinFrame = avatarMin.frame

8.2. Готовим avatarMax

Поскольку на предыдущих шагах мы скрыли все слои внутри profile, avatarMax тоже попал под раздачу. Теперь его полезно проявить:

avatarMax.props =
opacity: 1

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

avatarMax.props =
...
  originX: 0
originY: 0

avatarMax в финальном экране сдвинут правее, чем avatarMin. Чтобы не возникло визуального ощущения стыка, ещё до начала анимации, пока avatarMax не виден, его надо поставить на x avatarMin:

avatarMax.props =
...
  x: avatarMinFrame.x

Кукожим avatarMax до размера avatarMin. Тут пригодится арифметика, которую за нас будет делать Фреймер. Размер avatarMin — 48x48, avatarMax — 208х208. Чтобы узнать, на сколько нужно уменьшить avatarMax, чтобы он тоже стал 48, делим 48 на 208, получая иррациональное число 0,23 с кучей цифр после запятой.

Вместо того чтобы писать цифрами, подставляем ширины обоих слоёв и делим их друг на друга. Чтобы убедиться, что вернётся нужное нам число, тестируем в print:

print avatarMin.width / avatarMax.width # в консоли: 0.2307...

Прописываем это в scale:

avatarMax.props =
  ...
  scale: avatarMin.width / avatarMax.width

Теперь даже если размер аватара в скетч-макете изменится, анимация не поломается.

Когда все эти свойства прописаны, прячем avatarMax, но используем не opacity, а свойство visible.

avatarMax.props =
  ...
  visible: false

Разница между opacity и visible в том, что первое отвечает за анимируемую опасность от 0 до 1. Во втором не может быть анимации, оно либо видимо (true), либо нет (false). Оно полезно, чтобы мгновенно скрывать блок без анимации. Именно это и нужно по плану.

8.3. Подменяем аватар

Пишем состояние для avatarMin, в котором слой скрыт:

avatarMin.states.profileState = 
visible: false

Пишем состояние start для avatarMax, в котором он скрыт, но всё ещё маленький. Из него будет происходить увеличение. В отличие от всех остальных слоёв, у avatarMax будет три состояния: default, start и profileState.

avatarMax.states.start =
visible: true

В нём Фреймер ещё не знает ничего о том что аватар должен быть уменьшен. Надо дополнительно ему напомнить, иначе не будет работать обратный ход на уменьшение:

avatarMax.states.start =
  ...
  scale: avatarMin.width / avatarMax.width

Пишем состояние, в котором avatarMax виден имеет свою натуральную величину. Используем уже закоменченный код складки 5:

avatarMax.states.profileState =
scale: 1

8.5. Начинаем увеличивать

В next пишем, как должны меняться состояния аватара. Как только срабатывает триггер, avatarMin без анимации переключается в состояние profileState, в котором у него visible: false.

next = ->
  avatarMin.stateSwitch(“profileState”)
nextArray.push(next)

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

next = ->
  ...
  avatarMax.stateSwitch("start")
nextArray.push(next)

Мы заменили аватар и теперь можем его увеличивать до оригинального размера 208х208. За это отвечает свойство scale, которое увеличится от значения 0,43 до 1. Из состояния start мы переводим avatarMax в profileState:

next = ->
  ...
  avatarMax.stateCycle()
nextArray.push(next)

8.6. Делаем обратную анимацию

В состоянии profileState слой avatarMin скрыт, а занчит, нам больше недоступен триггер. Это повод сделать новый тригер на avatarMax. Мы уже сделали это на шаге 4, когда делали очередь анимаций. Здесь осталось заполнить функцию back. Возвращаем avatarMax в default:

back = ->
  avatarMax.stateCycle()
backArray.push(back)

Теперь нужно возвраить avatarMin в default, но сделать это нужно только когда анимация уменьшения avatarMax закончилась.

Если поставить команды подряд:

avatarMax.onTap ->
...
  avatarMax.stateCycle()
avatarMin.stateCycle()

… на обратной анимации мы увидим avatarMin, который должен быть скрыт. Чтобы разделить их во времени, нам нужен дилей.


Дилей — способ задерживать начало анимаций

Чтобы дождаться окончания какой-либо анимации, можно использовать функцию Utils.Delay. В неё помещается весь код, который должен отработать спустя заданное время.

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

back = ->

avatarMax.stateCycle()
  Utils.delay 2, ->
avatarMin.stateCycle()
backArray.push(back)

Чтобы нам не приходилось менять время анимации в разных местах проекта, используем нашу переменную animationTime:

  Utils.delay animationTime, ->

Мухобойка

Поведение состояний в проекте стало усложняться. Теперь есть слой avatarMax, у которого три состояния, а у других два.

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

Испытываем коллизию на практике: Нажимаем на развёрнутый аватар. Начинается анимация уменьшения. Кликаем по нему снова. Триггер снова срабатывает, переключая состояние блоку projectList, который, не доехав до default, едет обратно в profileState. Произошла коллизия анимации, прототип сломан, придётся обновить (Cmd +R).

Мухобойка состоит из трёх частей: команды if, знака обращения логики и свойства isAnimating.


Часть 1/3: Команда if

Команда if позволяет сделать логическую развилку. Простейший пример:

condition = true
if condition
print "условие выполняется, мы в ветке true"
else
print "сорян, не выполняется, мы в false"

Пока после if будет возвращаться true, будет срабатывать первая ветка. Второй может и вовсе не быть, и нам она не нужна.


Часть 2/3: Знак обращения логики

В Кофескрипте есть знак !, который позволяет превратить значение true в false, если надо. Пример:

a = false
print !a

В консоль вернётся true.


Часть 3/3: Свойство isAnimating

В Фреймере у слоя есть свойство isAnimating, которое возвращает true, когда слой движется:

print avatarMax.isAnimating

Пишем мухобойку

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

avatarMax.onTap ->
  # мухобойка
if !avatarMax.isAnimating
    # проверка на состояние
if state == "default"
      for i in [0..backArray.length-1]

# запускаем возвращение в projects
backArray[i]()

Теперь если avatarMax движется, запускать анимацию не получится: клик не даст эффекта. На ивент avatarMin мухобойка не нужна, потому что он мгновенно скрывается и не может принять второй клик, пока avatarMax его не проявит.

Итог шага 8: мы проработали поведение аватара. На него можно кликать и запускать переход между экранами туда-обратно.


Шаг 9. Анимируем зелёный индикатор online около аватара

Индикатор будет состоять из двух слоёв: onlineIconProjects и onlineIconProfile. Первый будет на экране проектов, второй на профиле. На этом шаге мы оживим первый.

Дальше всё как по накатанной из складки 5.

Пишем состояние

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

onlineIconProjects.states.profileState =
  scale: 0
  animationOptions:
time: animationTime / 2

Делаем смену состояния по триггеру

В next прописываем:

next = ->
onlineIconProjects.stateCycle()
nextArray.push(next)

9.3. Обратный ход onlineIconProjects

В back засовываем его в функцию delay со значением animationTime. В этом случае он не будет двигаться по диагонали, а будет появляться уже после перехода на экран projects. Это кажется более уместным.

back = ->
...
  Utils.delay animationTime, ->
onlineIconMin.stateCycle()
backArray.push(back)

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


Шаг 10. Проявляем серый блок openTasks

Полная аналогия шага 7 со слоем projectList, только ещё проще: участвует лишь сдвиг по x. Копируем складку 5, автозаменяем layer на openTasks.

Готовим openTasks к появлению. Проявляем опасность, задвигаем за экран. Для этого добавляем ширину блока к его координате х.

openTasksFrame = openTasks.frame
openTasks.props =
opacity: 1
x: openTasksFrame.x + openTasks.width

Делаем состояние, в котором возвращаем в исходную позицию:

openTasks.states.profileState =
x: openTasksFrame.x

В next пишем смену состояния. Чтобы слой openTasks не наехал на проезжающие мимо иконки экрана projects, ставим ему дилей в треть animationTime:

next = ->
  Utils.delay animationTime/3, ->
openTasks.stateCycle()
nextArray.push(next)

В back пишем обратный ход задвига:

back = ->
openTasks.stateCycle()
backArray.push(back)

Шаг 11. Проявляем имя Beatrice Harris

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

Сам знаешь, что делаем со складкой 5.

Готовим nameHeader

nameHeaderFrame = nameHeader.frame

Временно проявим nameHeader, чтобы работать с ним.

nameHeader.props = 
opacity: 1

Смещаем по горизонтали на 30*2:

nameHeader.props =
  ...
  x: nameHeaderFrame.x + 30*2

Пишем состояние, в котором он будет проявлен. nameHeader — это общий контейнер для обоих слов.

nameHeader.states.profileState =
opacity: 1

Теперь одновременно пропишем состояние обоим слоям внутри него при помощи цикла. У каждого из слоёв будет опасность 0,2 (20%), а в состояниях profileState будет полная. Также они будут смещены на 30*2 налево, откуда начнут движение. Это смещение нужно, чтобы был более очевиден рассинхрон в движениях двух слов.

for layer in nameHeader.children

layer.opacity = .2

layer.states.profileState =
opacity: 1
    x: layer.x — 30*2

При клике меняем состояние nameHeader на profileState:

next = ->
  nameHeader.stateCycle()
nextArray.push(next)

Он должен проявляться не сразу, а через половину времени animationTime.

next = ->
    Utils.delay animationTime/2, ->
      nameHeader.stateCycle()
nextArray.push(next)

Теперь проявляем его слои в цикле.

Цикл for-of

Это новый тип цикла, который очень похож на for-in, но содержит дополнительный элемент — инкремент. Это обычная переменная, которая на первом шаге равна 0 и на каждом последующем шаге увеличивается на 1. Мы будем использовать его, чтобы корректировать время анимации каждого слоя, делая запаздывание второго слова. При этом цикл всё так же будет проходить по слоям-детям, как в for-in.

next = ->
  Utils.delay animationTime/2, ->
for i, layer of nameHeader.children
nextArray.push(next)

Внутри цикла мы будем обращаться к переменной layer, которая содержит имя слоя на каждой итерации цикла. Всего будет две итерации — для beatrice и для harris.

Ключевой момент — строка delay. В ней мы делим глобальную скорость анимации animationTime на 6, чтобы получить значение покороче. Затем, умножаем его на инкремент. На первой итерации delay равен 0, на второй 0.33.

Затем мы запускаем смену состояния детей на profileState.

next = ->
    Utils.delay animationTime/2, ->

for i, layer of nameHeader.children
        layer.animationOptions = 
delay: animationTime/6 * i
        layer.stateCycle()
nextArray.push(next)

Меняем состояние контейнеру:

next = ->
  ...
  nameHeader.stateCycle()
nextArray.push(next)

Обратный ход nameHeader. Весь код внутри дилея копируем в back. В нём дилей не нужен:

back = ->
  nameHeader.stateCycle()
  for i, layer of nameHeader.children
    layer.animationOptions = 
delay: animationTime/6 * i
    layer.stateCycle()
nextArray.push(next)

Скрываем nameHeader, удаляя строку opacity: 1.

Итог: Теперь имя Beatrice Harris проявляется и исчезает с эффектом фейда и смещения.


Шаг 12. Проявляем значок online напротив имени

Тут всё просто: состояние и stateCycle в обоих тригерах.

Пишем ему состояние profileState:

onlineIconProfile.states.profileState =
opacity: 1

Проявляем его в next по дилею длительностью animationTime:

next = ->
Utils.delay animationTime, ->
onlineIconProfile.stateCycle()
nextArray.push(next)

Скрываем его:

back = ->
onlineIconProfile.stateCycle()
backArray.push(back)

Итог: мы закончили со вторым зелёным значком online. Он проявляется напротив имени и исчезает на странице Projects.


Шаг 13. Делаем Подпись Sales Manager

Тот же принцип, что и у Beatrice Harris. Разница в том, что этот блок сдвинут значительно сильнее, поэтому успевает больше разогнаться.

Сохраняем фрейм:

jobTitleFrame = jobTitle.frame

Готовим слой, изначально сдвинув его на 300 двойных пикселей вправо:

jobTitle.props =
x: jobTitleFrame.x + 300*2

Пишем состояние:

jobTitle.states.profileState =
opacity: 1
x: jobTitleFrame.x — 32*2

В цикле готовим детей и пишем им состояния:

for layer in jobTitle.children
  layer.props =
opacity: .2
  layer.states.profileState =
opacity: 1
x: layer.x + 32*2

В next прописываем поведение на дилее в четверть animationTime:

next = ->
Utils.delay animationTime/4, ->
      jobTitle.stateCycle()
      for i, layer of jobTitle.children
        layer.animationOptions = 
delay: animationTime/4 * i
        layer.stateCycle()
nextArray.push(next)

В back:

back = ->
jobTitle.stateCycle()

for i, layer of jobTitle.children
      layer.animationOptions = 
delay: animationTime/6 * i
      layer.stateCycle()
backArray.push(back)

Шаг 14. Формируем иконку из трёх точек

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

14.1.

Сохраняем фрейм:

moreIconFrame = moreIcon.frame

Проявляем:

moreIcon.props =
opacity: 1

Готовим детей и пишем им состояние пачкой. Задвигаем детей за экран, затем выдвигаем на moreIconFrame.x. Вычитаем компенсацию 32*2, чтобы добиться нужного положения иконки:

for layer in moreIcon.children
  layer.props =
x: — 48*2
  layer.states.profileState =
x: moreIconFrame.x — 32*2

Пишем поведение moreIcon в next, выдвигаем:

next = ->
 Utils.delay animationTime/2, ->
   for i, layer of moreIcon.children
     layer.animationOptions = 
delay: animationTime/6 * i
     layer.stateCycle()
nextArray.push(next)

Задвигаем:

back = ->
  for i, layer of moreIcon.children
    layer.animationOptions = 
delay: animationTime/6 * i
  layer.stateCycle()
backArray.push(back)

Шаг 15. Контейнер hours

Содержит одометр и график. Простейшее поведение.

Задаём слою настройки анимации пободрее, чтобы одометр был сразу на полную опасность:

hours.props =
animationOptions:
time: animationTime/6

Пишем состояние:

hours.states.profileState =
opacity: 1

next

next = ->
  Utils.delay animationTime/6, ->
hours.stateCycle()
nextArray.push(next)

back

back = ->
hours.stateCycle()
backArray.push(back)

Шаг 16. Одометр

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

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

Объекты Анимации

Раньше мы не сталкивались с Анимациями. Это тип объектов в Фреймере. В этом уроке мы не будем их разбирать подробно. Важно знать, что объект анимации можно создать, повесить его на какой-то слой и в нужный момент запустить. Так, не меняя состояния, слой будет двигаться или менять свойства.

План действий:

  1. кладём в переменную value значение, на которое нужно прокрутить цифры. Пример value — 1692.
  2. Сохраняем фрейм
  3. Задаём высоту ячейки с цифрой, в данном случае, 20*2
  4. Делаем из слоя одометра маску высотой с ячейку
  5. Конвертируем значение в массив, где каждая цифра занимает свою ячейку
  6. Создаём пустой массив animations1 для анимаций
  7. В цикле создаём объект анимации, которые вешаем на детей слоя одометра
  8. В том же цикле рассчитываем, до какого значения y должна сдвинуться шкала цифр. каждая шкала двигается на ту высоту, которую диктует массив из шага 5
  9. Высоту считаем из высоты одной цифры, умноженной на очередную цифру из value
  10. Объект анимации сохраняем в массив для анимаций
  11. По триггеру next запускаем все анимации из массива
  12. По триггеру back ставим все y шкал в одометре в 0000.

Более детальные комментарии по одометру даны в фреймер-проекте.


Шаг 17. graph — график в блоке Hours

Ш, не будем тратить время. Фрейм, подготовка, состояния, в обоих тригерах по stateCycle().

Маски

Для маскировки слоя graph используется слой graphMask. Чтобы маска работала, надо задать слою graph родителя graphMask и установить свойство clip в true:

graphMask.props =
clip: true
graph.props = 
parent: graphMask

Хороший способ контролировать форму объектов

Чтобы было легче работать с объектами, у которых нет краёв, зададим CSS-стиль с красной пунктирной обводкой:

graphMask.style =
"outline": "1px dashed red"

Изначально задвигаем за левый край маски, используя её же ширину в качестве размера сдвига:

graph.props =
...
x: graphFrame.x — graphMask.width

Делаем выдвинутое состояние:

graph.states.profileState =
x: graphFrame.x

Движение графика начинаем спустя четверть animationTime:

next = ->
  Utils.delay animationTime/4, ->
    graph.stateCycle()
nextArray.push(next)

Обратно задвигаем лишь спустя полный animationTime, чтобы слой hours к тому моменту успел скрыться и мы не увидели это обратное движение за кулисами.

Итог: мы закончили с графиком, по первому клику он выдвигается, по второму скрыт блоком hours.

Шаг 19. Заголовок Statistics

Для заголовка Statistics я выбрал название слоя statistics, которое весьма длинное. В этом шаге особенно чувствуется польза автозамены. Детально разбирать не буду, смотри предыдущие шаги. Остановлюсь только на интересном.

Размер изначального оттяга:

statisticsTitle.props = 
x: - statisticsTitle.width - 32*2

Время дилея в next — четверть animationTime, в back без дилея.


Шаг 20. Заголовок currentWeek

Та же история, что со Statistics. В оттяге использовал ширину артборда, к которой добавил сдвиг за экран:

currentWeek.props = 
x: artboard.width + 32*2

Дилей такой же как в Statistics.

Итог: По клику блоки Statistics и currentWeek одновременно выезжают с разных сторон, по второму заезжают обратно.


Шаг 21. Контейнер pie

Смотри код hours. Единственное различие: сделал анимацию пободрее, чтобы пайчарт как можно быстрее набрал полную опасность, иначе некрасиво. Сам пайчарт сделаем дальше.

pie.props =
  animationOptions:
time: animationTime / 6

Шаг 22. pieOdometer

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

Шаг 23. Пайчарт

Пайчарт — это круговая диаграмма. В нашем случае она показывает значение 65%. В кейсе она анимируется от нулевого значения до конечного.

Специально для кейса я написал анимирующийся пайчарт, которому можно задавать значение от 1 до 100%:

Смотреть в Фреймер Клауде

Это был один из самых интересных этапов работы над кейсом. Сначала я думал вставить пайчарт из Афтера, но смог реализовать его силами Фреймера.

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

Принцип работы

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

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

Ограничения

При работе над пайчартом мне пришлось отказаться от фиолетового градиента, потому что на 50% слишком был слишком очевиден.

Пайчарт анимируется линейно. Поскольку время перехода в развёрнутое состояние всегда одинаково для обеих долек, Анимация до 50% будет проходить вдвое быстрее, чем от 50 до 75%. Реализовывать полноценное сглаживание я посчитал пока нецелесообразным.

Шаг 24. Таблица статистики

Последний шаг — анимация четырёх цифр, формирующих таблицу. Код аналогичен шагу 11, на котором появляется заголовок Beatrice Harris.

Скрываем и показываем блок stats также как hours.

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

next = ->
Utils.delay animationTime/2, ->
    stats.stateCycle()
    for i, layer of stats.children
      layer.animationOptions =
delay: animationTime/10 * i
      layer.statCycle()
nextArray.push(next)

На обратном пути скорость задержки — одна шестая, без дилеев. Эта анимация будет происходить за кулисами, при скрытом stats.

Теперь ставим animationTime в единицу. Проект готов.


Послесловие

Теперь, когда я всё это написал, я хочу ответить на назревший вопрос.

Целесообразно ли делать настолько сложную анимацию на боевом проекте? Почему получилось столько кода и хлопот? Ведь в Афтере это было бы быстрее!

Фреймер против Афтера

  1. Интерактивность против мультика
    Да, признаю, было бы быстрее, если писать всё с нуля. Однако, оно было бы не интерактивно. Запустить такой переход на телефоне пальцем — это совершенно другой опыт, чем смотреть мультик. Рекомендую поставить Framer Mirror и скопировать ссылку проекта в него, чтобы почувствовать этот опыт.
  2. Скетч против Адоба
    Использовать скетч-макеты в АЕ можно с большой натяжкой, да и то недавно. Для этого используется кривоватый плагин Sketch2AE. Скетч и Фреймер дружат уже из коробки. Если твои макеты в Скетче, импортировать их в Фреймер будет значительно легче, чем в Афтер.
  3. Осознание технических ограничений против полёта фантазии
    Фреймер максимально близок к макетам и разработке. Делать нереалистичные вещи в нём на порядок сложнее. Афтер даёт свободу движения, но отдаляет от реальной разработки. В итоге, по анимации Афтера затянутся сроки разработки и было бы сложно представить, насколько трудозатратна реализация. Все хлопоты по настройке многочисленных циклов, дилеев, одометров и проверок на коллизии упали бы на плечи программистов. Рано или поздно кто-нибудь с ножницами пришёл бы порезать бюджет на финтифлюшки. При фреймер-подходе излишнее отрезается уже на этапе дизайна анимации: дизайнер, даже очень трудолюбивый, не станет делать то, что невозможно заанимировать. Фреймер-анимация стремится к аскетичности. Велика вероятность, что всё наанимированное в Афтере так и осталось бы мультиком.
  4. Код можно переиспользовать! Весомый аргумент — у тебя есть мой проект. Я подготовил тебе платформу для анимации бесшовников, которую ты теперь сможешь использовать на своих проектах. И этого Афтер никогда не позволит.
  5. Прототип в виде ссылки. Шерить мультик можно только на Дрибле. В боевом дизайне на рендер мультиков нет времени, надо показывать быстрый результат, желательно, в реальном времени. Фреймер Клауд — это ещё один весомый аргумент против Афтера и Принципла.

Получившийся прототип — это всё-таки скорее эксперимент и демонстрация возможностей, чем реальный боевой кейс. Сама анимация перегружена мелкими элементами, которые за секунду движения человек не уловит. Когда-то я читал, что на сценах космических баталий в Звёздных Войнах на экране одновременно присутствуют тысячи движущихся космических кораблей, которым кто-то прорисовал траекторию. Мой прототип чем-то напоминает мне эти баталии. Можно было бы сделать эту анимацию проще без вреда для восприятия. Но что теперь тебе стоит сделать простую анимацию бесшовника, когда ты повторил за мной эту?


Я веду телеграм-канал Скетч-дизайнер о дизайне интерфейсов в Скетче, Фреймер-анимации и дизайн-системах. Я рассказываю о плагинах и горячих клавишах, снимаю видеоуроки.

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

Поддержать проект

Ссылка на Киви-кошелёк

Оригинальный дизайн: UI8 и взят из этой гифки.