Привет! Меня зовут Даша, я фронтенд-разработчик отдела спецпроектов в KTS.
В нашем отделе мы часто занимаемся разработкой ярких и креативных проектов, которые содержат в себе анимации. Сегодня хочу поделиться нашим опытом, рассказать про несколько способов создания анимаций и показать, в каких случаях уместно ими пользоваться.
Для каждой из перечисленных технологий я буду приводить пример использования анимации в реальном проекте.
Оглавление
CSS
CSS лучше всего подходит для реализации простых анимаций, не требующих сложной логики. В первую очередь это небольшие анимации, позволяющие сделать наш интерфейс более “живым”: например, изменения состояния элемента при наведении, фокусе или нажатии. Производительность у CSS-анимаций очень высокая, поэтому если какую-то идею возможно реализовать с помощью этой технологии, я рекомендую пользоваться именно ей.
Про создание и оптимизацию CSS-анимаций и так написано немало, поэтому в своем материале я не буду останавливаться на них подробно. Если вам интересно почитать конкретно про эту технологию, рекомендую статью моей коллеги Кристины.
Когда использовать
для простых анимаций перехода из одного состояния в другое. Например, для изменения цвета, прозрачности, размера или положения при наведении курсора;
для анимаций, показывающих интерактивность элемента: ссылки, кнопки и т.д.;
в любой другой ситуации, когда реализовать задумку возможно при помощи CSS, и на это хватает времени.
Пример использования в проекте
Анимация интерактивных элементов с помощью CSS может быть проиллюстрирована на примере такой кнопочки:
Также CSS можно использовать для создания более продвинутых анимаций. Здесь, например, мы анимируем вещи вокруг пса Пельменя:
Еще мы использовали CSS для анимации переворота карточек в игре "Мемори Смешарики":
SVG
Разумеется, SVG – это не способ создания анимации, а формат для векторных изображений (логотипов, иконок или каких-то других простых форм). В этом разделе я хочу рассказать о том, как такие изображения можно анимировать.
Для анимации SVG существует 2 основных способа:
CSS-анимация;
SMIL-анимация.
С помощью CSS можно анимировать практически любой SVG-атрибут. Здесь все как обычно: используем @keyframes и animation. Однако иногда SVG может иметь особенный вид, и не в таких случаях использованием CSS не обойдешься. Тут нам и приходит на помощь технология SMIL (Synchronized Multimedia Integration Language).
По сравнению с CSS, SMIL является более функциональным и сложным инструментом, и использовать его стоит только при необходимости. Все-таки CSS обладает большей поддержкой браузеров и более производителен, поэтому если есть возможность использовать CSS для анимации SVG, лучше так и сделать.
В каких же ситуациях пригодится SMIL? Например, для:
анимации отдельных атрибутов SVG, которые не поддерживаются CSS;
синхронизации анимаций (SMIL предоставляет более гибкие инструменты управления временем);
более точного контроля над анимацией.
Как анимировать SVG через SMIL
Для этого нам понадобится тег <animate />
. Например, попробуем анимировать радиус круга:
<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="15" width="200" height="200" fill="red">
<animate
attributeName="r"
from="15"
to="50"
dur="3s"
repeatCount="indefinite" />
</circle>
</svg>
Получаем следующий результат:
При помощи элемента <animateTransform />
можно анимировать перемещения (translate), масштабирования (scale), вращения (rotate) и наклоны (skewX и skewY).
Например, создадим вращающийся зеленый квадрат:
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<rect x="50" y="50" width="100" height="100" fill="green">
<animateTransform
attributeName="transform"
type="rotate"
from="0 100 100"
to="360 100 100"
dur="5s"
repeatCount="indefinite"
/>
</rect>
</svg>
Он будет выглядеть так:
Это довольно простая анимация, и ее можно было бы реализовать при помощи CSS. Однако с помощью технологии SMIL можно воспользоваться средствами синхронизации.
Например, добавим еще один тег animateTransform
с атрибутом additive=”sum”
. Тогда две анимации наложатся друг на друга, и при этом им можно будет задать разные настройки по времени и количеству повторений. Добавим масштабирование:
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<rect x="50" y="50" width="100" height="100" fill="green">
<animateTransform
attributeName="transform"
type="rotate"
from="0 100 100"
to="360 100 100"
dur="5s"
repeatCount="indefinite"
/>
<animateTransform
attributeName="transform"
type="scale"
values="0.5; 1; 0.5"
keyTimes="0; 0.5; 1"
dur="4s"
repeatCount="indefinite"
additive='sum'
/>
</rect>
</svg>
И получим вот такой результат:
Возможно, вы зададитесь вопросом: “Что же случилось? Почему квадрат раньше вращался относительно центра, а теперь – нет?”. Все дело в том, что из-за добавления additive=”sum”
анимации накладываются, и при изменении размера квадрата (от анимации scale) меняется и его траектория движения. Так что к использованию такого суммирования анимаций нужно подходить очень внимательно.
Тег <animateMotion />
подходит для работы со сложными формами. Например, чтобы создать анимацию движения по сложной траектории. Для наглядности возьмем SVG-изображение кота:
<svg
width="300"
height="300"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 13C8 11.8954 8.89543 11 10 11H13.5C14.6046 11 15.5 11.8954 15.5 13V24.5H36V20.8028C36 20.4079 36.1169 20.0219 36.3359 19.6934L38.3359 16.6934C39.4336 15.0469 42 15.824 42 17.8028V19.5H44.382L45.7005 16.8629C46.5725 15.119 49.1696 15.5262 49.4661 17.4532L49.8579 20L50 20C51.1046 20 52 20.8954 52 22V30.25C52 31.1316 51.3481 31.861 50.5 31.9823V38C50.5 39.1046 49.6046 40 48.5 40H48V51.5C48 52.6046 47.1046 53.5 46 53.5H42C40.8954 53.5 40 52.6046 40 51.5V40H39.5V51.5C39.5 52.6046 38.6046 53.5 37.5 53.5H33.5C32.3954 53.5 31.5 52.6046 31.5 51.5V40H24.5V51.5C24.5 52.6046 23.6046 53.5 22.5 53.5H18.5C17.3954 53.5 16.5 52.6046 16.5 51.5V40H16V51.5C16 52.6046 15.1046 53.5 14 53.5H10C8.89543 53.5 8 52.6046 8 51.5V13ZM13.5 13L10 13V51.5H14V40C14 38.8954 14.8954 38 16 38H16.5C17.6046 38 18.5 38.8954 18.5 40V51.5H22.5V40C22.5 38.8954 23.3954 38 24.5 38H31.5C32.6046 38 33.5 38.8954 33.5 40V51.5H37.5V40C37.5 38.8954 38.3954 38 39.5 38H40C41.1046 38 42 38.8954 42 40V51.5H46V40C46 38.8954 46.8954 38 48 38H48.5V31.75C48.5 30.8684 49.1519 30.139 50 30.0177V22H49.8579C48.8708 22 48.0313 21.2798 47.8812 20.3041L47.4894 17.7573L46.1708 20.3944C45.832 21.072 45.1395 21.5 44.382 21.5H42C40.8954 21.5 40 20.6046 40 19.5V17.8028L38 20.8028V24.5C38 25.6046 37.1046 26.5 36 26.5H15.5C14.3954 26.5 13.5 25.6046 13.5 24.5V13Z"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M55.49 24.3586C55.5681 24.9053 55.1882 25.4118 54.6414 25.4899L47.6414 26.4899C47.0947 26.5681 46.5882 26.1882 46.5101 25.6414C46.4319 25.0947 46.8118 24.5882 47.3586 24.51L54.3586 23.51C54.9053 23.4319 55.4118 23.8118 55.49 24.3586ZM47 28C47 27.4477 47.4477 27 48 27H54.5C55.0523 27 55.5 27.4477 55.5 28C55.5 28.5523 55.0523 29 54.5 29H48C47.4477 29 47 28.5523 47 28Z"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M34.51 24.3586C34.4319 24.9053 34.8118 25.4118 35.3586 25.4899L42.3586 26.4899C42.9053 26.5681 43.4118 26.1882 43.4899 25.6414C43.5681 25.0947 43.1882 24.5882 42.6414 24.51L35.6414 23.51C35.0947 23.4319 34.5882 23.8118 34.51 24.3586ZM43 28C43 27.4477 42.5523 27 42 27H35.5C34.9477 27 34.5 27.4477 34.5 28C34.5 28.5523 34.9477 29 35.5 29H42C42.5523 29 43 28.5523 43 28Z"
fill="white"
/>
</svg>
К этому коту мы можем добавить точку, которая будет двигаться по его контуру. Для этого необходимо в тег <circle></circle>
положить animateMotion
с заданными настройками анимации. Также в тег нужно добавить mpath
, который будет ссылаться на path
– траекторию движения точки:
<circle cx="0" cy="0" r="1" fill="green">
<animateMotion dur="20s" repeatCount="indefinite">
<mpath href="#motionPath" />
</animateMotion>
</circle>
Объединяем все вместе:
<svg
width="300"
height="300"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 13C8 11.8954 8.89543 11 10 11H13.5C14.6046 11 15.5 11.8954 15.5 13V24.5H36V20.8028C36 20.4079 36.1169 20.0219 36.3359 19.6934L38.3359 16.6934C39.4336 15.0469 42 15.824 42 17.8028V19.5H44.382L45.7005 16.8629C46.5725 15.119 49.1696 15.5262 49.4661 17.4532L49.8579 20L50 20C51.1046 20 52 20.8954 52 22V30.25C52 31.1316 51.3481 31.861 50.5 31.9823V38C50.5 39.1046 49.6046 40 48.5 40H48V51.5C48 52.6046 47.1046 53.5 46 53.5H42C40.8954 53.5 40 52.6046 40 51.5V40H39.5V51.5C39.5 52.6046 38.6046 53.5 37.5 53.5H33.5C32.3954 53.5 31.5 52.6046 31.5 51.5V40H24.5V51.5C24.5 52.6046 23.6046 53.5 22.5 53.5H18.5C17.3954 53.5 16.5 52.6046 16.5 51.5V40H16V51.5C16 52.6046 15.1046 53.5 14 53.5H10C8.89543 53.5 8 52.6046 8 51.5V13ZM13.5 13L10 13V51.5H14V40C14 38.8954 14.8954 38 16 38H16.5C17.6046 38 18.5 38.8954 18.5 40V51.5H22.5V40C22.5 38.8954 23.3954 38 24.5 38H31.5C32.6046 38 33.5 38.8954 33.5 40V51.5H37.5V40C37.5 38.8954 38.3954 38 39.5 38H40C41.1046 38 42 38.8954 42 40V51.5H46V40C46 38.8954 46.8954 38 48 38H48.5V31.75C48.5 30.8684 49.1519 30.139 50 30.0177V22H49.8579C48.8708 22 48.0313 21.2798 47.8812 20.3041L47.4894 17.7573L46.1708 20.3944C45.832 21.072 45.1395 21.5 44.382 21.5H42C40.8954 21.5 40 20.6046 40 19.5V17.8028L38 20.8028V24.5C38 25.6046 37.1046 26.5 36 26.5H15.5C14.3954 26.5 13.5 25.6046 13.5 24.5V13Z"
fill="white"
id="motionPath"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M55.49 24.3586C55.5681 24.9053 55.1882 25.4118 54.6414 25.4899L47.6414 26.4899C47.0947 26.5681 46.5882 26.1882 46.5101 25.6414C46.4319 25.0947 46.8118 24.5882 47.3586 24.51L54.3586 23.51C54.9053 23.4319 55.4118 23.8118 55.49 24.3586ZM47 28C47 27.4477 47.4477 27 48 27H54.5C55.0523 27 55.5 27.4477 55.5 28C55.5 28.5523 55.0523 29 54.5 29H48C47.4477 29 47 28.5523 47 28Z"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M34.51 24.3586C34.4319 24.9053 34.8118 25.4118 35.3586 25.4899L42.3586 26.4899C42.9053 26.5681 43.4118 26.1882 43.4899 25.6414C43.5681 25.0947 43.1882 24.5882 42.6414 24.51L35.6414 23.51C35.0947 23.4319 34.5882 23.8118 34.51 24.3586ZM43 28C43 27.4477 42.5523 27 42 27H35.5C34.9477 27 34.5 27.4477 34.5 28C34.5 28.5523 34.9477 29 35.5 29H42C42.5523 29 43 28.5523 43 28Z"
fill="white"
/>
<circle cx="0" cy="0" r="1" fill="green">
<animateMotion dur="20s" repeatCount="indefinite">
<mpath href="#motionPath" />
</animateMotion>
</circle>
</svg>
И получаем точку, которая ползает по нашему векторному коту:
Когда использовать
при работе с SVG-изображениями;
для анимации отдельных атрибутов SVG;
для точной синхронизации анимаций;
для настройки отдельных параметров для разных анимаций SVG;
для настройки сложной траектории движения для SVG-элемента;
при работе с небольшим количеством отдельных SVG.
Пример использования в проекте
Здесь анимация создается за счет изменения свойств обводки пути (stroke-dasharray
и stroke-dashoffset
) с плавной анимацией через CSS-переходы:
WebP
Формат изображений WebP был разработан компанией Google в 2010 году. Он использует продвинутый алгоритм сжатия без видимых потерь в качестве. Вес картинок сокращается на 25-35 %. Главное его преимущество – это размер изображения.
Формат WebP хорошо оптимизирован для статичных изображений, но при работе с анимацией приходится сохранять данные для каждого кадра. Чем выше разрешение анимации, тем больше весит каждый кадр. Следовательно, если анимация состоит из большого количества кадров с высоким разрешением, общий вес файла быстро увеличивается. К тому же, вес анимации напрямую зависит от её длины. Если она содержит сотни или тысячи кадров, в некоторых случаях объем данных становится сопоставимым с видео.
При этом WebP-анимации использовать все равно проще, поскольку на видео накладывается ряд ограничений. Например, в Safari видео можно воспроизвести только после жеста пользователя (например, тапа на экран), что усложняет автоматическое использование анимации. У WebP таких проблем нет, и его можно использовать сразу при загрузке страницы.
Может показаться, что этот способ сильно напоминает секвенцию (если вы с ней не знакомы – не переживайте, ее мы обсудим в конце). Отчасти так и есть, ведь это тоже покадровая анимация, однако в случае с WebP все намного проще.
WebP-анимация подходит для одноразового использования на странице, поскольку у нее нет инструментов управления как таковых. К примеру, ее можно задействовать для создания красивого перехода между страницами (как в первом примере ниже) или для отображения в качестве движущейся картинки (как во втором примере ниже).
Изначально поддержка WebP была ограниченной, однако большинство современных браузеров поддерживает этот формат. При этом важно помнить, что некоторые люди продолжают пользоваться старыми браузерами, неспособными воспроизводить этот формат. Следовательно, использовать WebP следует осторожно, делая поправку на аудиторию.
Еще стоит отметить, что скорость анимации может зависеть от частоты смены кадров, и на слабом устройстве эта скорость будет заметно ниже. Соответственно, при использовании WebP вы не сможете со 100 % вероятностью контролировать длительность анимации.
Для создания WebP-анимаций потребуется специализированная программа, поддерживающая их создание и экспорт.
Для того, чтобы встроить webp анимацию в свое приложение, необходимо указать путь до нее в качестве src
для тега <img />
Когда использовать
для анимации сложного изображения, которое не требует интерактивности;
для создания анимации, которая не требует управления;
для коротких по длительности анимаций.
Примеры использования
Взлетающая птица с проекта для Газпромбанка:
В данном случае не подошло решение через секвенцию, так как она содержала слишком большое количество кадров. Ее производительность была настолько низкой, что вкладка в браузере Safari перезагружалась сама собой. Поэтому мы выбрали формат WebP.
Анимация пара от курицы и горения свечей:
Canvas
Canvas – базовый инструмент, используемый для рисования графики. По сути это HTML-элемент, который можно использовать для отрисовки анимаций (и не только). Для того, чтобы начать работу с Canvas, нужны лишь базовые знания HTML и JavaScript.
Не все браузеры поддерживают работу в Canvas, но отрисованные в нем анимации воспроизводятся в большинстве. К тому же, Canvas можно использовать и самостоятельно, но это может потребовать больше сил, времени и строк кода.
В таких случаях гораздо удобнее использовать сторонние движки, например, Phaser, PixiJS или Three.js, которые предоставляют удобные API и готовые решения для сложных задач. Эти движки могут работать поверх WebGL, что открывает новые возможности для сложных визуальных эффектов, 3D-графики и высокопроизводительных анимаций. Если же вы хотите использовать Canvas самостоятельно, то можете почитать туториал.
Canvas позволяет создавать как простые, так и сложные анимации с полной свободой в управлении каждым кадром, поэтому он отлично подходит для создания игр, визуализации данных и динамической графики.
Однако при использовании Canvas API на пределе его возможностей производительность может падать. Также на производительности может сказаться изменение масштаба, так как при нем требуется перерисовка. Если вы столкнетесь с такой проблемой, рекомендую ознакомиться со сборником советов по улучшению производительности.
Еще у Canvas могут быть затруднения с интерактивностью. Здесь будет необходимо создание собственной системы обработки кликов и ручной поиск целевого объекта для каждого события (правда, это актуально только в том случае, если вы используете "чистый" Canvas, а не движок).
Когда использовать
для отрисовки сложных динамичных игр, которые будет затруднительно сделать с помощью
<div />
.
Пример реализации
В одном из наших проектов мы реализовали анимацию оранжевой волны на чистом Canvas:
Lottie
Lottie (или Лотти) – это формат файла на основе JSON для векторной графической анимации.
Преимущества Лотти:
мультиплатформенность: файлы Лотти можно использовать практически на любой платформе в интернете или на мобильных устройствах. Анимации Лотти даже подходят для стикеров в Telegram;
анимация не зависит от разрешения и масштабируется во время выполнения;
размер файла очень маленький;
возможность редактировать параметры анимации, добавляя интерактивность прямо во время проигрывания.
Как правило, анимации Лотти создаются дизайнерами в специальных программах (например, Adobe After Effects), после чего экспортируются в формате JSON для встраивания в приложение. Готовые анимации можно найти на официальном сайте.
Сравним Лотти с привычными гифками. GIF-файлы хранят данные в виде растровых изображений для каждого кадра, что значительно увеличивает их размер. В свою очередь, формат JSON, используемый в Lottie, представляет анимацию с помощью векторной графики и описаний объектов, что делает файлы более компактными и оптимизированными для передачи. Кроме того, GIF не поддерживает точное управление воспроизведением, тогда как Lottie позволяет перематывать анимацию к произвольному кадру, обеспечивая гибкость и интерактивность.
Размер файла Lottie в сравнении с PNG и GIF:
Помимо Lottie существует формат dotLottie. Это формат файлов с открытым исходным кодом, который объединяет один или несколько файлов Lottie и связанные с ними ресурсы в один файл. Такие файлы представляют собой ZIP-архивы, сжатые методом сжатия Deflate, и имеют расширение .lottie (отсюда и название).
Файлы dotLottie имеют меньший размер, чем Lottie. Давайте сравним их на примере анимации с LottieFiles.
Размер Lottie-анимации:
Размер той же анимации, но в формате dotLottie:
При этом при конвертации в dotLottie анимации могут повреждаться. Вот пример (анимацию вновь позаимствовала с LottieFiles):
Можно заметить, что у анимации dotLottie появляются какие-то странные горизонтальные полосы. Но такое происходит далеко не всегда, так что форматом все же можно пользоваться, если при конвертации проверять каждую анимацию отдельно. Конвертировать Lottie в dotLottie можно здесь.
Как встроить Lottie-анимацию в свой проект
Например, с помощью lottie-web – библиотеки, позволяющей воспроизводить в веб-приложениях анимации Lottie:
import * as React from 'react';
import Lottie, { AnimationItem } from 'lottie-web';
const TestLottie: React.FC = () => {
const [animationItem, setAnimationItem] =
React.useState<AnimationItem | null>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (containerRef.current) {
const animation = Lottie.loadAnimation({
container: containerRef.current,
path: 'https://lottie.host/d08e7662-fa30-4250-85b2-e2b84099d740/2RdMSZgTtW.json'
});
setAnimationItem(animation);
}
return () => {
animationItem?.destroy();
};
}, []);
return (
<div ref={containerRef} className="div" />
);
}
export default TestLottie;
Здесь мы в animationItem
при помощи useState
сохраняем экземпляр анимации. АuseRef
создаёт ссылку containerRef
– она используется для получения доступа к элементу div
, в котором отображается анимация.
Далее в useEffect
мы загружаем анимацию и привязываем ее к контейнеру, при этом path
указывает путь к анимационному файлу. В функции очистки обязательно возвращаем animationItem?.destroy()
, чтобы уничтожить анимацию при размонтировании и предотвратить утечку памяти.
Получаем вот такую симпатичную анимацию в браузере:
Если вы хотите использовать для анимации локальный JSON, то вместо path
нужно использовать animationData
:
import testAnim from './test.json';
const animation = Lottie.loadAnimation({
container: containerRef.current,
animationData: testAnim
});
В качестве параметра для loadAnimation
можно можно также передать renederer
. Его значение по умолчанию равно svg
, но можно задать canvas
или html
.
const animation = Lottie.loadAnimation({
container: containerRef.current,
animationData: testAnim,
renderer: 'canvas',
});
Если передать canvas
, то получится следующий результат:
У animationItem
здесь есть различные методы управления. Самый простой пример – возможность ставить на паузу и воспроизводить анимацию:
const handlePlayClick = () => {
animationItem?.play();
}
const handlePauseClick = () => {
animationItem?.pause();
}
return (
<>
<div ref={containerRef} className="div"/>
<button onClick={handlePlayClick}>Play</button>
<button onClick={handlePauseClick}>Pause</button>
</>
);
Результат:
Как встроить dotLottie-анимацию в свой проект
Анимацию dotLottie можно встроить при помощи библиотеки @lottiefiles/dotlottie-react. Для этого необходимо взять компонент DotLottieReact:
<DotLottieReact
src={testAnim}
loop
autoplay
/>
В этой библиотеке поддерживается рендер только в качестве Canvas.
Для управления анимации здесь используется dotLottieRefCallback
:
import * as React from 'react';
import testAnim from './dotTest.lottie';
import { DotLottie, DotLottieReact } from '@lottiefiles/dotlottie-react';
const TestDotLottie: React.FC = () => {
const [dotLottie, setDotLottie] = React.useState<DotLottie | null>(null);
const play = () => {
dotLottie?.play();
};
const pause = () => {
dotLottie?.pause();
};
const stop = () => {
dotLottie?.stop();
};
return (
<>
<DotLottieReact
src={testAnim}
loop
autoplay
dotLottieRefCallback={setDotLottie}
/>
<div>
<button onClick={play}>Play</button>
<button onClick={pause}>Pause</button>
<button onClick={stop}>Stop</button>
</div>
</>
);
};
export default TestDotLottie;
Помимо стоимости рендеринга, Lottie также просчитывает сами анимации перед их применением. Если Lottie-анимация содержит очень много элементов, то эти расчеты могут забить наш поток (Thread). Для обхода проблемы забитого потока можно перенести Lottie в worker, однако там есть ограничения: Canvas использовать можно, а вот SVG-рендер уже не получится.
В целом, когда SVG много и они двигаются, это плохо для браузера, так как происходит очень много вычислений. Еще одним выходом в таком случае может быть замена на mp4.
Lottie-анимации можно сделать интерактивными, и на сайте LottieFiles этому посвящен интересный гайд. В нем описываются:
синхронизация анимации со скроллом;
зацикливание анимации на промежутке кадров;
синхронизация анимации с курсором;
воспроизведение анимации по клику или наведению;
переключение анимации по клику;
воспроизведение анимации, когда она видна;
объединение анимаций в цепочку.
Когда использовать
для реализации сложных векторных анимаций;
когда необходимо контролировать проигрывание анимации;
когда нужна интерактивность при взаимодействии с анимацией.
Пример использования в проекте
Анимация Сберкота:
Spine
Spine − программа для создания 2D-анимаций. Она удобна в дизайне и анимации игр или роликов. Spine использует скелетную анимацию, то есть объект состоит из набора “костей”, каждая из который отвечает за движение определенной части объекта. Взаимосвязанность костей позволяет создавать сложные и реалистичные движения.
Для Spine-анимаций хранятся только данные "костей", данные анимации и набор повторно использующихся картинок. Данный подход позволяет экономить память, так как требуется хранить меньше изображений и кадров. Также это упрощает процесс создания новых анимаций и персонажей, так как можно использовать один и тот же набор графики для множества различных анимаций.
Spine работает на основе интерполяции между ключевыми кадрами, что позволяет получить очень красивые переходы между движениями. Анимации можно плавно замедлять и ускорять, а система сама адаптирует промежуточные состояния между ключевыми кадрами. Это может быть очень полезно, когда необходимо менять темп движений персонажа в зависимости от определенных условий.
Сравним Spine-анимацию с секвенцией (анимацией на основе спрайт-листов). Слева на видео Spine, а справа − секвенция:
Также с помощью Spine-анимаций можно создавать плавные переходы, как это показано на видео далее. Для анимации слева добавлен плавный переход, в то время как на правой анимации происходит резкое переключение между действиями персонажа. Сравните:
К слову о размере файлов. На сайте Spine есть интересное сравнение количества памяти, необходимое для Spine- и sprite-анимаций:
Здесь видно, насколько меньше ресурсов необходимо для Spine-анимации.
Еще одним достоинством Spine является легкость кастомизации. Можно менять скины персонажей в рамках одной и той же анимации.
Наконец, анимации Spine можно накладывать друг на друга, то есть персонаж анимации может одновременно выполнять несколько действий.
Разумеется, без недостатков не обошлось. Главная трудность работы со Spine-анимациями заключается в том, что для их создания вам придется осваивать довольно сложные специализированные программы. Если у вас есть возможность постучаться за помощью к опытным дизайнерам, а не разбираться в их софте самостоятельно, возможно, будет проще так и сделать.
Чтобы разместить готовую Spine-анимацию в своем веб-приложении, можно использовать одну из предложенных сред выполнения: Canvas, Phaser, Three.js, WebGL и другие. Полный список сред выполнения представлен здесь, а примеры использования Spine-анимаций в различных средах можно найти вот тут.
Когда использовать
для создания игровых персонажей и объектов в 2D;
когда нужна гибкая анимация с возможностью смены скинов.
Недостатки
требует использования программ и навыков для создания скелетных анимаций;
для встраивания анимации необходимо изучить движки, которые позволят ее использовать;
Spine-анимация может по-разному рендериться в зависимости от движка, что приведет к визуальным отличиям и может потребовать больше времени на разработку.
Пример использования в проекте
Котик в проекте для Whiskas:
Секвенция
Секвенция − подход, основанный на последовательном показе заранее подготовленных изображений или кадров. Изображения собираются в спрайт-листы. При последовательном отображении эти изображения создают иллюзию движения.
Спрайт-лист содержит несколько кадров анимации, упакованных в один файл. Здесь каждый кадр представляет собой отдельный этап движения объекта. Например, если нужно создать анимацию бега персонажа, то в спрайт-листе будут отдельно отрисованы все этапы.
Для создания секвенции можно использовать как движки (например, Phaser), так и самостоятельно написать класс, который будет реализовывать все необходимые методы. Подробнее о том, как самостоятельно реализовать анимацию на спрайтах, можно почитать в хорошей статье-туториале.
Особенности секвенции:
анимация воспроизводится по строго заданному сценарию, и каждый кадр заготавливается заранее, следовательно, ее не так легко адаптировать;
качество и плавность анимации сильно зависят от количества кадров и скорости их смены: чем больше кадров, тем плавнее анимация, но и тем больше объем данных для загрузки.
Преимущества секвенции:
простота реализации: для того, чтобы ее реализовать нужно просто отрисовать кадры и настроить их отображение;
высокая художественная точность: художник имеет полный контроль над тем, как движется объект, что позволяет создавать сложные и детализированные анимации.
Недостатки:
большой размер файлов для анимации, что увеличивает время загрузки;
фиксированная анимация: для того, чтобы изменить анимацию, необходимо перерисовать спрайт-лист. Если же вам необходимо добавить персонажу какой-то элемент одежды, то это потребует перерисовки всех кадров;
можно использовать только для малого количества кадров, так как слишком большая секвенция будет непроизводительной и может привести к сбою работы браузера или приложения.
Когда использовать
для анимации игровых персонажей и элементов;
когда необходим контроль за каждым кадром анимации.
Примеры использования в проектах
Пример спрайт-листа с нашего проекта Мегабашня:
Пример анимации кубика с разбором кода и параметров
Пример реализации собственного класса для анимации вращения кубика для той же Мегабашни:
export interface SpriteOptions {
ctx: CanvasRenderingContext2D;
image: string;
width: number;
height: number;
frameWidth: number;
frameHeight: number;
lines: number;
columns: number;
ticksPerFrame?: number;
numberOfFrames?: number;
}
export class Sprite {
ctx: CanvasRenderingContext2D;
image: HTMLImageElement;
width: number;
height: number;
ticksPerFrame: number;
numberOfFrames: number;
frameWidth: number;
frameHeight: number;
frameIndex: number;
tickCount: number;
lines: number;
columns: number;
animation: ReturnType<typeof requestAnimationFrame> | null = null;
private _oneFrameWidth: number;
private _oneFrameHeight: number;
constructor(options: SpriteOptions) {
// Контекст канваса
this.ctx = options.ctx;
// Изображение спрайта, которое будет отрисовываться
const imageElement = new Image();
imageElement.src = options.image;
this.image = imageElement;
// Текущий фрейм спрайта
this.frameIndex = 0;
// Счетчик тиков, нужен для отслеживания, сколько тиков прошло с момента последнего отрисовывания
this.tickCount = 0;
// Кол-во тиков для смены кадра (при 60 fps, ticksPerFrame = 4 будет 15 кадров/с)
this.ticksPerFrame = options.ticksPerFrame ?? 0;
// Количество фреймов в спрайте
this.numberOfFrames = options.numberOfFrames ?? 1;
// Размеры выходного изображения (оно будет скейлиться)
this.width = options.width;
this.height = options.height;
// Размеры одного фрейма (для масштабирования)
this.frameWidth = options.frameWidth;
this.frameHeight = options.frameHeight;
// Кол-во линий и колонок в спрайте
this.lines = options.lines;
this.columns = options.columns;
// Размеры одного фрейма в спрайте
this._oneFrameWidth = options.width / options.columns;
this._oneFrameHeight = options.height / options.lines;
}
private _update() {
this.tickCount++;
if (this.tickCount > this.ticksPerFrame) {
this.tickCount = 0;
this.frameIndex = this.frameIndex + 1 > this.numberOfFrames ? 0 : this.frameIndex + 1;
}
}
private _render() {
this.ctx.clearRect(0, 0, this.frameWidth, this.frameHeight);
this.ctx.drawImage(
this.image,
(this.frameIndex % this.columns) * this._oneFrameWidth,
(Math.floor(this.frameIndex / this.columns) % this.lines) * this._oneFrameHeight,
this._oneFrameWidth,
this._oneFrameHeight,
0,
0,
this.frameWidth,
this.frameHeight
);
}
start() {
const loop = () => {
this._update();
this._render();
this.animation = window.requestAnimationFrame(loop);
};
this.animation = window.requestAnimationFrame(loop);
}
stop() {
if (this.animation) {
window.cancelAnimationFrame(this.animation);
}
this.ctx.clearRect(0, 0, this.frameWidth, this.frameHeight);
}
}
Здесь для описания параметров, передаваемых классу, используется SpriteOptions
, который состоит из:
ctx
− контекстCanvasRenderingContext2D
, куда будет отрисовываться спрайт;image
− путь к изображению спрайта;width
иheight
− размеры всего изображения спрайта;frameWidth
иframeHeight
− размеры одного кадра;lines
иcolumns
− количество строк и колонок кадров в спрайте;ticksPerFrame
(опционально) − количество “тиков” для смены кадра, управляет скоростью анимации;numberOfFrames
(опционально) − общее количество кадров.
В конструкторе инициализируем следующие поля класса:
ctx
− сохраняет контекст канваса;image
− загружает изображение спрайта вHTMLImageElement
;-
кадры и анимация:
frameIndex
− текущий индекс кадра;tickCount
− счетчик для отслеживания количества тиков с момента последнего отрисовывания;ticksPerFrame
− количество тиков, необходимое для смены кадра;numberOfFrames
− количество фреймов в спрайте;
-
размеры:
width
иheight
− размеры всего изображения;frameWidth
иframeHeight
− размеры одного кадра;_oneFrameWidth
и_oneFrameHeight
− размеры одного фрейма в спрайте, вычисленные автоматически.
Приватные методы класса:
_update()
− увеличивает счетчикtickCount
. Если прошло достаточно тиков (большеticksPerFrame
), то увеличиваетframeIndex
. Если текущий кадр был последним, то вframeIndex
записывает 0;-
_render()
− очищает предыдущий кадр (ctx.clearRect
) и отрисовывает следующий черезdrawImage
.Подробнее про метод
drawImage
CanvasRenderingContext2D:drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
image
− элемент, который мы отрисовываем;-
sx
иsy
− координата по осям X и Y левого верхнего угла изображения. Здесь это:(this.frameIndex % this.columns) * this._oneFrameWidth
для оси X;(Math.floor(this.frameIndex / this.columns) % this.lines) * this._oneFrameHeight
для оси Y;
sWidth
иsHeight
− ширина и высота прямоугольника исходного изображения, который будет отрисовываться (здесь беремthis._oneFrameWidth
иthis._oneFrameHeight
);dx
иdy
− начальная точка на канвасе, куда рисуется кадр (здесь всегда 0, 0);dWidth
иdHeight
− размеры, с которыми кадр будет отрисован на канвасе (здесь беремthis.frameWidth
иthis.frameHeight
).
Методы управления анимацией:
start()
− запускает цикл анимации с помощьюrequestAnimationFrame
. Внутриloop
вызываются_update
(обновление кадра) и_render
(отрисовка кадра), идентификатор сохраняется вthis.animation
, чтобы анимацию можно было остановить;stop()
− останавливает анимацию, вызываяcancelAnimationFrame
с сохраненным идентификатором, и очищает канвас.
Все эти параметры мы используем для реализации анимации броска кубика. Небольшой спойлер к тому, как она будет выглядеть:
Для этого напишем компонент Dice
со следующими состояниями:
-
diceResultLoading
− указывает, идет ли сейчас анимация броска (загрузка результата):если значение true, то анимация проигрывается;
если значение сменилось на false, это означает, что анимация завершена и пора показывать результат;
diceResult
− хранит результат броска кубика;isResultAnimation
− указывает, проигрывается ли CSS-анимация кубика.
Также внутри компонента будут следующие ссылки:
canvasRef
− на canvas, где проигрывается анимация;spriteRef
− на объект Sprite, который управляет анимацией.
Основные функции:
-
getDiceResult
− отвечает за симуляцию загрузки и генерацию результата броска кубика. Принцип ее работы следующий:сначала она устанавливает
diceResultLoading
в true;затем запускает
setTimeout
, который через заданное время установитdiceResultLoading
в false, запишет случайное число вdiceResult
и вызовет переданную callback-функцию (здесь будем передаватьhandleStartResultAnimation
, которую рассмотрим далее);
-
handleStartResultAnimation
− управляет запуском CSS-анимации результата:сначала устанавливает
isResultAnimation
в true;затем запускает
setTimeout
, который через заданное время выключает анимацию (ставитisResultAnimation
в false);
-
handleDiceClick
− обрабатывает клик на кубике:вызывает getDiceResult, передавая в нее
handleStartResultAnimation
в качестве коллбэка.
Анимация:
-
когда
diceResultLoading
становится true, выполняются:проверка наличия
<canvas>
черезcanvasRef
;получение контекста для рисования (
ctx
);-
создание нового объекта
Sprite
с параметрами для анимации:image
: изображение спрайта;width
,height
: размеры изображения;numberOfFrames
: общее количество кадров в спрайте;lines
,columns
: строки и столбцы (8x8);ticksPerFrame
: скорость смены кадров (1 тик = 1 кадр);
запуск анимации методом
spriteRef.current.start()
;
когда
diceResultLoading
становитсяfalse
, анимация спрайта останавливается вызовомspriteRef.current.stop()
.
Получаем такой компонент:
import cx from 'clsx';
import * as React from 'react';
import diceSprite from '../assets/images/dice/cube-sprite.png';
import s from './Dice.module.scss';
import { Sprite } from './Sprite';
import { DICE_IMAGES } from '../assets/images/dice';
const DICE_RESULT_ANIMATION_TIME = 1200;
type Props = {
onHandleNextStepFinished?: () => void;
};
type DiceResult = 1 | 2 | 3 | 4 | 5 | 6;
const Dice: React.FC<Props> = () => {
const [diceResultLoading, setDiceResultLoading] = React.useState(false);
const [diceResult, setDiceResult] = React.useState<DiceResult>(1);
const [isResultAnimation, setIsResultAnimation] = React.useState(false);
const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
const spriteRef = React.useRef<Sprite | null>(null);
const getDiceResult = React.useCallback((callback?: () => void) => {
setDiceResultLoading(true);
setTimeout(() => {
setDiceResultLoading(false);
setDiceResult(Math.ceil(Math.random() * 6) as DiceResult);
callback?.();
}, 2000);
}, []);
const handleStartResultAnimation = React.useCallback(() => {
setIsResultAnimation(true);
const timeout = setTimeout(() => {
setIsResultAnimation(false);
}, DICE_RESULT_ANIMATION_TIME);
return () => {
if (timeout) {
clearTimeout(timeout);
}
};
}, []);
const handleDiceClick = React.useCallback(() => {
getDiceResult(handleStartResultAnimation);
}, [getDiceResult, handleStartResultAnimation]);
React.useEffect(() => {
if (!diceResultLoading || !canvasRef.current) {
spriteRef.current?.stop();
return undefined;
}
const ctx = canvasRef.current.getContext('2d');
if (!ctx) {
return undefined;
}
spriteRef.current = new Sprite({
ctx,
image: diceSprite,
width: 2048,
height: 2048,
numberOfFrames: 60,
frameHeight: 100,
frameWidth: 100,
lines: 8,
columns: 8,
ticksPerFrame: 1,
});
spriteRef.current.start();
return () => {
spriteRef.current?.stop();
};
}, [diceResultLoading]);
return (
<div
className={cx(s['dice-wrapper'], { [s.inactive]: diceResultLoading })}
onClick={handleDiceClick}
>
<div className={s.dice}>
{!diceResultLoading && (
<img
src={DICE_IMAGES[diceResult - 1]}
className={cx(s.image, {
[s.image_withAnimation]: isResultAnimation,
})}
/>
)}
<canvas width={100} height={100} ref={canvasRef} />
</div>
</div>
);
};
export default Dice;
Если результат броска загружен (!diceResultLoading), отображается <img> с результатом кубика (DICE_IMAGES[diceResult - 1]) и небольшой CSS-анимацией. В момент загрузки результата показываем анимацию. На результат можно посмотреть чуть ниже.
Так выглядели анимации секвенции (бросок кубика и движение зайчика) в проекте Мегабашня:
Еще один пример использования секвенции в другом нашем проекте:
Другие инструменты
Помимо перечисленного важно упомянуть, что есть библиотеки, которые могут упростить и ускорить процесс создания сложных анимаций. В наших проектах чаще всего используются motion (Framer Motion) или react-spring. У них есть довольно подробная документация с примерами использования, так что здесь я не буду углубляться в детали.
React Spring использует физически обоснованный подход, оперируя понятиями массы, упругости и трения, что позволяет создавать естественные и реалистичные движения. Благодаря моделям физики элементы могут “подпрыгивать” и “затухать”, имитируя реальные физические законы.
Например, на проекте, где нужно было сделать красивую анимацию перелистывания карточек в колоде, мы взяли за основу пример с колодой карт из документации react-spring и доработали его в соответствии с требованиями задачи. Получилось такое:
motion (Framer Motion) − библиотека для создания анимаций, которая отличается простотой использования и ориентацией на декларативный подход. С помощью этой библиотеки можно быстро и без лишней сложности сделать различные анимации, начиная от простых, связанных с положением, цветом, прозрачностью, заканчивая различными анимациями перетаскивания элементов и их движением при скролле. Здесь лучше тоже обратиться к документации, она достаточно подробна и понятна.
Например, здесь для проекта был использован компонент Reorder и получилась такая красота:
Заключение
Выбирайте подход к созданию анимаций, отталкиваясь от ваших задач и ресурсов. Если вы анимируете элементы веб-интерфейса, не ограничивайтесь базовым CSS: несмотря на его простоту и универсальность, в некоторых ситуациях WebP, Canvas или Lottie могут показать себя более эффективными.
Для простых анимаций игровых персонажей и объектов не мудрите с тяжелыми секвенциями спрайтов, если анимацию можно реализовать с помощью Spine. Секвенцией я в целом рекомендую пользоваться только тогда, когда нужен полный контроль за каждым пикселем. Для анимации 2D-персонажей она чаще всего оказывается избыточной. Однако если вы не ограничены весом файлов и производительностью, она может стать для вас незаменимым инструментом.
Напоследок поделюсь материалами моих коллег-фронтендеров − рекомендую познакомиться с ними, чтобы узнать больше об анимациях и не только:
Летающий Санта и танцующие снегири: опыт реализации и оптимизации CSS-анимации
Не JavaScript’ом единым: как фронтенд-разработчику затащить на собесе
Если у вас есть опыт работы с другими способами создания анимаций, призываю поделиться им в комментариях. Вполне возможно, что эти технологии мы тоже возьмем на вооружение, и я разберу их подробно в своих будущих статях. Спасибо, что дочитали до конца!
Комментарии (3)
imouseR
07.12.2024 17:03Есть еще интересный инструмент Rive. Позволяет уменьшать размеры подгружаемых элементов анимации и упростить их создание. На Хабре выходила статья со сравнением Rive и Lottie. Название: «Они убивают Lottie — молодцы!»
nlinker
Спасибо за интесную статью, некоторые анимированные персонажи из приведённых иллюстраций довольно милые :-)
Скажите, а что вы думаете на счёт GSAP, доводилось ли использовать, если да, то удобен ли, насколько его возможностей достаточно для создания сложных анимаций?
d_romashhh Автор
Спасибо за ваш комментарий! Что касается GSAP, к сожалению, пока не имела возможности с ним работать, поэтому не могу оценить его удобство и функционал. Но думаю что обязательно изучу этот вопрос в будущем