使用 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 [selectedShapes, setSelectedShapes] = useState([]); const [selectionRect, setSelectionRect] = useState({ visible: false, x1: 0, y1: 0, x2: 0, y2: 0, }); const groupRef = useRef(); const trRef = useRef(); const layerRef = useRef(); const topLayerRef = useRef(); const selectionRectRef = useRef(); // 生成初始形状 useEffect(() => { const initialShapes = []; for (let i = 0; i < 10000; i++) { initialShapes.push({ id: i, x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight, radius: 10, fill: Konva.Util.getRandomColor(), stroke: null, name: 'shape', }); } setShapes(initialShapes); }, []); useEffect(() => { if (selectedShapes.length > 0 && groupRef.current) { trRef.current.nodes([groupRef.current]); groupRef.current.cache(); } else { trRef.current.nodes([]); if (groupRef.current) { groupRef.current.clearCache(); } } }, [selectedShapes]); const handleMouseDown = (e) => { // 如果在变换器上或组上按下鼠标则不执行任何操作 const clickedOnTransformer = e.target.getParent() === trRef.current; const clickedOnGroup = e.target.parent === groupRef.current; if (clickedOnTransformer || clickedOnGroup) { return; } const pointerPos = e.target.getStage().getPointerPosition(); setSelectionRect({ visible: true, x1: pointerPos.x, y1: pointerPos.y, x2: pointerPos.x, y2: pointerPos.y, }); // 将旧选择移回原始图层 if (groupRef.current && selectedShapes.length > 0) { const updatedShapes = [...shapes]; const transform = groupRef.current.getAbsoluteTransform(); selectedShapes.forEach(selectedShape => { const shapeIndex = updatedShapes.findIndex(s => s.id === selectedShape.id); if (shapeIndex !== -1) { const shape = updatedShapes[shapeIndex]; const absPos = transform.point({ x: shape.x, y: shape.y }); const scale = transform.decompose().scaleX; updatedShapes[shapeIndex] = { ...shape, x: absPos.x, y: absPos.y, radius: shape.radius * scale, stroke: null }; } }); setShapes(updatedShapes); setSelectedShapes([]); } // 重置组转换 if (groupRef.current) { groupRef.current.setAttrs({ x: 0, y: 0, scaleX: 1, scaleY: 1, rotation: 0, }); groupRef.current.clearCache(); } }; const handleMouseMove = (e) => { if (!selectionRect.visible) { return; } const pointerPos = e.target.getStage().getPointerPosition(); setSelectionRect({ ...selectionRect, x2: pointerPos.x, y2: pointerPos.y, }); }; const handleMouseUp = (e) => { if (!selectionRect.visible) { return; } // 用超时隐藏选择矩形,以便我们可以在点击事件中检查 setTimeout(() => { setSelectionRect({ ...selectionRect, visible: false, }); }); var shapes = e.target.getStage().find('.shape'); // 手动创建框,而不是使用 getClientRect() var 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) }; // 为了更好的性能,移除所有子元素 layerRef.current.removeChildren(); // 然后检查交集并将所有形状添加到正确的容器中 shapes.forEach((shape) => { var intersected = Konva.Util.haveIntersection( box, shape.getClientRect() ); if (intersected) { groupRef.current.add(shape); shape.stroke('blue'); } else { layerRef.current.add(shape); shape.stroke(null); } }); if (groupRef.current.children.length) { trRef.current.nodes([groupRef.current]); groupRef.current.cache(); } else { trRef.current.nodes([]); groupRef.current.clearCache(); } }; const handleClick = (e) => { // 如果我们正在用矩形选择,则不执行任何操作 if (selectionRect.visible) { return; } // 如果点击空白区域 - 移除所有选择 if (e.target === e.target.getStage()) { if (groupRef.current && selectedShapes.length > 0) { const updatedShapes = [...shapes]; const transform = groupRef.current.getAbsoluteTransform(); selectedShapes.forEach(selectedShape => { const shapeIndex = updatedShapes.findIndex(s => s.id === selectedShape.id); if (shapeIndex !== -1) { const shape = updatedShapes[shapeIndex]; const absPos = transform.point({ x: shape.x, y: shape.y }); const scale = transform.decompose().scaleX; updatedShapes[shapeIndex] = { ...shape, x: absPos.x, y: absPos.y, radius: shape.radius * scale, stroke: null }; } }); setShapes(updatedShapes); } setSelectedShapes([]); trRef.current.nodes([]); if (groupRef.current) { groupRef.current.clearCache(); } return; } }; // 计算选择矩形属性 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={handleClick} onTap={handleClick} > <Layer ref={layerRef}> {shapes.filter(shape => !selectedShapes.find(s => s.id === shape.id)).map(shape => ( <Circle key={shape.id} id={shape.id.toString()} x={shape.x} y={shape.y} radius={shape.radius} fill={shape.fill} stroke={shape.stroke} name="shape" /> ))} </Layer> <Layer ref={topLayerRef}> <Group ref={groupRef} draggable > {selectedShapes.map(shape => ( <Circle key={shape.id} id={shape.id.toString()} 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;