Easing и плавность

Когда цыплёнок не прыгает рывком, а мягко подъезжает к цели и сам притормаживает в конце — это и есть easing, главный секрет приятной анимации.

Главная идея урока: чтобы движение было плавным, не телепортируй объект к цели и не двигай его на одинаковый шаг. Каждый кадр сдвигай его на часть оставшегося расстояния до цели. Чем ближе — тем меньше шаг, тем мягче торможение.

Зачем тебе easing

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

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

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

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

Что такое easing

Метафора: половина пути до двери

Представь, что ты стоишь в коридоре, а в десяти метрах от тебя — дверь. И у тебя странное правило: каждый шаг ты проходишь ровно половину оставшегося до двери расстояния. Первый шаг — 5 метров (полпути). Второй — 2,5 метра (половина остатка). Третий — 1,25. Дальше шаги становятся всё мельче и мельче: ты несёшься в начале и почти ползёшь у самой двери, мягко к ней притираясь. Ты никогда не врежешься в неё рывком — ты к ней подкрадываешься.

Easing (произносится «и́зинг») — это приём плавного движения, когда объект каждый кадр приближается к цели на часть оставшегося расстояния и за счёт этого замедляется к концу.

Вот почему это так приятно глазу: в природе ничто не стартует и не тормозит мгновенно. Мяч, который ты катишь по полу, сначала катится быстро, а потом всё медленнее, пока не замрёт. Машина у светофора не встаёт в стену — она плавно гасит скорость. Easing воспроизводит ровно это ощущение замедления — и поэтому анимация с ним выглядит «дорого» и живо, а без него — дёргано и дёшево.

Чем это отличается от постоянной скорости

В прошлом уроке мы двигали цыплёнка так: x = x + 3;. Это значит «каждый кадр сдвигайся ровно на 3 пикселя». Шаг тут фиксированный — что в начале пути, что у самой цели цыплёнок идёт с одной и той же скоростью, как заводная игрушка. А когда он добирается до цели, останавливать его приходится вручную: писать if, ловить момент, иначе он просто проедет мимо и улетит за край.

Easing переворачивает эту идею. Шаг здесь не фиксированный, а пропорциональный — он зависит от того, как далеко осталось ехать. Сравни две строки внимательно:

  • x = x + 3; — шаг всегда 3, скорость постоянная, остановки нет.
  • x = x + (targetX - x) * 0.1; — шаг каждый раз свой: большой вдалеке, крошечный у цели.

Эта маленькая разница в формуле и даёт всё ощущение жизни. Постоянная скорость — это движение робота по рельсам. Easing — это движение чего-то живого, у чего есть вес и нежелание врезаться в стену. Запомни этот контраст: он объясняет, зачем мы вообще усложняем простое сложение.

Формула плавного приближения

Вся магия умещается в одну строку. У нас есть текущая позиция объекта x и цель targetX, к которой он стремится. Каждый кадр делаем так:

x = x + (targetX - x) * 0.1;

Разберём по кусочкам, потому что это сердце всего урока:

  • targetX - x — это оставшееся расстояние до цели. Если цыплёнок далеко, число большое; если близко — маленькое; если уже на месте — ноль.
  • * 0.1 — берём от этого расстояния только десятую часть. Это и есть наш «шаг половиной пути», только не половина, а одна десятая.
  • x = x + ... — прибавляем этот кусочек к текущей позиции, сдвигая цыплёнка чуть ближе к цели.

Фокус в том, что расстояние targetX - x пересчитывается каждый кадр заново. Пока цыплёнок далеко, десятая часть большого расстояния — это большой шаг, он мчится. По мере приближения расстояние тает, десятая часть от него становится крошечной — и цыплёнок сам собой замедляется. Никаких отдельных «команд тормозить» писать не нужно: торможение встроено в саму формулу.

Разбираем на примерах

Пример 1: цыплёнок едет к центру

Начнём с простого. Цыплёнок стартует у левого края, а его цель — центр холста. Смотри, как он подъезжает.

let x = 20;           // стартовая позиция цыплёнка
let targetX = 200;    // куда он едет (центр)

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

function draw() {
  background(135, 206, 235);

  // easing: сдвигаемся на 1/10 оставшегося пути
  x = x + (targetX - x) * 0.1;

  // тело CodeChick
  fill(255, 209, 64);
  circle(x, 200, 80);

  // клюв
  fill(255, 140, 30);
  triangle(x + 30, 195, x + 55, 200, x + 30, 210);
}

Результат: на голубом небе жёлтый цыплёнок стартует у левого края и быстро срывается вправо, к центру. По мере приближения к центру он заметно замедляется и мягко замирает ровно в точке x = 200, не дёргаясь и не проскакивая мимо. Движение похоже на то, как закрывается ящик с доводчиком — сначала бодро, потом нежно.

Обрати внимание: в коде нигде нет команды «остановись». Цыплёнок тормозит сам, потому что чем ближе он к 200, тем меньше становится (targetX - x), а значит и шаг. Это и есть красота easing — ты описываешь не само движение по шагам, а лишь стремление к цели.

Пример 2: коэффициент easing — крутилка плавности

Число 0.1 в формуле — это и есть та самая «крутилка» мягкости. Программисты называют его коэффициентом easing. Он всегда между 0 и 1, и от него зависит характер движения:

КоэффициентЧто получаем
0.02очень медленно, лениво, долгий подъезд
0.1мягко и плавно — золотая середина
0.3бодро, быстро, чуть резче
1.0телепорт: сразу в цель, без плавности

Давай вынесем коэффициент в отдельную переменную, чтобы им было удобно играть:

let x = 20;
let targetX = 320;
let ease = 0.08;   // крутилка плавности: меняй меня!

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

function draw() {
  background(135, 206, 235);

  x = x + (targetX - x) * ease;

  fill(255, 209, 64);
  circle(x, 200, 80);
}

Результат: цыплёнок плавно подъезжает к правой части холста. При ease = 0.08 подъезд получается заметно неторопливым и очень мягким. Поменяй число на 0.3 — и цыплёнок домчится почти мгновенно, с лёгким торможением в конце; поставь 0.02 — и он поползёт томно, словно нехотя.

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

Пример 3: резко против плавно — сравнение бок о бок

Чтобы разница врезалась в память, нарисуем двух цыплят разом. Верхний прыгает к цели рывком (как телепорт), нижний подъезжает плавно через easing. Цель у обоих — туда, куда ты кликнул мышью по горизонтали.

let easeX = 200;   // плавный цыплёнок
let targetX = 200; // общая цель по клику

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

function draw() {
  background(135, 206, 235);

  // ВЕРХНИЙ: резко — сразу в цель, без easing
  fill(230, 90, 90);
  circle(targetX, 130, 70);

  // НИЖНИЙ: плавно — через формулу easing
  easeX = easeX + (targetX - easeX) * 0.1;
  fill(255, 209, 64);
  circle(easeX, 270, 70);
}

function mousePressed() {
  targetX = mouseX;   // новая цель — туда, куда кликнули
}

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

Этот пример — главный аргумент урока. Один и тот же код целей, одна и та же цель — но плавный вариант ощущается в разы приятнее. Именно так разработчики игр и приложений делают интерфейс «вкусным»: добавляют easing там, где наивный код просто перескочил бы.

Пример 4: easing работает не только с координатами

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

let size = 80;        // текущий размер тела
let targetSize = 80;  // желаемый размер

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

function draw() {
  background(135, 206, 235);

  // если курсор близко к центру — цель больше
  let d = dist(mouseX, mouseY, 200, 200);
  if (d < 80) {
    targetSize = 160;   // раздуться
  } else {
    targetSize = 80;    // обычный размер
  }

  // easing того же вида, но для размера
  size = size + (targetSize - size) * 0.1;

  fill(255, 209, 64);
  circle(200, 200, size);
}

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

Заметь: строка easing size = size + (targetSize - size) * 0.1; устроена точь-в-точь как для координаты — просто буквы другие. Это и есть сила приёма: один раз понял формулу — и применяешь её к любому числу, которое хочешь оживить.

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

  • Перепутал порядок: x - targetX вместо targetX - x. Если вычесть наоборот, знак расстояния перевернётся, и цыплёнок поедет прочь от цели, улетая за край холста. Запомни направление: «куда хочу минус где я сейчас».

  • Коэффициент больше 1 или отрицательный. Easing-коэффициент живёт строго между 0 и 1. Поставишь 1.5 — цыплёнок будет проскакивать цель и дёргаться туда-сюда, как на пружине; поставишь отрицательное — улетит назад. Держись диапазона от 0 до 1.

  • Забыл, что цель должна меняться. Если targetX задан раз и навсегда и нигде не обновляется, цыплёнок один раз доедет до неё и навсегда замрёт — анимации больше не будет. Чтобы он снова поехал, цель надо менять (по клику, по таймеру, случайно).

  • Ждёшь, что объект «доедет точно» и остановится насовсем. Формально x приближается к цели бесконечно: шаги становятся микроскопическими, но строго нулём не становятся. На глаз это незаметно — разница в тысячные доли пикселя. Но если тебе важно поймать момент «приехал», сравнивай не на равенство, а на близость: if (abs(targetX - x) < 0.5).

  • Поставил easing-формулу, но всё равно дёргается. Чаще всего причина — забытый background() в начале draw() (тогда копятся старые кадры) или слишком большой коэффициент вроде 0.9, при котором плавности почти нет. Проверь обе вещи.

Мини-практика: цыплёнок гоняется за мышью

Собери свой скетч, где CodeChick плавно следует за курсором — это классическое и очень залипательное упражнение. План такой:

  1. Заведи переменные x и y для позиции цыплёнка (стартуй из центра, 200 и 200).
  2. В draw() каждый кадр делай easing по обеим осям: цель по x — это mouseX, цель по y — это mouseY. Получится две строки: x = x + (mouseX - x) * 0.1; и такая же для y.
  3. Нарисуй цыплёнка в точке (x, y): жёлтое тело и оранжевый клюв.
  4. Поэкспериментируй с коэффициентом: сделай 0.05 — цыплёнок будет лениво догонять мышь с запозданием, как на резинке; сделай 0.5 — будет почти приклеен к курсору.

Когда заработает, попробуй добавить «хвост»: убери background() или сделай его полупрозрачным (background(135, 206, 235, 40)), и за цыплёнком потянется мягкий шлейф из прошлых кадров. Это уже почти спецэффект из настоящей игры — а в основе всё та же одна строчка easing.

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

Итоги

Сегодня ты освоил приём, который отличает живую анимацию от деревянной:

  • Easing — это движение на часть оставшегося расстояния каждый кадр: чем ближе к цели, тем мельче шаг, тем мягче торможение.
  • Вся суть — в одной формуле: x = x + (targetX - x) * ease;, где targetX - x пересчитывается каждый кадр.
  • Коэффициент ease (от 0 до 1) — это крутилка плавности: меньше — ленивее и мягче, ближе к 1 — резче и быстрее.
  • Торможение получается само собой, без отдельных команд: оно встроено в то, что расстояние тает по мере приближения.

Теперь CodeChick умеет двигаться так, будто у него есть вес и инерция — мягко разгоняться и аккуратно подъезжать к цели. Это огромный шаг: твоя анимация перестала быть механической. Дальше будет ещё интереснее — в следующем уроке мы займёмся отскоком и упругостью: научим цыплёнка прыгать так, чтобы он отталкивался от стенок холста и пружинил. Easing и отскок вместе превратят его в настоящего живого героя. Увидимся на следующей странице!

Проверьте себя
1. В чём суть приёма easing?
AДвигать объект на одинаковый шаг каждый кадр
BКаждый кадр сдвигать объект на часть оставшегося до цели расстояния
CМгновенно перемещать объект в цель
DСлучайно менять позицию объекта
2. Что вычисляет выражение (targetX - x) в формуле easing?
AСкорость объекта в пикселях за кадр
BОставшееся расстояние от текущей позиции до цели
CКоэффициент плавности
DРазмер фигуры
3. Почему объект с easing замедляется к концу сам, без отдельной команды «тормозить»?
Ap5.js автоматически тормозит все фигуры
BПотому что коэффициент со временем уменьшается
CПотому что оставшееся расстояние тает, а шаг — это его доля, значит шаг тоже уменьшается
DПотому что frameRate падает
4. В каком диапазоне держат коэффициент easing?
AОт 0 до 1
BОт 1 до 10
CЛюбое число, лишь бы положительное
DТолько целые числа
5. Что произойдёт, если в формуле случайно написать (x - targetX) вместо (targetX - x)?
AНичего, движение будет таким же
BОбъект поедет прочь от цели, в обратную сторону
CОбъект остановится на месте
DСкетч выдаст ошибку
6. Цыплёнок один раз доехал до targetX и больше не движется. В чём, скорее всего, дело?
AКоэффициент easing слишком мал
BЗабыли вызвать background()
CЦель targetX задана один раз и нигде не обновляется
DХолст слишком маленький