互动散点图与 20000 个节点
本实验的目的是通过渲染 20000 个圆形来展示 Konva 能处理的节点数量。每个圆形对鼠标悬停事件敏感,可以被拖动并放置。这个实验还很好地演示了事件委托,其中附加到舞台的单个事件处理器处理的是圆形事件。
说明: 将鼠标悬停在节点上以查看更多信息,然后在舞台上拖动和放置它们。
- Vanilla
- React
- Vue
import Konva from 'konva';
// 创建舞台
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
// 添加节点到图层的函数
function addNode(obj, layer) {
const node = new Konva.Circle({
x: obj.x,
y: obj.y,
radius: 4,
fill: obj.color,
id: obj.id,
});
layer.add(node);
}
// 为所有圆形创建一个单独的图层
const circlesLayer = new Konva.Layer();
const tooltipLayer = new Konva.Layer();
const dragLayer = new Konva.Layer();
// 创建工具提示
const tooltip = new Konva.Label({
opacity: 0.75,
visible: false,
listening: false,
});
tooltip.add(
new Konva.Tag({
fill: 'black',
pointerDirection: 'down',
pointerWidth: 10,
pointerHeight: 10,
lineJoin: 'round',
shadowColor: 'black',
shadowBlur: 10,
shadowOffsetX: 10,
shadowOffsetY: 10,
shadowOpacity: 0.2,
})
);
tooltip.add(
new Konva.Text({
text: '',
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
fill: 'white',
})
);
tooltipLayer.add(tooltip);
// 构建数据
const data = [];
const colors = ['red', 'orange', 'cyan', 'green', 'blue', 'purple'];
for (let n = 0; n < 20000; n++) {
const x = Math.random() * width;
const y = height + Math.random() * 200 - 100 + (height / width) * -1 * x;
data.push({
x: x,
y: y,
id: n.toString(),
color: colors[Math.round(Math.random() * 5)],
});
}
// 将所有节点添加到单个图层
for (let n = 0; n < data.length; n++) {
addNode(data[n], circlesLayer);
}
// 将所有图层添加到舞台
stage.add(circlesLayer);
stage.add(dragLayer);
stage.add(tooltipLayer);
// 处理事件
let originalLayer;
stage.on('mouseover mousemove dragmove', function (evt) {
const node = evt.target;
if (node === stage) {
return;
}
if (node) {
// 更新工具提示
const mousePos = node.getStage().getPointerPosition();
tooltip.position({
x: mousePos.x,
y: mousePos.y - 5,
});
tooltip
.getText()
.text('节点:' + node.id() + ', 颜色:' + node.fill());
tooltip.show();
}
});
stage.on('mouseout', function (evt) {
tooltip.hide();
});
stage.on('mousedown', function (evt) {
const shape = evt.target;
if (shape) {
originalLayer = shape.getLayer();
shape.moveTo(dragLayer);
// 手动触发拖动和放置
shape.startDrag();
}
});
stage.on('mouseup', function (evt) {
const shape = evt.target;
if (shape) {
shape.moveTo(originalLayer);
}
});
import { useState, useEffect, useRef, useCallback, memo } from 'react';
import { Stage, Layer, Circle, Label, Tag, Text } from 'react-konva';
const CirclesLayer = ({ nodes, onMouseOver, onMouseMove, onMouseOut, onMouseDown, onMouseUp, onDragEnd }) => {
// Only re-render when the nodes array reference changes
return (
<Layer>
{nodes.map(node => (
<Circle
key={node.id}
x={node.x}
y={node.y}
radius={4}
fill={node.color}
onMouseOver={e => onMouseOver(e, node)}
onMouseMove={onMouseMove}
onMouseOut={onMouseOut}
onDragMove={onMouseMove}
onMouseDown={e => onMouseDown(e, node)}
onMouseUp={e => onMouseUp(e, node)}
onDragEnd={e => onDragEnd(e, node.id)}
draggable
/>
))}
</Layer>
);
};
// 记忆化 CirclesLayer 组件以防止不必要的重新渲染
const MemoizedCirclesLayer = memo(CirclesLayer);
const TooltipLayer = ({ tooltip }) => (
<Layer>
<Label
x={tooltip.x}
y={tooltip.y}
opacity={0.75}
visible={tooltip.visible}
>
<Tag
fill="black"
pointerDirection="down"
pointerWidth={10}
pointerHeight={10}
lineJoin="round"
shadowColor="black"
shadowBlur={10}
shadowOffsetX={10}
shadowOffsetY={10}
shadowOpacity={0.2}
/>
<Text
text={tooltip.text}
fontFamily="Calibri"
fontSize={18}
padding={5}
fill="white"
/>
</Label>
</Layer>
);
// 记忆化 TooltipLayer 以仅在工具提示属性更改时重新渲染
const MemoizedTooltipLayer = memo(TooltipLayer);
const App = () => {
const width = window.innerWidth;
const height = window.innerHeight;
// 为图层创建 refs
const dragLayerRef = useRef(null);
// 工具提示的状态
const [tooltip, setTooltip] = useState({
visible: false,
x: 0,
y: 0,
text: ''
});
// Keep positions in state, but update only after a drag for this large scene.
const [nodes, setNodes] = useState(() => {
const colors = ['red', 'orange', 'cyan', 'green', 'blue', 'purple'];
const data = [];
for (let n = 0; n < 20000; n++) {
const x = Math.random() * width;
const y = height + Math.random() * 200 - 100 + (height / width) * -1 * x;
data.push({
x,
y,
id: n,
color: colors[Math.round(Math.random() * 5)],
});
}
return data;
});
// 事件处理程序 - 包装在 useCallback 中以防止在每次渲染时重新创建函数
const handleMouseOver = useCallback((e, node) => {
const stage = e.target.getStage();
const pos = stage.getPointerPosition();
setTooltip({
visible: true,
x: pos.x,
y: pos.y - 5,
text: `节点:${node.id}, 颜色:${node.color}`
});
}, []);
const handleMouseMove = useCallback((e) => {
const stage = e.target.getStage();
const pos = stage.getPointerPosition();
setTooltip(prev => ({
...prev,
x: pos.x,
y: pos.y - 5
}));
}, []);
const handleMouseOut = useCallback(() => {
setTooltip(prev => ({
...prev,
visible: false
}));
}, []);
const handleMouseDown = useCallback((e, node) => {
// 如果需要,处理拖动
}, []);
const handleMouseUp = useCallback((e, node) => {
// 如果需要,处理拖动
}, []);
const handleDragEnd = useCallback((e, id) => {
const { x, y } = e.target.position();
setNodes(current =>
current.map(node => (node.id === id ? { ...node, x, y } : node))
);
}, []);
return (
<Stage width={width} height={height}>
{/* 为所有圆形渲染单个图层 */}
<MemoizedCirclesLayer
nodes={nodes}
onMouseOver={handleMouseOver}
onMouseMove={handleMouseMove}
onMouseOut={handleMouseOut}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onDragEnd={handleDragEnd}
/>
{/* 拖动层 - 如果需要 */}
<Layer ref={dragLayerRef} />
{/* 工具提示层 */}
<MemoizedTooltipLayer tooltip={tooltip} />
</Stage>
);
};
export default App;
<template>
<v-stage :config="stageConfig" @mousedown="handleStageMouseDown" @mouseup="handleStageMouseUp">
<v-layer ref="circlesLayerRef">
<v-circle
v-for="node in nodes"
:key="node.id"
:config="{
x: node.x,
y: node.y,
radius: 4,
fill: node.color,
id: node.id
}"
@mouseover="handleMouseOver($event, node)"
@mousemove="handleMouseMove"
@mouseout="handleMouseOut"
@dragmove="handleMouseMove"
/>
</v-layer>
<v-layer ref="dragLayerRef"></v-layer>
<v-layer>
<v-label
:config="{
x: tooltip.x,
y: tooltip.y,
opacity: 0.75,
visible: tooltip.visible
}"
>
<v-tag
:config="{
fill: 'black',
pointerDirection: 'down',
pointerWidth: 10,
pointerHeight: 10,
lineJoin: 'round',
shadowColor: 'black',
shadowBlur: 10,
shadowOffsetX: 10,
shadowOffsetY: 10,
shadowOpacity: 0.2
}"
/>
<v-text
:config="{
text: tooltip.text,
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
fill: 'white'
}"
/>
</v-label>
</v-layer>
</v-stage>
</template>
<script setup>
import { ref, computed, onMounted, reactive } from 'vue';
const width = window.innerWidth;
const height = window.innerHeight;
const stageConfig = {
width: width,
height: height
};
// 用于工具提示处理
const tooltip = reactive({
visible: false,
x: 0,
y: 0,
text: ''
});
// 将所有节点存储在一个单独的数组中 - 初始化设置后不再反应
const nodes = ref([]);
const circlesLayerRef = ref(null);
const dragLayerRef = ref(null);
// 这个键仅在节点数据更改时更改,防止不必要的重新渲染
const circlesKey = ref(0);
onMounted(() => {
const colors = ['red', 'orange', 'cyan', 'green', 'blue', 'purple'];
const data = [];
for (let n = 0; n < 20000; n++) {
const x = Math.random() * width;
const y = height + Math.random() * 200 - 100 + (height / width) * -1 * x;
data.push({
x: x,
y: y,
id: n.toString(),
color: colors[Math.round(Math.random() * 5)],
});
}
// 一次设置节点并增加键以触发单个渲染
nodes.value = data;
circlesKey.value++;
});
function handleMouseOver(e, node) {
const stage = e.target.getStage();
const pos = stage.getPointerPosition();
tooltip.visible = true;
tooltip.x = pos.x;
tooltip.y = pos.y - 5;
tooltip.text = `节点:${node.id}, 颜色:${node.color}`;
}
function handleMouseMove(e) {
const stage = e.target.getStage();
const pos = stage.getPointerPosition();
if (tooltip.visible) {
tooltip.x = pos.x;
tooltip.y = pos.y - 5;
}
}
function handleMouseOut() {
tooltip.visible = false;
}
// 使用原生 Konva API 的拖动和放置处理程序
function handleStageMouseDown(e) {
// 获取目标形状
const shape = e.target;
// 只有在它不是舞台本身时才处理
if (shape && shape !== e.target.getStage()) {
// 存储对原始图层的引用以供后用
shape._originalLayer = shape.getParent();
// 手动使用 Konva 的 API 将形状移动到拖动层
// 这避免了在拖动期间 Vue 的反应系统
if (dragLayerRef.value && dragLayerRef.value.getNode()) {
shape.moveTo(dragLayerRef.value.getNode());
// 启动拖动操作
shape.startDrag();
}
}
}
function handleStageMouseUp(e) {
// 获取目标形状
const shape = e.target;
// 只有在它不是舞台本身时才处理
if (shape && shape !== e.target.getStage()) {
// 如果我们有原始层的引用,则移回原始图层
if (shape._originalLayer) {
shape.moveTo(shape._originalLayer);
// 清除引用
shape._originalLayer = null;
}
}
}
</script>