I am creating a web based drawing board. Part of the functionality of this is to draw shapes (like lines).
I'm finding myself needing to keep track of all the points drawn in the canvas in an array. Specifically because I need to clear the board and redraw the points when drawing lines and other shapes to make it feel "interactive". To clarify the functionality, I've created this JSFiddle https://jsfiddle.net/hwa95rcx/35/
HTML:
<canvas id="test" width="500" height="500">
</canvas>
JavasScript:
let lineStartX = 0;
let lineStartY = 0;
const drawingPoints = [];
let mouseDown = false;
const c = document.getElementById("test");
const ctx = c.getContext("2d");
c.onmousedown = (e) => {
lineStartX = e.clientX;
lineStartY = e.clientY;
mouseDown = true;
}
c.onmousemove = (e) => {
if (mouseDown) {
ctx.beginPath();
ctx.clearRect(0, 0, 500, 500);
redraw();
ctx.moveTo(lineStartX, lineStartY);
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
}
}
c.onmouseup = (e) => {
mouseDown = false;
drawingPoints.push({
startX: lineStartX,
startY: lineStartY,
lineEndX: e.clientX,
lineEndY: e.clientY
});
}
function redraw() {
drawingPoints.forEach(el => {
ctx.moveTo(el.startX, el.startY);
ctx.lineTo(el.lineEndX, el.lineEndY);
ctx.stroke()
});
}
that exemplifies the reason I need to store an array of points to redraw.
I would like to avoid doing this because at large quantities of data, the drawing slows down or even crashes because we are looping through all the points to redraw them. Is there a more performant way of achieving this interactive shape drawing effect I am looking for?
Your redraw function calls stroke() for every sub-segment, but never calls beginPath(). This means that when you have two sub-segments, the first one will get painted at its own turn, and then again when the second sub-segment calls stroke().
You can see this quite clearly by looking at your first strokes and how their antialiasing darkens the more sub-segments there are.
You are actually asking the computer to render magnitudes more items than required, this is what is making your app slow and eventually crash.
So you could call beginPath() in the loop, but the best is instead to remove entirely stroke() from there. This way, at every frame you only paint one single path. The GPU has only one single operation to do and you should be fine for days of drawings.
let lineStartX = 0;
let lineStartY = 0;
const drawingPoints = [];
let mouseDown = false;
const c = document.getElementById("test");
const ctx = c.getContext("2d");
c.onmousedown = (e) => {
lineStartX = e.clientX;
lineStartY = e.clientY;
mouseDown = true;
}
c.onmousemove = (e) => {
if (mouseDown) {
ctx.beginPath();
ctx.clearRect(0, 0, 500, 500);
redraw();
ctx.moveTo(lineStartX, lineStartY);
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
}
}
c.onmouseup = (e) => {
mouseDown = false;
drawingPoints.push({
startX: lineStartX,
startY: lineStartY,
lineEndX: e.clientX,
lineEndY: e.clientY
});
}
function redraw() {
// 'redraw' only traces the previous sub-segments
drawingPoints.forEach(el => {
ctx.moveTo(el.startX, el.startY);
ctx.lineTo(el.lineEndX, el.lineEndY);
});
}
<canvas id="test" width="500" height="500"></canvas>
Note that if you measure that even the tracing takes some computation time (though this would be surprising), you may want to try storing the sub-path in a Path2D object instead of calling the drawing methods every time:
let lineStartX = 0;
let lineStartY = 0;
const path = new Path2D();
let mouseDown = false;
const c = document.getElementById("test");
const ctx = c.getContext("2d");
c.onmousedown = (e) => {
lineStartX = e.clientX;
lineStartY = e.clientY;
mouseDown = true;
}
c.onmousemove = (e) => {
if (mouseDown) {
ctx.beginPath();
ctx.clearRect(0, 0, 500, 500);
redraw();
ctx.moveTo(lineStartX, lineStartY);
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
}
}
c.onmouseup = (e) => {
mouseDown = false;
path.moveTo(lineStartX, lineStartY);
path.lineTo(e.clientX, e.clientY);
}
function redraw() {
ctx.stroke(path);
}
<canvas id="test" width="500" height="500"></canvas>