使用 Konva 进行调整大小压力测试
这是一个用于同时选择和调整许多形状的压力测试演示。
该演示使用两个核心 Konva 功能来提升性能:
1. 图层
调整大小的形状被移动到另一个图层(另一个画布元素)。因此,当您调整所选形状的大小时,我们不需要重绘其他形状。
2. 缓存
在 select 时,我将所有选定的形状移动到一个组中并缓存该组。缓存操作会将组转换为位图图像。在屏幕上重绘这样的组要快得多。
说明:尝试选择几个形状并调整它们的大小/旋转它们。
- Vanilla
- React
import Konva from 'konva';
// 首先我们需要创建一个舞台
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
// 用于所有形状的图层
var layer = new Konva.Layer();
stage.add(layer);
for (var i = 0; i < 10000; i++) {
var shape = new Konva.Circle({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
radius: 10,
name: 'shape',
fill: Konva.Util.getRandomColor(),
});
layer.add(shape);
}
// 用于变换组的顶层图层
var topLayer = new Konva.Layer();
stage.add(topLayer);
var group = new Konva.Group({
draggable: true,
});
topLayer.add(group);
var tr = new Konva.Transformer();
topLayer.add(tr);
// 添加一个新功能,让我们添加绘制选择矩形的能力
var selectionRectangle = new Konva.Rect({
fill: 'rgba(0,0,255,0.5)',
visible: false,
});
topLayer.add(selectionRectangle);
var x1, y1, x2, y2;
stage.on('mousedown touchstart', (e) => {
// 如果在变换器上按下鼠标,则不执行任何操作
if (e.target.getParent() === tr) {
return;
}
// 如果在组上按下鼠标,则不执行任何操作
if (e.target.parent === group) {
return;
}
x1 = stage.getPointerPosition().x;
y1 = stage.getPointerPosition().y;
x2 = stage.getPointerPosition().x;
y2 = stage.getPointerPosition().y;
selectionRectangle.setAttrs({
x: x1,
y: y1,
width: 0,
height: 0,
visible: true,
});
// 将旧选区移回原始图层
group.children.slice().forEach((shape) => {
const transform = shape.getAbsoluteTransform();
shape.moveTo(layer);
shape.setAttrs(transform.decompose());
});
// 重置组变换
group.setAttrs({
x: 0,
y: 0,
scaleX: 1,
scaleY: 1,
rotation: 0,
});
group.clearCache();
});
stage.on('mousemove touchmove', () => {
// 如果没有开始选择,则不执行任何操作
if (!selectionRectangle.visible()) {
return;
}
x2 = stage.getPointerPosition().x;
y2 = stage.getPointerPosition().y;
selectionRectangle.setAttrs({
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1),
});
});
stage.on('mouseup touchend', () => {
// 如果没有开始选择,则不执行任何操作
if (!selectionRectangle.visible()) {
return;
}
// 在超时中更新可见性,以便我们可以在点击事件中检查它
setTimeout(() => {
selectionRectangle.visible(false);
});
var shapes = stage.find('.shape');
var box = selectionRectangle.getClientRect();
// 移除所有子项以获得更好的性能
layer.removeChildren();
// 然后检查交集并将所有形状添加到正确的容器中
shapes.forEach((shape) => {
var intersected = Konva.Util.haveIntersection(
box,
shape.getClientRect()
);
if (intersected) {
group.add(shape);
shape.stroke('blue');
} else {
layer.add(shape);
shape.stroke(null);
}
});
if (group.children.length) {
tr.nodes([group]);
group.cache();
} else {
tr.nodes([]);
group.clearCache();
}
});
// 点击应选择/取消选择形状
stage.on('click tap', function (e) {
// 如果我们正在使用矩形选择,则不执行任何操作
if (selectionRectangle.visible()) {
return;
}
// 如果点击空白区域 - 移除所有选择
if (e.target === stage) {
tr.nodes([]);
return;
}
});
import { useState, useRef, useEffect } from 'react';
import { Stage, Layer, Circle, Group, Transformer, Rect } from 'react-konva';
import Konva from 'konva';
const App = () => {
const [shapes, setShapes] = useState([]);
const [selectedIds, setSelectedIds] = useState([]);
const [selectionRect, setSelectionRect] = useState({
visible: false,
x1: 0,
y1: 0,
x2: 0,
y2: 0,
});
const [groupKey, setGroupKey] = useState(0);
const groupRef = useRef();
const trRef = useRef();
const selectionRectRef = useRef();
// 一次性生成 1 万个形状
useEffect(() => {
const items = [];
for (let i = 0; i < 10000; i++) {
items.push({
id: i,
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
radius: 10,
fill: Konva.Util.getRandomColor(),
});
}
setShapes(items);
}, []);
// 附加/分离变换器并缓存
useEffect(() => {
if (selectedIds.length && groupRef.current) {
trRef.current.nodes([groupRef.current]);
groupRef.current.cache();
} else {
trRef.current.nodes([]);
if (groupRef.current) groupRef.current.clearCache();
}
}, [selectedIds]);
// 用于将组变换应用于形状并提交到 React 状态的工具
const applyGroupTransform = () => {
if (!selectedIds.length || !groupRef.current) return;
const transform = groupRef.current.getAbsoluteTransform();
const { scaleX } = transform.decompose();
setShapes((prev) =>
prev.map((shape) => {
if (!selectedIds.includes(shape.id)) return shape;
const pos = transform.point({ x: shape.x, y: shape.y });
return {
...shape,
x: pos.x,
y: pos.y,
radius: shape.radius * scaleX,
};
})
);
};
const pointerPos = (e) => e.target.getStage().getPointerPosition();
const handleMouseDown = (e) => {
// 忽略点击变换器或组
if (e.target.getParent() === trRef.current || e.target.parent === groupRef.current) {
return;
}
// 完成之前的选择(如果有)
applyGroupTransform();
if (selectedIds.length) {
setSelectedIds([]);
setGroupKey((k) => k + 1); // 重置组以进行新的变换
}
const p = pointerPos(e);
setSelectionRect({ visible: true, x1: p.x, y1: p.y, x2: p.x, y2: p.y });
};
const handleMouseMove = (e) => {
if (!selectionRect.visible) return;
const p = pointerPos(e);
setSelectionRect((prev) => ({ ...prev, x2: p.x, y2: p.y }));
};
const handleMouseUp = (e) => {
if (!selectionRect.visible) return;
setTimeout(() => setSelectionRect((prev) => ({ ...prev, visible: false })), 0);
const stage = e.target.getStage();
const nodes = stage.find('.shape');
const box = {
x: Math.min(selectionRect.x1, selectionRect.x2),
y: Math.min(selectionRect.y1, selectionRect.y2),
width: Math.abs(selectionRect.x2 - selectionRect.x1),
height: Math.abs(selectionRect.y2 - selectionRect.y1),
};
const ids = [];
nodes.forEach((node) => {
if (Konva.Util.haveIntersection(box, node.getClientRect())) {
ids.push(Number(node.id()));
}
});
setSelectedIds(ids);
};
const handleStageClick = (e) => {
// 忽略属于选择矩形绘制部分的点击
if (selectionRect.visible) return;
if (e.target === e.target.getStage()) {
// 点击空白区域:应用变换并清除选择
applyGroupTransform();
if (selectedIds.length) {
setSelectedIds([]);
setGroupKey((k) => k + 1);
}
}
};
const selectionRectProps = {
fill: 'rgba(0,0,255,0.5)',
visible: selectionRect.visible,
x: Math.min(selectionRect.x1, selectionRect.x2),
y: Math.min(selectionRect.y1, selectionRect.y2),
width: Math.abs(selectionRect.x2 - selectionRect.x1),
height: Math.abs(selectionRect.y2 - selectionRect.y1),
ref: selectionRectRef,
};
return (
<Stage
width={window.innerWidth}
height={window.innerHeight}
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
onMouseMove={handleMouseMove}
onTouchMove={handleMouseMove}
onMouseUp={handleMouseUp}
onTouchEnd={handleMouseUp}
onClick={handleStageClick}
onTap={handleStageClick}
>
<Layer>
{shapes
.filter((s) => !selectedIds.includes(s.id))
.map((shape) => (
<Circle
key={shape.id}
id={String(shape.id)}
x={shape.x}
y={shape.y}
radius={shape.radius}
fill={shape.fill}
name="shape"
/>
))}
</Layer>
<Layer>
<Group key={groupKey} ref={groupRef} draggable>
{shapes
.filter((s) => selectedIds.includes(s.id))
.map((shape) => (
<Circle
key={shape.id}
id={String(shape.id)}
x={shape.x}
y={shape.y}
radius={shape.radius}
fill={shape.fill}
stroke="blue"
strokeWidth={2}
name="shape"
/>
))}
</Group>
<Transformer ref={trRef} />
<Rect {...selectionRectProps} />
</Layer>
</Stage>
);
};
export default App;