Velocity.js используется прежде всего как высокопроизводительный инструмент для анимации DOM-элементов, но его реальная сила проявляется в связке с пользовательскими взаимодействиями — особенно с touch- и gesture-событиями на мобильных устройствах.
Мобильное взаимодействие в браузере строится вокруг трёх базовых уровней событий:
touchstart, touchmove,
touchendpointerdown, pointermove,
pointerupVelocity.js не предоставляет собственного слоя жестов, но эффективно работает как исполнитель анимаций, когда жест уже распознан логикой приложения.
Система touch-событий в браузере отличается от мышиной модели:
touchmoveСтруктура объекта TouchEvent:
touches — все активные касанияtargetTouches — касания на конкретном элементеchangedTouches — изменённые касанияКлючевая проблема при работе с анимациями — избыточная частота
touchmove. Без оптимизации это приводит к лагам, если на
каждый event запускать анимацию.
Типовой паттерн: touch-событие не анимирует напрямую, а изменяет состояние, которое затем интерпретируется Velocity.
const el = document.querySelector('.card');
let startX = 0;
let currentX = 0;
let isDragging = false;
el.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
isDragging = true;
});
el.addEventListener('touchmove', (e) => {
if (!isDragging) return;
currentX = e.touches[0].clientX - startX;
Velocity(el, {
translateX: currentX
}, {
duration: 0,
queue: false
});
});
el.addEventListener('touchend', () => {
isDragging = false;
if (Math.abs(currentX) > 120) {
Velocity(el, {
translateX: currentX > 0 ? 500 : -500,
opacity: 0
}, {
duration: 300,
easing: "easeOutQuad"
});
} else {
Velocity(el, {
translateX: 0
}, {
duration: 250,
easing: "spring"
});
}
});
Ключевой момент: duration: 0 в touchmove
отключает анимацию и превращает Velocity в слой мгновенного применения
трансформации.
Gesture-система строится не браузером, а разработчиком. Основная задача — интерпретировать поток координат в действия:
let startX = 0;
let startY = 0;
let startTime = 0;
el.addEventListener('touchstart', (e) => {
const t = e.touches[0];
startX = t.clientX;
startY = t.clientY;
startTime = Date.now();
});
el.addEventListener('touchend', (e) => {
const t = e.changedTouches[0];
const dx = t.clientX - startX;
const dy = t.clientY - startY;
const dt = Date.now() - startTime;
const isHorizontal = Math.abs(dx) > Math.abs(dy);
const isFast = dt < 300;
const isLongEnough = Math.abs(dx) > 80;
if (isHorizontal && isFast && isLongEnough) {
Velocity(el, {
translateX: dx > 0 ? 400 : -400,
opacity: 0
}, {
duration: 280,
easing: "easeOutCubic"
});
}
});
Gesture здесь — это не событие, а вычисляемое состояние.
Вместо разделения touch/mouse используется унифицированная модель:
pointerdownpointermovepointerupПреимущество — одинаковая логика для всех устройств.
let active = false;
let startX = 0;
el.addEventListener('pointerdown', (e) => {
active = true;
startX = e.clientX;
});
el.addEventListener('pointermove', (e) => {
if (!active) return;
const dx = e.clientX - startX;
Velocity(el, {
translateX: dx
}, {
duration: 0,
queue: false
});
});
el.addEventListener('pointerup', () => {
active = false;
Velocity(el, {
translateX: 0
}, {
duration: 300
});
});
Pointer Events упрощают архитектуру жестов, убирая необходимость писать отдельные обработчики для мобильных и десктопных устройств.
Velocity использует систему очередей анимации. При работе с жестами важно отключать очередь:
Velocity(el, {
scale: 1.1
}, {
queue: false
});
Если этого не делать, движения пальца будут накапливаться в очереди, создавая задержку между жестом и визуальным откликом.
Главная проблема — частота обновлений координат.
Рекомендуемые принципы:
touchmove должен только обновлять состояние, без сложной логики.
Velocity эффективно работает с:
translateXtranslateYscalerotateи хуже с:
widthheighttop/leftИногда применяется троттлинг через
requestAnimationFrame:
let latestX = 0;
let ticking = false;
el.addEventListener('pointermove', (e) => {
latestX = e.clientX;
if (!ticking) {
requestAnimationFrame(() => {
Velocity(el, {
translateX: latestX
}, { duration: 0, queue: false });
ticking = false;
});
ticking = true;
}
});
Типичный сценарий — карточки, которые можно смахивать:
Архитектура:
function finalize(el, dx) {
const threshold = 100;
if (Math.abs(dx) > threshold) {
Velocity(el, {
translateX: dx > 0 ? 600 : -600,
rotateZ: dx > 0 ? 10 : -10,
opacity: 0
}, {
duration: 250
});
} else {
Velocity(el, {
translateX: 0,
rotateZ: 0
}, {
duration: 200
});
}
}
Pinch требует анализа двух точек касания:
let initialDistance = 0;
function getDistance(t1, t2) {
return Math.hypot(
t2.clientX - t1.clientX,
t2.clientY - t1.clientY
);
}
el.addEventListener('touchmove', (e) => {
if (e.touches.length !== 2) return;
const [t1, t2] = e.touches;
const distance = getDistance(t1, t2);
const scale = distance / initialDistance;
Velocity(el, {
scale: scale
}, {
duration: 0,
queue: false
});
});
Velocity здесь используется только как слой визуального обновления, без участия в вычислении жеста.
Частая проблема — конфликт scroll и drag.
Решения:
e.preventDefault() в touchmovetouch-action: none.card {
touch-action: none;
}
Это позволяет полностью передать управление жестами JavaScript-логике.
Устойчивый подход строится на трёх уровнях:
1. Input layer
2. Gesture layer
3. Animation layer
Такое разделение позволяет избегать смешивания логики и обеспечивает предсказуемость поведения интерфейса.
touchmove без
queue: false