How to snap shapes positions on dragging with Konva?

How to snap draggable shapes to each other?

This demo will demonstrate how to implement snapping of objects to all edges of the stage on to all edges of other objects.

Also I found another related demos on the internet that may be useful:

  1. Post: https://medium.com/@pierrebleroux/snap-to-grid-with-konvajs-c41eae97c13f
  2. Demo:https://codepen.io/pierrebleroux/pen/gGpvxJ

Instruction: try to drag and object. See how it snaps to other objects.

Konva Custom Fontview raw
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/[email protected]/konva.min.js"></script>
<meta charset="utf-8" />
<title>Konva Snapping of shapes Demo</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #f0f0f0;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
var width = window.innerWidth;
var height = window.innerHeight;
var GUIDELINE_OFFSET = 5;

var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});

var layer = new Konva.Layer();
stage.add(layer);

// first generate random rectangles
for (var i = 0; i < 5; i++) {
layer.add(
new Konva.Rect({
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
width: 50 + Math.random() * 50,
height: 50 + Math.random() * 50,
fill: Konva.Util.getRandomColor(),
rotation: Math.random() * 360,
draggable: true,
name: 'object',
})
);
}

// were can we snap our objects?
function getLineGuideStops(skipShape) {
// we can snap to stage borders and the center of the stage
var vertical = [0, stage.width() / 2, stage.width()];
var horizontal = [0, stage.height() / 2, stage.height()];

// and we snap over edges and center of each object on the canvas
stage.find('.object').forEach((guideItem) => {
if (guideItem === skipShape) {
return;
}
var box = guideItem.getClientRect();
// and we can snap to all edges of shapes
vertical.push([box.x, box.x + box.width, box.x + box.width / 2]);
horizontal.push([box.y, box.y + box.height, box.y + box.height / 2]);
});
return {
vertical: vertical.flat(),
horizontal: horizontal.flat(),
};
}

// what points of the object will trigger to snapping?
// it can be just center of the object
// but we will enable all edges and center
function getObjectSnappingEdges(node) {
var box = node.getClientRect();
var absPos = node.absolutePosition();

return {
vertical: [
{
guide: Math.round(box.x),
offset: Math.round(absPos.x - box.x),
snap: 'start',
},
{
guide: Math.round(box.x + box.width / 2),
offset: Math.round(absPos.x - box.x - box.width / 2),
snap: 'center',
},
{
guide: Math.round(box.x + box.width),
offset: Math.round(absPos.x - box.x - box.width),
snap: 'end',
},
],
horizontal: [
{
guide: Math.round(box.y),
offset: Math.round(absPos.y - box.y),
snap: 'start',
},
{
guide: Math.round(box.y + box.height / 2),
offset: Math.round(absPos.y - box.y - box.height / 2),
snap: 'center',
},
{
guide: Math.round(box.y + box.height),
offset: Math.round(absPos.y - box.y - box.height),
snap: 'end',
},
],
};
}

// find all snapping possibilities
function getGuides(lineGuideStops, itemBounds) {
var resultV = [];
var resultH = [];

lineGuideStops.vertical.forEach((lineGuide) => {
itemBounds.vertical.forEach((itemBound) => {
var diff = Math.abs(lineGuide - itemBound.guide);
// if the distance between guild line and object snap point is close we can consider this for snapping
if (diff < GUIDELINE_OFFSET) {
resultV.push({
lineGuide: lineGuide,
diff: diff,
snap: itemBound.snap,
offset: itemBound.offset,
});
}
});
});

lineGuideStops.horizontal.forEach((lineGuide) => {
itemBounds.horizontal.forEach((itemBound) => {
var diff = Math.abs(lineGuide - itemBound.guide);
if (diff < GUIDELINE_OFFSET) {
resultH.push({
lineGuide: lineGuide,
diff: diff,
snap: itemBound.snap,
offset: itemBound.offset,
});
}
});
});

var guides = [];

// find closest snap
var minV = resultV.sort((a, b) => a.diff - b.diff)[0];
var minH = resultH.sort((a, b) => a.diff - b.diff)[0];
if (minV) {
guides.push({
lineGuide: minV.lineGuide,
offset: minV.offset,
orientation: 'V',
snap: minV.snap,
});
}
if (minH) {
guides.push({
lineGuide: minH.lineGuide,
offset: minH.offset,
orientation: 'H',
snap: minH.snap,
});
}
return guides;
}

function drawGuides(guides) {
guides.forEach((lg) => {
if (lg.orientation === 'H') {
var line = new Konva.Line({
points: [-6000, 0, 6000, 0],
stroke: 'rgb(0, 161, 255)',
strokeWidth: 1,
name: 'guid-line',
dash: [4, 6],
});
layer.add(line);
line.absolutePosition({
x: 0,
y: lg.lineGuide,
});
} else if (lg.orientation === 'V') {
var line = new Konva.Line({
points: [0, -6000, 0, 6000],
stroke: 'rgb(0, 161, 255)',
strokeWidth: 1,
name: 'guid-line',
dash: [4, 6],
});
layer.add(line);
line.absolutePosition({
x: lg.lineGuide,
y: 0,
});
}
});
}

layer.on('dragmove', function (e) {
// clear all previous lines on the screen
layer.find('.guid-line').forEach((l) => l.destroy());

// find possible snapping lines
var lineGuideStops = getLineGuideStops(e.target);
// find snapping points of current object
var itemBounds = getObjectSnappingEdges(e.target);

// now find where can we snap current object
var guides = getGuides(lineGuideStops, itemBounds);

// do nothing of no snapping
if (!guides.length) {
return;
}

drawGuides(guides);

var absPos = e.target.absolutePosition();
// now force object position
guides.forEach((lg) => {
switch (lg.snap) {
case 'start': {
switch (lg.orientation) {
case 'V': {
absPos.x = lg.lineGuide + lg.offset;
break;
}
case 'H': {
absPos.y = lg.lineGuide + lg.offset;
break;
}
}
break;
}
case 'center': {
switch (lg.orientation) {
case 'V': {
absPos.x = lg.lineGuide + lg.offset;
break;
}
case 'H': {
absPos.y = lg.lineGuide + lg.offset;
break;
}
}
break;
}
case 'end': {
switch (lg.orientation) {
case 'V': {
absPos.x = lg.lineGuide + lg.offset;
break;
}
case 'H': {
absPos.y = lg.lineGuide + lg.offset;
break;
}
}
break;
}
}
});
e.target.absolutePosition(absPos);
});

layer.on('dragend', function (e) {
// clear all previous lines on the screen
layer.find('.guid-line').forEach((l) => l.destroy());
});
</script>
</body>
</html>
Enjoying Konva? Please consider to support the project.