React Konva-撤消免费绘制线条

如何解决React Konva-撤消免费绘制线条

我一直关注this tutorial,了解如何使用react和konva构建白板,它为形状提供了撤消功能,但对线条不起作用,因为线条没有以相同的方式添加到图层中。如何为自由绘制线实施撤消?

编辑:

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

我有一个公共存储库,您可以签出(如果方便的话可以进行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),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;

解决方法

如果我理解的正确,您说的是,对于单独添加的形状,有一个简单的“撤消”过程,但是对于将点数组用于其线段的线,则没有简单的撤消-并且在代码中没有代码您正在关注的教程吗?

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

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

Konvajs line tutorial中指出:

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

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

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

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

在以下代码段中,两个按钮链接到“撤消”行的代码。 “按段撤消”按钮显示如何弹出line.points数组中的最后两个条目以删除该行的一部分,而“按行撤消”按钮则删除整个行。这不是一个具体的React示例,但最终您将在您的React案例中创建与此非常相似的内容。

// 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>

,

作为解决方案,您应该对行使用相同的反应模式。在使用Konva.Line时,不建议手动创建形状实例(如新的react-konva)。

就像在render()组件中所做的那样,只需定义状态并从中做出正确的HomePage

您可以将所有形状存储在一个阵列中。或使用单独的行。因此,以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

然后下一步是如何实现撤消/重做。您只需要保留状态更改的历史记录。在此处查看演示:https://konvajs.org/docs/react/Undo-Redo.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-
参考1 参考2 解决方案 # 点击安装源 协议选择 http:// 路径填写 mirrors.aliyun.com/centos/8.3.2011/BaseOS/x86_64/os URL类型 软件库URL 其他路径 # 版本 7 mirrors.aliyun.com/centos/7/os/x86
报错1 [root@slave1 data_mocker]# kafka-console-consumer.sh --bootstrap-server slave1:9092 --topic topic_db [2023-12-19 18:31:12,770] WARN [Consumer clie
错误1 # 重写数据 hive (edu)&gt; insert overwrite table dwd_trade_cart_add_inc &gt; select data.id, &gt; data.user_id, &gt; data.course_id, &gt; date_format(
错误1 hive (edu)&gt; insert into huanhuan values(1,&#39;haoge&#39;); Query ID = root_20240110071417_fe1517ad-3607-41f4-bdcf-d00b98ac443e Total jobs = 1
报错1:执行到如下就不执行了,没有显示Successfully registered new MBean. [root@slave1 bin]# /usr/local/software/flume-1.9.0/bin/flume-ng agent -n a1 -c /usr/local/softwa
虚拟及没有启动任何服务器查看jps会显示jps,如果没有显示任何东西 [root@slave2 ~]# jps 9647 Jps 解决方案 # 进入/tmp查看 [root@slave1 dfs]# cd /tmp [root@slave1 tmp]# ll 总用量 48 drwxr-xr-x. 2
报错1 hive&gt; show databases; OK Failed with exception java.io.IOException:java.lang.RuntimeException: Error in configuring object Time taken: 0.474 se
报错1 [root@localhost ~]# vim -bash: vim: 未找到命令 安装vim yum -y install vim* # 查看是否安装成功 [root@hadoop01 hadoop]# rpm -qa |grep vim vim-X11-7.4.629-8.el7_9.x
修改hadoop配置 vi /usr/local/software/hadoop-2.9.2/etc/hadoop/yarn-site.xml # 添加如下 &lt;configuration&gt; &lt;property&gt; &lt;name&gt;yarn.nodemanager.res