How to snap shapes positions on canvas with Konva?

Get last news, demos, posts from Konva

How to snap canvas shape to other shapes?

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();
return {
vertical: [
{
guide: Math.round(box.x),
offset: Math.round(node.x() - box.x),
snap: 'start'
},
{
guide: Math.round(box.x + box.width / 2),
offset: Math.round(node.x() - box.x - box.width / 2),
snap: 'center'
},
{
guide: Math.round(box.x + box.width),
offset: Math.round(node.x() - box.x - box.width),
snap: 'end'
}
],
horizontal: [
{
guide: Math.round(box.y),
offset: Math.round(node.y() - box.y),
snap: 'start'
},
{
guide: Math.round(box.y + box.height / 2),
offset: Math.round(node.y() - box.y - box.height / 2),
snap: 'center'
},
{
guide: Math.round(box.y + box.height),
offset: Math.round(node.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, lg.lineGuide, 6000, lg.lineGuide],
stroke: 'rgb(0, 161, 255)',
strokeWidth: 1,
name: 'guid-line',
dash: [4, 6]
});
layer.add(line);
layer.batchDraw();
} else if (lg.orientation === 'V') {
var line = new Konva.Line({
points: [lg.lineGuide, -6000, lg.lineGuide, 6000],
stroke: 'rgb(0, 161, 255)',
strokeWidth: 1,
name: 'guid-line',
dash: [4, 6]
});
layer.add(line);
layer.batchDraw();
}
});
}

layer.on('dragmove', function(e) {
// clear all previous lines on the screen
layer.find('.guid-line').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);

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

layer.on('dragend', function(e) {
// clear all previous lines on the screen
layer.find('.guid-line').destroy();
layer.batchDraw();
});

layer.draw();
</script>
</body>
</html>
Next