问题描述
我一直关注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激发时,您已经在线阵列中抛出了多个点。
要定义线的路径,应使用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