Drag and drop

Для работы с drag-and-drop необходимо сначала создать экземпляр Two.js и привязать его к DOM-элементу. Обычно используется элемент <div> как контейнер:

const elem = document.getElementById('draw-area');
const two = new Two({
  width: 800,
  height: 600,
  type: Two.Types.canvas
}).appendTo(elem);

Здесь width и height задают размеры сцены, а type определяет рендерер: canvas, svg или webgl. После этого создаётся базовая сцена, на которую можно добавлять фигуры.


Создание интерактивных фигур

Two.js предоставляет несколько встроенных фигур: Rectangle, Circle, Ellipse, Polygon, Path. Для drag-and-drop важны следующие свойства:

  • position.x и position.y — координаты центра фигуры.
  • translation — альтернатива position, используемая для смещения.
  • draggable — логика перемещения задаётся вручную через обработчики событий мыши.

Пример создания круга:

const circle = two.makeCircle(100, 100, 50);
circle.fill = '#FF8000';
circle.stroke = '#000000';
circle.linewidth = 2;

Добавление фигур в сцену автоматически отображает их после вызова:

two.update();

Отслеживание событий мыши

Two.js сам по себе не реализует drag-and-drop. Для этого используются нативные события браузера: mousedown, mousemove, mouseup.

let selected = null;

elem.addEventListener('mousedown', function(event) {
  const rect = elem.getBoundingClientRect();
  const mouseX = event.clientX - rect.left;
  const mouseY = event.clientY - rect.top;

  two.scene.children.forEach(shape => {
    if (shape.contains(mouseX, mouseY)) {
      selected = shape;
      selected.offset = {
        x: mouseX - shape.translation.x,
        y: mouseY - shape.translation.y
      };
    }
  });
});

elem.addEventListener('mousemove', function(event) {
  if (selected) {
    const rect = elem.getBoundingClientRect();
    selected.translation.x = event.clientX - rect.left - selected.offset.x;
    selected.translation.y = event.clientY - rect.top - selected.offset.y;
    two.update();
  }
});

elem.addEventListener('mouseup', function() {
  selected = null;
});

Ключевые моменты:

  • shape.contains(x, y) — метод, проверяющий попадание курсора внутрь фигуры. Для кастомных фигур может потребоваться вручную реализовать проверку.
  • translation изменяет положение фигуры без пересоздания объекта.
  • Обновление сцены после каждого движения необходимо для отображения изменений.

Использование локальных координат

Для сложных сцен с группами объектов полезно работать с локальными координатами. Two.js позволяет объединять объекты в Group:

const group = two.makeGroup(circle, rectangle);
group.translation.set(200, 200);

Drag-and-drop может применяться как к отдельным фигурам, так и к группам:

if (selectedGroup) {
  selectedGroup.translation.x = mouseX - selectedGroup.offset.x;
  selectedGroup.translation.y = mouseY - selectedGroup.offset.y;
}

При этом смещение внутри группы сохраняет относительное расположение дочерних объектов.


Ограничение движения и коллизии

Для реализации ограничений перемещения можно проверять новые координаты перед их присвоением:

const minX = 0, maxX = 800;
const minY = 0, maxY = 600;

selected.translation.x = Math.min(Math.max(mouseX - selected.offset.x, minX), maxX);
selected.translation.y = Math.min(Math.max(mouseY - selected.offset.y, minY), maxY);

Для столкновений между объектами можно использовать bounding box:

function isColliding(a, b) {
  const ax = a.translation.x, ay = a.translation.y;
  const bx = b.translation.x, by = b.translation.y;
  return Math.abs(ax - bx) < (a.width/2 + b.width/2) &&
         Math.abs(ay - by) < (a.height/2 + b.height/2);
}

Анимация при перетаскивании

Для плавного движения можно использовать метод Two.js two.bind('update', callback):

two.bind('update', function() {
  if (selected) {
    // Здесь можно реализовать плавное смещение с инерцией
    selected.translation.x += (targetX - selected.translation.x) * 0.1;
    selected.translation.y += (targetY - selected.translation.y) * 0.1;
  }
}).play();

Это позволяет создавать эффект “тянущегося” объекта, когда фигура плавно движется к курсору.


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

Для touch-событий используются touchstart, touchmove, touchend с аналогичной логикой:

elem.addEventListener('touchstart', function(event) {
  const touch = event.touches[0];
  // логика как для mousedown
});

elem.addEventListener('touchmove', function(event) {
  const touch = event.touches[0];
  // логика как для mousemove
  event.preventDefault();
}, { passive: false });

elem.addEventListener('touchend', function() {
  selected = null;
});

Важно использовать event.preventDefault() для предотвращения скролла на мобильных устройствах при drag-and-drop.


Оптимизация для сложных сцен

  • Дебаунс обновления: при большом количестве объектов обновлять сцену можно с использованием requestAnimationFrame или throttling.
  • Использование групп: объединение объектов в группы уменьшает вычисления попадания курсора.
  • Кэширование hitbox: заранее вычислять границы фигур для ускорения проверки contains.

Расширение функционала

  • Snap-to-grid: привязка к сетке с округлением координат.
  • Drag with constraints: ограничение движения по одной оси или внутри полигона.
  • Z-index: изменение порядка фигур при перетаскивании для правильного отображения перекрытий.

Эти методы позволяют создавать полноценные интерактивные сцены с динамическими объектами, полностью управляемыми через drag-and-drop с Two.js.