16

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 arrays stacks, one for undo and 1 for redo. Anytime you draw and release the mouse it saves the canvas image to the undo array stack by push. if you draw something else and release it will do the same. However if you click undo it will pop the top image of undo array and print that to canvas and then push it onto the redo stack.

redo when clicked will pop from itself and push to undo. the top of undo will be printed after each mouse off.

Is this the right way or is there a better one?

jww
  • 97,681
  • 90
  • 411
  • 885
Steven Barrett
  • 311
  • 2
  • 6
  • 14
  • You could try with [fabric.js](http://fabricjs.com/), which allows free drawing and wrap each shape into an object (see [here](http://fabricjs.com/fabric-intro-part-4/), it should make it simplier to do – Jacopofar Jun 17 '13 at 14:59
  • 1
    Don't forget to clear the redo stack when a new action is saved on the undo stack. – Bergi Jun 17 '13 at 15:00
  • 1
    Saving entire images may be memory-heavy. You might limit the stack sizes, or try just saving changes between images (essentially, every stroke). – Waleed Khan Jun 17 '13 at 15:05
  • Yes the every stroke is the way I want to do it and maybe with a stack of 10 from 0-9. I cant seem to get it working however :/ I am following http://www.yankov.us/canvasundo/ – Steven Barrett Jun 17 '13 at 15:52

3 Answers3

23

A word of warning!

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

However, your idea of progressively saving the user’s drawings in an array is still a good idea.

Instead of saving the whole canvas as an image, just create an array of points to record every mousemove the user makes as they are drawing. This is your “drawing array” that can be used to fully redraw your canvas.

Whenever the user drags the mouse they are creating a polyline (a group of connected line segments). When the user drags to create a line, save that mousemove point to your drawing array and extend their 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 “undo”, just pop the last point off 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();
}

Redo is logically more tricky.

The simplest Redo is when the user can only redo immediately after an undo. Save each “undo” point in your separate “redo” array. Then if the user wants to redo, you can just add the redo bits back to the to the main array.

The complication is if you let the user “redo” after they have done more drawing.

For example, you could end up with a dog with 2 tails: a newly drawn tail and a second “redo” tail !

So if you allow redo’s after additional drawing, you would need a way to keep the user from being confused during redo. Matt Greer’s idea of “layering” redos is one good way. Just alter that idea by saving the redo points, not the entire canvas image. Then the user could toggle the redo on/off to see if they would like to keep their redo.

Here is an example of using an undo array I created for a previous question: Drawing to canvas like in paint

Here is that code and a Fiddle: 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>
Community
  • 1
  • 1
markE
  • 102,905
  • 11
  • 164
  • 176
  • 2
    I also considered this approach, but IMO it doesn't scale well when you add more tools, like a fill bucket. Truthfully though none of the systems mentioned on this page scale well, as when you move beyond undo/redo for tools (for example, undoing deletion of a layer), this system breaks down a lot. I found that out the hard way :) – Matt Greer Jun 17 '13 at 17:48
  • 2
    @MattGreer: Greetings...good to meet you! If you add the concept of a fill-bucket, I see what you're saying about scaling. But in that scenario you could introduce meta-commands. For example, instead of recording every pixel that was bucket-filled, you would record just the word "fill" plus a reference to the path being filled. When you replay the "fill" command, a flood-fill function would be executed on the specified path. Scalability maintained! This worked well on a project I did to allow an inspector map imperfections. – markE Jun 17 '13 at 18:06
  • Flood fill algorithm is not cheap to do in javascript. Imagine a case where a user is filling the entire canvas board with different colors for say 100 times. If they do an undo now, we will have to invoke flood-fill for all the 99 previous events which is going to keep the thread occupied for a significant amount of time. – vighnesh153 Aug 19 '23 at 07:27
  • @vighnesh153 Yep, this is to illustrate the algo only. There are certainly efficiencies to be made...probably caching, etc in production. – markE Aug 22 '23 at 13:01
8

That's the basic idea of what I did for my paint app; and it does work well, except this approach can be very memory intensive.

So a slight tweak that I did was only store undo/redo clips that are the size of the last action the user did. So if they just draw a tiny smidge of the canvas, you can store a tiny canvas that is a fraction of the full size, and save a lot of memory.

My undo/redo system lives in Painter.js. I wrote this app two years ago, so my memory is a bit hazy but I can help explain things if you decide to decode what I did.

Matt Greer
  • 60,826
  • 17
  • 123
  • 123
1

Try to implement the Command design pattern.

There's another similar question here: Best design pattern for "undo" feature

Community
  • 1
  • 1