ARTICLE AD BOX
I am creating an whiteboard application just like "exelidraw". I'm struggling to create an eraser tool using canvas element which when encounter with any shape or drawing, removes it entirely, just like in excalidraw.
I've tried setting globalCompositeOperation property of the canvasAPI to destination-out but this approach will remove the shape(or drawing) pixel-by-pixel just like in msPaint. But I need a object-based approach where, when the eraser removes the entire shape(or drawing) when encounter with the shape(or drawing) at any point.
Below is the code of how I pixel-based approach that I used for creating the eraser functionality.
(just a rough code for eraser logic)
type Shape = {type: "eraser"; points: {x: number; y: number;}[]}; export class Game { private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; private existingShapes: Shape[]; private clicked: boolean; private startX = 0; private startY = 0; private selectedTool: Tool = "eraser"; private currentPencilStroke: { x: number; y:number }[] = [] constructor(canvas: HTMLCanvasElement) { this.canvas = canvas; this.ctx = canvas.getContext("2d")!; this.existingShapes = []; this.clicked = false; this.init(); this.initHandlers(); this.initMouseHandlers(); } destroy() { this.canvas.removeEventListener("mousedown", this.mouseDownHandler); this.canvas.removeEventListener("mouseup", this.mouseUpHandler); this.canvas.removeEventListener("mousemove", this.mouseMoveHandler); } // ClearCanvas(){} -> Clears everything; Redraws the permanent shapes; Draw the current preview shape clearCanvas() { this.ctx.globalCompositeOperation = "source-over"; // Clears the entire canvas: this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // Sets the background to black: this.ctx.fillStyle = "rgba(0, 0, 0)"; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); // Redraws Existing Shapes this.existingShapes.map((shape) => { this.ctx.globalCompositeOperation = "source-over" this.ctx.strokeStyle = "rgba(255, 255, 255)"; // Drawing logic of each shape type if(shape.type === "eraser"){ this.ctx.strokeStyle = "rgba(0, 0, 0)"; this.ctx.lineWidth = 20 if(shape.points && shape.points.length > 1){ this.ctx.beginPath(); this.ctx.moveTo(shape.points[0].x, shape.points[0].y); shape.points.forEach(point => { this.ctx.lineTo(point.x, point.y); }); this.ctx.stroke(); } } }); } // mouseDownHandler: User presses the mouse button; shapes begin to draw; Records where the user started to clicking mouseDownHandler = (e: MouseEvent) => { this.clicked = true; this.startX = e.clientX; this.startY = e.clientY; if (this.selectedTool === "eraser") { this.currentPencilStroke = [{x: this.startX, y: this.startY}] } }; // mouseUpHandler: When user release the mouse button, it finalizes the shape and saves it permanently mouseUpHandler = (e: MouseEvent) => { this.clicked = false; const width = e.clientX - this.startX; const height = e.clientY - this.startY; const selectedTool = this.selectedTool; let shape: Shape | null = null; if(selectedTool === "eraser" && this.currentPencilStroke.length > 1) { shape = { type: "eraser", points: this.currentPencilStroke } this.currentPencilStroke = []; } // mouseMoveHandler: While user drag the mouse it shows live preview of the shape. mouseMoveHandler = (e: MouseEvent) => { if (this.clicked) { const width = e.clientX - this.startX; const height = e.clientY - this.startY; this.clearCanvas(); this.ctx.strokeStyle = "rgba(255, 255, 255)"; const selectedTool: Tool = this.selectedTool; if(selectedTool === "eraser") { this.currentPencilStroke.push({x: e.clientX, y: e.clientY}) this.ctx.globalCompositeOperation = "source-over" this.ctx.strokeStyle = "rgba(0, 0, 0)"; this.ctx.lineWidth = 20 this.ctx.beginPath(); this.ctx.moveTo(this.currentPencilStroke[0].x, this.currentPencilStroke[0].y) this.currentPencilStroke.forEach(point => { this.ctx.lineTo(point.x, point.y) }); this.ctx.stroke() this.ctx.globalCompositeOperation = "source-over" } } }; initMouseHandlers() { this.canvas.addEventListener("mousedown", this.mouseDownHandler); this.canvas.addEventListener("mouseup", this.mouseUpHandler); this.canvas.addEventListener("mousemove", this.mouseMoveHandler); } }