Прерывание spring анимаций

Работа spring-анимаций в Motion One основана на физической модели упругости, где движение определяется начальными условиями (позиция, скорость) и параметрами системы (жёсткость, демпфирование, масса). В отличие от линейных таймлайнов, spring-переходы обладают инерцией, что делает их особенно чувствительными к прерыванию: новая анимация должна учитывать уже накопленную скорость и текущее состояние объекта.

Spring-анимация в Motion One вычисляется на каждом кадре как интеграция физической системы:

  • текущее положение
  • текущая скорость
  • целевая точка
  • коэффициенты упругости и демпфирования

Каждый тик анимационного цикла обновляет состояние, пока система не достигнет состояния покоя.

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

Причины прерывания spring-анимаций

Прерывание возникает в следующих сценариях:

  • запуск новой анимации на том же свойстве
  • пользовательское взаимодействие (drag, hover-out)
  • программный вызов остановки контроллера
  • смена состояния UI до завершения текущего движения

Spring особенно чувствителен к этим ситуациям, потому что его физическая модель предполагает сохранение скорости.

Контроллер анимации и базовое прерывание

Каждый вызов animate() возвращает контроллер, управляющий жизненным циклом анимации:

import { animate } from "motion";

const controls = animate(
  element,
  { x: 300 },
  { easing: "spring" }
);

Прерывание выполняется через:

controls.stop();

Поведение stop()

При вызове stop():

  • текущий кадр фиксируется как финальное состояние
  • расчёт spring прекращается
  • дальнейшие обновления не выполняются
  • velocity сохраняется только в рамках текущего кадра, но не используется автоматически в новой анимации

Важно: stop() не выполняет «плавного завершения», оно именно обрывает интеграцию движения.

Прерывание через запуск новой анимации

Наиболее частый сценарий — перекрытие spring-анимаций:

animate(element, { x: 300 }, { easing: "spring" });

animate(element, { x: 0 }, { easing: "spring" });

В этом случае Motion One:

  • автоматически останавливает предыдущую анимацию
  • создаёт новый spring-инстанс
  • берёт текущее визуальное состояние как стартовую точку

Такой подход называется implicit interruption.

Проблема потери скорости (velocity discontinuity)

При резком прерывании возникает важный эффект: потеря кинетической энергии.

Spring-система ожидает:

  • непрерывность скорости
  • сохранение импульса

Однако при стандартном stop() или замене анимации:

  • velocity обнуляется или пересчитывается
  • движение может выглядеть «ломаным»
  • возможен визуальный скачок траектории

Перенос скорости в новую spring-анимацию

Для сохранения физической непрерывности требуется учитывать текущую скорость элемента.

Motion One предоставляет доступ к вычисляемым значениям через внутреннее состояние контроллера (в упрощённой модели — через повторное использование spring-конфигурации).

Типовой паттерн:

let controls = animate(element, { x: 400 }, { easing: "spring" });

element.addEventListener("pointerdown", () => {
  controls.stop();

  controls = animate(element, { x: 0 }, {
    easing: "spring"
  });
});

Однако для более физически корректного поведения требуется концепция rebound animation, где начальная скорость учитывается явно.

Перекрытие spring-анимаций и приоритет последней

Motion One использует модель «last write wins»:

  • новая анимация имеет приоритет
  • предыдущая автоматически деактивируется
  • DOM-значение берётся как стартовое состояние

Это означает:

  • нет необходимости вручную очищать предыдущую анимацию
  • состояние всегда консистентно
  • прерывание происходит детерминированно

Прерывание во время drag-интеракций

Самый сложный сценарий — совмещение spring и пользовательского drag.

Типичная схема:

  1. spring-анимация доводит объект до цели
  2. пользователь начинает перетаскивание
  3. анимация должна немедленно прерваться
  4. управление передаётся pointer-событиям
let controls;

controls = animate(box, { x: 500 }, { easing: "spring" });

box.addEventListener("pointerdown", () => {
  controls.stop();
});

Ключевой момент: остановка должна происходить без задержки, иначе spring продолжит «дожимать» позицию, конфликтуя с вводом пользователя.

Прерывание и реакция на новое target-состояние

Spring-анимации в Motion One не просто переходят между значениями — они пересчитывают систему относительно новой цели.

При прерывании важно различать:

  • смену цели без остановки (re-targeting)
  • полную остановку с новым стартом

Re-targeting (переориентация)

Если не вызывать stop(), а просто запустить новую анимацию:

  • Motion One интерпретирует текущую позицию как исходную
  • создаётся новый spring с новой целью
  • визуально это выглядит как «перенаправление» движения

Конкуренция spring-анимаций на разных свойствах

Прерывание может быть частичным:

  • x анимируется spring’ом
  • opacity обновляется отдельно
animate(element, { x: 300 }, { easing: "spring" });
animate(element, { opacity: 0.5 }, { duration: 0.2 });

В этом случае:

  • spring и timeline не конфликтуют
  • прерывание происходит только на уровне конкретного свойства
  • система дифференцирует независимые анимационные каналы

Мгновенное прерывание и визуальная стабильность

При резком stop() возможны артефакты:

  • скачок позиции на один кадр
  • резкая остановка без easing-out
  • потеря ощущения инерции

Для сглаживания применяется стратегия:

  • фиксировать текущую скорость
  • запускать новый spring с меньшей жёсткостью
  • компенсировать импульс демпфированием

Сложные цепочки прерываний

При многократных вызовах:

animate(el, { x: 100 }, { easing: "spring" });
animate(el, { x: 300 }, { easing: "spring" });
animate(el, { x: 0 }, { easing: "spring" });

формируется цепочка из:

  • мгновенных остановок предыдущих spring-инстансов
  • пересоздания физической системы
  • постоянного пересчёта начальных условий

Это может привести к эффекту «дрожания цели», если новые цели приходят быстрее, чем система успевает стабилизироваться.

Практическая модель устойчивого прерывания

Стабильное управление spring-анимациями строится на трёх принципах:

  • единый контроллер на свойство
  • обязательная остановка перед новым запуском при конфликтующих состояниях
  • минимизация пересоздания spring-инстансов

Типовой шаблон:

let controls;

function goTo(value) {
  controls?.stop();

  controls = animate(element, { x: value }, {
    easing: "spring"
  });
}

Такой подход предотвращает накопление конкурирующих физических систем и обеспечивает предсказуемую динамику движения.