跳到主要内容

选中、缩放与旋转

Konva.Transformer 是一个特殊节点,它把控制柄画在被选中的图形周围, 让用户直接拖拽就能缩放和旋转。

用法​

Transformer 自己也是一个节点,要 add 到图层上,再用 nodes() 告诉它当前操作哪些图形:

const tr = new Konva.Transformer();
layer.add(tr);
tr.nodes([rect]);

nodes() 接受数组,传多个图形即可同时变换;传空数组 tr.nodes([]) 取消选中。

下面的演示在此基础上实现了完整的选择交互:点击选中、Shift 点击多选、在空白处拖拽框选。

/**
* select_and_transform/Basic_Demo.html
*/
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/konva@10/konva.min.js"></script>
<meta charset="utf-8">
<title>Konva Transformer Basic Demo</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #F0F0F0;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
var stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});

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

layer.add(new Konva.Rect({
x: 60, y: 60, width: 100, height: 90,
fill: 'red', name: 'rect', draggable: true
}));
layer.add(new Konva.Rect({
x: 250, y: 100, width: 150, height: 90,
fill: 'green', name: 'rect', draggable: true
}));

var tr = new Konva.Transformer();
layer.add(tr);

// 框选用的半透明矩形。listening: false 让它不拦截指针事件,
// 否则松手时 e.target 会是它自己而不是舞台。
var selectionRectangle = new Konva.Rect({
fill: 'rgba(0,0,255,0.5)',
visible: false,
listening: false
});
layer.add(selectionRectangle);

var x1, y1, x2, y2, selecting = false;

stage.on('mousedown touchstart', function (e) {
// 点在图形上时不启动框选,交给下面的选中逻辑
if (e.target !== stage) return;
e.evt.preventDefault();
x1 = x2 = stage.getPointerPosition().x;
y1 = y2 = stage.getPointerPosition().y;
selecting = true;
selectionRectangle.width(0);
selectionRectangle.height(0);
});

stage.on('mousemove touchmove', function (e) {
if (!selecting) return;
e.evt.preventDefault();
x2 = stage.getPointerPosition().x;
y2 = stage.getPointerPosition().y;
selectionRectangle.setAttrs({
visible: true,
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1)
});
});

stage.on('mouseup touchend', function (e) {
selecting = false;
if (!selectionRectangle.visible()) return;
e.evt.preventDefault();

var box = selectionRectangle.getClientRect();
var selected = stage.find('.rect').filter(function (shape) {
return Konva.Util.haveIntersection(box, shape.getClientRect());
});
tr.nodes(selected);

// 留到下一轮事件循环再隐藏,好让紧随其后的 click 能判断出
// 「这次点击是框选的收尾」,从而不把刚选中的图形清空
setTimeout(function () {
selectionRectangle.visible(false);
});
});

stage.on('click tap', function (e) {
// 框选刚结束时不要把这次点击当成选中操作
if (selectionRectangle.visible()) return;

if (e.target === stage) {
tr.nodes([]);
return;
}
if (!e.target.hasName('rect')) return;

var metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey;
var isSelected = tr.nodes().indexOf(e.target) >= 0;

if (!metaPressed && !isSelected) {
tr.nodes([e.target]);
} else if (metaPressed && isSelected) {
var nodes = tr.nodes().slice();
nodes.splice(nodes.indexOf(e.target), 1);
tr.nodes(nodes);
} else if (metaPressed && !isSelected) {
tr.nodes(tr.nodes().concat([e.target]));
}
});

// 首屏就选中一个图形,让读者立刻看到控制柄
tr.nodes([stage.findOne('.rect')]);
</script>

</body>
</html>

常见问题​

为什么 Transformer 不显示?​

三个最常见的原因:忘了把它 add 到图层;nodes() 传的是单个节点而不是数组; 或者被操作的图形本身是 visible(false)。

还有一个不那么明显的:Transformer 必须和被操作的图形在同一个图层上。 跨图层时控制柄会画在另一张画布上,位置和层级都不对,看起来就像没出现。

点击空白处为什么取消不了选中?​

点到空白处时 e.target 是 stage 本身,要判断这个条件来清空:

stage.on('click tap', (e) => {
if (e.target === stage) {
tr.nodes([]);
}
});

如果同时实现了框选,还要额外排除一种情况:框选结束时浏览器也会派发一次 click, 不处理的话刚框选好的图形会立刻被清空。上面的演示用「延迟一轮事件循环再隐藏框选矩形」 来区分这两种点击。

缩放之后 width 没变,变的是 scaleX?​

这是 Transformer 的设计:它改的是 scaleX / scaleY,不是 width / height。

对大多数图形没有区别,但对文字和带描边的图形会出问题——字形会被拉伸变形, 描边会跟着变粗。这两种情况的处理办法见 缩放文字与 描边不随缩放变粗。

性能提示​

Transformer 在每次变换中都要重算控制柄位置。Konva 10.4.0 起做了优化—— 未变化的图形边界会被复用、选择框未变时不重建锚点——但选中节点的数量仍然是主要成本。

批量操作节点时(例如一次性移动几十个图形),先 tr.nodes([]) 解除关联、 操作完再重新关联,可以避免中间过程触发大量无意义的重算。

另外,框选逻辑里的 getClientRect() 对每个候选图形都要算一次包围盒。 图形数量很多时,先用一次粗筛(例如只比较中心点是否落在框内)缩小范围, 再对候选集做精确相交判断。