От автора: используйте линейную интерполяцию для создания плавного и эффективного UX перетаскивания с использованием vanilla JavaScript.
Недавно я написал статью о реализации функции перетаскивания с использованием vanilla JavaScript. На этот раз я хочу применить линейную интерполяцию для логики перетаскивания, чтобы перетаскиваемый объект плавно «догонял» курсор / точку касания пользователя, а не сразу следовал за ним:
Без линейной интерполяции
С линейной интерполяцией
Основы
Линейная интерполяция — это способ проецирования данных между известными состояниями. В случае функции перетаскивания его можно использовать для создания координат между текущим и будущим положением перетаскиваемого элемента, чтобы мы могли создать плавный переход между этими двумя состояниями. Основная функция, которую мы собираемся использовать:
function lerp(start, end, amt) { return (1-amt)*start+amt*end };
start — представляет начальное состояние (число)
end — конечное состояние (число)
amt — сумма интерполяции между двумя (от 0,0 до 1,0)
Итак, если наша начальная координата равна 100, координата конечного состояния равна 200, а величина, которую мы хотим интерполировать для каждого цикла перерисовки (~ кадр), равна 0,1, тогда:
Frame 1 = (1-0.1)*100 + 0.1*200 = 110; Frame 2 = (1-0.1)*110 + 0.1*200 = 119; Frame 2 = (1-0.1)*119 + 0.1*200 = 127.1; ... Frame N = (1-0.1)*199 + 0.1*200 = 199.1;
Теперь, если мы подумаем о варианте использования перетаскивания, приведенная выше функция создает последовательность координат, через которые перетаскиваемый элемент должен будет пройти.
Начальная настройка
Вот исходный код, который присоединяет прослушиватели, нужные нам для базового функционала перетаскивания:
<div class="container"> <div style="top: 100px; left: 100px" class="box"></div> <div style="top: 200px; left: 200px" class="box"></div> <div style="top: 300px; left: 300px" class="box"></div> <div style="top: 400px; left: 400px" class="box"></div> </div>
.container { width: 600px; height: 600px; background-color: darkgrey; touch-action: none; user-select: none; } .box { position: absolute; width: 100px; height: 100px; background-color: black; }
Приведенный код отображает контейнер, к которому будут прикреплены прослушиватели событий, и блоки, которые позже станут перетаскиваемыми. Теперь мы добавим JavaScript, который работает с магией перетаскивания, пока еще ничего не перемещая:
const container = document.querySelector('.container'); container.addEventListener('pointerdown', userPressed, { passive: true }); var element, bbox, inputX, inputY, boxCenterX, boxCenterY, raf; function userPressed(event) { element = event.target; if (element.classList.contains('box')) { console.log("picked up!") container.addEventListener('pointermove', userMoved, { passive: true }); container.addEventListener('pointerup', userReleased, { passive: true }); container.addEventListener('pointercancel', userReleased, { passive: true }); }; }; function userMoved(event) { console.log("dragging!") }; function userReleased(event) { console.log("dropped!") container.removeEventListener('pointermove', userMoved); container.removeEventListener('pointerup', userReleased); container.removeEventListener('pointercancel', userReleased); };
Несколько примечаний:
Мы используем Pointer Events, которые обеспечивают сочетание сенсорного ввода и ввода с помощью мыши для поддержки современных устройств и перекрестного ввода.
Мы подключаем пассивных прослушивателей и делаем это динамически, чтобы предотвратить загрязнение событий, поэтому одновременно будут активны только необходимые слушатели.
Наши правила touch-action и user-select функции CSS гарантируют, что мы не запускаем какое-либо поведение браузера по умолчанию (прокрутка, нативное перетаскивание и т. д.).
Мы также заботимся о прерванных событиях касания, используя прослушиватель pointercancel и обрабатывая его так же, как pointerup.
Добавление линейной интерполяции
Принимая во внимание пользовательский опыт, мы должны предположить, что интерполяция должна стать «активной» после того, как пользователь нажимает на указатель, а затем становится «неактивной» после того, как пользователь отпускает указатель. Так что-то должно вызывать функцию Lerp (= линейная интерполяция) непрерывно между событиями pointerdown и pointerup/pointercancel.
Мы могли бы, конечно, использовать setInterval, но лучшим вариантом был бы RequestAnimationFrame API, поскольку он дает нам точный таймер кадра рендеринга 60 кадров в секунду (1000 мс / 60 = ~ 16,7 мс). Реализуем эту часть:
const container = document.querySelector('.container'); container.addEventListener('pointerdown', userPressed, { passive: true }); var element, raf; function userPressed(event) { element = event.target; if (element.classList.contains('box')) { console.log("picked up!") container.addEventListener('pointermove', userMoved, { passive: true }); container.addEventListener('pointerup', userReleased, { passive: true }); container.addEventListener('pointercancel', userReleased, { passive: true }); raf = requestAnimationFrame(userMovedRaf); }; }; function userMoved(event) { console.log("dragging!") }; function userMovedRaf() { console.log("calling LERP") raf = requestAnimationFrame(userMovedRaf) }; function userReleased(event) { console.log("dropped!") container.removeEventListener('pointermove', userMoved); container.removeEventListener('pointerup', userReleased); container.removeEventListener('pointercancel', userReleased); if (raf) { cancelAnimationFrame(raf); raf = null; }; }; function lerp (start, end, amt) { return (1-amt)*start+amt*end };
Теперь все наши методы вызываются правильно, и мы можем реализовать логику, которая позволяет перерисовывать прямоугольник, используя интерполированные координаты:
Во-первых, нам нужно предоставить события pointerdown и pointermove, которые начинают получать входные координаты (inputX, inputY), используемые в каждом кадре анимации функцией LERP.
Затем нам нужно убедиться, что мы также захватили ограничивающий прямоугольник клиента, который мы будем использовать для вычисления центральной точки перетаскиваемого блока, гарантируя, что это центр блока, который «догоняет» указатель пользователя, а не только верхний левый угол.
Полная реализация приведена ниже:
const container = document.querySelector('.container'); container.addEventListener('pointerdown', userPressed, { passive: true }); var element, bbox, inputX, inputY, boxCenterX, boxCenterY, raf; function userPressed(event) { element = event.target; if (element.classList.contains('box')) { inputX = event.clientX; inputY = event.clientY; bbox = element.getBoundingClientRect(); boxCenterX = bbox.left + bbox.width/2; boxCenterY = bbox.top + bbox.height/2; container.addEventListener('pointermove', userMoved, { passive: true }); container.addEventListener('pointerup', userReleased, { passive: true }); container.addEventListener('pointercancel', userReleased, { passive: true }); raf = requestAnimationFrame(userMovedRaf); }; }; function userMoved(event) { inputX = event.clientX; inputY = event.clientY; }; function userMovedRaf() { let x = lerp(boxCenterX, inputX, 0.05); let y = lerp(boxCenterY, inputY, 0.05); element.style.left = x - bbox.width/2 + "px"; element.style.top = y - bbox.width/2 + "px"; boxCenterX = x; boxCenterY = y; raf = requestAnimationFrame(userMovedRaf) }; function userReleased(event) { container.removeEventListener('pointermove', userMoved); container.removeEventListener('pointerup', userReleased); container.removeEventListener('pointercancel', userReleased); if (raf) { cancelAnimationFrame(raf); raf = null; }; }; function lerp (start, end, amt) { return (1-amt)*start+amt*end };
Заключение
Мы реализовали другой вид перетаскивания на чистом JavaScript, используя базовую линейную интерполяцию:
Мы использовали современный Pointer Events API.
Вместо setInterval мы использовали API RequestAnimationFrame.
Линейная интерполяция — отличный способ создать плавное перетаскивание для пользователей, особенно в тех случаях, когда вам нужно убедиться, что курсор / точка ввода касания пользователя не всегда блокируется перетаскиваемым элементом. Полная демонстрация доступна на codepen.io:
Спасибо за прочтение!
Автор: Sergey Rudenko
Источник: https://medium.com
Редакция: Команда webformyself.