Drag and drop с анимацией

Механика перетаскивания (drag-and-drop) в веб-интерфейсах строится вокруг отслеживания координат указателя и изменения положения элемента в реальном времени. Библиотека anime.js не реализует drag-and-drop напрямую, но предоставляет мощные инструменты для плавной анимации, которые идеально дополняют обработчики событий.

Ключевая идея — разделение логики:

  • события мыши / тач-события отвечают за перемещение
  • anime.js — за плавность, инерцию и визуальные эффекты

Подключение anime.js

<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>

Минимальная реализация перетаскивания

const element = document.querySelector('.draggable');

let isDragging = false;
let offsetX = 0;
let offsetY = 0;

element.addEventListener('mousedown', (e) => {
  isDragging = true;
  offsetX = e.clientX - element.offsetLeft;
  offsetY = e.clientY - element.offsetTop;
});

document.addEventListener('mousemove', (e) => {
  if (!isDragging) return;

  element.style.left = `${e.clientX - offsetX}px`;
  element.style.top = `${e.clientY - offsetY}px`;
});

document.addEventListener('mouseup', () => {
  isDragging = false;
});

Здесь отсутствует анимация — перемещение происходит мгновенно. Это базовый слой, поверх которого добавляется anime.js.


Плавное перемещение через anime.js

Вместо прямого изменения style.left/top используется анимация:

document.addEventListener('mousemove', (e) => {
  if (!isDragging) return;

  anime({
    targets: element,
    left: e.clientX - offsetX,
    top: e.clientY - offsetY,
    duration: 100,
    easing: 'linear'
  });
});

Особенность: короткая длительность создаёт эффект “следования” за курсором.


Использование transform вместо left/top

Для повышения производительности предпочтительнее использовать transform: translate:

let posX = 0;
let posY = 0;

document.addEventListener('mousemove', (e) => {
  if (!isDragging) return;

  posX = e.clientX - offsetX;
  posY = e.clientY - offsetY;

  anime({
    targets: element,
    translateX: posX,
    translateY: posY,
    duration: 100,
    easing: 'easeOutQuad'
  });
});

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

  • не вызывает reflow
  • работает через GPU
  • более плавная анимация

Добавление инерции (эффект “броска”)

После отпускания элемента можно имитировать инерцию:

let velocityX = 0;
let velocityY = 0;
let lastX = 0;
let lastY = 0;

document.addEventListener('mousemove', (e) => {
  if (!isDragging) return;

  velocityX = e.clientX - lastX;
  velocityY = e.clientY - lastY;

  lastX = e.clientX;
  lastY = e.clientY;

  anime({
    targets: element,
    translateX: `+=${velocityX}`,
    translateY: `+=${velocityY}`,
    duration: 50,
    easing: 'linear'
  });
});

document.addEventListener('mouseup', () => {
  isDragging = false;

  anime({
    targets: element,
    translateX: `+=${velocityX * 10}`,
    translateY: `+=${velocityY * 10}`,
    duration: 800,
    easing: 'easeOutExpo'
  });
});

Результат:

  • элемент продолжает движение после отпускания
  • создаётся ощущение физики

Ограничение области перетаскивания

const container = document.querySelector('.container');

document.addEventListener('mousemove', (e) => {
  if (!isDragging) return;

  let x = e.clientX - offsetX;
  let y = e.clientY - offsetY;

  const maxX = container.clientWidth - element.clientWidth;
  const maxY = container.clientHeight - element.clientHeight;

  x = Math.max(0, Math.min(x, maxX));
  y = Math.max(0, Math.min(y, maxY));

  anime({
    targets: element,
    translateX: x,
    translateY: y,
    duration: 80,
    easing: 'easeOutQuad'
  });
});

Эффект “поднятия” элемента при захвате

element.addEventListener('mousedown', () => {
  anime({
    targets: element,
    scale: 1.1,
    boxShadow: '0 15px 30px rgba(0,0,0,0.2)',
    duration: 200,
    easing: 'easeOutQuad'
  });
});

document.addEventListener('mouseup', () => {
  anime({
    targets: element,
    scale: 1,
    boxShadow: '0 5px 10px rgba(0,0,0,0.1)',
    duration: 300,
    easing: 'easeOutQuad'
  });
});

Создаётся визуальная глубина и ощущение взаимодействия.


Снэппинг (прилипание к сетке)

const gridSize = 100;

document.addEventListener('mouseup', () => {
  isDragging = false;

  const snappedX = Math.round(posX / gridSize) * gridSize;
  const snappedY = Math.round(posY / gridSize) * gridSize;

  anime({
    targets: element,
    translateX: snappedX,
    translateY: snappedY,
    duration: 400,
    easing: 'easeOutElastic(1, .6)'
  });
});

Используется для:

  • интерфейсов конструкторов
  • редакторов
  • дашбордов

Работа с несколькими draggable-элементами

document.querySelectorAll('.draggable').forEach(el => {
  let isDragging = false;

  el.addEventListener('mousedown', () => {
    isDragging = true;
  });

  document.addEventListener('mousemove', (e) => {
    if (!isDragging) return;

    anime({
      targets: el,
      translateX: e.clientX,
      translateY: e.clientY,
      duration: 80
    });
  });

  document.addEventListener('mouseup', () => {
    isDragging = false;
  });
});

Drop-зоны с анимацией

const dropZone = document.querySelector('.dropzone');

document.addEventListener('mouseup', () => {
  const rect = dropZone.getBoundingClientRect();
  const elRect = element.getBoundingClientRect();

  const isInside =
    elRect.left > rect.left &&
    elRect.right < rect.right &&
    elRect.top > rect.top &&
    elRect.bottom < rect.bottom;

  if (isInside) {
    anime({
      targets: element,
      backgroundColor: '#4CAF50',
      duration: 300
    });
  } else {
    anime({
      targets: element,
      backgroundColor: '#f44336',
      duration: 300
    });
  }
});

Возврат в исходную позицию

const startX = 0;
const startY = 0;

document.addEventListener('mouseup', () => {
  if (!isDroppedCorrectly) {
    anime({
      targets: element,
      translateX: startX,
      translateY: startY,
      duration: 600,
      easing: 'easeOutElastic(1, .5)'
    });
  }
});

Drag с физикой пружины

anime({
  targets: element,
  translateX: posX,
  translateY: posY,
  duration: 500,
  easing: 'spring(1, 80, 10, 0)'
});

Параметры:

  • жесткость
  • трение
  • масса

Даёт максимально естественное движение.


Поддержка touch-событий

element.addEventListener('touchstart', (e) => {
  const touch = e.touches[0];
  isDragging = true;
  offsetX = touch.clientX - element.offsetLeft;
  offsetY = touch.clientY - element.offsetTop;
});

document.addEventListener('touchmove', (e) => {
  if (!isDragging) return;

  const touch = e.touches[0];

  anime({
    targets: element,
    translateX: touch.clientX - offsetX,
    translateY: touch.clientY - offsetY,
    duration: 80
  });
});

document.addEventListener('touchend', () => {
  isDragging = false;
});

Оптимизация производительности

1. Ограничение количества анимаций

anime.remove(element);

Перед запуском новой анимации удаляет предыдущую.

2. requestAnimationFrame вместо частых вызовов Используется при сложной логике.

3. Минимизация layout-свойств

  • избегать top/left
  • использовать transform

Комбинирование эффектов

anime.timeline()
  .add({
    targets: element,
    scale: 1.2,
    duration: 200
  })
  .add({
    targets: element,
    rotate: 5,
    duration: 100
  })
  .add({
    targets: element,
    rotate: 0,
    duration: 100
  });

Используется при:

  • захвате элемента
  • успешном drop
  • ошибке

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

Для сложных интерфейсов:

  • отдельный модуль для drag
  • отдельный модуль для анимаций
  • централизованное состояние (позиция, скорость)

Пример структуры:

class Draggable {
  constructor(element) {
    this.el = element;
    this.x = 0;
    this.y = 0;
  }

  move(x, y) {
    this.x = x;
    this.y = y;

    anime({
      targets: this.el,
      translateX: x,
      translateY: y
    });
  }
}

Типичные ошибки

  • дублирование анимаций → рывки
  • использование left/top → падение FPS
  • отсутствие debounce → перегрузка
  • игнорирование touch → плохая мобильная поддержка
  • отсутствие ограничения → элементы “улетают”

Практическое применение

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

anime.js в связке с drag-and-drop позволяет добиться:

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