互动散点图与 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, useMemo, useCallback, memo } from 'react'; import { Stage, Layer, Circle, Label, Tag, Text } from 'react-konva'; const CirclesLayer = ({ nodes, onMouseOver, onMouseMove, onMouseOut, onMouseDown, onMouseUp }) => { // 只有在节点数组引用更改时才重新渲 染 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)} 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: '' }); // 节点数据的状态 - 使用 useMemo 确保在重新渲染时不会重新生成 const nodes = useMemo(() => { 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; }, [width, height]); // 事件处理程序 - 包装在 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) => { // 如果需要, 处理拖动 }, []); return ( <Stage width={width} height={height}> {/* 为所有圆形渲染单个图层 */} <MemoizedCirclesLayer nodes={nodes} onMouseOver={handleMouseOver} onMouseMove={handleMouseMove} onMouseOut={handleMouseOut} onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} /> {/* 拖动层 - 如果需要 */} <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>