Тригонометрия для арта: sin и cos

Две функции — sin и cos — превращают скучный счётчик кадров в плавную волну и в движение по кругу. Сегодня они заставят CodeChick покачиваться, будто он плывёт на воде.
sin и cos — это не страшная формула из учебника, а два колёсика, которые берут угол и выдают число, плавно гуляющее от -1 до 1 и обратно. Если кормить их растущим углом каждый кадр, получится волна — а из волны рождается любое плавное движение в анимации.

Зачем это вообще нужно

Открой любую игру или приложение и присмотрись к мелочам. Иконка уведомления, которая мягко дышит — становится чуть больше, чуть меньше. Кнопка «Играть», которая еле заметно покачивается, маня тебя нажать. Облака, которые лениво плывут по небу в платформере. Лоадер-спиннер, который крутится по кругу, пока грузится ролик. Всё это — не случайные дёрганья. За каждым таким плавным движением почти всегда стоят две функции: sin и cos.

В прошлом уроке про цикл for и ряды фигур ты выстраивал цыплят и зёрна в ровные ряды и сетки — это была статика, картинка стояла на месте. Сегодня мы добавим в эту картину жизнь: научимся брать угол и превращать его в плавное колебание. CodeChick перестанет стоять столбиком — он начнёт покачиваться вверх-вниз, будто сидит на волне, а потом и вовсе побежит по кругу.

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

Метафора: колесо обозрения

Забудь на минуту про треугольники из школы. Представь колесо обозрения в парке — большое, медленно крутится. Ты сидишь в кабинке и едешь по кругу. У твоей кабинки в каждый момент есть две вещи: высота над землёй и сдвиг вбок от центра колеса.

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

sin(угол) и cos(угол) берут угол поворота и возвращают число от -1 до 1. sin — это высота точки на колесе, cos — её сдвиг вбок. Чем больше угол, тем дальше кабинка проехала по кругу.

Главное, что нужно запомнить про эти числа: они никогда не вылезают за пределы от -1 до 1. Что бы ты ни подставил — хоть маленький угол, хоть огромный — на выходе всегда будет аккуратное число в этом диапазоне. Это как качели: как сильно их ни раскачивай по фазе, они не улетят в космос — есть крайняя левая точка и крайняя правая, между ними они и ходят.

Углы в p5.js считаются в радианах

Есть одна ловушка, о которой надо сказать сразу. sin и cos в p5.js по умолчанию ждут угол не в привычных градусах (0–360), а в радианах. Не пугайся слова: радианы — это просто другая линейка для углов. Полный круг — это не 360, а TWO_PI (примерно 6.28). Половина круга — PI (примерно 3.14). Четверть — HALF_PI.

На практике тебе почти не придётся считать радианы в уме. Ты будешь просто плавно увеличивать угол маленькими шажками каждый кадр — например, на 0.05, — и колесо само поедет. А PI и TWO_PI — это готовые константы p5.js, их не надо запоминать числом, достаточно писать имя.

Пример 1. Волна из чисел

Прежде чем что-то рисовать, давай просто посмотрим глазами, какие числа выдаёт sin, когда угол растёт. Это самый важный пример урока — пойми его, и всё остальное пойдёт легко.

let ugol = 0;

function setup() {
  createCanvas(400, 200);
}

function draw() {
  background(255, 245, 200);

  let v = sin(ugol);
  // v гуляет от -1 до 1

  // превращаем -1..1 в высоту 0..200
  let y = map(v, -1, 1, 180, 20);

  fill(255, 140, 0);
  noStroke();
  ellipse(200, y, 30, 30);

  ugol = ugol + 0.05;
}

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

Что здесь происходит по шагам

  1. Снаружи функций мы завели ugol = 0 — переменную состояния, которая хранит текущий угол поворота нашего «колеса» между кадрами.
  2. Каждый кадр sin(ugol) выдаёт число от -1 до 1 и кладёт его в v.
  3. map(v, -1, 1, 180, 20) — переводчик диапазонов. Он берёт число из диапазона -1..1 и растягивает его в координату по вертикали: -1 станет 180 (низ), 1 станет 20 (верх). Без этого шага кружок дёргался бы в пределах одного пикселя, ведь sin сам по себе крошечный.
  4. Рисуем кружок на этой высоте по центру холста (x всегда 200).
  5. В самом конце прибавляем к углу 0.05. Это и есть «колесо медленно поехало»: на следующем кадре угол чуть больше, sin выдаст чуть другое число, кружок сместится. За много кадров складывается плавная волна.

Поиграй с числом 0.05. Поставь 0.2 — кружок заметается вверх-вниз быстро, как нервный. Поставь 0.01 — он будет ползать величаво и медленно. Это число — скорость вращения колеса.

Пример 2. CodeChick качается на волне

Теперь главная цель урока — заставим самого цыплёнка покачиваться по синусоиде, будто он сидит на воде и его несёт лёгкая волна.

let ugol = 0;

function setup() {
  createCanvas(400, 400);
  noStroke();
}

function draw() {
  background(180, 220, 255);

  // базовая высота 200, качаемся на 40 пикселей вверх-вниз
  let y = 200 + sin(ugol) * 40;

  // тело
  fill(255, 215, 0);
  ellipse(200, y, 150, 140);

  // голова
  ellipse(200, y - 80, 100, 90);

  // клюв
  fill(255, 140, 0);
  triangle(245, y - 85, 245, y - 65, 285, y - 75);

  // глаз
  fill(0);
  ellipse(215, y - 90, 14, 14);

  ugol = ugol + 0.04;
}

Результат: на светло-голубом небе жёлтый цыплёнок с оранжевым клювом мягко покачивается вверх и вниз вокруг центра холста. Он поднимается примерно на 40 пикселей выше середины, плавно тормозит, опускается на 40 пикселей ниже и снова всплывает. Голова, клюв и глаз двигаются вместе с телом единым целым — будто цыплёнок и правда сидит на невидимой волне и блаженно покачивается.

Сердце примера — строка с высотой

Вся магия в одной строке: let y = 200 + sin(ugol) * 40;. Разберём её по кусочкам, это очень частый рецепт.

  • 200 — это центр качания, точка, вокруг которой всё колышется. Середина холста по высоте.
  • sin(ugol) — это волна, число от -1 до 1.
  • * 40 — это амплитуда, размах качания. Мы растягиваем крошечное -1..1 до -40..40. Поставишь * 100 — цыплёнок будет нырять размашисто, почти на весь экран; поставишь * 10 — будет еле заметно подрагивать.

Формула центр + sin(угол) * размах — твой главный инструмент для любого плавного колебания: дыхание иконки, парение облака, мерцание звезды. Запомни её как таблицу умножения.

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

Пример 3. Бег по кругу: sin и cos вместе

До сих пор мы использовали только sin для вертикали. А если взять sin для одной оси и cos для другой — точка побежит по идеальному кругу. Помнишь колесо обозрения: sin даёт высоту, cos — сдвиг вбок. Вместе они и есть кабинка, едущая по окружности.

let ugol = 0;
let cx = 200;
let cy = 200;
let r = 120;

function setup() {
  createCanvas(400, 400);
  noStroke();
}

function draw() {
  background(255, 245, 200);

  // гнездо-центр
  fill(150, 110, 60);
  ellipse(cx, cy, 60, 60);

  // точка на окружности по углу
  let x = cx + cos(ugol) * r;
  let y = cy + sin(ugol) * r;

  // цыплёнок бежит по кругу
  fill(255, 215, 0);
  ellipse(x, y, 40, 40);

  ugol = ugol + 0.03;
}

Результат: в центре кремового холста лежит коричневый кружок-гнездо. Вокруг него по ровной круговой орбите бежит маленький жёлтый цыплёнок — как Луна вокруг планеты. Он движется с одинаковой скоростью, не ускоряясь и не замедляясь, и через равные промежутки возвращается в ту же точку. Орбита идеально круглая.

Рецепт точки на окружности

Две строки делают всю работу:

let x = cx + cos(ugol) * r;
let y = cy + sin(ugol) * r;

Читай так: cx, cy — центр круга (наше гнездо), r — радиус (как далеко от центра летит цыплёнок). cos(ugol) отвечает за сдвиг по горизонтали, sin(ugol) — за сдвиг по вертикали. Когда угол растёт, пара (cos, sin) обходит весь круг, и точка едет по орбите. Это тот самый рецепт, по которому рисуют стрелки часов, спиннеры загрузки, планеты в симуляторах и круговые меню в играх.

Хочешь эллипс вместо круга? Дай разный радиус для x и y, например cos(ugol) * 150 и sin(ugol) * 80 — орбита станет вытянутой. Поэкспериментируй.

Частые ошибки и подводные камни

1. Забывают про map или умножение — и движения «не видно»

Самая частая жалоба новичка: «я написал sin(ugol), а цыплёнок стоит на месте». Так и должно быть! sin выдаёт числа от -1 до 1 — это меньше пикселя, глаз такое смещение не заметит. Обязательно умножай результат на амплитуду (* 40, * 100) или прогоняй через map, иначе волна спрячется в одну точку.

2. Думают, что угол в градусах

В p5.js sin и cos по умолчанию считают угол в радианах, а не в градусах. Если прибавлять к углу по 1 каждый кадр, думая, что это «один градус», движение будет дёрганым и слишком быстрым — ведь 1 радиан это аж 57 градусов. Для плавности прибавляй маленькие доли: 0.02, 0.05. Полный круг — это TWO_PI, примерно 6.28, а не 360.

3. Не увеличивают угол — волна замирает

Если забыть строку ugol = ugol + 0.05; в конце draw(), угол навсегда останется нулём, sin(0) всегда даст 0, и цыплёнок застынет в одной позе. Колесо едет только потому, что угол растёт каждый кадр. Нет роста угла — нет движения.

4. Путают sin и cos местами в круге

Для движения по кругу cos идёт на горизонталь (x), а sin — на вертикаль (y). Если перепутать и поставить обе функции на одну ось, точка не побежит по кругу, а будет ездить по диагонали туда-сюда. Запомни связку: cos — «вбок» (по горизонтали слева направо), sin — «вверх-вниз».

5. Объявляют ugol внутри draw()

Если написать let ugol = 0; внутри draw(), угол будет рождаться заново нулём на каждом кадре и никогда не вырастет. Переменная угла, как любая переменная состояния, должна жить снаружи всех функций, в самом верху файла. Только тогда она копит значение от кадра к кадру.

Мини-проект: дышащее солнце над гнездом

Собери собственную сцену, где сразу две вещи живут по синусоиде. База — пример 2, дальше доделай сам:

  1. Нарисуй цыплёнка, который качается вверх-вниз, как в примере 2 (формула 200 + sin(ugol) * 40).
  2. Добавь в угол холста солнце — жёлтый круг. Пусть оно дышит: его диаметр меняется по формуле 60 + sin(ugol) * 15. Так радиус будет плавно расти и уменьшаться.
  3. Сделай так, чтобы солнце и цыплёнок качались не в такт. Для этого заведи второй угол или прибавляй к углу солнца сдвиг: используй sin(ugol + PI) — тогда, когда цыплёнок вверху, солнце будет в нижней фазе.
  4. Усложни: пусти по небу облако, которое движется по горизонтали через cos: let oblakoX = 200 + cos(ugol) * 150;. Оно будет плавно уплывать к краю и возвращаться.

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

Итоги

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

  • sin и cos берут угол и возвращают плавное число от -1 до 1. sin — высота точки на колесе, cos — её сдвиг вбок.
  • Чтобы движение было видно, результат надо растянуть: умножить на амплитуду или прогнать через map.
  • Рецепт колебания: центр + sin(угол) * размах. Рецепт движения по кругу: x = cx + cos(угол) * r, y = cy + sin(угол) * r.
  • Угол надо плавно увеличивать каждый кадр маленькими шажками (0.020.05), и хранить его в переменной снаружи функций. Углы в p5.js — в радианах, полный круг это TWO_PI.

Теперь у тебя в руках инструмент для любого плавного движения — а это половина всей красивой анимации. В следующем уроке мы соединим волну с циклом for и нарисуем сразу ряд точек по синусоиде — получится настоящая бегущая волна из десятков элементов, как эквалайзер в музыкальном плеере. Тот же sin, только теперь много раз за кадр. До встречи в коде!

Проверьте себя
1. Какие числа возвращает функция sin(ugol) в p5.js?
AЛюбые числа от 0 до 360
BТолько целые числа
CЧисла от -1 до 1
DЧисла от 0 до радиуса круга
2. Почему кружок не двигается, если просто написать ellipse(200, sin(ugol), 30, 30)?
Asin выдаёт числа от -1 до 1 — это меньше пикселя, смещения не видно
Bsin работает только внутри setup()
CНужно сначала вызвать createCanvas заново
Dellipse не умеет принимать sin
3. В строке y = 200 + sin(ugol) * 40 за что отвечает число 40?
AЗа скорость качания
BЗа амплитуду — размах колебания вверх-вниз
CЗа центр качания
DЗа частоту кадров
4. Как получить точку, бегущую по кругу с центром (cx, cy) и радиусом r?
Ax = cx + sin(ugol) * r и y = cy + sin(ugol) * r
Bx = cx + cos(ugol) * r и y = cy + sin(ugol) * r
Cx = cos(ugol) и y = sin(ugol)
Dx = cx * ugol и y = cy * ugol
5. В каких единицах sin и cos по умолчанию ждут угол в p5.js?
AВ градусах (0–360)
BВ пикселях
CВ радианах (полный круг — TWO_PI)
DВ процентах
6. Что произойдёт, если убрать строку ugol = ugol + 0.05; в конце draw()?
AАнимация ускорится
BУгол останется нулём, и фигура застынет на месте
Cp5.js выдаст ошибку
DФигура начнёт двигаться по кругу