You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 01:52:06