Механика drag-and-drop в библиотеке Motion One строится на управлении
состоянием элемента через трансформации (transform) и
события указателя (pointer events). В отличие от громоздких
решений, Motion One предоставляет минималистичный API, позволяющий
создавать плавные, аппаратно-ускоренные перемещения.
Ключевая идея — изменение координат элемента в ответ на действия
пользователя с использованием animate и
style.
Для реализации перетаскивания используется набор событий:
pointerdown — начало взаимодействияpointermove — перемещениеpointerup — завершениеПример базовой структуры:
const element = document.querySelector('.draggable');
let isDragging = false;
let startX = 0;
let startY = 0;
element.addEventListener('pointerdown', (event) => {
isDragging = true;
startX = event.clientX;
startY = event.clientY;
element.setPointerCapture(event.pointerId);
});
element.addEventListener('pointermove', (event) => {
if (!isDragging) return;
const dx = event.clientX - startX;
const dy = event.clientY - startY;
element.style.transform = `translate(${dx}px, ${dy}px)`;
});
element.addEventListener('pointerup', () => {
isDragging = false;
});
Для повышения плавности движения и добавления инерции применяется
функция animate:
import { animate } from "motion";
element.addEventListener('pointerup', () => {
isDragging = false;
animate(element, {
transform: "translate(0px, 0px)"
}, {
duration: 0.5,
easing: "ease-out"
});
});
В данном случае элемент возвращается в исходное положение с анимацией.
Чтобы избежать “скачков” при повторном перетаскивании, необходимо сохранять текущее смещение:
let x = 0;
let y = 0;
element.addEventListener('pointermove', (event) => {
if (!isDragging) return;
const dx = event.clientX - startX;
const dy = event.clientY - startY;
element.style.transform = `translate(${x + dx}px, ${y + dy}px)`;
});
element.addEventListener('pointerup', (event) => {
isDragging = false;
x += event.clientX - startX;
y += event.clientY - startY;
});
Часто требуется ограничить перемещение границами контейнера:
const container = document.querySelector('.container');
element.addEventListener('pointermove', (event) => {
if (!isDragging) return;
const rect = container.getBoundingClientRect();
let dx = event.clientX - startX + x;
let dy = event.clientY - startY + y;
dx = Math.max(0, Math.min(dx, rect.width - element.offsetWidth));
dy = Math.max(0, Math.min(dy, rect.height - element.offsetHeight));
element.style.transform = `translate(${dx}px, ${dy}px)`;
});
Инерция делает взаимодействие более “живым”. Для этого вычисляется скорость движения:
let velocityX = 0;
let velocityY = 0;
let lastTime = 0;
element.addEventListener('pointermove', (event) => {
if (!isDragging) return;
const now = performance.now();
const dt = now - lastTime;
velocityX = (event.clientX - startX) / dt;
velocityY = (event.clientY - startY) / dt;
lastTime = now;
});
Применение после отпускания:
element.addEventListener('pointerup', () => {
animate(element, {
transform: `translate(${x + velocityX * 200}px, ${y + velocityY * 200}px)`
}, {
duration: 0.8,
easing: "ease-out"
});
});
Motion One поддерживает пружинные анимации:
animate(element, {
transform: "translate(0px, 0px)"
}, {
type: "spring",
stiffness: 200,
damping: 20
});
Параметры:
stiffness — жесткость (скорость возврата)damping — затухание (устранение колебаний)Каждый элемент должен иметь собственное состояние:
document.querySelectorAll('.draggable').forEach((element) => {
let x = 0;
let y = 0;
let isDragging = false;
element.addEventListener('pointerdown', (event) => {
isDragging = true;
startX = event.clientX;
startY = event.clientY;
});
element.addEventListener('pointermove', (event) => {
if (!isDragging) return;
const dx = event.clientX - startX;
const dy = event.clientY - startY;
element.style.transform = `translate(${x + dx}px, ${y + dy}px)`;
});
element.addEventListener('pointerup', (event) => {
isDragging = false;
x += event.clientX - startX;
y += event.clientY - startY;
});
});
Для реализации “сброса” элемента в определённую область используется проверка пересечения:
const dropZone = document.querySelector('.dropzone');
element.addEventListener('pointerup', () => {
const elRect = element.getBoundingClientRect();
const zoneRect = dropZone.getBoundingClientRect();
const isInside =
elRect.left < zoneRect.right &&
elRect.right > zoneRect.left &&
elRect.top < zoneRect.bottom &&
elRect.bottom > zoneRect.top;
if (isInside) {
animate(element, {
transform: `translate(${zoneRect.left}px, ${zoneRect.top}px)`
});
}
});
Ключевые принципы:
transform вместо
top/leftwill-change: transform в CSSpointermove.draggable {
will-change: transform;
touch-action: none;
}
События pointer уже унифицированы и работают с:
Важно отключить стандартные жесты браузера:
.draggable {
touch-action: none;
}
Для сложных интерфейсов состояние перетаскивания может храниться отдельно:
const state = {
items: []
};
При перемещении:
state.items[index].x = x;
state.items[index].y = y;
Это позволяет:
Motion One позволяет совмещать drag с другими эффектами:
element.addEventListener('pointerdown', () => {
animate(element, {
scale: 1.1
}, { duration: 0.2 });
});
element.addEventListener('pointerup', () => {
animate(element, {
scale: 1
}, { duration: 0.2 });
});
Прямое управление DOM:
Через состояние:
1. Потеря pointer capture Без
setPointerCapture элемент может “терять” курсор.
2. Резкие скачки координат Причина — отсутствие сохранения текущего положения.
3. Низкая производительность Использование
top/left вместо transform.
4. Игнорирование touch-action Приводит к конфликтам со скроллом.
element.style.transform = `translateX(${x + dx}px)`;
const grid = 50;
const snappedX = Math.round((x + dx) / grid) * grid;
const snappedY = Math.round((y + dy) / grid) * grid;
Создание копии элемента при перетаскивании:
const clone = element.cloneNode(true);
document.body.appendChild(clone);
Motion One может автоматически анимировать изменения layout:
import { animate, layout } from "motion";
Это позволяет реализовать:
При разработке сложных интерфейсов:
Такая архитектура позволяет строить:
Выделение логики в отдельный контроллер:
function createDraggable(element) {
let x = 0;
let y = 0;
let isDragging = false;
element.addEventListener('pointerdown', (event) => {
isDragging = true;
startX = event.clientX;
startY = event.clientY;
});
element.addEventListener('pointermove', (event) => {
if (!isDragging) return;
const dx = event.clientX - startX;
const dy = event.clientY - startY;
element.style.transform = `translate(${x + dx}px, ${y + dy}px)`;
});
element.addEventListener('pointerup', (event) => {
isDragging = false;
x += event.clientX - startX;
y += event.clientY - startY;
});
}
Использование:
createDraggable(document.querySelector('.box'));
Качественный drag-and-drop определяется:
Motion One предоставляет инструменты для достижения этих характеристик через: