Skip to main content

如何在 React 中实现自由绘图功能?

该演示展示了如何以“React 方式”实现一个自由绘图应用,并且具有完整的矢量表示。

这样的实现应该适用于许多白板应用。它允许你简单地添加 撤销/重做功能 并将完整状态保存到后端。

注意:如果状态中有太多的线条,它会变得更慢。因此,如果你想启用数百或数千条线的绘图,你将需要进行一些额外的优化。

该演示展示了如何:

  1. 使用 React.useRef 跟踪绘图状态以提高性能
  2. 将线条作为矢量数据存储在 React 状态中
  3. 处理鼠标/触摸事件进行绘图
  4. 使用 globalCompositeOperation 实现笔和橡皮擦工具
  5. 创建具有圆角和张力的平滑线条
import React from 'react';
import { Stage, Layer, Line, Text } from 'react-konva';

const App = () => {
  const [tool, setTool] = React.useState('pen');
  const [lines, setLines] = React.useState([]);
  const isDrawing = React.useRef(false);

  const handleMouseDown = (e) => {
    isDrawing.current = true;
    const pos = e.target.getStage().getPointerPosition();
    setLines([...lines, { tool, points: [pos.x, pos.y] }]);
  };

  const handleMouseMove = (e) => {
    // 没有绘图 - 跳过
    if (!isDrawing.current) {
      return;
    }
    const stage = e.target.getStage();
    const point = stage.getPointerPosition();
    let lastLine = lines[lines.length - 1];
    // 添加点
    lastLine.points = lastLine.points.concat([point.x, point.y]);

    // 替换最后一条线
    lines.splice(lines.length - 1, 1, lastLine);
    setLines(lines.concat());
  };

  const handleMouseUp = () => {
    isDrawing.current = false;
  };

  return (
    <div>
      <select
        value={tool}
        onChange={(e) => {
          setTool(e.target.value);
        }}
      >
        <option value="pen"></option>
        <option value="eraser">橡皮擦</option>
      </select>
      <Stage
        width={window.innerWidth}
        height={window.innerHeight}
        onMouseDown={handleMouseDown}
        onMousemove={handleMouseMove}
        onMouseup={handleMouseUp}
        onTouchStart={handleMouseDown}
        onTouchMove={handleMouseMove}
        onTouchEnd={handleMouseUp}
      >
        <Layer>
          <Text text="开始绘图吧" x={5} y={30} />
          {lines.map((line, i) => (
            <Line
              key={i}
              points={line.points}
              stroke="#df4b26"
              strokeWidth={5}
              tension={0.5}
              lineCap="round"
              lineJoin="round"
              globalCompositeOperation={
                line.tool === 'eraser' ? 'destination-out' : 'source-over'
              }
            />
          ))}
        </Layer>
      </Stage>
    </div>
  );
};

export default App;