英文:
Canvas: redrawing entire canvas when drawing shapes with mouse causes stutters
问题
我正在开发一个使用HTML5画布的协作白板。我想要用鼠标绘制形状,为此,我会清除整个画布,重新绘制之前在画布上的元素,然后将新形状添加在上面。
我使用socket.io在客户端之间发送数据。在画布的鼠标抬起事件监听器中,我会像这样发送数据:
canvas.addEventListener('mouseup', function () {
canvas.removeEventListener('mousemove', onPaint, false);
canvas.isDrawing = false;
let canvasImg = canvas.toDataURL('image/png');
socket.emit('canvas-data', canvasImg);
}, false);
然后,我有一个React钩子来接收socket事件并设置whiteboardImage。然后将其绘制到画布上,像这样:
useEffect(() => {
if (open) {
let image = new Image();
let canvas = canvasRef.current;
canvas.whiteboardImage = whiteboardImage;
let ctx = canvas.getContext('2d');
image.onload = () => {
ctx.drawImage(image, 0, 0);
};
image.src = whiteboardImage;
}
}, [whiteboardImage, open]);
在绘制形状时,我在鼠标移动事件监听器中这样做:
if (canvas.toolState === 'line') {
if (canvas.isDrawing) {
// 清除并获取旧白板图像
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
let image = new Image();
image.onload = () => {
ctx.drawImage(image, 0, 0);
};
image.src = canvas.whiteboardImage;
// 绘制新线条
ctx.beginPath();
ctx.moveTo(mouse.x, mouse.y);
ctx.lineTo(shape_start.x, shape_start.y);
ctx.stroke();
}
} // ...所有其他形状
这个问题对所有形状都适用,但对铅笔工具不适用,因为使用铅笔绘制时不需要清除画布。
据我所知,清除单个笔触是不可能的。即使我将所有绘制的东西存储为带有其路径的对象,我仍然需要清除画布并重新绘制,这仍然会导致卡顿。
有什么方法可以解决这个问题,如何使其更流畅?
一个最小、可重现的示例
const Whiteboard = ({ open }) => {
const canvasRef = useRef(null);
const [whiteboardImage, setWhiteboardImage] = useState(null);
// 从其他客户端接收白板数据的socket事件
useEffect(() => {
socket.on('canvas-data', (data) => {
let image = new Image();
let canvas = canvasRef.current;
let ctx = canvas.getContext('2d');
image.onload = () => {
ctx.drawImage(image, 0, 0);
};
image.src = data;
setWhiteboardImage(data);
});
}, []);
// 如果画布打开,设置鼠标监听器
useEffect(() => {
if (open) {
drawOnCanvas();
}
}, [open, canvasRef]);
const drawOnCanvas = () => {
let canvas = canvasRef.current;
if (canvas) {
let ctx = canvas.getContext('2d');
let rect = canvas.getBoundingClientRect();
let sketch = document.querySelector('#sketch');
let sketch_style = getComputedStyle(sketch);
canvas.width = parseInt(sketch_style.getPropertyValue('width'));
canvas.height = parseInt(sketch_style.getPropertyValue('height'));
// 初始化对象
let mouse = { x: 0, y: 0 };
let last_mouse = { x: 0, y: 0 };
let shape_start = { x: 0, y: 0 };
/* 鼠标捕获工作 */
canvas.addEventListener('mousemove', function (e) {
mouse.x = e.pageX - rect.x;
mouse.y = e.pageY - rect.y;
}, false);
/* 在绘图应用程序上绘制 */
ctx.lineWidth = 5;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.strokeStyle = 'black';
canvas.addEventListener('mousedown', function (e) {
shape_start = { x: mouse.x, y: mouse.y };
canvas.isDrawing = true;
canvas.addEventListener('mousemove', onPaint, false);
}, false);
canvas.addEventListener('mouseup', function () {
canvas.removeEventListener('mousemove', onPaint, false);
canvas.isDrawing = false;
let canvasImg = canvas.toDataURL('image/png');
socket.emit('canvas-data', canvasImg);
}, false);
// 用鼠标绘制直线
let onPaint = function () {
// 清除并获取旧白板图像
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
if (whiteboardImage) {
let image = new Image();
image.onload = () => {
ctx.drawImage(image, 0, 0);
};
image.src = whiteboardImage;
}
// 绘制新线条
ctx.beginPath();
ctx.moveTo(mouse.x, mouse.y);
ctx.lineTo(shape_start.x, shape_start.y);
ctx.stroke();
};
}
return (
<div>
<canvas ref={canvasRef} />
</div>
);
};
};
我尽量使示例尽可能简化。在这个示例中,您只能绘制直线,没有像gif中那样的画笔工具。卡顿问题是由于在onPaint函数中绘制形状的方式引起的。清除和重新绘制是问题的关键。
英文:
I am developing a collaborative whiteboard using the html5 canvas. I want to draw shapes with the mouse, to do so, I am clearing the entire canvas, redrawing the elements previously on the canvas and then adding the new shape on top.
I am using socket.io to send data between clients. On the mouse up event listener for the canvas, I emit data like this:
canvas.addEventListener('mouseup', function () {
canvas.removeEventListener('mousemove', onPaint, false);
canvas.isDrawing = false;
let canvasImg = canvas.toDataURL('image/png')
socket.emit('canvas-data', canvasImg)
}, false);
And then I have a react hook to receive the socket event and set whiteboardImage. It is then painted on to the canvas like this:
useEffect(() => {
if (open) {
let image = new Image();
let canvas = canvasRef.current;
canvas.whiteboardImage = whiteboardImage
let ctx = canvas.getContext('2d');
image.onload = () => {
ctx.drawImage(image, 0, 0)
}
image.src = whiteboardImage
}
}, [whiteboardImage, open])
When drawing shapes, I do so in the mouse move event listener like this:
if (canvas.toolState === 'line') {
if (canvas.isDrawing) {
// clear and get old whiteboard image
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
let image = new Image();
image.onload = () => {
ctx.drawImage(image, 0, 0)
}
image.src = canvas.whiteboardImage
// draw new line
ctx.beginPath();
ctx.moveTo(mouse.x, mouse.y);
ctx.lineTo(shape_start.x, shape_start.y);
ctx.stroke();
}
} // ... all the other shapes
This problem occurs for all shapes, but not the pencil tool, because I do not have to clear the canvas when drawing with pencil.
As far as I am aware, it is impossible to clear only one stroke. Even if I store everything drawn as an object with its path, I would still have to clear the canvas and redraw, which would still cause a stutter.
Is there anything I can do, how do I make it smooth?
A Minimal, Reproducable Example
const Whiteboard = ({ open }) => {
const canvasRef = useRef(null);
const [whiteboardImage, setWhiteboardImage] = useState(null)
// socket event to receive whiteboard data from other clients
useEffect(() => {
socket.on('canvas-data', (data) => {
let image = new Image();
let canvas = canvasRef.current;
let ctx = canvas.getContext('2d');
image.onload = () => {
ctx.drawImage(image, 0, 0)
}
image.src = data
setWhiteboardImage(data)
})
}, [])
// sets up mouselisteners if the canvas is open
useEffect(() => {
if (open) {
drawOnCanvas()
}
}, [open, canvasRef])
const drawOnCanvas = () => {
let canvas = canvasRef.current;
if (canvas) {
let ctx = canvas.getContext('2d');
let rect = canvas.getBoundingClientRect();
let sketch = document.querySelector('#sketch');
let sketch_style = getComputedStyle(sketch);
canvas.width = parseInt(sketch_style.getPropertyValue('width'));
canvas.height = parseInt(sketch_style.getPropertyValue('height'));
// initializing objects
let mouse = { x: 0, y: 0 };
let last_mouse = { x: 0, y: 0 };
let shape_start = { x: 0, y: 0 };
/* Mouse Capturing Work */
canvas.addEventListener('mousemove', function (e) {
mouse.x = e.pageX - rect.x;
mouse.y = e.pageY - rect.y;
}, false);
/* Drawing on Paint App */
ctx.lineWidth = 5;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.strokeStyle = 'black';
canvas.addEventListener('mousedown', function (e) {
shape_start = { x: mouse.x, y: mouse.y }
canvas.isDrawing = true;
canvas.addEventListener('mousemove', onPaint, false);
}, false);
canvas.addEventListener('mouseup', function () {
canvas.removeEventListener('mousemove', onPaint, false);
canvas.isDrawing = false;
let canvasImg = canvas.toDataURL('image/png')
socket.emit('canvas-data', canvasImg)
}, false);
// draws straight line with mouse
let onPaint = function () {
// clear and get old whiteboard image
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
if (whiteboardImage) {
let image = new Image();
image.onload = () => {
ctx.drawImage(image, 0, 0)
}
image.src = whiteboardImage
}
// draw new line
ctx.beginPath();
ctx.moveTo(mouse.x, mouse.y);
ctx.lineTo(shape_start.x, shape_start.y);
ctx.stroke();
}
}
return (
<div>
<canvas ref={canvasRef} />
</div>
)
}
I tried to make it as minimal as possible. In this example, you can only draw lines, there is no brush tools like in the gifs. The stuttering comes from the way I draw shapes in the onPaint function. It is the clearing and redrawing that is the issue.
答案1
得分: 2
你在每次鼠标移动时都加载白板图像 - 这会导致图像闪烁。你应该平滑加载它,并只在画布绘制中使用加载的图像(不要在那里加载它)。
const [image, setImage] = useState(null);
// 下一行不再需要
// const [whiteboardImage, setWhiteboardImage] = useState(null)
useEffect(() => {
socket.on('canvas-data', (data) => {
let image = new Image();
let canvas = canvasRef.current;
let ctx = canvas.getContext('2d');
image.onload = () => {
setImage(image);
}
image.src = data;
});
}, []);
useEffect(() => {
if (open && canvasRef.current) {
drawOnCanvas()
}
}, [open, canvasRef, image])
const drawOnCanvas = () => {
let canvas = canvasRef.current;
let ctx = canvas.getContext('2d');
let rect = canvas.getBoundingClientRect();
// 用鼠标绘制直线
let onPaint = function () {
// 清除并获取旧的白板图像
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
if (image) {
ctx.drawImage(image, 0, 0);
}
// 绘制新线条
ctx.beginPath();
ctx.moveTo(mouse.x, mouse.y);
ctx.lineTo(shape_start.x, shape_start.y);
ctx.stroke();
}
onPaint();
}
但正如Kaiido建议的那样,最好不要使用图像数据,而是使用点和参数。它将是这样的:
const Whiteboard = ({ open }) => {
const canvasRef = useRef(null);
const [elements, setElements] = useState(null)
// 从其他客户端接收白板数据的套接字事件
useEffect(() => {
socket.on('canvas-data', (data) => {
setElements(data);
// 这里的data应该是一个画布元素列表的样子
// 它可以是这样的:
// [
// {type: "line", start: {x: 0, y: 0}, end: {x: 40, y: 10}},
// {type: "circle", center: {x: 20, y: 20}, r: 7},
// {type: "polyline", points: [{x: 0, y: 0}, {x: 2, y: 2}, {x: 5, y: 3}, ...]},
// ]
})
}, [])
// 如果画布打开,设置鼠标监听器
useEffect(() => {
if (open && canvasRef.current) {
drawOnCanvas()
}
}, [open, canvasRef, elements])
const drawElement = (ctx, elem) => {
switch (elem.type){
case 'line':
ctx.beginPath();
ctx.moveTo(elem.start.x, elem.start.y);
ctx.lineTo(elem.end.x, elem.end.y);
ctx.stroke();
case 'polyline':
// ...
}
};
const drawOnCanvas = () => {
let canvas = canvasRef.current;
if (canvas) {
let ctx = canvas.getContext('2d');
let rect = canvas.getBoundingClientRect();
let sketch = document.querySelector('#sketch');
let sketch_style = getComputedStyle(sketch);
canvas.width = parseInt(sketch_style.getPropertyValue('width'));
canvas.height = parseInt(sketch_style.getPropertyValue('height'));
/* 鼠标捕获工作 */
const getPos = (e) => {
return {x: e.pageX - rect.x, y: e.pageY - rect.y};
};
/* 在画布上绘制 */
ctx.lineWidth = 5;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.strokeStyle = 'black';
canvas.addEventListener('mousedown', function (e) {
canvas.currentElement = {type: "line", start: getPos(e), end: getPos(e)};
canvas.addEventListener('mousemove', onPaint, false);
}, false);
canvas.addEventListener('mouseup', function () {
canvas.removeEventListener('mousemove', onPaint, false);
elements.push(canvas.currentElement);
socket.emit('canvas-data', elements);
canvas.currentElement = null;
setElements(elements);
}, false);
// 用鼠标绘制直线
let onPaint = function (e) {
if (e)
canvas.currentElement.end = getPos(e);
// 清除并获取旧的白板图像
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
elements.forEach(el => drawElement(ctx, el));
if (e)
drawElement(ctx, canvas.currentElement);
}
onPaint();
}
};
return (
<div>
<canvas ref={canvasRef} />
</div>
);
}
你可以看到,在这种情况下,我们只使用对象工作,并且需要为每种类型的元素(线条、圆圈等)实现绘制方法,然后在鼠标事件监听器中只需更改currentElement对象。
这种方法要好得多,因为它不依赖于画布大小。
使用你的方法,如果有人在移动设备上的屏幕右侧绘制圆圈(小屏幕),具有更宽屏幕的其他人将看到裁剪的图像。在面向对象的方法中,这个问题被消除了。
希望这对你有所帮助。
英文:
You load whiteboardImage every mousemove call - it cause blinking image. You should load it smoothly and use loaded image only in your canvas drawing (do not load it there).
const [image, setImage] = useState(null);
// next line not required anymore
// const [whiteboardImage, setWhiteboardImage] = useState(null)
useEffect(() => {
socket.on('canvas-data', (data) => {
let image = new Image();
let canvas = canvasRef.current;
let ctx = canvas.getContext('2d');
image.onload = () => {
setImage(image);
}
image.src = data;
});
}, []);
useEffect(() => {
if (open && canvasRef.current) {
drawOnCanvas()
}
}, [open, canvasRef, image])
const drawOnCanvas = () => {
let canvas = canvasRef.current;
let ctx = canvas.getContext('2d');
let rect = canvas.getBoundingClientRect();
...
// draws straight line with mouse
let onPaint = function () {
// clear and get old whiteboard image
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
if (image) {
ctx.drawImage(image, 0, 0);
}
// draw new line
ctx.beginPath();
ctx.moveTo(mouse.x, mouse.y);
ctx.lineTo(shape_start.x, shape_start.y);
ctx.stroke();
}
onPaint();
}
But as Kaiido suggested it's much better to use not image data for it but points and parameters. It will be something like:
const Whiteboard = ({ open }) => {
const canvasRef = useRef(null);
const [elements, setElements] = useState(null)
// socket event to receive whiteboard data from other clients
useEffect(() => {
socket.on('canvas-data', (data) => {
setElements(data);
// here data should be kind of list of canvas elements
// it could be something like:
// [
// {type: "line", start: {x: 0, y: 0}, end: {x: 40, y: 10}},
// {type: "circle", center: {x: 20, y: 20}, r: 7},
// {type: "polyline", points: [{x: 0, y: 0}, {x: 2, y: 2}, {x: 5, y, 3}, ...]},
// ]
})
}, [])
// sets up mouselisteners if the canvas is open
useEffect(() => {
if (open && canvasRef.current) {
drawOnCanvas()
}
}, [open, canvasRef, elements])
const drawElement = (ctx, elem) => {
switch (elem.type){
case 'line':
ctx.beginPath();
ctx.moveTo(elem.start.x, elem.start.y);
ctx.lineTo(elem.end.x, elem.end.y);
ctx.stroke();
case 'polyline':
// ...
}
};
const drawOnCanvas = () => {
let canvas = canvasRef.current;
if (canvas) {
let ctx = canvas.getContext('2d');
let rect = canvas.getBoundingClientRect();
let sketch = document.querySelector('#sketch');
let sketch_style = getComputedStyle(sketch);
canvas.width = parseInt(sketch_style.getPropertyValue('width'));
canvas.height = parseInt(sketch_style.getPropertyValue('height'));
/* Mouse Capturing Work */
const getPos = (e) => {
return {x: e.pageX - rect.x, y: e.pageY - rect.y};
};
/* Drawing on Paint App */
ctx.lineWidth = 5;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.strokeStyle = 'black';
canvas.addEventListener('mousedown', function (e) {
canvas.currentElement = {type: "line", start: getPos(e), end: getPos(e)};
canvas.addEventListener('mousemove', onPaint, false);
}, false);
canvas.addEventListener('mouseup', function () {
canvas.removeEventListener('mousemove', onPaint, false);
elements.push(canvas.currentElement);
socket.emit('canvas-data', elements);
canvas.currentElement = null;
setElements(elements);
}, false);
// draws straight line with mouse
let onPaint = function (e) {
if (e)
canvas.currentElement.end = getPos(e);
// clear and get old whiteboard image
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
elements.forEach(el => drawElement(ctx, el));
if (e)
drawElement(ctx, canvas.currentElement);
}
onPaint();
}
};
return (
<div>
<canvas ref={canvasRef} />
</div>
);
}
You can see that in this case we just work with objects and we need to implement drawing methods for each type of element (line, circle, etc) and then on mouse event listeners we just changing currentElement object.
This approach is much better cause it doesn't depend on canvas size.
With your approach if someone will draw circle on the right side of screen on mobile (small screen) other people with wider screen will see cropped image. In object oriented approach this problem is eliminated.
Hope it will help.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论