Анимация списков — одна из наиболее сложных задач в интерфейсной анимации. В отличие от одиночных элементов, списки требуют синхронизации множества объектов, учёта их динамического добавления, удаления и перестановки. Библиотека Popmotion предоставляет низкоуровневые инструменты, позволяющие гибко управлять такими сценариями.
Ключевые аспекты:
Для работы со списками используются следующие функции:
animate — универсальный инструмент для анимации
значенийspring — физически правдоподобные переходыtween — линейные и кривые интерполяцииstyler — управление DOM-элементамиtimeline — координация последовательностейПример базовой анимации элемента:
import { animate, styler } from "popmotion";
const element = document.querySelector(".item");
const style = styler(element);
animate({
from: { opacity: 0, y: 50 },
to: { opacity: 1, y: 0 },
duration: 300,
onUpdate: style.set
});
При добавлении элемента важно:
Пример:
function animateIn(element) {
const style = styler(element);
animate({
from: { opacity: 0, y: 20 },
to: { opacity: 1, y: 0 },
duration: 250,
onUpdate: style.set
});
}
Типичный сценарий:
Удаление сложнее, так как элемент исчезает из DOM. Необходимо:
function animateOut(element, onComplete) {
const style = styler(element);
animate({
from: { opacity: 1, y: 0 },
to: { opacity: 0, y: -20 },
duration: 200,
onUpdate: style.set,
onComplete
});
}
Использование:
animateOut(item, () => {
item.remove();
});
Главная проблема — резкое изменение позиций. Решение — техника FLIP:
function animateReorder(items) {
const firstRects = new Map();
items.forEach(item => {
firstRects.set(item, item.getBoundingClientRect());
});
// изменение порядка в DOM
reorderDOM(items);
items.forEach(item => {
const lastRect = item.getBoundingClientRect();
const firstRect = firstRects.get(item);
const deltaY = firstRect.top - lastRect.top;
const style = styler(item);
animate({
from: { y: deltaY },
to: { y: 0 },
duration: 300,
onUpdate: style.set
});
});
}
При работе со списками важно избегать хаотичных движений. Для этого применяются:
delay)timeline)items.forEach((item, index) => {
const style = styler(item);
animate({
from: { opacity: 0, y: 10 },
to: { opacity: 1, y: 0 },
delay: index * 50,
duration: 200,
onUpdate: style.set
});
});
Эффект: каскадное появление элементов.
Пружинные анимации особенно хорошо подходят для списков:
import { spring } from "popmotion";
spring({
from: 50,
to: 0,
stiffness: 200,
damping: 20
}).start({
update: value => {
element.style.transform = `translateY(${value}px)`;
}
});
Преимущества:
При изменении содержимого элемента его высота меняется. Это может ломать поток.
Решение:
function animateHeight(element, newHeight) {
const currentHeight = element.offsetHeight;
animate({
from: currentHeight,
to: newHeight,
duration: 300,
onUpdate: value => {
element.style.height = `${value}px`;
}
});
}
Для повышения производительности используются:
transform вместо top/leftПример:
animate({
from: { scale: 0.9, opacity: 0 },
to: { scale: 1, opacity: 1 },
duration: 200,
onUpdate: v => {
element.style.transform = `scale(${v.scale})`;
element.style.opacity = v.opacity;
}
});
Анимация должна быть связана с состоянием данных:
let items = [];
function addItem(data) {
const element = createElement(data);
list.appendChild(element);
animateIn(element);
items.push(element);
}
При удалении:
function removeItem(element) {
animateOut(element, () => {
items = items.filter(i => i !== element);
});
}
При работе с большим количеством элементов важно:
getBoundingClientRectОптимизации:
Резкие скачки элементов
Причина: отсутствие FLIP-анимации Решение: всегда учитывать предыдущие позиции
Дёргание при быстрой вставке
Причина: пересечение анимаций Решение: отмена предыдущих анимаций или использование spring
Проблемы с высотой
Причина: auto-значения Решение: явное управление высотой
Потеря производительности
Причина: большое количество DOM-операций Решение: батчинг изменений
При длинных списках:
Popmotion позволяет реализовать перетаскивание:
element.addEventListener("pointerdown", () => {
animate({
from: 0,
to: 100,
onUpdate: v => {
element.style.transform = `translateY(${v}px)`;
}
});
});
В сочетании с FLIP можно добиться плавной перестановки элементов.
Комбинация:
Создаёт цельное восприятие интерфейса.
Разделение ответственности:
Хорошая практика — вынос анимаций в отдельные функции:
const animations = {
enter: animateIn,
exit: animateOut,
move: animateReorder
};
Это упрощает поддержку и масштабирование проекта.