Команда JavaScript for Devs подготовила перевод статьи о том, почему тригонометрические функции стали «most hated» возможностью CSS и как их можно использовать с пользой. Мы разберёмся, что делают sin()
и cos()
, и посмотрим на практические примеры: от круговых раскладок до затухающих анимаций.
В CSS нет по-настоящему «ужасных» возможностей, верно? В конце концов, всё зависит от личного опыта и мнения. Но если попробовать прийти к какому-то консенсусу, результаты опроса State of CSS 2025 — хорошая отправная точка. Я так и сделал: заглянул в раздел с наградами и нашёл там «Most Hated Feature» — титул, которого не заслуживает ни одна возможность CSS…

Честно говоря, я в шоке. Неужели тригонометрические функции настолько ненавидят? Понимаю, что «ненавидеть» — не то же самое, что называть что-то «худшим», но звучит всё равно неприятно. Да и драматизирую я немного — ведь «всего лишь 9,1 % участников действительно ненавидят тригонометрию». Но всё равно — слишком много негатива, на мой вкус.
Хочу, чтобы этих 9,1 % стало меньше. Поэтому в этой серии разберём практические примеры применения тригонометрических функций CSS. Будем рассматривать их по частям — материала довольно много, и проще учиться, когда информация разделена на небольшие, легко усваиваемые блоки. И начнём мы, пожалуй, с самых популярных функций этой «худшей» возможности — sin() и cos().
А что вообще такое cos() и sin()?
Этот раздел для тех, кто пока не до конца понимает, как работают cos() и sin(), или просто хочет освежить память. Если вы блистали на контрольных по тригонометрии в школе — смело переходите к следующему разделу!
Что мне кажется забавным в cos() и sin() — и, думаю, именно из-за этого вокруг них столько путаницы, — так это то, что их можно описать множеством разных способов. И искать долго не придётся: на странице Википедии можно найти просто головокружительное количество тонких и нюансированных определений.
Это настоящая проблема обучения в веб-разработке. Мне кажется, что многие из этих определений слишком общие и не объясняют сути того, что именно делают тригонометрические функции вроде sin() и cos(). А другие, наоборот, чересчур академические и сложные — чтобы их понять, нужно иметь чуть ли не учёную степень.
Так что давайте придерживаться золотой середины: единичной окружности.
Познакомьтесь с единичной окружностью. Это окружность радиусом в одну единицу:

Пока что она одинока… где-то в космосе. Давайте поместим её на декартову систему координат (ту самую классическую схему с осями X и Y). Каждая точка в пространстве описывается декартовыми координатами:
Координата X — горизонтальная ось, определяет положение точки слева или справа.
Координата Y — вертикальная ось, определяет положение точки сверху или снизу.

В этом демо мы можем перемещаться по единичной окружности, откладывая угол, который измеряется от положительной оси X против часовой стрелки.
Если идти по часовой стрелке, используем отрицательные углы. Как говорил мой учитель физики: «Время идёт в минус!»
Обратите внимание: каждому углу соответствует уникальная точка на единичной окружности. Как ещё можно описать эту точку с помощью декартовых координат?
Когда угол равен 0°, координаты X и Y будут 1 и 0 — соответственно (1, 0). Мы можем так же легко определить координаты для других «круглых» углов — 90°, 180° и 270°. Но для любого другого угла мы заранее не знаем, где именно окажется точка на окружности.
Если бы только существовала пара функций, которые принимают угол и выдают нужные координаты…
Именно это и делают функции CSS cos() и sin(). Они тесно связаны: cos() вычисляет координату X, а sin()возвращает координату Y.
Поиграйтесь с ползунком в демо, чтобы увидеть, как связаны эти две функции, и обратите внимание, что вместе они образуют прямоугольный треугольник с начальной точкой на единичной окружности:
Думаю, на этом этапе этого вполне достаточно, чтобы понять, как работают cos() и sin(). Они связаны с декартовыми координатами, что позволяет отслеживать точку на единичной окружности по углу — независимо от того, какого размера сама окружность.
Теперь давайте посмотрим, как мы можем применять cos() и sin() в повседневной работе с CSS. Всегда полезно добавить немного практического контекста к теоретическим математическим концепциям.
Круговое расположение
Если опираться на определение cos() и sin() через единичную окружность, становится очевидно, что их можно использовать для создания кругового расположения в CSS. Начнём с базовой заготовки — одного ряда круглых элементов:
Скажем, мы хотим расположить каждый элемент по окружности большего круга. Сначала нужно «сказать» CSS, сколько всего элементов у нас есть, а также задать каждому элементу его индекс (порядковый номер). Это можно сделать с помощью встроенной CSS-переменной, в которой хранится порядковый номер:
<ul style="--total: 9">
<li style="--i: 0">0</li>
<li style="--i: 1">1</li>
<li style="--i: 2">2</li>
<li style="--i: 3">3</li>
<li style="--i: 4">4</li>
<li style="--i: 5">5</li>
<li style="--i: 6">6</li>
<li style="--i: 7">7</li>
<li style="--i: 8">8</li>
</ul>
Примечание: когда функции
sibling-index()
иsibling-count()
получат поддержку в браузерах, этот шаг станет намного проще и лаконичнее (и они правда классные!). Пока же я просто хардкодю индексы через встроенные CSS-переменные.
Чтобы разместить элементы по окружности, их нужно распределить равномерно по углу. Для этого делим 360deg (полный оборот) на общее количество элементов — в данном примере их 8. Затем для каждого элемента умножаем этот угол на его индекс:
li {
--rotation: calc(360deg / var(--total) * var(--i));
}
Ещё нам нужно отодвинуть элементы от центра. Для этого задаём радиус окружности через отдельную переменную:
ul {
--radius: 10rem;
}
Теперь у нас есть угол и радиус. Осталось вычислить координаты X и Y для каждого элемента.
И вот тут в дело вступают cos() и sin(). С их помощью получаем координаты X и Y на единичной окружности, а потом умножаем их на значение --radius
, чтобы получить итоговую позицию на большом круге:
li {
/* ... */
position: absolute;
transform: translateX(calc(cos(var(--rotation)) * var(--radius)))
translateY(calc(sin(var(--rotation)) * var(--radius)));
}
Готово! Теперь у нас есть восемь круглых элементов, равномерно расположенных по окружности большего круга:
И при этом нам не понадобились «магические числа»! Мы просто передали CSS радиус окружности, а дальше CSS сам сделал всю тригонометрическую «магическую кухню», из-за которой многие и считают это «худшей» возможностью CSS. Надеюсь, мне удалось немного смягчить ваше отношение к ним, если именно это вас останавливало!
И не обязательно ограничиваться полным кругом! Можно выстроить элементы по полуокружности, выбрав 180deg вместо 360deg:
Это открывает массу возможностей для расположения элементов. Например, что если нам нужно круговое меню, которое «раскрывается» из центра за счёт анимации радиуса окружности? Легко сделать и такое. Нажмите или наведите на заголовок — и пункты меню выстроятся по окружности!
Волнистое расположение
С расположением можно играться ещё больше! Если, например, отложить координаты cos() и sin() на графике с двумя осями, вы увидите пару волн, которые периодически идут вверх и вниз. И обратите внимание: они смещены друг относительно друга по горизонтальной оси (X).

Откуда берутся эти волны? Вспомним единичную окружность: значения cos() и sin() колеблются между −1 и 1. Иными словами, длины совпадают, когда угол на единичной окружности меняется. Если изобразить это колебание на графике, мы получим наши волны и увидим, что они как бы зеркалят друг друга.

Можем ли мы расположить элемент по одной из этих волн? Разумеется. Начнём с того же ряда круглях элементов, что мы делали ранее. На этот раз, однако, длина ряда выходит за пределы вьюпорта, вызывая переполнение.
Мы присвоим каждому элементу индекс, как и раньше, но теперь нам не нужно знать общее количество элементов. В прошлый раз их было восемь, так что давайте поднимем до 10 и сделаем вид, что мы этого не знаем:
<ul>
<li style="--i: 0"></li>
<li style="--i: 1"></li>
<li style="--i: 2"></li>
<li style="--i: 3"></li>
<li style="--i: 4"></li>
<li style="--i: 5"></li>
<li style="--i: 6"></li>
<li style="--i: 7"></li>
<li style="--i: 8"></li>
<li style="--i: 9"></li>
<li style="--i: 10"></li>
</ul>
Мы хотим менять вертикальное положение элемента вдоль волны sin() или cos(), то есть смещать каждый элемент в зависимости от его порядка в индексе. Умножаем индекс элемента на некоторый угол, который передаём в функцию sin() — она возвращает коэффициент, определяющий, насколько высоко или низко должен находиться элемент на волне. Затем умножаем результат на величину в единицах длины — я взял половину от полного размера элемента.
Вот математика на языке CSS:
li {
transform: translateY(calc(sin(60deg * var(--i)) * var(--shape-size) / 2));
}
Я использую значение 60deg, потому что получающиеся волны выглядят плавнее, чем при некоторых других значениях. Но его можно менять как угодно, чтобы получить волну «круче». Поиграйтесь с переключателем в следующем демо и посмотрите, как меняется «интенсивность» волны в зависимости от угла:
Это отличный пример, чтобы понять, с чем мы работаем. Но как применить это на практике? Представьте, что у нас есть две такие волнистые цепочки из кружков, и мы хотим переплести их между собой — примерно как двойная спираль ДНК.
Допустим, мы начинаем с HTML-структуры: два ненумерованных списка, вложенные в ещё один ненумерованный список. Эти два вложенных списка — это две волны, из которых складывается цепочка:
<ul class="waves">
<!-- First wave -->
<li>
<ul class="principal">
<!-- Circles -->
<li style="--i: 0"></li>
<li style="--i: 1"></li>
<li style="--i: 2"></li>
<li style="--i: 3"></li>
<!-- etc. -->
</ul>
</li>
<!-- Second wave -->
<li>
<ul class="secondary">
<!-- Circles -->
<li style="--i: 0"></li>
<li style="--i: 1"></li>
<li style="--i: 2"></li>
<li style="--i: 3"></li>
<!-- etc. -->
</ul>
</li>
</ul>
Похоже на предыдущие примеры, верно? Мы по-прежнему работаем с ненумерованным списком, элементы которого индексируются CSS-переменной, но теперь у нас два таких списка, и оба находятся внутри третьего списка. Не обязательно использовать именно списки, но я оставил их, чтобы позже использовать как «якоря» для дополнительного оформления.
Чтобы избежать проблем, проигнорируем два прямых элемента <li>
во внешнем списке, которые содержат вложенные списки, с помощью display: contents
.
.waves > li { display: contents; }
Обратите внимание, что одна из цепочек — principal, а другая — secondary. Разница в том, что цепочка secondary расположена позади цепочки principal. Я использую слегка разные цвета фона для элементов каждой цепочки, чтобы их проще было различать при прокрутке блока с переполнением.
Мы можем поменять порядок отображения цепочек, используя стековый контекст:
.principal {
position: relative;
z-index: 2;
}
.secondary { position: absolute; }
Так мы располагаем одну цепочку поверх другой. Далее мы изменим вертикальное положение каждого элемента с помощью «ненавистных» функций sin() и cos(). Помните, что они как бы отражения друг друга, и разница между ними создаёт смещение, благодаря которому получается эффект пересекающихся цепочек:
.principal {
/* ... */
li {
transform: translateY(calc(sin(60deg * var(--i)) * var(--shape-size) / 2));
}
}
.secondary {
/* ... */
li {
transform: translateY(calc(cos(60deg * var(--i)) * var(--shape-size) / 2));
}
}
Мы можем ещё сильнее подчеркнуть смещение, сдвинув волну .secondary
дополнительно на 60deg:
.secondary {
/* ... */
li {
transform: translateY(calc(cos(60deg * var(--i) + 60deg) * var(--shape-size) / 2));
}
}
В следующем демо показано, как волны пересекаются при угловом смещении в 60deg. Попробуйте подвигать ползунок — вы увидите, как изменяется угол пересечения волн.
Я же говорил, всё это можно применить на практике. Как насчёт добавить немного игривости и выразительности в hero-баннер?
Затухающие колебательные анимации
Последний пример навёл меня на мысль: можно ли использовать возвратно-поступательное движение sin() и cos()для создания анимаций? Первое, что пришло в голову — анимация, которая тоже движется туда-обратно, что-то вроде маятника или подпрыгивающего шарика.
Разумеется, это довольно просто — можно сделать одной строкой:
.element {
animation: someAnimation 1s infinite alternate;
}
Такое движение «туда-обратно» называется колебательным движением. И хотя cos() или sin() действительно можно использовать для моделирования колебаний в CSS, это было бы своего рода изобретением велосипеда (только менее удобного).
Но я понял, что идеальное колебательное движение — маятник, который качается вечно, или шарик, который никогда не перестаёт прыгать — в реальности не существует. Любое движение со временем затухает, как пружина, которая постепенно останавливается:

Есть специальный термин, который это описывает: затухающие колебания (damped oscillatory movement). И угадайте что? Их тоже можно смоделировать в CSS с помощью функции cos()! Если построить график во времени, мы увидим, что движение идёт туда-обратно, постепенно приближаясь к положению покоя1.

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

Формула состоит из трёх частей:
e⁻ᵞᵗ — из-за отрицательного показателя экспонента со временем становится всё меньше, и движение постепенно останавливается. Умножается на коэффициент затухания (γ), который определяет, как быстро затухает движение.
a — начальная амплитуда колебания, то есть исходное положение элемента.
cos(ωt − α) — задаёт само колебание во времени. Время умножается на частоту (ω), которая определяет скорость2 колебаний элемента. Мы также можем вычесть из времени α, чтобы задать сдвиг начальной фазы колебаний.
Ладно, теории хватит! Как сделать это в CSS? Начнём с заготовки: одинокого кружка, стоящего сам по себе:
Мы можем определить несколько CSS-переменных, которые пригодятся, ведь мы уже знаем формулу, с которой работаем:
:root {
--circle-size: 60px;
--amplitude: 200px; /* Амплитуда — это расстояние, поэтому указываем в пикселях */
--damping: 0.3;
--frequency: 0.8;
--offset: calc(pi/2); /* То же самое, что 90deg! (но в радианах) */
}
Используя эти переменные, можно посмотреть, как будет выглядеть анимация на графике, например, в GeoGebra:

На графике видно, что анимация начинается с 0px (спасибо offset
), достигает пика примерно в 140px и полностью затухает примерно через 25 с. Но я точно не собираюсь ждать 25 секунд, чтобы анимация закончилась, поэтому создадим свойство --progress
, которое будет анимироваться от 0 до 25 и выступать в роли «времени» в нашей формуле.
Напоминаю: чтобы анимировать или транзитировать кастомное свойство, его нужно зарегистрировать с помощью правила @Properties
@property --progress {
syntax: "<number>";
initial-value: 0;
inherits: true;
}
@keyframes movement {
from { --progress: 0; }
to { --progress: 25; }
}
Теперь остаётся реализовать формулу движения элемента, но уже на языке CSS:
.circle {
--oscillation: calc(
(exp(-1 * var(--damping) * var(--progress))) *
var(--amplitude) *
cos(var(--frequency) * (var(--progress)) - var(--offset))
);
transform: translateX(var(--oscillation));
animation: movement 1s linear infinite;
}
Результат:
Даже такая версия выглядит довольно эффектно — но пока затухание происходит только по оси X. Что, если применить его сразу по обеим осям? Для этого можно скопировать ту же формулу, но заменить cos() на sin() для Y:
.circle {
--oscillation-x: calc(
(exp(-1 * var(--damping) * var(--progress))) *
var(--amplitude) *
cos(var(--frequency) * (var(--progress)) - var(--offset))
);
--oscillation-y: calc(
(exp(-1 * var(--damping) * var(--progress))) *
var(--amplitude) *
sin(var(--frequency) * (var(--progress)) - var(--offset))
);
transform: translateX(var(--oscillation-x)) translateY(var(--oscillation-y));
animation: movement 1s linear infinite;
}
Результат:
Это выглядит ещё лучше! Получилось круговое затухающее движение — и всё благодаря cos() и sin().
Но помимо того, что это просто красиво, как это можно применить в реальной вёрстке?
Далеко искать не пришлось — вот, например, сайдбар, который я недавно делал: его пункты меню «въезжают» в область видимости с эффектом затухающего движения.
Результат:
Круто, правда?
Русскоязычное JavaScript сообщество

Друзья! Эту статью перевела команда «JavaScript for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Frontend. Подписывайтесь, чтобы быть в курсе и ничего не упустить!
Продолжаем тригонометрическую тему!
Оказывается, найти применение для «самой ненавистной функции CSS» не так уж сложно — может, пора начать относиться к тригонометрическим функциям с любовью? Но погодите. В CSS есть ещё несколько тригонометрических функций, о которых мы пока не говорили. В следующих материалах мы продолжим исследовать, что умеют такие функции, как tan() и обратные функции.
CSS Trigonometric Functions: «Самая ненавистная возможность CSS»
sin() и cos() (вы здесь!)
Разбираемся с функцией CSS tan() (скоро опубликуем)
Обратные функции: asin(), acos(), atan() и atan2() (скоро опубликуем)
И да, чуть не забыл — вот ещё одно демо, которое я сделал с использованием cos() и sin(), но не включил в эту статью. Всё же его стоит посмотреть — там я добавил ещё больше «закрученности», чтобы показать, до какого безумия можно дойти.
Zenitchik
Я такое в школе на QBASIC-е рисовал. С этими же самыми формулами.