skip to Main Content

I’m making a very simple ‘pixel-painting’ program using HTML5/Canvas.
I’d like to give the user an option to go back in ‘history’, like the History panel in Photoshop / Adobe programs.

Basically it would be an undo button, but you’d be able to go back to the start of your actions, and there would also be a log showing the details of each action.

Is this possible? How would I even start storing this data?

How much memory is available within the Chrome browser in order to allow this on one page? – (Sorry if that is silly to ask, still quite new to Javascript and working within the browser.)

I have read this undo button Question, which is similar but I’d like to make the info about data being stored visible.

Thank you so so much for any help you can give!

2

Answers


  1. You could copy the current canvas to a separate one each time an action is performed. Simply displaying old canvases could serve as an action log.

    You can send a canvas to drawImage directly:

    destContext.drawImage( srcCanvas, 0, 0 );
    

    If that approach consumes too much memory, an alternative is to store all the commands in a stack, remove the last element when undoing, and redraw everything from scratch.

    Login or Signup to reply.
  2. You would need to build a simple undo-redo stack. Then you need to decide if you will store vector data or image data. The latter is more efficient but can also take up much more memory. You may have cases where you want to store both types of data (path on top of images).

    The method would be in simple steps:

    • Store initial state for the new document. Keep a stack pointer pointing to the next free slot (for example using an array).
    • When mouse down is hit (or some other operation that will cause a change is started) move stack pointer forward.
    • When mouse button is released, make a snapshot, create thumbnail etc. It’s up to you if you want to store the drawing as points or as a bitmap. If bitmap data you can get around storage space by compressing it using for example zip. Move stack pointer forward. If there exists snapshots in the stack at this point remove them.
    • When you need to undo, simply draw back previous stored step and move the stack pointer back. By keeping the snapshots you can do redo by moving stack pointer forward and redraw the snapshot, if any.
    • And finally, to visualize the undo-redo stack you can simply render each snapshot to a separate canvas at scale and extract that as an image which you put in the list.

    Note: when creating a undo state it’s important to clear any snapshots after the new stack pointer position. This is because if undo has been used, redo can be used if no changes. However, if undo was used and new drawing was added this would invalidate the next states so they have to be removed.

    As to browser memory it will depend on the user’s system. Some have a few gigabytes, other has a lot. There is no way to know. You would have to chose a UX strategy suitable for your scenario as well as target audience.

    Example

    This does not implement the logistics for handling the sync of the thumbnails, but has most other parts. I’ll leave the rest as an exercise.

    var ctx = c.getContext("2d"),
        stack = [],                  // undo-redo stack
        sp = 0,                      // stack pointer
        isDown = false;              // for drawing (demo)
    
    capture();                       // create an initial undo capture (blank)
    
    ctx.lineCap = "round";           // setup line for demo
    ctx.lineWidth = 4;
    
    // simple draw mechanism
    c.onmousedown = function(e) {
      sp++;                          // on mouse down, move stack pointer to next slot
      isDown = true;                 // NOTE: clear any snapshots after this point (not shown)
      var pos = getXY(e);            // start drawing some line - how you draw is up to you
      ctx.beginPath();
      ctx.moveTo(pos.x, pos.y);
    }
    
    window.onmousemove = function(e) {
      if (!isDown) return;           // only for drawing
      var pos = getXY(e);
      ctx.lineTo(pos.x, pos.y);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(pos.x, pos.y);
    }
    
    window.onmouseup = function() {
      if (!isDown) return;
      isDown = false;
      capture();                     // capture an undo state
      makeThumb();                   // create and insert a thumbnail of state
    };
    
    function capture() {
      stack[sp] = c.toDataURL();     // one way, you could use getImageData, 
                                     // or store points instead.. it's up to you
    }
    
    // Creates a thumbnail of current canvas and insert into visible undo stack
    function makeThumb() {
      var canvas = document.createElement("canvas");
      canvas.width = canvas.height = 64;
      var ctxTmp = canvas.getContext("2d");
      ctxTmp.drawImage(c, 0, 0, canvas.width, canvas.height);
      undos.appendChild(canvas);
    }
    
    // UNDO button clicked
    undo.onclick = function() {
      var img = new Image;           // restore previous state/snapshot
      img.onload = function() {
        ctx.clearRect(0, 0, c.width, c.height);
        ctx.drawImage(this, 0, 0);
      }
      
      // move stack pointer back and get previous snapshot
      if (sp > 0) img.src = stack[--sp];
    };
    
    // REDO button clicked
    redo.onclick = function() {
      
      // anything we can redo?
      if (sp < stack.length) {
        var img = new Image;
        img.onload = function() {
          ctx.clearRect(0, 0, c.width, c.height);
          ctx.drawImage(this, 0, 0);
        }
      
        // move stack pointer forward and get next snapshot
        img.src = stack[++sp];
      }
    };
    
    function getXY(e) {
      var r = c.getBoundingClientRect();
      return {x: e.clientX - r.left, y: e.clientY - r.top}
    }
    #c {background:#ccc}
    <button id=undo>Undo</button>
    <button id=redo>Redo</button><br>
    <canvas id=c width=500 height=500></canvas>
    <div id=undos></div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search