Реализуем touch жесты на vanilla js. Часть 2 (drag, resize)
В предыдущей части мы подробно разобрали как устроены touch события и реализовали жест rotate, самое время добавить оставшиеся.
С момента выхода первой части прошло не мало времени, надеюсь заключительная часть не разочарует читателя.
Создаем тестовый стенд (аналогичный 1ой части):
HTML
CSS
#rect {
background-color: red;
width: 500px;
height: 500px;
}
JS
import "./styles.css";
const rect = document.getElementById("rect");
prepareTouches — простая функция для получения координат нажатий в удобном формате массива объектов с полями x и y. Обратите внимание, что clientX не всегда может быть подходящим значением.
Определиться между clientX, pageX и screenX можно тут:
https://developer.mozilla.org/en-US/docs/Web/API/Touch
// Функция для препроцессинга нажатий
function prepareTouches(e) {
return Array.prototype.map.call(e.targetTouches, (t) => {
return {
x: t.clientX,
y: t.clientY,
};
});
}
Хендлер touchstart. В нем мы будем обрабатывать событие начала движения и сохранять стартовые данные:
// Обработка события начала движения
rect.addEventListener("touchstart", (e) => {
e.preventDefault();
const touches = prepareTouches(e);
console.log(e);
});
Хендлер touchmove — обрабатывает последующие движения пальцами до момента отрыва их от экрана.
// Обрабатываем процесс движения по экрану
rect.addEventListener("touchmove", (e) => {
e.preventDefault();
const touches = prepareTouches(e);
console.log(touches);
});
Примеры кода обрабатывают именно события нажатия, поэтому для их просмотра нужно использовать эмулятор или реальный тач девайс.
Драг одним пальцем
Попробуем решить задачу в лоб, у нас есть координаты касания пальцем, которые мы можем получить из события. Давайте просто сместим прямоугольник на значение координат пальца через translate.
rect.addEventListener("touchmove", (e) => {
e.preventDefault();
const touches = prepareTouches(e);
// Этот обработчик события актуален только для движения одним пальцем, добавим условие
if (touches.length === 1) {
// Получаем координаты нашей единственной точки
const { x, y } = touches[0];
// Смещаем квадрат на эти координаты
rect.style.transform = `translate(${x}px, ${y}px)`;
}
});
В момент начала движения наш квадрат «прыгает» таким образом, что верхний левый угол квадрата смещается на позицию курсора. Попробуем понять почему так происходит.
Причина в том, что в момент начала движения палец может быть расположен в любой точке квадрата, формируя произвольный отступ от его верхнего левого угла (touchOffset). На этот самый отступ и происходит скачок.
Нам нужно учитывать расстояние touchOffset для корректного позиционирования
Мы должны найти «Расстояние скачка» или touchOffset и вычесть его значения из координат смещения квадрата.
touchOffsetX = touchX - dragPosition.x
Определим объекты для хранения состояния:
// Смещение нашего квадрата относительно начала координат
const dragPosition = {
x: 0,
y: 0,
};
// Тут мы будем хранить позицию пальца относительно верхнего, правого угла прямоугольника
const touchOffset = {
x: 0,
y: 0,
};
Модифицируем обработчик события начала движения и сохраняем offset пальца:
rect.addEventListener("touchstart", (e) => {
e.preventDefault();
const touches = prepareTouches(e);
// Этот обработчик события актуален только для движения одним пальцем
if (touches.length === 1) {
const { x, y } = touches[0];
// Считаем стартовый отступ пальца от начала координат
touchOffset.x = x - dragPosition.x;
touchOffset.y = y - dragPosition.y;
}
});
Добавляем в обработчик движения поправку на dragStartOffset:
rect.addEventListener("touchmove", (e) => {
e.preventDefault();
const touches = prepareTouches(e);
// Этот обработчик события актуален только для движения одним пальцем
if (touches.length === 1) {
// Получаем координаты нашей единственной точки
const { x, y } = touches[0];
// Смещаем начальные координаты на высчитанный на старте отступ
dragPosition.x = x - touchOffset.x;
dragPosition.y = y - touchOffset.y;
// Смещаем квадрат на эти координаты
rect.style.transform = `translate(${dragPosition.x}px, ${dragPosition.y}px)`;
}
});
Теперь наш драг работает, так как нужно.
Код: https://codesandbox.io/p/sandbox/habr-drag-1-finger-l22q62
Драг двумя пальцами
Решение этой задачи будет похожим на предыдущее, но нам придется «усреднить» точки нажатия путем нахождения среднего арифметического их координат. С этой усредненной точкой мы будем работать точно так же, как при драге одним пальцем.
При драге 2 мя пальцами, нам нужно найти усредненную точку
Реализуем функцию, которая найдет среднее арифметическое всех x и y нажатий:
function touchesMean(points) {
// Суммируем координаты x и y всех точек
const sumX = points.reduce((sum, point) => sum + point.x, 0);
const sumY = points.reduce((sum, point) => sum + point.y, 0);
return {
x: sumX / points.length,
y: sumY / points.length,
};
}
Аналогично предыдущей части найдем стартовый отступ от верхнего левого угла в момент начала движения:
rect.addEventListener("touchstart", (e) => {
e.preventDefault();
const touches = prepareTouches(e);
// Убеждаемся в том что нажатий больше 1
if (touches.length > 1) {
// Находим усреднённую точку
const { x, y } = touchesMean(touches);
// Вычисляем и сохраняем отступ (как в прошлой части)
touchOffset.x = x - dragPosition.x;
touchOffset.y = y - dragPosition.y;
}
});
Реализуем обработчик последующего движения:
rect.addEventListener("touchmove", (e) => {
e.preventDefault();
const touches = prepareTouches(e);
// Убеждаемся в том что нажатий больше 1
if (touches.length > 1) {
// Находим усреднённую точку
const { x, y } = touchesMean(touches);
// Вычисляем и сохраняем отступ (как в прошлой части)
dragPosition.x = x - touchOffset.x;
dragPosition.y = y - touchOffset.y;
// Устанавливаем отступ для прямоугольника
rect.style.transform = `translate(${dragPosition.x}px, ${dragPosition.y}px)`;
}
});
Код:
https://codesandbox.io/p/sandbox/habr-drag-2-fingers-kptqp8
Ресайз
Чтобы понять, как должно работать изменение размера картинки, посмотрим внимательно на жест, который должен его осуществлять
Изменение расстояния между пальцами меняет размер нашего квадрата. Начнем с того, что получим это расстояние.
Построим воображаемый прямоугольник через точки касания экрана таким образом, чтобы его гипотенуза проходила через наши точки.
Построим прямоугольный треугольник с гипотенузой в точках касания
Найти гипотенузу нам поможет теорема Пифагора.
В js уже реализована функция Math.hypot для того, чтобы найти расстояние AB
AB = Math.hypot (B.y — A.y, B.x — A.x);
Реализуем это выражение в виде функции:
function calcTouchDistance(touches) {
return Math.hypot(touches[1].y - touches[0].y, touches[1].x - touches[0].x);
}
Определяем переменные для хранения промежуточного состояния:
// Размер нашего прямоугольника
let rectSize = {
width: 500,
height: 500,
};
// Расстояние между нажатиями
let touchDistance = 0;
В момент начала движения нам нужно сохранить исходное расстояние между пальцами:
rect.addEventListener("touchstart", (e) => {
e.preventDefault();
// Получаем список нажатий
const touches = prepareTouches(e);
// Проверяем что нажатий минимум 2 (иначе это поведение не имеет смысла)
if (touches.length > 1) {
// Сохраняем исходное положение пальцев
touchDistance = calcTouchDistance(touches);
}
});
При обработке последующего движения мы каждый раз находим отношение новой длинны отрезка между пальцами к старой и умножаем длину сторон квадрата на это отношение:
rectSizeWidth *= touchDistanceNew / touchDistancePrev
rect.addEventListener("touchmove", (e) => {
e.preventDefault();
// Получаем список нажатий
const touches = prepareTouches(e);
// Проверяем что нажатий минимум 2
if (touches.length > 1) {
// Сохраняем предыдущее расстояние между нажатиями
const touchDistancePrev = touchDistance;
// Находим новое расстония между нажатиями в текущем кадре
touchDistance = calcTouchDistance(touches);
// Рассчитываем во сколько раз изменилось расстояние
// в текущем кадре (сравнивая с предыдущим)
const frameChangeRatio = touchDistance / touchDistancePrev;
// Модифицируем размер прямоугольника
rectSize.width = rectSize.width * frameChangeRatio;
rectSize.height = rectSize.height * frameChangeRatio;
// Применяем размер
rect.style.width = `${rectSize.width}px`;
rect.style.height = `${rectSize.height}px`;
}
});
Дисклеймер: чтобы упростить код я просто продублировал стартовые значение rectSize из css.
Код:
https://codesandbox.io/p/sandbox/rotate-7gh6pk
Заключение
Мы закончили с реализацией базовых тач жестов. Примеры кода показывают работу каждого жеста в отдельности, а как их объединить — будет определять архитектура вашего проекта. Вы можете оставить их в виде отдельных функций и добавлять на элементы dom по мере необходимости, либо объединить и свалить весь код в touchstart и touchmove хендлеры.
P.S. Десктоп
Обработка событий мыши уже немного выходит за пределы темы с жестами, пробежимся по ней обзорно.
Большую часть «математики» из обработки тач жестов вы сможем использовать повторно.
addEventListener должно устанавливать хендлеры на события мыши, а не тача
Для реализации rotate и resize потребуются отобразить дополнительные элементы интерфейса
Драг мышью
Реализуем функцию для процессинга события нажатия:
// Препроцессим нажатия
function prepareMouseEvent(e) {
return [
{
x: e.screenX,
y: e.screenY,
},
];
}
Флоу обработки драга в общем виде будет выглядеть так:
// Тут мы будем хранить статус, обрабатываем ли мы в данный момент события мыши
let mouseDrag = false;
rect.addEventListener("mousedown", (e) => {
// Начинаем обработку события
mouseDrag = true;
});
rect.addEventListener("mousemove", (e) => {
if (mouseDrag) {
// Обрабатываем событие движения (если статус обработки положительный)
}
});
rect.addEventListener("mouseup", () => {
// Заканчваем обработку
mouseDrag = false;
});
mouseDrag нам потребуется для того чтобы отсечь ложные срабатывания mouseMove, когда мышь двигается над квадратом, при этом предварительного нажатия по квадрату нажатия не было.
Непосредственно обработка координат будет аналогична обработке тач мува одним пальцем.
Код можно посмотреть тут:
https://codesandbox.io/p/sandbox/habr-desktop-drag-ztw5 px
Для реализации
rotate и resize нам потребуется добавить иконки соответствующих действий к нашей фигуре и привязать хендлеры обработки событий уже к ним.