Механика перетаскивания (drag-and-drop) в веб-интерфейсах строится вокруг отслеживания координат указателя и изменения положения элемента в реальном времени. Библиотека anime.js не реализует drag-and-drop напрямую, но предоставляет мощные инструменты для плавной анимации, которые идеально дополняют обработчики событий.
Ключевая идея — разделение логики:
<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.
Вместо прямого изменения 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: 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'
});
});
Преимущества:
После отпускания элемента можно имитировать инерцию:
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)'
});
});
Используется для:
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;
});
});
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)'
});
}
});
anime({
targets: element,
translateX: posX,
translateY: posY,
duration: 500,
easing: 'spring(1, 80, 10, 0)'
});
Параметры:
Даёт максимально естественное движение.
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/lefttransformanime.timeline()
.add({
targets: element,
scale: 1.2,
duration: 200
})
.add({
targets: element,
rotate: 5,
duration: 100
})
.add({
targets: element,
rotate: 0,
duration: 100
});
Используется при:
Для сложных интерфейсов:
Пример структуры:
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
});
}
}
anime.js в связке с drag-and-drop позволяет добиться: