Пользовательская логика drag

Snap.svg предоставляет мощные инструменты для реализации интерактивного перетаскивания (drag) элементов SVG. Метод drag позволяет привязать к элементу три функции:

  1. onMove – вызывается при перемещении элемента.
  2. onStart – вызывается в момент начала перетаскивания.
  3. onEnd – вызывается при завершении перетаскивания.

Простейший синтаксис:

element.drag(onMove, onStart, onEnd);
  • element – объект Snap.svg (paper.circle(), paper.rect() и т.д.).
  • onMove получает параметры (dx, dy, x, y, event), где dx и dy – смещение от точки начала drag, x и y – текущие координаты курсора, event – объект события.
  • onStart вызывается без аргументов, но часто используется для запоминания исходного состояния элемента.
  • onEnd вызывается после завершения перетаскивания для выполнения финальных действий.

Пример простого drag

var s = Snap(800, 600);
var circle = s.circle(100, 100, 50).attr({fill: "#4CAF50"});

var startX, startY;

circle.drag(
    function(dx, dy) { // onMove
        this.attr({
            cx: startX + dx,
            cy: startY + dy
        });
    },
    function() { // onStart
        startX = this.attr("cx");
        startY = this.attr("cy");
    },
    function() { // onEnd
        console.log("Перетаскивание завершено");
    }
);

В этом примере круг можно перемещать мышью. Значения dx и dy обеспечивают точное смещение относительно начальной позиции.


Drag с ограничениями

Для создания ограничений перемещения используют проверку координат в функции onMove. Например, чтобы круг нельзя было вывести за пределы канвы:

circle.drag(
    function(dx, dy) {
        let newX = Math.min(Math.max(startX + dx, 50), 750);
        let newY = Math.min(Math.max(startY + dy, 50), 550);
        this.attr({cx: newX, cy: newY});
    },
    function() {
        startX = this.attr("cx");
        startY = this.attr("cy");
    }
);

Здесь Math.min и Math.max ограничивают координаты, учитывая радиус круга.


Перетаскивание с сохранением смещения

Если требуется, чтобы элемент не «скакал» к позиции курсора, важно хранить разницу между координатами курсора и координатами элемента:

var offsetX, offsetY;

circle.drag(
    function(dx, dy, x, y) {
        this.attr({
            cx: x - offsetX,
            cy: y - offsetY
        });
    },
    function(startX, startY, event) {
        var bbox = this.getBBox();
        offsetX = startX - bbox.cx;
        offsetY = startY - bbox.cy;
    }
);

Такой подход особенно полезен для сложных SVG-объектов, где точка привязки не совпадает с центром фигуры.


Drag нескольких элементов

Для организации drag группы элементов можно использовать Snap.g():

var group = s.group(
    s.rect(0, 0, 100, 50).attr({fill: "#2196F3"}),
    s.text(50, 30, "Перетащи меня").attr({fill: "#fff", "text-anchor":"middle"})
);

var startX, startY;

group.drag(
    function(dx, dy) {
        this.attr({
            transform: `translate(${startX + dx}, ${startY + dy})`
        });
    },
    function() {
        var t = this.transform().localMatrix;
        startX = t.e;
        startY = t.f;
    }
);

Применение transform позволяет перемещать всю группу как единое целое, сохраняя относительное положение элементов внутри.


Drag с ограничением по оси

Иногда нужно движение только по одной оси. Для ограничения по X или Y:

// Только по X
circle.drag(
    function(dx) {
        this.attr({cx: startX + dx});
    },
    function() {
        startX = this.attr("cx");
    }
);

// Только по Y
circle.drag(
    function(_, dy) {
        this.attr({cy: startY + dy});
    },
    function() {
        startY = this.attr("cy");
    }
);

Использование _ в параметрах позволяет игнорировать ненужное смещение.


Drag с эффектом инерции

Для плавного «отпуска» объекта после drag можно реализовать простую инерцию через setInterval или requestAnimationFrame:

var velocity = {x:0, y:0};

circle.drag(
    function(dx, dy) {
        this.attr({cx: startX + dx, cy: startY + dy});
        velocity.x = dx - (velocity.prevX || 0);
        velocity.y = dy - (velocity.prevY || 0);
        velocity.prevX = dx;
        velocity.prevY = dy;
    },
    function() {
        startX = this.attr("cx");
        startY = this.attr("cy");
        velocity = {x:0, y:0};
    },
    function() {
        var elem = this;
        function animate() {
            velocity.x *= 0.9;
            velocity.y *= 0.9;
            startX += velocity.x;
            startY += velocity.y;
            elem.attr({cx: startX, cy: startY});
            if(Math.abs(velocity.x) > 0.5 || Math.abs(velocity.y) > 0.5) {
                requestAnimationFrame(animate);
            }
        }
        requestAnimationFrame(animate);
    }
);

Такой подход создаёт ощущение естественной физики, когда объект продолжает движение после отпускания.


Drag и события мыши

Snap.svg позволяет комбинировать drag с другими событиями (mouseover, mouseout, click), обеспечивая гибкое управление элементами:

circle.hover(
    function() { this.attr({fill: "#FF5722"}); },
    function() { this.attr({fill: "#4CAF50"}); }
);

circle.click(function() {
    console.log("Клик на элементе!");
});

Комбинируя drag с hover и click, можно создавать сложные интерактивные интерфейсы SVG.


Сохранение позиции элемента

Для сохранения положения элементов после drag удобно использовать атрибут data или собственные свойства объекта:

circle.drag(
    function(dx, dy) {
        this.attr({cx: startX + dx, cy: startY + dy});
    },
    function() { startX = this.attr("cx"); startY = this.attr("cy"); },
    function() {
        this.data("finalPosition", {x: this.attr("cx"), y: this.attr("cy")});
    }
);

Позже можно получить сохранённую позицию через circle.data("finalPosition").


Эти техники позволяют строить сложную пользовательскую логику drag, создавать интерактивные SVG-приложения и игры, а также контролировать движения объектов с высокой точностью и гибкостью.