如何使用React Konva实现可绘制可滚动的无限画布?
React + React Konva 实现无限画布分步指南
核心逻辑
无限画布靠平移视口容器+动态管理可见元素实现,既模拟无限空间,又避免一次性渲染过多元素拖垮性能。React Konva的Stage和Layer可以配合手势库轻松实现拖拽、缩放功能。
步骤1:安装依赖
先装好需要的包:
npm install react-konva konva react-use-gesture
用react-use-gesture处理拖拽、缩放手势,比原生事件监听更省心。
步骤2:实现基础画布平移
先做一个能拖拽平移的画布框架:
import React, { useState } from 'react'; import { Stage, Layer, Rect, Circle } from 'react-konva'; import { useDrag } from 'react-use-gesture'; const InfiniteCanvas = () => { // 控制画布偏移的状态,实现平移 const [canvasOffset, setCanvasOffset] = useState({ x: 0, y: 0 }); // 绑定拖拽手势,平移画布 const bindDrag = useDrag(({ down, movement: [mx, my] }) => { if (!down) return; setCanvasOffset(prev => ({ x: prev.x + mx, y: prev.y + my })); }); return ( <div style={{ width: '100vw', height: '100vh', overflow: 'hidden' }}> <Stage width={window.innerWidth} height={window.innerHeight} position={canvasOffset} {...bindDrag()} style={{ cursor: 'grab' }} > <Layer> {/* 示例元素,后续可替换为用户绘制内容 */} <Rect x={100} y={100} width={50} height={50} fill="red" /> <Circle x={300} y={200} radius={30} fill="blue" /> </Layer> </Stage> </div> ); }; export default InfiniteCanvas;
步骤3:添加自由绘制功能
给画布加上鼠标绘制线条的能力,注意要区分“拖拽画布”和“绘制”的状态:
import React, { useState, useRef } from 'react'; import { Stage, Layer, Rect, Circle, Line } from 'react-konva'; import { useDrag } from 'react-use-gesture'; const InfiniteCanvas = () => { const [canvasOffset, setCanvasOffset] = useState({ x: 0, y: 0 }); const [isDrawing, setIsDrawing] = useState(false); const [drawnLines, setDrawnLines] = useState([]); const currentLinePoints = useRef([]); // 拖拽画布的手势,绘制时禁止平移 const bindDrag = useDrag(({ down, movement: [mx, my] }) => { if (!down || isDrawing) return; setCanvasOffset(prev => ({ x: prev.x + mx, y: prev.y + my })); }); // 开始绘制:记录起点(抵消画布偏移,得到全局坐标) const startDrawing = (e) => { setIsDrawing(true); const x = e.evt.offsetX - canvasOffset.x; const y = e.evt.offsetY - canvasOffset.y; currentLinePoints.current = [{ x, y }]; }; // 绘制中:实时更新线条路径 const drawLine = (e) => { if (!isDrawing) return; const x = e.evt.offsetX - canvasOffset.x; const y = e.evt.offsetY - canvasOffset.y; currentLinePoints.current = [...currentLinePoints.current, { x, y }]; setDrawnLines(prev => [...prev.slice(0, -1), currentLinePoints.current]); }; // 结束绘制:保存完整线条 const stopDrawing = () => { if (isDrawing) { setDrawnLines(prev => [...prev, currentLinePoints.current]); setIsDrawing(false); } }; return ( <div style={{ width: '100vw', height: '100vh', overflow: 'hidden' }}> <Stage width={window.innerWidth} height={window.innerHeight} position={canvasOffset} {...bindDrag()} onMouseDown={startDrawing} onMouseMove={drawLine} onMouseUp={stopDrawing} onMouseLeave={stopDrawing} style={{ cursor: isDrawing ? 'crosshair' : 'grab' }} > <Layer> {/* 渲染所有已绘制的线条 */} {drawnLines.map((line, idx) => ( <Line key={idx} points={line.flatMap(p => [p.x, p.y])} stroke="black" strokeWidth={2} tension={0.5} lineCap="round" lineJoin="round" /> ))} {/* 示例固定元素 */} <Rect x={100} y={100} width={50} height={50} fill="red" /> <Circle x={300} y={200} radius={30} fill="blue" /> </Layer> </Stage> </div> ); }; export default InfiniteCanvas;
步骤4:性能优化——只渲染可见元素
如果画布上元素太多,只渲染当前视口内的内容:
// 在组件内添加计算可见区域的函数 const getVisibleArea = () => { return { minX: -canvasOffset.x, maxX: -canvasOffset.x + window.innerWidth, minY: -canvasOffset.y, maxY: -canvasOffset.y + window.innerHeight }; }; // 假设你有一个elements数组存储所有画布元素,过滤出可见的 const visibleElements = elements.filter(el => { const area = getVisibleArea(); // 这里假设元素有x、y、width、height属性,判断是否与可见区域重叠 return ( el.x + el.width > area.minX && el.x < area.maxX && el.y + el.height > area.minY && el.y < area.maxY ); });
步骤5:添加缩放功能
用useWheel实现滚轮缩放画布:
import { useDrag, useWheel } from 'react-use-gesture'; // 新增缩放比例状态 const [scale, setScale] = useState(1); // 绑定滚轮缩放手势 const bindWheel = useWheel(({ delta }) => { const zoomRate = delta[1] > 0 ? 1.1 : 0.9; // 限制缩放范围,避免过大或过小 setScale(prev => Math.max(0.1, Math.min(5, prev * zoomRate))); }); // 修改Stage组件,添加scale和transformOrigin <Stage width={window.innerWidth} height={window.innerHeight} position={canvasOffset} scale={{ x: scale, y: scale }} transformOrigin="center" {...bindDrag()} {...bindWheel()} // 其他事件保持不变 />
关键注意事项
- 坐标转换:所有元素用全局坐标存储,绘制时要抵消
canvasOffset得到正确的全局位置。 - 手势冲突:绘制时要禁止画布平移,避免操作混乱。
- 性能控制:大量元素时一定要做可见区域过滤,不然会卡顿。
内容的提问来源于stack exchange,提问作者Sheikh Yawar
相关产品推荐
相关产品推荐

