Анимация динамических списков в Popmotion строится вокруг управления состоянием DOM-элементов и плавного перехода между изменениями структуры. Основная сложность возникает не в самих анимациях, а в синхронизации визуального положения элементов с их логическим порядком при добавлении, удалении и сортировке.
Ключевая техника, применяемая для динамических интерфейсов, — FLIP (First, Last, Invert, Play). Она позволяет анимировать любое изменение геометрии элементов без привязки к конкретной анимационной библиотеке, однако в связке с Popmotion становится особенно гибкой.
FLIP состоит из четырёх этапов:
Перед любыми изменениями списка необходимо сохранить текущее
положение элементов. Для этого используется
getBoundingClientRect.
const items = document.querySelectorAll('.item');
const firstPositions = new Map();
items.forEach(item => {
const id = item.dataset.id;
firstPositions.set(id, item.getBoundingClientRect());
});
На этом этапе фиксируется визуальное состояние, независимое от данных, которые будут изменены дальше.
После фиксации координат выполняется обновление DOM: добавление, удаление или сортировка элементов.
function renderList(data) {
const container = document.querySelector('.list');
container.innerHTML = '';
data.forEach(item => {
const el = document.createElement('div');
el.className = 'item';
el.dataset.id = item.id;
el.textContent = item.title;
container.appendChild(el);
});
}
Этот этап вызывает мгновенный перерасчёт layout, поэтому визуально элементы «прыгают» в новые позиции.
После обновления DOM необходимо снова измерить элементы.
const itemsAfter = document.querySelectorAll('.item');
const lastPositions = new Map();
itemsAfter.forEach(item => {
const id = item.dataset.id;
lastPositions.set(id, item.getBoundingClientRect());
});
Разница между старым и новым положением определяет параметры анимации.
const deltas = new Map();
lastPositions.forEach((lastRect, id) => {
const firstRect = firstPositions.get(id);
if (!firstRect) return;
deltas.set(id, {
x: firstRect.left - lastRect.left,
y: firstRect.top - lastRect.top
});
});
Эти значения задают начальную трансформацию для анимации.
Popmotion предоставляет styler, позволяющий управлять
стилями как анимируемыми значениями.
import { styler, tween, easing } from 'popmotion';
const itemStylers = new Map();
document.querySelectorAll('.item').forEach(item => {
itemStylers.set(item.dataset.id, styler(item));
});
Каждый элемент получает собственный контроллер стилей.
Перед запуском анимации элементы мгновенно перемещаются в исходное
визуальное состояние с помощью transform.
deltas.forEach((delta, id) => {
const itemStyler = itemStylers.get(id);
itemStyler.set({
x: delta.x,
y: delta.y
});
});
На этом этапе элементы выглядят так, будто не меняли позицию.
Popmotion используется для плавного перехода к нулевому смещению.
import { animate } from 'popmotion';
deltas.forEach((_, id) => {
const itemStyler = itemStylers.get(id);
animate({
from: { x: itemStyler.get('x'), y: itemStyler.get('y') },
to: { x: 0, y: 0 },
duration: 500,
ease: easing.cubicBezier(0.2, 0.8, 0.2, 1),
onUpdate: v => itemStyler.set(v)
});
});
Такой подход создаёт эффект плавного перемещения элементов между позициями.
Новые элементы не участвуют в FLIP-вычислении, поэтому требуют отдельного подхода. Обычно применяется стартовая трансформация и последующий tween.
import { spring } from 'popmotion';
const newItem = document.querySelector('.item.new');
const newStyler = styler(newItem);
newStyler.set({
opacity: 0,
scale: 0.8
});
spring({
from: { opacity: 0, scale: 0.8 },
to: { opacity: 1, scale: 1 },
stiffness: 200,
damping: 20
}).start(v => newStyler.set(v));
Использование spring-модели делает появление элементов более естественным.
Удаление требует сохранения элемента в DOM до завершения анимации.
function removeItem(el) {
const s = styler(el);
tween({
from: { opacity: 1, scale: 1 },
to: { opacity: 0, scale: 0.8 },
duration: 300
}).start({
update: v => s.set(v),
complete: () => el.remove()
});
}
При сортировке списка FLIP-алгоритм становится основным механизмом. Важный момент — идентификаторы элементов должны оставаться стабильными.
function reorder(data) {
const first = capturePositions();
renderList(data);
const last = capturePositions();
applyFlip(first, last);
}
Popmotion позволяет создавать каскадные эффекты через последовательный запуск анимаций.
import { stagger } from 'popmotion';
const items = document.querySelectorAll('.item');
stagger(items, {
start: { opacity: 0, y: 20 },
end: { opacity: 1, y: 0 },
duration: 400,
delay: 50
});
Это усиливает визуальную иерархию при перерисовке списков.
Основная проблема динамических списков — расхождение между данными и DOM. Popmotion не управляет состоянием напрямую, поэтому требуется явное разделение:
Такой подход предотвращает конфликт между перерендером и активной анимацией.
Для повышения производительности используется только transform, без изменения layout-свойств.
styler.set({
transform: `
translateX(${x}px)
translateY(${y}px)
`
});
Это позволяет избежать forced reflow и сохранить 60 FPS при больших списках.
При работе с сотнями элементов применяется несколько стратегий:
value позволяет хранить анимируемые состояния вне DOM и
синхронизировать их при необходимости.
import { value } from 'popmotion';
const x = value(0, v => {
el.style.transform = `translateX(${v}px)`;
});
x.set(100);
При динамических списках это полезно для промежуточных состояний между перерендером и визуальным обновлением.
Сложные списки требуют одновременного управления несколькими типами анимаций:
Popmotion позволяет запускать их параллельно без конфликта состояний.
animate({ from: 0, to: 1, onUpdate: v => opacity.set(v) });
spring({ from: y0, to: y1, onUpdate: v => y.set(v) });
tween({ from: s0, to: s1, onUpdate: v => scale.set(v) });
При частом обновлении списка предыдущие анимации могут конфликтовать с новыми. Решение заключается в хранении контроллеров и их отмене.
let currentAnimation;
function runAnimation(config) {
if (currentAnimation) currentAnimation.stop();
currentAnimation = animate(config);
}
Это предотвращает накопление анимационных процессов.
При потоковых обновлениях данных применяется batching:
Такой подход критичен для интерфейсов реального времени.
Popmotion позволяет объединять физические модели движения с FLIP-анимацией. Например, spring используется для имитации инерции при перемещении карточек.
spring({
from: delta,
to: 0,
stiffness: 180,
damping: 25,
mass: 1
}).start(v => styler.set({ x: v }));
При изменении содержимого элементов (например, раскрывающиеся карточки) требуется отдельный FLIP по высоте.
const firstHeight = el.getBoundingClientRect().height;
el.classList.add('expanded');
const lastHeight = el.getBoundingClientRect().height;
animate({
from: firstHeight,
to: lastHeight,
onUpdate: h => el.style.height = `${h}px`
});
В сложных интерфейсах динамический список разбивается на слои:
Popmotion используется преимущественно на слоях layout и transition, обеспечивая независимость анимаций от бизнес-логики.