Undo / Redo for Canvas Paint Program

I need to implement a undo / redo system for my paint program: http://www.taffatech.com/Paint.html

My idea that I came up with is to have 2 stack arrays, one for undo and 1 for replay. Each time you draw and release the mouse, it saves the canvas image onto the stack of the pending array by clicking. if you draw something else and let go, he will do the same. However, if you click Undo, you will see the top image of the undo array and print it on the canvas, and then click on the redo stack.

redo, when clicked, jumps out of himself and clicks to cancel. the top of the undo will be printed after turning off each mouse.

Is it right or is there a better one?

+5
source share
3 answers

A word of warning!

Saving the entire canvas as an undo / redo image is memory intensive and performance killer.

However, your idea of ​​gradually storing user drawings in an array is still a good idea.

Instead of saving the entire canvas as an image, just create an array of dots to record each musomov that the user makes when he draws. This is your "drawing array", which can be used to completely redraw the canvas.

Whenever the user drags the mouse, they create a polyline (a group of connected line segments). When the user drags to create a line, save that mousemove point in your drawing array and extend your polyline to the current mousemove position.

function handleMouseMove(e) { // calc where the mouse is on the canvas mouseX = parseInt(e.clientX - offsetX); mouseY = parseInt(e.clientY - offsetY); // if the mouse is being dragged (mouse button is down) // then keep drawing a polyline to this new mouse position if (isMouseDown) { // extend the polyline ctx.lineTo(mouseX, mouseY); ctx.stroke(); // save this x/y because we might be drawing from here // on the next mousemove lastX = mouseX; lastY = mouseY; // Command pattern stuff: Save the mouse position and // the size/color of the brush to the "undo" array points.push({ x: mouseX, y: mouseY, size: brushSize, color: brushColor, mode: "draw" }); } } 

If the user wants to “cancel”, just pull the last point from the drawing array:

 function undoLastPoint() { // remove the last drawn point from the drawing array var lastPoint=points.pop(); // add the "undone" point to a separate redo array redoStack.unshift(lastPoint); // redraw all the remaining points redrawAll(); } 

Logically repeating is more difficult.

The simplest repetition is when the user can only restart immediately after cancellation. Store each undo in a separate redo array. Then, if the user wants to repeat, you can simply add a repeat bit to the main array.

The complication is if you allow the user to “repeat” after they have made more drawings.

For example, you might have a dog with two tails: a newly drawn tail and a second tail, “redo”!

So, if you allow the replay after the additional drawing, you will need a way to avoid confusion during the replay. Matt Grears' idea of ​​"repeating layers" is one of the good ways. Just change this idea, retaining the repeat points, not the entire canvas. The user can then turn on / off the repeat to see if they want to save their repeat.

Here is an example of using the undo array that I created for the previous question: Drawing on canvas, as in paint

Here is this code and script: http://jsfiddle.net/m1erickson/AEYYq/

 <!doctype html> <html> <head> <link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css --> <script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script> <!--[if lt IE 9]><script type="text/javascript" src="../excanvas.js"></script><![endif]--> <style> body{ background-color: ivory; } canvas{border:1px solid red;} </style> <script> $(function(){ var canvas=document.getElementById("canvas"); var ctx=canvas.getContext("2d"); var lastX; var lastY; var mouseX; var mouseY; var canvasOffset=$("#canvas").offset(); var offsetX=canvasOffset.left; var offsetY=canvasOffset.top; var isMouseDown=false; var brushSize=20; var brushColor="#ff0000"; var points=[]; function handleMouseDown(e){ mouseX=parseInt(e.clientX-offsetX); mouseY=parseInt(e.clientY-offsetY); // Put your mousedown stuff here ctx.beginPath(); if(ctx.lineWidth!=brushSize){ctx.lineWidth=brushSize;} if(ctx.strokeStyle!=brushColor){ctx.strokeStyle=brushColor;} ctx.moveTo(mouseX,mouseY); points.push({x:mouseX,y:mouseY,size:brushSize,color:brushColor,mode:"begin"}); lastX=mouseX; lastY=mouseY; isMouseDown=true; } function handleMouseUp(e){ mouseX=parseInt(e.clientX-offsetX); mouseY=parseInt(e.clientY-offsetY); // Put your mouseup stuff here isMouseDown=false; points.push({x:mouseX,y:mouseY,size:brushSize,color:brushColor,mode:"end"}); } function handleMouseMove(e){ mouseX=parseInt(e.clientX-offsetX); mouseY=parseInt(e.clientY-offsetY); // Put your mousemove stuff here if(isMouseDown){ ctx.lineTo(mouseX,mouseY); ctx.stroke(); lastX=mouseX; lastY=mouseY; // command pattern stuff points.push({x:mouseX,y:mouseY,size:brushSize,color:brushColor,mode:"draw"}); } } function redrawAll(){ if(points.length==0){return;} ctx.clearRect(0,0,canvas.width,canvas.height); for(var i=0;i<points.length;i++){ var pt=points[i]; var begin=false; if(ctx.lineWidth!=pt.size){ ctx.lineWidth=pt.size; begin=true; } if(ctx.strokeStyle!=pt.color){ ctx.strokeStyle=pt.color; begin=true; } if(pt.mode=="begin" || begin){ ctx.beginPath(); ctx.moveTo(pt.x,pt.y); } ctx.lineTo(pt.x,pt.y); if(pt.mode=="end" || (i==points.length-1)){ ctx.stroke(); } } ctx.stroke(); } function undoLast(){ points.pop(); redrawAll(); } ctx.lineJoin = "round"; ctx.fillStyle=brushColor; ctx.lineWidth=brushSize; $("#brush5").click(function(){ brushSize=5; }); $("#brush10").click(function(){ brushSize=10; }); // Important! Brush colors must be defined in 6-digit hex format only $("#brushRed").click(function(){ brushColor="#ff0000"; }); $("#brushBlue").click(function(){ brushColor="#0000ff"; }); $("#canvas").mousedown(function(e){handleMouseDown(e);}); $("#canvas").mousemove(function(e){handleMouseMove(e);}); $("#canvas").mouseup(function(e){handleMouseUp(e);}); // hold down the undo button to erase the last line segment var interval; $("#undo").mousedown(function() { interval = setInterval(undoLast, 100); }).mouseup(function() { clearInterval(interval); }); }); // end $(function(){}); </script> </head> <body> <p>Drag to draw. Use buttons to change lineWidth/color</p> <canvas id="canvas" width=300 height=300></canvas><br> <button id="undo">Hold this button down to Undo</button><br><br> <button id="brush5">5px Brush</button> <button id="brush10">10px Brush</button> <button id="brushRed">Red Brush</button> <button id="brushBlue">Blue Brush</button> </body> </html> 
+12
source

This is the main idea of ​​what I did for my paint app ; and it works well, except that this approach can be very intense.

So the little tweak that I did was just to store canceled clips, which are the size of the last action the user did. Therefore, if they just draw a tiny piece of canvas, you can save a tiny canvas that is part of the full size and save a ton of memory.

My undo / redo system works in Painter.js . I wrote this application two years ago, so my memory is a bit vague, but I can help explain if you decided to decode what I did.

+5
source

Try to implement a team design template .

There is another similar question here: Best design template for the "undo" feature

0
source

Source: https://habr.com/ru/post/1486644/


All Articles