
В предыдущей части мы подробно разобрали как устроены touch события и реализовали жест rotate, самое время добавить оставшиеся.
С момента выхода первой части прошло не мало времени, надеюсь заключительная часть не разочарует читателя.
Создаем тестовый стенд (аналогичный 1ой части):
HTML
<div id="rect"></div>
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 и вычесть его значения из координат смещения квадрата.
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
Драг двумя пальцами
Решение этой задачи будет похожим на предыдущее, но нам придется "усреднить" точки нажатия путем нахождения среднего арифметического их координат. С этой усредненной точкой мы будем работать точно так же, как при драге одним пальцем.

Реализуем функцию, которая найдет среднее арифметическое всех 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-ztw5px
Для реализации
rotate и resize нам потребуется добавить иконки соответствующих действий к нашей фигуре и привязать хендлеры обработки событий уже к ним.