Как сделать анимацию по контуру в CSS

Как сделать анимацию по контуру в CSS

Содержание
  1. Создание пончика
  2. Анимация прогресса
  3. Перемещение по кругу
  4. Итоговый результат

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

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

Недавно передо мной встала задача создать состояние загрузки для одного проекта, поэтому, естественно, я обратился к CodePen за вдохновением. Мне нужна была круглая форма, и недостатка в примерах нет. Во многих случаях подход представляет собой некоторую комбинацию использования свойства CSS border-radius для получения круглой формы и @keyframes для ее вращения от 0deg до 360deg.

Мне нужно было немного больше, чем это. В частности, мне нужна была форма пончика, которая заполняла бы индикатор прогресса по мере продвижения от 0% до 100%. К счастью, я нашел отличные примеры пончиков, которые можно использовать для вдохновения, и несколько различных подходов. Например, я мог бы использовать ”трюк” SVG с анимируемым обводкой с комбинацией stroke-dasharray и stroke-dashoffset. У Темани Афифа есть сотни примеров, в которых используется сочетание градиентов и масок CSS.

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

Видите? У самоката есть круговая дорожка, которая заполняется градиентом по мере движения вокруг фигуры. Если вы используете Firefox, у вас, скорее всего, возникнут проблемы с демонстрацией, потому что она опирается на пользовательский @property, который Firefox пока не поддерживает. Тем не менее, он поддерживается в версии Nightly, так что, возможно, скоро мы сможем рассчитывать на полную поддержку.

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

Создание пончика

Круги в CSS довольно просты. Мы можем нарисовать его в SVG и полностью забыть о CSS. Это правильный подход, но мне удобнее работать с такими вещами непосредственно в CSS. Начнем с единственного элемента в HTML:

<div class="progress-circle"></div>

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

.progress-circle {
	ширина: 200px;
	aspect-ratio: 1;
}

Теперь мы можем закруглить фигуру с помощью свойства border-radius:

.progress-circle {
	ширина: 200px;
	aspect-ratio: 1;
	border-radius: 50%;
}

Вот и наша фигура! Конечно, мы еще ничего не видим, потому что не залили ее цветом. Давайте сделаем это с помощью conic-gradient. Нам нужен именно такой, потому что по умолчанию градиент движется по кругу, начиная с 0% и завершая полный круг на 360deg.

.progress-circle {
	ширина: 200px;
	aspect-ratio: 1;
	border-radius: 50%;
	background: conic-gradient(red 10%, #eee 0);
}

Пока все хорошо:

То, на что мы смотрим, похоже на круговую диаграмму, верно? Мы создали круглую форму и заполнили ее коническим градиентом, который начинается с красного и резко останавливается на #eee, заполняя остальную часть пирога светло-серым цветом.

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

Я пошел другим путем, отчасти для удобства, а отчасти для того, чтобы продемонстрировать, как CSS способен решать задачи разными способами. Так что вы можете пойти иным путем, чем тот, который мы демонстрируем здесь. Мой подход заключается в использовании псевдоэлемента ::before в .progress-circle. Мы помещаем его поверх конического градиента с абсолютным позиционированием, заливаем сплошным цветом и размещаем так, чтобы он затмевал часть основной фигуры. По сути, это меньший круг сплошного цвета поверх большего круга, заполненного градиентом.

.progress-circle {
	/* предыдущие стили */
	position: relative;
}
.progress-circle::before {
	content: '';
	позиция: абсолютная;
	вставка: 20px;
	border-radius: inherit;
	фон: белый;
}

Обратите внимание на то, что мы делаем для позиционирования меньшего круга. Поскольку мы работаем с ::before, нам нужно свойство CSS content, чтобы оно отображалось, даже с пустым значением. Далее мы используем абсолютное позиционирование, устанавливая меньший круг в центр с inset, применяемым во всех направлениях. Мы можем наследовать свойство border-radius большего круга, прежде чем установить сплошной цвет фона. Мы не забываем установить относительное позиционирование для большего круга, чтобы (а) задать контекст суммирования и (б) удержать меньший круг в границах большего круга.

Вот и все для пончика! Мы сделали это чисто в CSS, используя комбинацию свойств border-radius, conic-gradient и хорошо расположенный псевдо-элемент ::before.

Анимация прогресса

Работали ли вы с пользовательскими свойствами CSS? Я имею в виду не просто определение --some-variable со значением, а использование @property для регистрации свойства с пользовательским синтаксисом. Волшебно, как это позволяет нам интерполировать между значениями, которые мы обычно не можем, например, значения цвета и угла в градиентах.

Когда мы регистрируем пользовательское свойство CSS, мы должны указать его тип, например, является ли значение <длина>, <число>, <цвет> или любой из 11 других типов, которые поддерживаются на момент, когда я пишу это. Таким образом, браузер понимает, с каким значением он работает, и когда наступит время, он сможет обновить значение переменной для анимации.

Я собираюсь зарегистрировать пользовательское свойство под названием --p, что является сокращением для его синтаксиса, <процент>, с начальным значением 10%, которое будет ”стартовой” точкой для индикатора прогресса.

@property --p {
	синтаксис: '';
	наследуется: false;
	initial-value: 10%;
}

Теперь мы можем использовать переменную --p там, где нам это нужно, например, там, где жесткий цвет останавливается между red и #eee в коническом градиенте большого круга, который мы используем в качестве отправной точки.

.progress-circle {
	/* предыдущие стили */
	background: conic-gradient(red var(--p), #eee 0);
}

Мы хотим перейти от начального значения пользовательского свойства 10% к большему проценту, чтобы переместить жесткую цветовую остановку градиента по форме. Поэтому давайте настроим CSS transition, который будет обновлять значение --p.

.progress-circle {
/_ предыдущие стили _/
фон: conic-gradient(red var(--p), #eee 0);
переход: --p 2s линейный;
}

Мы собираемся обновлять значение при наведении, переходя от 10% к 80%:

.progress-circle:hover {
	--p: 80%;
}

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

.progress-circle {
/_ предыдущие стили _/
cursor: progress;
}

Наш круг готов! Теперь мы можем навести курсор на элемент, и жесткий цвет конического градиента перестанет переходить от 10% к 80% за меньшим кругом, который скрывает остальную часть градиента, создавая форму пончика. Мы зарегистрировали пользовательское @свойство с начальным значением, применили его к градиенту и обновили значение при наведении.

Перемещение по кругу

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

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

Давайте добавим самокат в HTML в качестве эмодзи:

<div class="progress-circle">
	<div class="progress-indicator">🛵</div>
</div>

Если бы мы решили создать начальную форму пончика с помощью SVG, то в качестве дорожки можно было бы использовать тот же путь, который мы использовали для большого круга. Однако мы можем получить те же возможности создания пути в CSS с помощью свойства offset-path. Это настолько похоже на написание SVG в CSS, что мы можем использовать точно такие же координаты для круга SVG в path():

.chart-indicator {
/_ предыдущие стили _/
offset: path('M 100, 0 a 100 100 0 1 1 -.1 0 z');
}

Координаты пути SVG трудно читать, но именно это мы и делаем в данном конкретном пути:

  1. M 100, 0: Это перемещает положение начальной точки в системе координат X-Y, где 100 расположено вдоль оси X и равно радиусу большего круга, или половине его ширины, 200px. Начальная точка установлена на 0 по оси Y, располагаясь в верхней части фигуры. Таким образом, мы начинаем с верхнего центра большей окружности.
  2. a 100 100: Это задает дугу с горизонтальным и вертикальным радиусами 100, давая нам новый круг. Несмотря на то, что технически мы не видим окружность, она нарисована, обеспечивая самокату невидимую дорожку, повторяющую форму большей окружности.

И еще одно! У нас есть отправная точка для самоката благодаря координатам в offset-path. Свойство CSS offset-distance позволяет нам определить конечную точку, куда мы планируем сместить самокат, которая в точности равна пользовательскому свойству --p.

.chart-indicator {
	/* предыдущие стили */
	offset-path: path('M 100, 0 a 100 100 0 1 1 -.1 0 z');
	offset-distance: var(--p);
}

Мы уже обновляем наше пользовательское свойство --p при наведении, чтобы помочь переместить позицию жесткой остановки конического градиента с начального значения 10% до 80%. Мы должны сделать то же самое для скутера, чтобы они двигались вместе.

.progress-circle:hover > .progress-indicator {
	--p: 80%;
}

Я использую комбинатор дочерних элементов (>), поскольку индикатор является прямым потомком круга. Если ваш дизайн включает дополнительные элементы или требует, чтобы самокат был дальнейшим потомком, то вместо этого можно использовать общий селектор потомков.

Итоговый результат

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

/* Пользовательское свойство */
@property --p {
	синтаксис: '<percentage>';
	наследуется: false;
	initial-value: 10%;
}

/* Большой круг */
.progress-circle {
	-размер: 200px;
	--p: 10%; /* запасной вариант при отсутствии поддержки @property */

	background: conic-gradient(red calc(-60% + var(--p)), rgb(224, 187, 77) var(--p), #eee 0);
	border-radius: 50%;
	положение: относительное;
	margin: auto;
	курсор: прогресс;
}

/* Псевдоэлемент маленького круга */
.progress-circle::before {
	content: 'Продвижение от десяти до восьмидесяти процентов';
	position: absolute;
	insert: 20px;
	text-align: center;
	padding: 50px;
	font: italic 9pt 'Enriqueta';
	border-radius: inherit;
	фон: белый;
}

/* Дорожка для самоката */
.progress-indicator {
	-размер: min-content;
	смещение: path('M 100,0 a 100 100 0 1 1 -.1 0 z');
	смещение-расстояние: var(--p);
	шрифт: 43pt serif;
	transform: rotateY(180deg) translateX(-6px);
}

/* Обновление начального значения при :hover */
.progress-circle:hover,
.progress-circle:hover > .progress-indicator {
	--p: 80%;
}

/* Управляет шириной большего круга и дорожки самоката */
.progress-circle,
.progress-indicator {
	ширина: var(--size);
	переход: --p 2s linear;
}

Скутер и сплошной градиент - это только одна идея. Как насчет разных объектов с разными траекториями?

Я называл этот компонент и ”индикатором прогресса”, и”загрузчик” во всей статье. Поэтому в примере я использую общий <div> в качестве <фигуры>, но вы с тем же успехом можете использовать его в более семантических HTML-элементах, таких как <прогресс> или <измеритель>, в зависимости от вашего конкретного случая использования. Для обеспечения доступности можно использовать описательный текст, который можно объявить предложениями, описывающими данные с помощью вспомогательных технологий.

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