Javascript 在react画布绘制npm模块中实现橡皮擦
我想实现擦除功能,其中几乎是正常的,橡皮擦工作 问题是笔刷颜色没有从道具获取值,而是获取默认值 你能检查一下我的代码吗?我做错了什么Javascript 在react画布绘制npm模块中实现橡皮擦,javascript,reactjs,npm,canvas,Javascript,Reactjs,Npm,Canvas,我想实现擦除功能,其中几乎是正常的,橡皮擦工作 问题是笔刷颜色没有从道具获取值,而是获取默认值 你能检查一下我的代码吗?我做错了什么 import React, { PureComponent } from "react"; import PropTypes from "prop-types"; import { LazyBrush } from "lazy-brush"; import { Catenary } from "
import React, { PureComponent } from "react";
import PropTypes from "prop-types";
import { LazyBrush } from "lazy-brush";
import { Catenary } from "catenary-curve";
import ResizeObserver from "resize-observer-polyfill";
import CoordinateSystem, { IDENTITY } from "./coordinateSystem";
import drawImage from "./drawImage";
import { DefaultState } from "./interactionStateMachine";
import makePassiveEventOption from "./makePassiveEventOption";
function midPointBtw(p1, p2) {
return {
x: p1.x + (p2.x - p1.x) / 2,
y: p1.y + (p2.y - p1.y) / 2
};
}
const canvasStyle = {
display: "block",
position: "absolute"
};
const canvasTypes = [
{
name: "interface",
zIndex: 15
},
{
name: "drawing",
zIndex: 11
},
{
name: "temp",
zIndex: 12
},
{
name: "grid",
zIndex: 10
}
];
const dimensionsPropTypes = PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]);
const boundsProp = PropTypes.shape({
min: PropTypes.number.isRequired,
max: PropTypes.number.isRequired,
});
export default class CanvasDraw extends PureComponent {
static propTypes = {
onChange: PropTypes.func,
loadTimeOffset: PropTypes.number,
lazyRadius: PropTypes.number,
brushRadius: PropTypes.number,
brushColor: PropTypes.string,
catenaryColor: PropTypes.string,
gridColor: PropTypes.string,
backgroundColor: PropTypes.string,
hideGrid: PropTypes.bool,
canvasWidth: dimensionsPropTypes,
canvasHeight: dimensionsPropTypes,
disabled: PropTypes.bool,
imgSrc: PropTypes.string,
saveData: PropTypes.string,
immediateLoading: PropTypes.bool,
hideInterface: PropTypes.bool,
enablePanAndZoom: PropTypes.bool,
mouseZoomFactor: PropTypes.number,
zoomExtents: boundsProp,
clampLinesToDocument: PropTypes.bool,
erase: PropTypes.bool,
};
static defaultProps = {
onChange: null,
loadTimeOffset: 5,
lazyRadius: 12,
brushRadius: 10,
brushColor: "#444",
catenaryColor: "#0a0302",
gridColor: "rgba(150,150,150,0.17)",
backgroundColor: "#FFF",
hideGrid: false,
canvasWidth: 400,
canvasHeight: 400,
disabled: false,
imgSrc: "",
saveData: "",
immediateLoading: false,
hideInterface: false,
enablePanAndZoom: false,
mouseZoomFactor: 0.01,
zoomExtents: { min: 0.33, max: 3 },
clampLinesToDocument: false,
erase: false,
};
///// public API /////////////////////////////////////////////////////////////
constructor(props) {
super(props);
this.canvas = {};
this.ctx = {};
this.catenary = new Catenary();
this.points = [];
this.lines = [];
this.erasedLines = [];
this.mouseHasMoved = true;
this.valuesChanged = true;
this.isDrawing = false;
this.isPressing = false;
this.deferRedrawOnViewChange = false;
this.interactionSM = new DefaultState();
this.coordSystem = new CoordinateSystem({
scaleExtents: props.zoomExtents,
documentSize: { width: props.canvasWidth, height: props.canvasHeight }
});
this.coordSystem.attachViewChangeListener(this.applyView.bind(this));
}
getImageAsBase64 = () => {
const canvases = this.canvas;
const ctx = canvases.grid.getContext('2d');
ctx.drawImage(canvases.temp, 0, 0)
ctx.drawImage(canvases.drawing, 0, 0)
return canvases.grid.toDataURL()
}
undo = () => {
let lines = [];
if (this.lines.length) {
lines = this.lines.slice(0, -1);
} else if (this.erasedLines.length) {
lines = this.erasedLines.pop();
}
this.clearExceptErasedLines();
this.simulateDrawingLines({ lines, immediate: true });
this.triggerOnChange();
};
eraseAll = () => {
this.erasedLines.push([ ...this.lines ]);
this.clearExceptErasedLines();
this.triggerOnChange();
};
clear = () => {
this.erasedLines = [];
this.clearExceptErasedLines();
this.resetView();
};
resetView = () => {
return this.coordSystem.resetView();
};
setView = (view) => {
return this.coordSystem.setView(view);
};
getSaveData = () => {
// Construct and return the stringified saveData object
return JSON.stringify({
lines: this.lines,
width: this.props.canvasWidth,
height: this.props.canvasHeight
});
};
loadSaveData = (saveData, immediate = this.props.immediateLoading) => {
if (typeof saveData !== "string") {
throw new Error("saveData needs to be of type string!");
}
const { lines, width, height } = JSON.parse(saveData);
if (!lines || typeof lines.push !== "function") {
throw new Error("saveData.lines needs to be an array!");
}
this.clear();
if (
width === this.props.canvasWidth &&
height === this.props.canvasHeight
) {
this.simulateDrawingLines({
lines,
immediate
});
} else {
// we need to rescale the lines based on saved & current dimensions
const scaleX = this.props.canvasWidth / width;
const scaleY = this.props.canvasHeight / height;
const scaleAvg = (scaleX + scaleY) / 2;
this.simulateDrawingLines({
lines: lines.map(line => ({
...line,
points: line.points.map(p => ({
x: p.x * scaleX,
y: p.y * scaleY
})),
brushRadius: line.brushRadius * scaleAvg
})),
immediate
});
}
};
///// private API ////////////////////////////////////////////////////////////
///// React Lifecycle
componentDidMount() {
this.lazy = new LazyBrush({
radius: this.props.lazyRadius * window.devicePixelRatio,
enabled: true,
initialPoint: {
x: window.innerWidth / 2,
y: window.innerHeight / 2
}
});
this.chainLength = this.props.lazyRadius * window.devicePixelRatio;
this.canvasObserver = new ResizeObserver((entries, observer) =>
this.handleCanvasResize(entries, observer)
);
this.canvasObserver.observe(this.canvasContainer);
this.drawImage();
this.loop();
window.setTimeout(() => {
const initX = window.innerWidth / 2;
const initY = window.innerHeight / 2;
this.lazy.update(
{ x: initX - this.chainLength / 4, y: initY },
{ both: true }
);
this.lazy.update(
{ x: initX + this.chainLength / 4, y: initY },
{ both: false }
);
this.mouseHasMoved = true;
this.valuesChanged = true;
this.clearExceptErasedLines();
// Load saveData from prop if it exists
if (this.props.saveData) {
this.loadSaveData(this.props.saveData);
}
}, 100);
// Attach our wheel event listener here instead of in the render so that we can specify a non-passive listener.
// This is necessary to prevent the default event action on chrome.
// https://github.com/facebook/react/issues/14856
this.canvas.interface && this.canvas.interface.addEventListener(
"wheel",
this.handleWheel,
makePassiveEventOption()
);
}
componentDidUpdate(prevProps) {
if (prevProps.lazyRadius !== this.props.lazyRadius) {
// Set new lazyRadius values
this.chainLength = this.props.lazyRadius * window.devicePixelRatio;
this.lazy.setRadius(this.props.lazyRadius * window.devicePixelRatio);
}
if (prevProps.saveData !== this.props.saveData) {
this.loadSaveData(this.props.saveData);
}
if (JSON.stringify(prevProps) !== JSON.stringify(this.props)) {
// Signal this.loop function that values changed
this.valuesChanged = true;
}
this.coordSystem.scaleExtents = this.props.zoomExtents;
if (!this.props.enablePanAndZoom) {
this.coordSystem.resetView();
}
}
componentWillUnmount = () => {
this.canvasObserver.unobserve(this.canvasContainer);
this.canvas.interface && this.canvas.interface.removeEventListener("wheel", this.handleWheel);
};
render() {
return (
<div
className={this.props.className}
style={{
display: "block",
background: this.props.backgroundColor,
touchAction: "none",
width: this.props.canvasWidth,
height: this.props.canvasHeight,
...this.props.style
}}
ref={container => {
if (container) {
this.canvasContainer = container;
}
}}
>
{canvasTypes.map(({ name, zIndex }) => {
const isInterface = name === "interface";
return (
<canvas
key={name}
ref={canvas => {
if (canvas) {
this.canvas[name] = canvas;
this.ctx[name] = canvas.getContext("2d");
if (isInterface) {
this.coordSystem.canvas = canvas;
}
}
}}
style={{ ...canvasStyle, zIndex }}
onMouseDown={isInterface ? this.handleDrawStart : undefined}
onMouseMove={isInterface ? this.handleDrawMove : undefined}
onMouseUp={isInterface ? this.handleDrawEnd : undefined}
onMouseOut={isInterface ? this.handleDrawEnd : undefined}
onTouchStart={isInterface ? this.handleDrawStart : undefined}
onTouchMove={isInterface ? this.handleDrawMove : undefined}
onTouchEnd={isInterface ? this.handleDrawEnd : undefined}
onTouchCancel={isInterface ? this.handleDrawEnd : undefined}
/>
);
})}
</div>
);
}
///// Event Handlers
handleWheel = e => {
this.interactionSM = this.interactionSM.handleMouseWheel(e, this);
};
handleDrawStart = e => {
this.interactionSM = this.interactionSM.handleDrawStart(e, this);
this.mouseHasMoved = true;
};
handleDrawMove = e => {
this.interactionSM = this.interactionSM.handleDrawMove(e, this);
this.mouseHasMoved = true;
};
handleDrawEnd = e => {
this.interactionSM = this.interactionSM.handleDrawEnd(e, this);
this.mouseHasMoved = true;
};
applyView = () => {
if (!this.ctx.drawing) {
return;
}
canvasTypes.map(({ name }) => this.ctx[name]).forEach(ctx => {
this.clearWindow(ctx);
const m = this.coordSystem.transformMatrix;
ctx.setTransform(m.a, m.b, m.c, m.d, m.e, m.f);
});
if (!this.deferRedrawOnViewChange) {
this.drawGrid(this.ctx.grid);
this.redrawImage();
this.loop({ once: true });
const lines = this.lines;
this.lines = [];
this.simulateDrawingLines({ lines, immediate: true });
}
};
handleCanvasResize = (entries) => {
const saveData = this.getSaveData();
this.deferRedrawOnViewChange = true;
try {
for (const entry of entries) {
const { width, height } = entry.contentRect;
this.setCanvasSize(this.canvas.interface, width, height);
this.setCanvasSize(this.canvas.drawing, width, height);
this.setCanvasSize(this.canvas.temp, width, height);
this.setCanvasSize(this.canvas.grid, width, height);
this.coordSystem.documentSize = { width, height };
this.drawGrid(this.ctx.grid);
this.drawImage();
this.loop({ once: true });
}
this.loadSaveData(saveData, true);
} finally {
this.deferRedrawOnViewChange = false;
}
};
///// Helpers
clampPointToDocument = (point) => {
if (this.props.clampLinesToDocument) {
return {
x: Math.max(Math.min(point.x, this.props.canvasWidth), 0),
y: Math.max(Math.min(point.y, this.props.canvasHeight), 0),
};
} else {
return point;
}
};
redrawImage = () => {
this.image && this.image.complete && drawImage({ ctx: this.ctx.grid, img: this.image });
};
simulateDrawingLines = ({ lines, immediate }) => {
// Simulate live-drawing of the loaded lines
// TODO use a generator
let curTime = 0;
let timeoutGap = immediate ? 0 : this.props.loadTimeOffset;
lines.forEach(line => {
const { points, brushColor, brushRadius } = line;
// Draw all at once if immediate flag is set, instead of using setTimeout
if (immediate) {
// Draw the points
this.drawPoints({
points,
brushColor,
brushRadius
});
// Save line with the drawn points
this.points = points;
this.saveLine({ brushColor, brushRadius });
return;
}
// Use timeout to draw
for (let i = 1; i < points.length; i++) {
curTime += timeoutGap;
window.setTimeout(() => {
this.drawPoints({
points: points.slice(0, i + 1),
brushColor: point.erase ? "erase" : this.props.brushColor,
brushRadius
});
}, curTime);
}
curTime += timeoutGap;
window.setTimeout(() => {
// Save this line with its props instead of this.props
this.points = points;
this.saveLine({ brushColor, brushRadius });
}, curTime);
});
};
setCanvasSize = (canvas, width, height) => {
canvas.width = width;
canvas.height = height;
canvas.style.width = width;
canvas.style.height = height;
};
drawPoints = ({ points, brushColor, brushRadius }) => {
this.ctx.temp.lineJoin = "round";
this.ctx.temp.lineCap = "round";
this.ctx.temp.strokeStyle === "erase" ? "#dbb7bb" : brushColor;
this.ctx.drawing.globalCompositeOperation = brushColor === "erase" ? "destination-out" : "source-over";
this.clearWindow(this.ctx.temp);
this.ctx.temp.lineWidth = brushRadius * 2;
let p1 = points[0];
let p2 = points[1];
this.ctx.temp.moveTo(p2.x, p2.y);
this.ctx.temp.beginPath();
for (var i = 1, len = points.length; i < len; i++) {
// we pick the point between pi+1 & pi+2 as the
// end point and p1 as our control point
var midPoint = midPointBtw(p1, p2);
this.ctx.temp.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
p1 = points[i];
p2 = points[i + 1];
}
// Draw last line as a straight line while
// we wait for the next point to be able to calculate
// the bezier control point
this.ctx.temp.lineTo(p1.x, p1.y);
this.ctx.temp.stroke();
};
saveLine = ({ brushColor, brushRadius } = {}) => {
if (this.points.length < 2) return;
if (this.points[0].erase) {
brushColor = "erase";
}
// Save as new line
this.lines.push({
points: [...this.points],
brushColor: brushColor || this.props.brushColor,
brushRadius: brushRadius || this.props.brushRadius
});
// Reset points array
this.points.length = 0;
// Copy the line to the drawing canvas
this.inClientSpace([this.ctx.drawing, this.ctx.temp], () => {
this.ctx.drawing.drawImage(
this.canvas.temp,
0, 0, this.canvas.drawing.width, this.canvas.drawing.height
);
});
// Clear the temporary line-drawing canvas
this.clearWindow(this.ctx.temp);
this.triggerOnChange();
};
triggerOnChange = () => {
this.props.onChange && this.props.onChange(this);
};
clearWindow = ctx => {
this.inClientSpace([ctx], () => ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height));
};
clearExceptErasedLines = () => {
this.lines = [];
this.valuesChanged = true;
this.clearWindow(this.ctx.drawing);
this.clearWindow(this.ctx.temp);
};
loop = ({ once = false } = {}) => {
if (this.mouseHasMoved || this.valuesChanged) {
const pointer = this.lazy.getPointerCoordinates();
const brush = this.lazy.getBrushCoordinates();
this.drawInterface(this.ctx.interface, pointer, brush);
this.mouseHasMoved = false;
this.valuesChanged = false;
}
if (!once) {
window.requestAnimationFrame(() => {
this.loop();
});
}
};
inClientSpace = (ctxs, action) => {
ctxs.forEach(ctx => {
ctx.save();
ctx.setTransform(IDENTITY.a, IDENTITY.b, IDENTITY.c, IDENTITY.d, IDENTITY.e, IDENTITY.f);
});
try {
action();
} finally {
ctxs.forEach(ctx => ctx.restore());
}
};
///// Canvas Rendering
drawImage = () => {
if (!this.props.imgSrc) return;
// Load the image
this.image = new Image();
// Prevent SecurityError "Tainted canvases may not be exported." #70
this.image.crossOrigin = "anonymous";
// Draw the image once loaded
this.image.onload = this.redrawImage;
this.image.src = this.props.imgSrc;
};
drawGrid = ctx => {
if (this.props.hideGrid) return;
this.clearWindow(ctx);
const gridSize = 25;
const { viewMin, viewMax } = this.coordSystem.canvasBounds;
const minx = Math.floor(viewMin.x / gridSize - 1) * gridSize;
const miny = Math.floor(viewMin.y / gridSize - 1) * gridSize;
const maxx = viewMax.x + gridSize;
const maxy = viewMax.y + gridSize;
ctx.beginPath();
ctx.setLineDash([5, 1]);
ctx.setLineDash([]);
ctx.strokeStyle = this.props.gridColor;
ctx.lineWidth = 0.5;
let countX = minx;
while (countX < maxx) {
countX += gridSize;
ctx.moveTo(countX, miny);
ctx.lineTo(countX, maxy);
}
ctx.stroke();
let countY = miny;
while (countY < maxy) {
countY += gridSize;
ctx.moveTo(minx, countY);
ctx.lineTo(maxx, countY);
}
ctx.stroke();
};
drawInterface = (ctx, pointer, brush) => {
if (this.props.hideInterface) return;
this.clearWindow(ctx);
// Color brush preview according to erase prop
const brushColor = this.props.erase ? "#dbb7bb" : this.props.brushColor;
// Draw brush preview
ctx.beginPath();
ctx.fillStyle = this.props.brushColor;
ctx.arc(brush.x, brush.y, this.props.brushRadius, 0, Math.PI * 2, true);
ctx.fill();
// Draw mouse point (the one directly at the cursor)
ctx.beginPath();
ctx.fillStyle = this.props.catenaryColor;
ctx.arc(pointer.x, pointer.y, 4, 0, Math.PI * 2, true);
ctx.fill();
// Draw catenary
if (this.lazy.isEnabled()) {
ctx.beginPath();
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.setLineDash([2, 4]);
ctx.strokeStyle = this.props.catenaryColor;
this.catenary.drawToCanvas(
this.ctx.interface,
brush,
pointer,
this.chainLength
);
ctx.stroke();
}
// Draw brush point (the one in the middle of the brush preview)
ctx.beginPath();
ctx.fillStyle = this.props.catenaryColor;
ctx.arc(brush.x, brush.y, 2, 0, Math.PI * 2, true);
ctx.fill();
};
}
我遵循这个概念来实现橡皮擦
// Add erase key to the first point in eraser lines
const point = this.props.erase ? { ...this.lazy.brush.toObject(), erase: true } : this.lazy.brush.toObject();