---
title: "Плавные переходы (tween) и функции плавности на JavaScript: ease-in, ease-out и платформа по маршруту"
description: "Как сделать плавное движение в 2D-игре без физики: формула твина от прошедшего времени, функции плавности linear, ease-in, ease-out, ease-in-out и с перелётом, их аналоги в CSS cubic-bezier, платформа по маршруту с постоянной скоростью и перенос стоящего на ней игрока. Код проверен в Node.js 24."
slug: plavnye-perehody-tween-i-funkcii-plavnosti-na-javascript
date: 2026-11-05
updated: 2026-11-05
scheduled_at: "2026-11-05T09:00:00+03:00"
rubric: praktika-i-uroki
tags: ["tween", "easing", "animaciya", "javascript", "novichkam"]
reading_time: 9
draft: false
summary: "Твин (tween) — это изменение числа от начального значения к конечному за заданное время, а функция плавности решает, как распределить это изменение: равномерно, с разгоном, с торможением или с перелётом. Значение нужно считать от прошедшего времени по формуле «начало + (конец − начало) × плавность(t)», тогда ворота откроются за 0,4 с одинаково на 60 и 144 Гц. Для платформы по маршруту из нескольких точек позицию считают по пройденному расстоянию, а игроку, стоящему на ней, каждый кадр добавляют смещение платформы."
---

## Коротко

Твин меняет число от начального значения к конечному за заданное время, а функция плавности задаёт характер движения: равномерно, с разгоном, с торможением или с перелётом. Считайте значение из прошедшего времени, а не прибавляйте шаг каждый кадр: тогда результат одинаков на 60 и 144 Гц. Ниже — формулы пяти функций плавности, таблица выбора, код твина и платформы по маршруту с пассажиром, проверенный в Node.js 24.

## Содержание

1. Формула твина: от прошедшего времени, а не от прошлого значения
2. Функции плавности: пять формул
3. Те же кривые в CSS: cubic-bezier
4. Таблица: какую плавность брать в игре
5. Платформа по маршруту: постоянная скорость по длине пути
6. Пассажир на платформе
7. Код на JavaScript
8. Практический сценарий: ворота и лифт за вечер
9. Когда вам это не подойдёт

## Формула твина: от прошедшего времени, а не от прошлого значения

Значение твина в любой момент вычисляется из прошедшего времени по одной формуле:

```text
t        = min(прошло / длительность, 1)
значение = начало + (конец − начало) × плавность(t)
```

Частая ошибка — прибавлять к значению «шаг за кадр»: при 144 Гц твин длиной 0,4 с занимает 57,6 кадра, шагов получается 58, и ворота уезжают на 96,67 пикселя вместо 96. Формула от времени даёт точный конец, потому что при `t = 1` любая функция плавности возвращает 1. Время ограничивают длительностью, а событие «твин закончился» вызывают ровно один раз.

## Функции плавности: пять формул

Функция плавности получает долю времени `t` от 0 до 1 и возвращает долю пути. Пяти функций хватает для большинства игровых задач:

```text
linear(t)    = t
inQuad(t)    = t²                              — разгон
outQuad(t)   = 1 − (1 − t)²                    — торможение
inOutQuad(t) = 2t², если t < 0,5;
               иначе 1 − (−2t + 2)² / 2        — разгон и торможение
outBack(t)   = 1 + 2,70158 × (t − 1)³ + 1,70158 × (t − 1)²   — с перелётом
```

В середине времени (`t = 0,5`) они дают 0,5, 0,25, 0,75, 0,5 и 1,088 соответственно. Значение больше 1 у `outBack` — это и есть перелёт: объект проскакивает цель максимум на 10% пути (при `t ≈ 0,58`) и возвращается. Сборник взаимозаменяемых функций плавности публикует Роберт Пеннер; формулы выше — квадратичный вариант и функция с перелётом, остальные подключаются так же — заменой одной функции.

## Те же кривые в CSS: cubic-bezier

В CSS функции плавности задаются кривыми Безье, и ключевые слова — это готовые кривые. По MDN, `ease-in` равно `cubic-bezier(0.42, 0, 1, 1)`, `ease-out` — `cubic-bezier(0, 0, 0.58, 1)`, `ease-in-out` — `cubic-bezier(0.42, 0, 0.58, 1)`, а `ease` — `cubic-bezier(0.25, 0.1, 0.25, 1)`. Есть и ступенчатая функция `steps()`, которая делит анимацию на равные интервалы.

Для интерфейса поверх холста, свёрстанного в HTML, этого достаточно. Для объектов на холсте CSS не работает, и формулы выше считают в коде игры. Квадратичные функции близки к CSS-ключевым словам по характеру, но не совпадают с ними точно.

## Таблица: какую плавность брать в игре

Выбор зависит от того, что должен почувствовать игрок: старт, остановку или удар.

| Задача | Функция | Длительность | Почему |
|---|---|---|---|
| Окно или счётчик выезжает | outQuad | 0,2–0,3 с | Быстрая реакция, мягкая остановка |
| Окно уходит за край | inQuad | 0,15–0,25 с | Не задерживает игрока |
| Ворота или дверь открываются | outQuad | 0,3–0,5 с | Видно начало, нет удара в конце |
| Лифт между двумя этажами | inOutQuad | зависит от высоты | Плавный старт и остановка, игрок не подпрыгивает |
| Монета летит в счётчик, значок награды «пружинит» | outBack | 0,3–0,4 с | Перелёт даёт ощущение «упругости» |
| Платформа по длинному маршруту | linear по расстоянию | скорость в пикс/с | Предсказуемая скорость для прыжка |

Значения длительности — наши стартовые, их подбирают на глаз в игре.

## Платформа по маршруту: постоянная скорость по длине пути

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

```text
пройдено = скорость × время  (туда-обратно: по модулю 2 × длина маршрута)
найти отрезок, где лежит «пройдено», и интерполировать внутри него
```

Для маршрута 300 пикселей вправо и 400 вверх длина 700; при скорости 100 пикс/с через 5 с платформа на середине вертикального отрезка, а через 9 с — там же, но на обратном пути.

## Пассажир на платформе

Игроку, стоящему на движущейся платформе, каждый кадр добавляют её смещение за этот кадр. Порядок в кадре: сдвинуть платформу, запомнить смещение, сдвинуть стоящих на ней, затем применить собственное движение игрока и проверить столкновения. Если делать наоборот, игрок «соскальзывает» или проваливается сквозь платформу, которая едет вверх.

## Код на JavaScript

Код содержит пять функций плавности, твин от прошедшего времени, маршрут с постоянной скоростью и перенос пассажира. Он проверен в Node.js 24, числа в комментариях совпадают с выводом.

```js
// Функции плавности: t от 0 до 1 → доля пути от 0 до 1
const ease = {
  linear: (t) => t,
  inQuad: (t) => t * t,
  outQuad: (t) => 1 - (1 - t) * (1 - t),
  inOutQuad: (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2),
  outBack: (t) => {
    const c1 = 1.70158, c3 = c1 + 1;
    return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
  },
};

// Твин считает значение от прошедшего времени, а не прибавляет шаг к значению
function tween(target, key, to, duration, fn, onDone) {
  return { target, key, from: target[key], to, duration, fn, onDone, elapsed: 0, done: false };
}

function updateTween(tw, dt) {
  if (tw.done) return;
  tw.elapsed = Math.min(tw.elapsed + dt, tw.duration);
  const k = tw.fn(tw.elapsed / tw.duration);
  tw.target[tw.key] = tw.from + (tw.to - tw.from) * k;
  if (tw.elapsed === tw.duration) { tw.done = true; if (tw.onDone) tw.onDone(); }
}

// Платформа по маршруту: постоянная скорость по длине пути, туда и обратно
function makePath(points) {
  const lens = [];
  let total = 0;
  for (let i = 1; i < points.length; i++) {
    const l = Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y);
    lens.push(l);
    total += l;
  }
  return { points, lens, total };
}

function pointAt(path, dist) {
  let d = Math.max(0, Math.min(dist, path.total));
  for (let i = 0; i < path.lens.length; i++) {
    if (d <= path.lens[i]) {
      const a = path.points[i], b = path.points[i + 1], k = d / path.lens[i];
      return { x: a.x + (b.x - a.x) * k, y: a.y + (b.y - a.y) * k };
    }
    d -= path.lens[i];
  }
  return { ...path.points[path.points.length - 1] };
}

// Пройденное расстояние за время t при скорости v и движении туда-обратно
function pingPongDist(path, v, t) {
  const cycle = 2 * path.total;
  const s = (v * t) % cycle;
  return s <= path.total ? s : cycle - s;
}

// Пассажир: игрок, стоящий на платформе, получает её смещение за кадр
function carry(player, before, after) {
  player.x += after.x - before.x;
  player.y += after.y - before.y;
}

// Проверки
console.log(['linear', 'inQuad', 'outQuad', 'inOutQuad', 'outBack']
  .map((n) => n + ' ' + ease[n](0.5).toFixed(3)).join(', '));
// linear 0.500, inQuad 0.250, outQuad 0.750, inOutQuad 0.500, outBack 1.088

function runGate(fps) {
  const gate = { y: 0 };
  let finished = 0;
  const tw = tween(gate, 'y', -96, 0.4, ease.outQuad, () => finished++);
  for (let i = 0; i < fps; i++) updateTween(tw, 1 / fps); // 1 секунда
  return gate.y + ' ' + finished;
}
console.log(runGate(60), '|', runGate(144)); // -96 1 | -96 1

const path = makePath([{ x: 0, y: 0 }, { x: 300, y: 0 }, { x: 300, y: -400 }]);
console.log(path.total); // 700
console.log(pointAt(path, pingPongDist(path, 100, 5))); // { x: 300, y: -200 }
console.log(pointAt(path, pingPongDist(path, 100, 9))); // { x: 300, y: -200 } — на обратном пути

const player = { x: 140, y: -48 };
let prev = pointAt(path, pingPongDist(path, 100, 1));
for (let i = 1; i <= 60; i++) {
  const now = pointAt(path, pingPongDist(path, 100, 1 + i / 60));
  carry(player, prev, now);
  prev = now;
}
console.log(Math.round(player.x), Math.round(player.y)); // 240 -48 — проехал вместе с платформой 100 пикс
```

Ворота уезжают на 96 пикселей и вызывают завершение ровно один раз и на 60, и на 144 Гц; игрок за 1 секунду проезжает с платформой 100 пикселей.

## Практический сценарий: ворота и лифт за вечер

Добавить в уровень ворота и лифт можно за 5 шагов:

1. **Ворота.** По событию «рычаг нажат» запустите твин высоты ворот на 96 пикселей, `outQuad`, 0,4 с; столкновения ворот отключайте в обработчике завершения.
2. **Лифт.** Задайте маршрут из 2–4 точек и скорость 80–120 пикс/с; позицию считайте по пройденному расстоянию.
3. **Пассажир.** Определите, что игрок стоит на лифте (низ игрока на верхней грани лифта), и добавляйте ему смещение лифта каждый кадр.
4. **Проверка частоты.** Запустите сцену при 60 и 144 Гц или с искусственно большим `dt`: конечные позиции должны совпасть.
5. **Отмена.** Если рычаг переключают во время движения, начинайте новый твин от текущего значения, а не от исходного, иначе ворота «прыгнут».

## Когда вам это не подойдёт

Твины хороши для заранее известного движения, но не везде:

- **Движение, на которое влияют силы:** отскоки, падение, толчки от взрыва. Здесь нужна скорость и интегрирование по времени, а не твин между двумя точками.
- **Сложная анимация персонажа:** десятки связанных костей или кадров удобнее делать покадровой или скелетной анимацией в специализированном редакторе.
- **Плавное слежение за движущейся целью:** камера за героем — это сглаживание с экспонентой, а не твин к точке, которая всё время меняется.

Мы разрабатываем **J2DRU** — браузерный 2D игровой движок, где сцены, спрайты и игровая логика собираются в браузере, а проект можно углублять на TypeScript. Движок в разработке и публично пока не открыт, заявку можно оставить на [странице раннего доступа](/early-access?utm_source=blog&utm_medium=article&utm_campaign=plavnye-perehody-tween-i-funkcii-plavnosti-na-javascript) (подача заявки не гарантирует приглашение).

**Источники (проверено: ноябрь 2026):** [MDN — easing-function](https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function) (значения cubic-bezier для ключевых слов, steps()), [Robert Penner — Easing Functions](http://robertpenner.com/easing/) (сборник сменных функций плавности). Формулы квадратичных функций и все числа примеров — наш расчёт, проверенный в Node.js 24.

**Читайте также:**
- [Камера, параллакс и game feel в 2D-игре](/blog/kamera-parallaks-i-game-feel-v-2d-igre)
- [Движение по сетке против свободного движения в 2D-игре](/blog/dvizhenie-po-setke-protiv-svobodnogo-dvizheniya-v-2d-igre)
- [Игровой цикл и физика столкновений AABB](/blog/igrovoj-cikl-i-fizika-stolknovenij-aabb)
