В системах анимации Popmotion управление жизненным циклом анимаций строится вокруг объектов действий (actions), которые могут запускаться, подписываться и завершаться. Одним из ключевых механизмов является возможность отмены выполнения. Однако при неправильной организации отмены возникают состояния гонки, утечки подписок, «залипшие» анимации и неконсистентные значения.
Любое действие в Popmotion возвращает объект с методами управления:
startstopsubscribeПри вызове stop() действие прекращает эмиссию значений.
На первый взгляд этого достаточно, чтобы гарантировать завершение
анимации.
import { tween } from 'popmotion';
const animation = tween({
from: 0,
to: 100,
duration: 1000
}).start({
update: v => console.log(v),
complete: () => console.log('done')
});
animation.stop();
В этом случае анимация прекращается, но внутренние эффекты могут остаться незавершёнными, если подписчики или композиции действий устроены сложнее.
Наиболее частая ошибка возникает при повторном создании анимации без остановки предыдущей.
let currentAnimation;
function startAnimation() {
currentAnimation = tween({
from: 0,
to: 200,
duration: 800
}).start(v => {
console.log(v);
});
}
Если startAnimation вызывается несколько раз подряд,
предыдущие экземпляры продолжают существовать, если не были явно
остановлены.
Корректный вариант требует явного контроля:
let currentAnimation;
function startAnimation() {
if (currentAnimation) {
currentAnimation.stop();
}
currentAnimation = tween({
from: 0,
to: 200,
duration: 800
}).start(v => {
console.log(v);
});
}
Однако даже в этом случае возможна логическая ошибка:
stop() не всегда гарантирует прекращение всех побочных
эффектов внутри сложных цепочек.
Popmotion позволяет строить цепочки трансформаций через
pipe. В таких случаях отмена внешнего действия не всегда
останавливает внутренние подписки, если они были созданы отдельно.
import { tween, pipe, clamp } from 'popmotion';
const transform = pipe(
v => v * 2,
clamp(0, 100)
);
const animation = tween({
from: 0,
to: 50,
duration: 1000
}).start(v => {
const result = transform(v);
console.log(result);
});
Если остановить animation, но трансформация используется
в другом активном контексте, она продолжит работать логически, хотя
поток данных остановлен. Ошибка здесь заключается в смешивании чистых
функций и активных подписок.
Распространённая проблема возникает при асинхронном запуске анимаций, когда ссылка на действие теряется.
function animateBox() {
tween({
from: 0,
to: 300,
duration: 1200
}).start(v => {
box.style.transform = `translateX(${v}px)`;
});
}
Отмена в таком случае невозможна, потому что объект действия не сохранён. При повторных вызовах создаются независимые анимации, которые невозможно остановить извне.
Исправленный подход:
let boxAnimation;
function animateBox() {
if (boxAnimation) {
boxAnimation.stop();
}
boxAnimation = tween({
from: 0,
to: 300,
duration: 1200
}).start(v => {
box.style.transform = `translateX(${v}px)`;
});
}
chainПри использовании chain несколько действий выполняются
последовательно. Ошибка возникает, когда останавливается только первый
элемент цепочки.
import { chain, tween } from 'popmotion';
const sequence = chain(
tween({ from: 0, to: 100, duration: 500 }),
tween({ from: 100, to: 200, duration: 500 })
);
const action = sequence.start(v => {
console.log(v);
});
Вызов:
action.stop();
останавливает текущий элемент цепи, но следующий может уже быть запущен или подготовлен к запуску в рамках внутреннего планировщика. Это приводит к ситуации, когда часть цепочки продолжает исполняться.
Корректное управление требует внешнего флага состояния:
let isStopped = false;
const sequence = chain(
tween({ from: 0, to: 100, duration: 500 }),
tween({ from: 100, to: 200, duration: 500 })
);
const action = sequence.start(v => {
if (isStopped) return;
console.log(v);
});
function cancel() {
isStopped = true;
action.stop();
}
spring и инерционных анимацийИнерционные анимации создают дополнительную сложность, поскольку их завершение зависит от внутренних условий, а не фиксированного времени.
import { spring } from 'popmotion';
const animation = spring({
from: 0,
to: 100,
stiffness: 100
}).start(v => {
console.log(v);
});
При вызове stop() анимация прекращается, но последнее
значение может быть уже вычислено и отправлено в update на
следующем кадре. Это создаёт эффект «последнего лишнего кадра».
При параллельном запуске нескольких анимаций над одним объектом возникает состояние гонки отмен:
let animationA;
let animationB;
function runA() {
animationA = tween({ from: 0, to: 100, duration: 500 }).start(v => {
box.style.left = v + 'px';
});
}
function runB() {
animationB = tween({ from: 100, to: 0, duration: 500 }).start(v => {
box.style.left = v + 'px';
});
}
Если runA и runB вызываются быстро друг за
другом, остановка одной анимации не гарантирует, что другая не
перезапишет результат в следующем кадре. В результате DOM получает
значения от уже «устаревшей» анимации.
Решение заключается в введении идентификатора актуальности:
let currentId = 0;
function runA() {
const id = ++currentId;
tween({ from: 0, to: 100, duration: 500 }).start(v => {
if (id !== currentId) return;
box.style.left = v + 'px';
});
}
function runB() {
const id = ++currentId;
tween({ from: 100, to: 0, duration: 500 }).start(v => {
if (id !== currentId) return;
box.style.left = v + 'px';
});
}
При использовании низкоуровневого API subscribe отмена
требует явного удаления подписки. Ошибка возникает, когда разработчик
вызывает stop(), но оставляет активные подписчики.
const animation = tween({
from: 0,
to: 100,
duration: 1000
});
const subscription = animation.subscribe(v => {
console.log(v);
});
animation.stop();
Подписка остаётся активной и может получать финальные или промежуточные значения в зависимости от внутренней реализации планировщика. Правильный подход требует явного отписывания:
subscription.unsubscribe();
animation.stop();
При создании сложных композиций через composite или
пользовательские обёртки ошибка отмены часто возникает из-за того, что
внутренние действия живут собственной жизнью.
function customAnimation() {
const anim = tween({ from: 0, to: 100, duration: 1000 });
return anim.start(v => {
console.log(v);
});
}
Если customAnimation возвращает только результат
start, внешний код не имеет доступа к исходному действию, и
корректная отмена невозможна.
Правильная архитектура требует возврата управляющего объекта:
function customAnimation() {
const anim = tween({ from: 0, to: 100, duration: 1000 });
const controller = anim.start(v => {
console.log(v);
});
return {
stop: () => controller.stop()
};
}
Даже при корректной остановке анимации состояние может оставаться в промежуточном значении. Это создаёт иллюзию «некорректной отмены».
const animation = tween({
from: 0,
to: 100,
duration: 1000
}).start(v => {
box.style.width = v + 'px';
});
animation.stop();
После остановки ширина остаётся на последнем значении, так как Popmotion не откатывает состояние автоматически. Это не ошибка библиотеки, а следствие императивного характера обновления DOM.
Для предсказуемого поведения требуется ручной возврат:
animation.stop();
box.style.width = '0px';