我正在关注如何使用 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;