Skip to main content

Canvas 吸附与对齐引导线 — 拖动时吸附形状

如何将可拖动形状相互吸附

此演示会将拖动的形状吸附到舞台和其他形状。每个形状都可以在其左侧、中心、右侧、顶部、中部和底部边缘处吸附。

以下外部示例展示了网格吸附:

  1. 网格吸附文章
  2. 网格吸附演示

说明: 将矩形拖动到另一个矩形或舞台边缘附近。蓝色引导线显示当前的吸附位置。

import Konva from 'konva';

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);

// 首先生成随机矩形
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',
})
);
}

// 我们可以在哪里对齐我们的对象?
function getLineGuideStops(skipShape) {
// 我们可以对齐舞台边缘和舞台中心
var vertical = [0, stage.width() / 2, stage.width()];
var horizontal = [0, stage.height() / 2, stage.height()];

// 我们还可以对齐画布上每个对象的边缘和中心
stage.find('.object').forEach((guideItem) => {
if (guideItem === skipShape) {
return;
}
var box = guideItem.getClientRect();
// 我们可以对齐形状的所有边缘
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(),
};
}

// 对象的哪些点将触发对齐?
// 它可以仅仅是对象的中心
// 但是我们将启用所有边缘和中心
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',
},
],
};
}

// 查找所有对齐的可能性
function getGuides(lineGuideStops, itemBounds) {
var resultV = [];
var resultH = [];

lineGuideStops.vertical.forEach((lineGuide) => {
itemBounds.vertical.forEach((itemBound) => {
var diff = Math.abs(lineGuide - itemBound.guide);
// 如果导线和对象对齐点之间的距离很近,我们可以考虑这次对齐
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 = [];

// 找到最近的对齐
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) {
// 清除屏幕上所有之前的线
layer.find('.guid-line').forEach((l) => l.destroy());

// 查找可能的对齐线
var lineGuideStops = getLineGuideStops(e.target);
// 查找当前对象的对齐点
var itemBounds = getObjectSnappingEdges(e.target);

// 现在查找我们可以将当前对象对齐的位置
var guides = getGuides(lineGuideStops, itemBounds);

// 如果没有对齐则不做任何操作
if (!guides.length) {
return;
}

drawGuides(guides);

var absPos = e.target.absolutePosition();
// 现在强制设置对象位置
guides.forEach((lg) => {
switch (lg.orientation) {
case 'V': {
absPos.x = lg.lineGuide + lg.offset;
break;
}
case 'H': {
absPos.y = lg.lineGuide + lg.offset;
break;
}
}
});
e.target.absolutePosition(absPos);
});

layer.on('dragend', function (e) {
// 清除屏幕上所有之前的线
layer.find('.guid-line').forEach((l) => l.destroy());
});