Анимация списков

Анимация списков — одна из наиболее сложных задач в интерфейсной анимации. В отличие от одиночных элементов, списки требуют синхронизации множества объектов, учёта их динамического добавления, удаления и перестановки. Библиотека Popmotion предоставляет низкоуровневые инструменты, позволяющие гибко управлять такими сценариями.

Ключевые аспекты:

  • управление состоянием элементов
  • отслеживание позиций
  • плавные переходы между состояниями
  • координация нескольких анимаций

Базовые инструменты 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
  });
}

Типичный сценарий:

  1. Элемент добавляется в DOM
  2. Применяется начальный стиль
  3. Запускается анимация появления

Анимация удаления элементов

Удаление сложнее, так как элемент исчезает из 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();
});

Перестановка элементов (reordering)

Главная проблема — резкое изменение позиций. Решение — техника FLIP:

  • First — сохранить начальные позиции
  • Last — определить новые позиции
  • Invert — вычислить разницу
  • Play — анимировать переход

Реализация FLIP с Popmotion

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
  });
});

Эффект: каскадное появление элементов.

Использование spring для естественности

Пружинные анимации особенно хорошо подходят для списков:

import { spring } from "popmotion";

spring({
  from: 50,
  to: 0,
  stiffness: 200,
  damping: 20
}).start({
  update: value => {
    element.style.transform = `translateY(${value}px)`;
  }
});

Преимущества:

  • отсутствие резкости
  • адаптивная скорость
  • визуальная “жизненность”

Анимация изменения размеров

При изменении содержимого элемента его высота меняется. Это может ломать поток.

Решение:

  1. зафиксировать текущую высоту
  2. измерить новую
  3. анимировать между ними
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
  • GPU-ускорение

Пример:

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);
  });
}

Производительность

При работе с большим количеством элементов важно:

  • избегать layout thrashing
  • минимизировать чтение getBoundingClientRect
  • использовать requestAnimationFrame (встроен в Popmotion)

Оптимизации:

  • группировка измерений
  • кеширование значений
  • использование transform вместо layout-свойств

Частые ошибки

Резкие скачки элементов

Причина: отсутствие FLIP-анимации Решение: всегда учитывать предыдущие позиции

Дёргание при быстрой вставке

Причина: пересечение анимаций Решение: отмена предыдущих анимаций или использование spring

Проблемы с высотой

Причина: auto-значения Решение: явное управление высотой

Потеря производительности

Причина: большое количество DOM-операций Решение: батчинг изменений

Продвинутые паттерны

Виртуализация + анимация

При длинных списках:

  • рендерится только видимая часть
  • анимации применяются только к активным элементам

Drag & Drop

Popmotion позволяет реализовать перетаскивание:

element.addEventListener("pointerdown", () => {
  animate({
    from: 0,
    to: 100,
    onUpdate: v => {
      element.style.transform = `translateY(${v}px)`;
    }
  });
});

В сочетании с FLIP можно добиться плавной перестановки элементов.

Смешанные анимации

Комбинация:

  • появления
  • перемещения
  • изменения размеров

Создаёт цельное восприятие интерфейса.

Архитектурные подходы

Разделение ответственности:

  • логика данных (массив элементов)
  • DOM-представление
  • слой анимации

Хорошая практика — вынос анимаций в отдельные функции:

const animations = {
  enter: animateIn,
  exit: animateOut,
  move: animateReorder
};

Это упрощает поддержку и масштабирование проекта.