События касания для мобильных устройств

SVG.js предоставляет мощные возможности для обработки событий, включая поддержку сенсорных взаимодействий на мобильных устройствах. События касания (touchstart, touchmove, touchend, touchcancel) позволяют создавать интерактивные графические элементы, реагирующие на прикосновения.

В SVG.js события касания обрабатываются через метод .on(), аналогично стандартным DOM-событиям. Пример базовой привязки события:

const draw = SVG().addTo('body').size(300, 300);
const circle = draw.circle(100).fill('#f06');

circle.on('touchstart', function(event) {
  this.fill({ color: '#0f6' });
});

В этом примере при касании круга цвет изменяется. Объект event содержит стандартные свойства TouchEvent, включая touches, targetTouches и changedTouches, что позволяет точно определять координаты и идентификаторы касаний.


Основные события касания

  1. touchstart — возникает при начале касания.
  2. touchmove — срабатывает при перемещении пальца по элементу.
  3. touchend — фиксируется при отпускании пальца.
  4. touchcancel — вызывается, если касание прерывается системой (например, при входящем звонке или выходе за пределы экрана).

Работа с координатами касания

Координаты касания можно получить через объект event.touches. Для первого пальца это выглядит так:

circle.on('touchmove', function(event) {
  const touch = event.touches[0];
  const x = touch.clientX;
  const y = touch.clientY;
  this.center(x, y);
});

Метод center(x, y) позволяет перемещать SVG-элемент к позиции касания, создавая эффект перетаскивания.

Особенности:

  • clientX и clientY дают координаты относительно окна браузера.
  • Для относительных координат SVG используется метод point(x, y) объекта SVG, чтобы преобразовать координаты в локальные для SVG-контейнера.
const point = draw.point(touch.clientX, touch.clientY);
this.center(point.x, point.y);

Обработка множественных касаний

SVG.js позволяет отслеживать несколько касаний одновременно, что полезно для жестов масштабирования и вращения:

circle.on('touchmove', function(event) {
  for (let i = 0; i < event.touches.length; i++) {
    const touch = event.touches[i];
    console.log(`Касание ${i}: x=${touch.clientX}, y=${touch.clientY}`);
  }
});

Каждое касание имеет уникальный identifier, позволяющий различать пальцы при сложных жестах.


Жесты масштабирования и вращения

Для реализации pinch-to-zoom и rotation gestures используется комбинация событий touchstart, touchmove и touchend. Пример вычисления масштаба при двух касаниях:

let initialDistance = null;

circle.on('touchstart', function(event) {
  if (event.touches.length === 2) {
    const dx = event.touches[0].clientX - event.touches[1].clientX;
    const dy = event.touches[0].clientY - event.touches[1].clientY;
    initialDistance = Math.sqrt(dx*dx + dy*dy);
  }
});

circle.on('touchmove', function(event) {
  if (event.touches.length === 2 && initialDistance) {
    const dx = event.touches[0].clientX - event.touches[1].clientX;
    const dy = event.touches[0].clientY - event.touches[1].clientY;
    const newDistance = Math.sqrt(dx*dx + dy*dy);
    const scale = newDistance / initialDistance;
    this.scale(scale);
  }
});

Прерывание событий и предотвращение скролла

На мобильных устройствах браузер по умолчанию может интерпретировать касания как прокрутку. Для предотвращения этого используется event.preventDefault():

circle.on('touchmove', function(event) {
  event.preventDefault();
  const touch = event.touches[0];
  const point = draw.point(touch.clientX, touch.clientY);
  this.center(point.x, point.y);
});

Это гарантирует, что движение пальца изменяет только SVG-элемент, а не страницу.


Совместная работа с мышью и сенсорными событиями

Для кроссплатформенной совместимости часто комбинируют события mousedown/mousemove/mouseup с touchstart/touchmove/touchend:

function startDrag(event) {
  const e = event.touches ? event.touches[0] : event;
  this.data('offset', { x: e.clientX - this.cx(), y: e.clientY - this.cy() });
}

function moveDrag(event) {
  event.preventDefault();
  const e = event.touches ? event.touches[0] : event;
  const offset = this.data('offset');
  this.center(e.clientX - offset.x, e.clientY - offset.y);
}

circle.on('mousedown touchstart', startDrag)
      .on('mousemove touchmove', moveDrag);

Такой подход обеспечивает одинаковое поведение для настольных и мобильных устройств, сохраняя удобство работы с интерактивными SVG-элементами.


Особенности производительности

  • Debounce для touchmove — при быстром движении пальца события срабатывают часто, что может снижать FPS. Использование throttle или requestAnimationFrame повышает производительность.
  • Избегание избыточной перерисовки — лучше изменять только атрибуты, влияющие на видимую позицию элемента (cx, cy, transform), вместо полного пересоздания элементов.
  • Сокращение DOM-операций — объединение изменений в одну функцию минимизирует нагрузку на движок рендеринга.

Пример комплексного взаимодействия

Комбинация касания, перетаскивания и масштабирования:

const rect = draw.rect(150, 100).fill('#09f').move(50, 50);
let initialDistance = null;
let startPos = null;

rect.on('touchstart', function(event) {
  if (event.touches.length === 1) {
    const touch = event.touches[0];
    startPos = { x: touch.clientX, y: touch.clientY };
  } else if (event.touches.length === 2) {
    const dx = event.touches[0].clientX - event.touches[1].clientX;
    const dy = event.touches[0].clientY - event.touches[1].clientY;
    initialDistance = Math.sqrt(dx*dx + dy*dy);
  }
});

rect.on('touchmove', function(event) {
  event.preventDefault();
  if (event.touches.length === 1 && startPos) {
    const touch = event.touches[0];
    const dx = touch.clientX - startPos.x;
    const dy = touch.clientY - startPos.y;
    this.dmove(dx, dy);
    startPos = { x: touch.clientX, y: touch.clientY };
  } else if (event.touches.length === 2 && initialDistance) {
    const dx = event.touches[0].clientX - event.touches[1].clientX;
    const dy = event.touches[0].clientY - event.touches[1].clientY;
    const newDistance = Math.sqrt(dx*dx + dy*dy);
    const scale = newDistance / initialDistance;
    this.scale(scale);
    initialDistance = newDistance;
  }
});

Этот подход демонстрирует управление элементом с одного пальца для перемещения и двумя пальцами для масштабирования, полностью реализуя стандартное сенсорное взаимодействие на мобильных устройствах.