I'm building an application around a 1000x1000 canvas, where you can select areas of the canvas by dragging the mouse around.
In order to remember the area selected from the user I save the coordinates in an array and I draw them with imageData:
const applySelectedPixelLayer = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(1, 1);
imageData.data[0] = 0; // R value
imageData.data[1] = 0; // G value
imageData.data[2] = 0; // B value
imageData.data[3] = 255; // A value
for (let i = 0; i < selectedCoordinates.length; i++) {
ctx.putImageData(
imageData,
selectedCoordinates[i][0],
selectedCoordinates[i][1]
);
}
}, [selectedCoordinates]);
Basically the user can click pixel on the canvas or drag the mouse to select a rectangle of pixels. When the user releases the mouse pointer, I add the coordinates selected to selectedCoordinates and then I call the applySelectedPixelLayer function to update the canvas.
The problem is that it's incredibly slow, it takes 20/30 seconds to see the result if a lot of pixels are selected.
Is there a more effective way to mark the selected pixel on a canvas?
Like mentioned in the comments, you should try to work with larger areas of image data in one go.
Here's an example that lets you cut out a part of some random image.
getImageData to get the selected pixels from the canvas,return statement in snippet below), use coordinates to modify existing pixel dataconst cvs = document.createElement("canvas");
cvs.width = cvs.height = 512;
const ctx = cvs.getContext("2d");
let imageData = null;
let offset = [];
let dragStart = null;
let dragPos = null;
let copied = null;
// Generate some image
const init = () => {
const size = cvs.width / 8;
for (let i = 0; i < 64; i += 1) {
const y = Math.floor(i / 8);
const x = i % 8;
ctx.fillStyle = `hsl(${Math.random() * 255} 100% 50%)`;
ctx.fillRect(x * size, y * size, size, size);
}
imageData = ctx.getImageData(0, 0, cvs.width, cvs.height);
ctx.clearRect(0, 0, cvs.width, cvs.height);
draw();
}
const draw = () => {
ctx.clearRect(0, 0, cvs.width, cvs.height);
ctx.putImageData(imageData, 0, 0);
if (dragStart && dragPos) {
const [ x1, y1 ] = dragStart;
const [ x2, y2 ] = dragPos;
ctx.fillStyle = "rgba(255,255,255,0.9)";
ctx.strokeStyle = "rgba(255, 255, 255)";
ctx.lineWieght = 2;
ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
}
}
const onMove = e => {
const [ dx, dy ] = offset;
dragPos = [
e.clientX - dx,
e.clientY - dy
];
draw();
}
const onSelectionEnd = e => {
const [ x1, y1 ] = dragStart;
const [ x2, y2 ] = dragPos;
dragStart = null;
dragPos = null;
// A) To get the image you selected, you can do:
draw();
const selection = ctx.getImageData(
Math.min(x1, x2),
Math.min(y1, y2),
Math.abs(x1 - x2),
Math.abs(y1 - y2)
);
imageData = selection;
return;
// B) To clear the pixels from the original image, you could do something like:
for (let y = Math.min(y1, y2); y < Math.max(y1, y2); y += 1) {
for (let x = Math.min(x1, x2); x < Math.max(x1, x2); x += 1) {
const pi = y * cvs.width * 4 + x * 4;
imageData.data[pi + 0] = 255;
imageData.data[pi + 1] = 255;
imageData.data[pi + 2] = 255;
imageData.data[pi + 3] = 255;
}
}
}
cvs.addEventListener("mousedown", e => {
const { top, left } = cvs.getBoundingClientRect();
offset = [ top, left ];
onMove(e);
dragStart = dragPos;
cvs.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onSelectionEnd, { once: true });
});
init();
document.body.appendChild(cvs);