0

我正在关注如何使用 react 和 konva 构建白板的本教程,它为形状提供了撤消功能,但不适用于线条,因为线条没有以相同的方式添加到图层中。如何实现免费绘制线的撤消?

编辑:

为了扩展我的问题,以下是相关代码:

我有一个公开的 repo,你可以查看(如果更容易的话,可以做一个 PR)。

https://github.com/ChristopherHButler/Sandbox-react-whiteboard

我也有一个演示,你可以在这里试用:

https://whiteboard-rho.now.sh/

这是相关代码

线路组件:

import Konva from "konva";

export const addLine = (stage, layer, mode = "brush") => {

  let isPaint = false;
  let lastLine;

  stage.on("mousedown touchstart", function(e) {
    isPaint = true;
    let pos = stage.getPointerPosition();
    lastLine = new Konva.Line({
      stroke: mode == "brush" ? "red" : "white",
      strokeWidth: mode == "brush" ? 5 : 20,
      globalCompositeOperation:
        mode === "brush" ? "source-over" : "destination-out",
      points: [pos.x, pos.y],
      draggable: mode == "brush",
    });
    layer.add(lastLine);
  });

  stage.on("mouseup touchend", function() {
    isPaint = false;
  });

  stage.on("mousemove touchmove", function() {
    if (!isPaint) {
      return;
    }

  const pos = stage.getPointerPosition();
    let newPoints = lastLine.points().concat([pos.x, pos.y]);
    lastLine.points(newPoints);
    layer.batchDraw();
  });

};

主页组件:

import React, { useState, createRef } from "react";
import { v1 as uuidv1 } from 'uuid';
import ButtonGroup from "react-bootstrap/ButtonGroup";
import Button from "react-bootstrap/Button";

import { Stage, Layer } from "react-konva";

import Rectangle from "../Shapes/Rectangle";
import Circle from "../Shapes/Circle";
import { addLine } from "../Shapes/Line";
import { addTextNode } from "../Shapes/Text";
import Image from "../Shapes/Image";




const HomePage = () => {

  const [rectangles, setRectangles] = useState([]);
  const [circles, setCircles] = useState([]);
  const [images, setImages] = useState([]);
  const [selectedId, selectShape] = useState(null);
  const [shapes, setShapes] = useState([]);
  const [, updateState] = useState();
  const stageEl = createRef();
  const layerEl = createRef();
  const fileUploadEl = createRef();

  const getRandomInt = max => {
    return Math.floor(Math.random() * Math.floor(max));
  };

  const addRectangle = () => {
    const rect = {
      x: getRandomInt(100),
      y: getRandomInt(100),
      width: 100,
      height: 100,
      fill: "red",
      id: `rect${rectangles.length + 1}`,
    };
    const rects = rectangles.concat([rect]);
    setRectangles(rects);
    const shs = shapes.concat([`rect${rectangles.length + 1}`]);
    setShapes(shs);
  };

  const addCircle = () => {
    const circ = {
      x: getRandomInt(100),
      y: getRandomInt(100),
      width: 100,
      height: 100,
      fill: "red",
      id: `circ${circles.length + 1}`,
    };
    const circs = circles.concat([circ]);
    setCircles(circs);
    const shs = shapes.concat([`circ${circles.length + 1}`]);
    setShapes(shs);
  };

  const drawLine = () => {
    addLine(stageEl.current.getStage(), layerEl.current);
  };

  const eraseLine = () => {
    addLine(stageEl.current.getStage(), layerEl.current, "erase");
  };

  const drawText = () => {
    const id = addTextNode(stageEl.current.getStage(), layerEl.current);
    const shs = shapes.concat([id]);
    setShapes(shs);
  };

  const drawImage = () => {
    fileUploadEl.current.click();
  };

  const forceUpdate = React.useCallback(() => updateState({}), []);

  const fileChange = ev => {
    let file = ev.target.files[0];
    let reader = new FileReader();
    reader.addEventListener(
      "load",
      () => {
        const id = uuidv1();
        images.push({
          content: reader.result,
          id,
        });
        setImages(images);
        fileUploadEl.current.value = null;
        shapes.push(id);
        setShapes(shapes);
        forceUpdate();
      },
      false
    );
    if (file) {
      reader.readAsDataURL(file);
    }
  };

  const undo = () => {
    const lastId = shapes[shapes.length - 1];
    let index = circles.findIndex(c => c.id == lastId);
    if (index != -1) {
      circles.splice(index, 1);
      setCircles(circles);
    }
    index = rectangles.findIndex(r => r.id == lastId);
    if (index != -1) {
      rectangles.splice(index, 1);
      setRectangles(rectangles);
    }
    index = images.findIndex(r => r.id == lastId);
    if (index != -1) {
      images.splice(index, 1);
      setImages(images);
    }
    shapes.pop();
    setShapes(shapes);
    forceUpdate();
  };

  document.addEventListener("keydown", ev => {
    if (ev.code == "Delete") {
      let index = circles.findIndex(c => c.id == selectedId);
      if (index != -1) {
        circles.splice(index, 1);
        setCircles(circles);
      }
      index = rectangles.findIndex(r => r.id == selectedId);
      if (index != -1) {
        rectangles.splice(index, 1);
        setRectangles(rectangles);
      }
      index = images.findIndex(r => r.id == selectedId);
      if (index != -1) {
        images.splice(index, 1);
        setImages(images);
      }
      forceUpdate();
    }
  });

  return (
    <div className="home-page">
      <ButtonGroup style={{ marginTop: '1em', marginLeft: '1em' }}>
        <Button variant="secondary" onClick={addRectangle}>
          Rectangle
        </Button>
        <Button variant="secondary" onClick={addCircle}>
          Circle
        </Button>
        <Button variant="secondary" onClick={drawLine}>
          Line
        </Button>
        <Button variant="secondary" onClick={eraseLine}>
          Erase
        </Button>
        <Button variant="secondary" onClick={drawText}>
          Text
        </Button>
        <Button variant="secondary" onClick={drawImage}>
          Image
        </Button>
        <Button variant="secondary" onClick={undo}>
          Undo
        </Button>
      </ButtonGroup>
      <input
        style={{ display: "none" }}
        type="file"
        ref={fileUploadEl}
        onChange={fileChange}
      />
      <Stage
        style={{ margin: '1em', border: '2px solid grey' }}
        width={window.innerWidth * 0.9}
        height={window.innerHeight - 150}
        ref={stageEl}
        onMouseDown={e => {
          // deselect when clicked on empty area
          const clickedOnEmpty = e.target === e.target.getStage();
          if (clickedOnEmpty) {
            selectShape(null);
          }
        }}
      >
        <Layer ref={layerEl}>
          {rectangles.map((rect, i) => {
            return (
              <Rectangle
                key={i}
                shapeProps={rect}
                isSelected={rect.id === selectedId}
                onSelect={() => {
                  selectShape(rect.id);
                }}
                onChange={newAttrs => {
                  const rects = rectangles.slice();
                  rects[i] = newAttrs;
                  setRectangles(rects);
                }}
              />
            );
          })}
          {circles.map((circle, i) => {
            return (
              <Circle
                key={i}
                shapeProps={circle}
                isSelected={circle.id === selectedId}
                onSelect={() => {
                  selectShape(circle.id);
                }}
                onChange={newAttrs => {
                  const circs = circles.slice();
                  circs[i] = newAttrs;
                  setCircles(circs);
                }}
              />
            );
          })}
          {images.map((image, i) => {
            return (
              <Image
                key={i}
                imageUrl={image.content}
                isSelected={image.id === selectedId}
                onSelect={() => {
                  selectShape(image.id);
                }}
                onChange={newAttrs => {
                  const imgs = images.slice();
                  imgs[i] = newAttrs;
                }}
              />
            );
          })}
        </Layer>
      </Stage>
    </div>
  );
}

export default HomePage;
4

2 回答 2

2

作为一种解决方案,您应该只对线条使用相同的反应模式。当Konva.Line您使用react-konva.

只需定义您的状态并从中进行更正render(),就像您在HomePage组件中所做的那样。

您可以将所有形状存储在一个数组中。或者使用单独的 for 行。所以要画线,react-konva你可以这样做:

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

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

  const handleMouseMove = (e) => {
    // no drawing - skipping
    if (!isDrawing.current) {
      return;
    }
    const stage = e.target.getStage();
    const point = stage.getPointerPosition();
    let lastLine = lines[lines.length - 1];
    // add point
    lastLine = lastLine.concat([point.x, point.y]);

    // replace last
    lines.splice(lines.length - 1, 1, lastLine);
    setLines(lines.concat());
  };

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

  return (
    <Stage
      width={window.innerWidth}
      height={window.innerHeight}
      onMouseDown={handleMouseDown}
      onMousemove={handleMouseMove}
      onMouseup={handleMouseUp}
    >
      <Layer>
        <Text text="Just start drawing" />
        {lines.map((line, i) => (
          <Line key={i} points={line} stroke="red" />
        ))}
      </Layer>
    </Stage>
  );
};

演示:https ://codesandbox.io/s/hungry-architecture-v380jlvwrl?file=/index.js

那么接下来就是如何实现undo/redo了。您只需要保留状态更改的历史记录。看看这里的演示:https ://konvajs.org/docs/react/Undo-Redo.html

于 2020-09-11T14:12:10.433 回答
2

如果我理解这一点,您说对于单独添加的形状有一个简单的“撤消”过程,但是对于使用点数组作为其线段的线,没有简单的撤消 - 并且您在教程中没有代码下列的?

我不能给你一个反应代码示例,但我可以解释一些你需要编写代码的概念。

白板中的“手绘线”创建为一系列点。您 mousedown 并记下第一个点,然后您移动鼠标,并在触发当前鼠标位置的每个 movemove 事件上将其添加到数组的末尾。当你完成 line 并触发 mouseup 时,你已经将多个点扔到 line array 中。

Konvajs 行教程中,它指出:

要定义线的路径,您应该使用 points 属性。如果您有三个具有 x 和 y 坐标的点,您应该将 points 属性定义为:[x1, y1, x2, y2, x3, y3]。

[因为...] 平面数字数组应该比对象数组工作得更快并且使用更少的内存。

所以 - 您的线作为单独的值添加到 line.points 数组中。

现在让我们考虑撤消-您可能已经在那里,但无论如何我都会写出来-撤消您需要擦除数组中最后两个条目的行的单个段。要擦除整条线 - 您可以使用标准shape.remove()shape.destroy()方法。

在以下代码段中,两个按钮链接到“撤消”行的代码。'Undo by segment' 按钮显示如何弹出 line.points 数组中的最后两个条目以删除线段,而“Undo by line”按钮则删除整行。这不是一个专门的反应示例,​​但您最终将在您的反应案例中创建非常接近此的东西。

// Code to erase line one segment at a time.
$('#undosegment').on('click', function(){

  // get the last line we added to the canvas - tracked via lines array in this demo
  if (lines.length === 0){
    return;
  }
  lastLine = lines[lines.length - 1];
  
  let pointsArray = lastLine.points(); // get current points in line

  if (pointsArray.length === 0){  // no more points so destroy this line object.
    lastLine.destroy();
    layer.batchDraw();
    lines.pop();  // remove from our lines-tracking array.
    return;
  }

  // remove last x & y entrie, pop appears to be fastest way to achieve AND adjust array length
  pointsArray.pop();  // remove the last Y pos
  pointsArray.pop();  // remove the last X pos

  lastLine.points(pointsArray); // give the points back into the line

  layer.batchDraw();

})

// Code to erase entire lines.
$('#undoline').on('click', function(){

  // get the last line we added to the canvas - tracked via lines array in this demo
  if (lines.length === 0){
    return;
  }
  lastLine = lines[lines.length - 1];
  lastLine.destroy();  // remove from our lines-tracking array.
  lines.pop();

  layer.batchDraw();

})



// code from here on is all about drawing the lines. 

let 
    stage = new Konva.Stage({
          container: 'container',
          width: $('#container').width(),
          height: $('#container').height()
        }),
        
      // add a layer to draw on
      layer = new Konva.Layer();
      
      stage.add(layer);
      stage.draw();        
      
let isPaint = false;
let lastLine;      

let lines = [];

stage.on('mousedown', function(){

    isPaint = true;
    let pos = stage.getPointerPosition();
    
    lastLine = new Konva.Line({ stroke: 'magenta', strokeWidth: 4, points: [pos.x, pos.y]});
    layer.add(lastLine);
    
    lines.push(lastLine);
    
})

stage.on("mouseup touchend", function() {
  isPaint = false;
});
  
stage.on("mousemove touchmove", function() {
  if (!isPaint) {
    return;
  }
  const pos = stage.getPointerPosition();
  let newPoints = lastLine.points().concat([pos.x, pos.y]);

  lastLine.points(newPoints);
  layer.batchDraw();

});
body {
  margin: 10;
  padding: 10;
  overflow: hidden;
  background-color: #f0f0f0;
}
#container {
border: 1px solid silver;
width: 500px;
height: 300px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva@^3/konva.min.js"></script>
<p>Click and drag to draw a line </p>
<p>
  <button id='undosegment'>Undo by segment</button>  <button id='undoline'>Undo by line</button>
</p>
<div id="container"></div>

于 2020-09-11T10:50:51.000 回答