Home > Back-end >  How to implement undo function for HTML canvas using JavaScript?
How to implement undo function for HTML canvas using JavaScript?

Time:04-03

I am making a paint app in HTML using JavaScript and Flask-Python. Currently, I am able to draw lots of pencil drawings and shapes like rectangles/circles without any problem. The following functionality that I am trying to implement for this application is a undo function.

I store the strokes and the canvas drawing data in a JS object in the following manner:

canvas_data = { "pencil": [], "line": [], "rectangle": [], "circle": [], "eraser": [], "last_action": -1 };

All of the key names should be self-explanatory except last_action. I use this last_action variable to know the last category used by the user so that I can later use this information to implement the undo function.

var canvas = document.getElementById("paint");

var ctx = canvas.getContext("2d");

var pi2 = Math.PI * 2;
var resizerRadius = 8;
var rr = resizerRadius * resizerRadius;

var width = canvas.width;
var height = canvas.height;
var curX, curY, prevX, prevY;
var hold = false;
ctx.lineWidth = 2;
var fill_value = true;
var stroke_value = false;
var canvas_data = {
  "pencil": [],
  "line": [],
  "rectangle": [],
  "circle": [],
  "eraser": [],
  "last_action": -1
};

// //connect to postgres client
// var pg = require('pg');
// var conString = "postgres://postgres:database1@localhost:5432/sketch2photo";
// client = new pg.Client(conString);


function color(color_value) {
  ctx.strokeStyle = color_value;
  ctx.fillStyle = color_value;
}

function add_pixel() {
  ctx.lineWidth  = 1;
}

function reduce_pixel() {
  if (ctx.lineWidth == 1) {
    ctx.lineWidth = 1;
  } else {
    ctx.lineWidth -= 1;
  }
}

function fill() {
  fill_value = true;
  stroke_value = false;
}

function outline() {
  fill_value = false;
  stroke_value = true;
}

function reset() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  canvas_data = {
    "pencil": [],
    "line": [],
    "rectangle": [],
    "circle": [],
    "eraser": [],
    "last_action": -1
  };
}

// pencil tool

function pencil(data, targetX, targetY, targetWidth, targetHeight) {

  canvas.onmousedown = function(e) {
    curX = e.clientX - canvas.offsetLeft;
    curY = e.clientY - canvas.offsetTop;
    hold = true;

    prevX = curX;
    prevY = curY;
    ctx.beginPath();
    ctx.moveTo(prevX, prevY);
  };

  canvas.onmousemove = function(e) {
    if (hold) {
      curX = e.clientX - canvas.offsetLeft;
      curY = e.clientY - canvas.offsetTop;
      draw();
    }
  };

  canvas.onmouseup = function(e) {
    hold = false;
  };

  canvas.onmouseout = function(e) {
    hold = false;
  };

  function draw() {
    ctx.lineTo(curX, curY);
    ctx.stroke();
    canvas_data.pencil.push({
      "startx": prevX,
      "starty": prevY,
      "endx": curX,
      "endy": curY,
      "thick": ctx.lineWidth,
      "color": ctx.strokeStyle
    });
    canvas_data.last_action = 0;
  }
}

// line tool

function line() {

  canvas.onmousedown = function(e) {
    img = ctx.getImageData(0, 0, width, height);
    prevX = e.clientX - canvas.offsetLeft;
    prevY = e.clientY - canvas.offsetTop;
    hold = true;
  };

  canvas.onmousemove = function linemove(e) {
    if (hold) {
      ctx.putImageData(img, 0, 0);
      curX = e.clientX - canvas.offsetLeft;
      curY = e.clientY - canvas.offsetTop;
      ctx.beginPath();
      ctx.moveTo(prevX, prevY);
      ctx.lineTo(curX, curY);
      ctx.stroke();
      canvas_data.line.push({
        "startx": prevX,
        "starty": prevY,
        "endx": curX,
        "endY": curY,
        "thick": ctx.lineWidth,
        "color": ctx.strokeStyle
      });
      ctx.closePath();
      canvas_data.last_action = 1;
    }
  };

  canvas.onmouseup = function(e) {
    hold = false;
  };

  canvas.onmouseout = function(e) {
    hold = false;
  };
}

// rectangle tool

function rectangle() {

  canvas.onmousedown = function(e) {
    img = ctx.getImageData(0, 0, width, height);
    prevX = e.clientX - canvas.offsetLeft;
    prevY = e.clientY - canvas.offsetTop;
    hold = true;
  };

  canvas.onmousemove = function(e) {
    if (hold) {
      ctx.putImageData(img, 0, 0);
      curX = e.clientX - canvas.offsetLeft - prevX;
      curY = e.clientY - canvas.offsetTop - prevY;
      ctx.strokeRect(prevX, prevY, curX, curY);
      if (fill_value) {
        ctx.fillRect(prevX, prevY, curX, curY);
      }
      canvas_data.rectangle.push({
        "startx": prevX,
        "starty": prevY,
        "width": curX,
        "height": curY,
        "thick": ctx.lineWidth,
        "stroke": stroke_value,
        "stroke_color": ctx.strokeStyle,
        "fill": fill_value,
        "fill_color": ctx.fillStyle
      });
      canvas_data.last_action = 2;

    }
  };

  canvas.onmouseup = function(e) {
    hold = false;
  };

  canvas.onmouseout = function(e) {
    hold = false;
  };
}

// circle tool

function circle() {

  canvas.onmousedown = function(e) {
    img = ctx.getImageData(0, 0, width, height);
    prevX = e.clientX - canvas.offsetLeft;
    prevY = e.clientY - canvas.offsetTop;
    hold = true;
  };

  canvas.onmousemove = function(e) {
    if (hold) {
      ctx.putImageData(img, 0, 0);
      curX = e.clientX - canvas.offsetLeft;
      curY = e.clientY - canvas.offsetTop;
      ctx.beginPath();
      ctx.arc(Math.abs(curX   prevX) / 2, Math.abs(curY   prevY) / 2, Math.sqrt(Math.pow(curX - prevX, 2)   Math.pow(curY - prevY, 2)) / 2, 0, Math.PI * 2, true);
      ctx.closePath();
      ctx.stroke();
      if (fill_value) {
        ctx.fill();
      }
      canvas_data.circle.push({
        "startx": prevX,
        "starty": prevY,
        "radius": curX - prevX,
        "thick": ctx.lineWidth,
        "stroke": stroke_value,
        "stroke_color": ctx.strokeStyle,
        "fill": fill_value,
        "fill_color": ctx.fillStyle
      });
      canvas_data.last_action = 3;
    }
  };

  canvas.onmouseup = function(e) {
    hold = false;
  };

  canvas.onmouseout = function(e) {
    hold = false;
  };
}

// eraser tool

function eraser() {

  canvas.onmousedown = function(e) {
    curX = e.clientX - canvas.offsetLeft;
    curY = e.clientY - canvas.offsetTop;
    hold = true;

    prevX = curX;
    prevY = curY;
    ctx.beginPath();
    ctx.moveTo(prevX, prevY);
  };

  canvas.onmousemove = function(e) {
    if (hold) {
      curX = e.clientX - canvas.offsetLeft;
      curY = e.clientY - canvas.offsetTop;
      draw();
    }
  };

  canvas.onmouseup = function(e) {
    hold = false;
  };

  canvas.onmouseout = function(e) {
    hold = false;
  };

  function draw() {
    ctx.lineTo(curX, curY);
    var curr_strokeStyle = ctx.strokeStyle;
    ctx.strokeStyle = "#ffffff";
    ctx.stroke();
    canvas_data.pencil.push({
      "startx": prevX,
      "starty": prevY,
      "endx": curX,
      "endy": curY,
      "thick": ctx.lineWidth,
      "color": ctx.strokeStyle
    });
    canvas_data.last_action = 4;
    ctx.strokeStyle = curr_strokeStyle;
  }
}


// Function to undo the last action by the user 
function undo_pixel() {
  // Print that function has been called 
  console.log("undo_pixel() called");
  // Print the last action that was performed
  console.log(canvas_data.last_action);

  switch (canvas_data.last_action) {
    case 0:
    case 4:
      console.log("Case 0 or 4");
      canvas_data.pencil.pop();
      canvas_data.last_action = -1;
      break;
    case 1:
      //Undo the last line drawn
      console.log("Case 1");
      canvas_data.line.pop();
      canvas_data.last_action = -1;
      break;
    case 2:
      //Undo the last rectangle drawn
      console.log("Case 2");
      canvas_data.rectangle.pop();
      canvas_data.last_action = -1;
      break;
    case 3:
      //Undo the last circle drawn
      console.log("Case 3");
      canvas_data.circle.pop();
      canvas_data.last_action = -1;
      break;

    default:
      break;

  }
  // Redraw the canvas
  redraw_canvas();

}

// Function to redraw all the shapes on the canvas 
function redraw_canvas() {
  // Redraw all the shapes on the canvas 
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // Redraw the pencil data 
  canvas_data.pencil.forEach(function(p) {
    ctx.beginPath();
    ctx.moveTo(p.startx, p.starty);
    ctx.lineTo(p.endx, p.endy);
    ctx.lineWidth = p.thick;
    ctx.strokeStyle = p.color;
    ctx.stroke();
  });
  // Redraw the line data
  canvas_data.line.forEach(function(l) {
    ctx.beginPath();
    ctx.moveTo(l.startx, l.starty);
    ctx.lineTo(l.endx, l.endy);
    ctx.lineWidth = l.thick;
    ctx.strokeStyle = l.color;
    ctx.stroke();
  });
  // Redraw the rectangle data
  canvas_data.rectangle.forEach(function(r) {
    ctx.beginPath();
    ctx.rect(r.startx, r.starty, r.width, r.height);
    ctx.lineWidth = r.thick;
    ctx.strokeStyle = r.color;
    if (r.fill) {
      ctx.fillStyle = r.fill_color;
      ctx.fillRect(startx, starty, width, height);
    }
    ctx.stroke();
  });
  // Redraw the circle data
  canvas_data.circle.forEach(function(c) {
    // "startx": prevX, "starty": prevY, "radius": curX - prevX, "thick": ctx.lineWidth, "stroke": stroke_value, "stroke_color": ctx.strokeStyle, "fill": fill_value, "fill_color": ctx.fillStyle
    ctx.beginPath();
    ctx.arc(c.startx, c.starty, c.radius, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.stroke();
    if (c.fill) {
      ctx.fillStyle = c.fill_color;
      ctx.fill();
    }
  });
}

$("#paint1").mousedown(function(e) {
  handleMouseDown(e);
});
$("#paint1").mouseup(function(e) {
  handleMouseUp(e);
});
$("#paint1").mouseout(function(e) {
  handleMouseOut(e);
});
$("#paint1").mousemove(function(e) {
  handleMouseMove(e);
});
html {
  min-width: 1500px;
  position: relative;
}

#toolset {
  width: 100px;
  height: 340px;
  position: absolute;
  left: 0px;
  top: 50px;
  background: #35d128;
}

#paint {
  position: absolute;
  left: 130px;
  top: 50px;
}

#colorset {
  position: absolute;
  left: 0px;
  top: 450px;
  width: 300px;
}

#title {
  position: absolute;
  left: 500px;
}

#penciltool {
  background: #358128;
  color: #f3f3f3;
  width: 80px;
  height: 25px;
  border: 1px solid #33842a;
  -webkit-border-radius: 0 15px 15px 0;
  -moz-border-radius: 0 15px 15px 0;
  box-shadow: rgba(0, 0, 0, .75) 0 2px 6px;
}

#linetool {
  background: #358128;
  color: #f3f3f3;
  width: 80px;
  height: 25px;
  border: 1px solid #33842a;
  -webkit-border-radius: 0 15px 15px 0;
  -moz-border-radius: 0 15px 15px 0;
  box-shadow: rgba(0, 0, 0, .75) 0 2px 6px;
}

#rectangletool {
  background: #358128;
  color: #f3f3f3;
  width: 80px;
  height: 25px;
  border: 1px solid #33842a;
  -webkit-border-radius: 0 15px 15px 0;
  -moz-border-radius: 0 15px 15px 0;
  box-shadow: rgba(0, 0, 0, .75) 0 2px 6px;
}

#circletool {
  background: #358128;
  color: #f3f3f3;
  width: 80px;
  height: 25px;
  border: 1px solid #33842a;
  -webkit-border-radius: 0 15px 15px 0;
  -moz-border-radius: 0 15px 15px 0;
  box-shadow: rgba(0, 0, 0, .75) 0 2px 6px;
}

#erasertool {
  background: #358128;
  color: #f3f3f3;
  width: 80px;
  height: 25px;
  border: 1px solid #33842a;
  -webkit-border-radius: 0 15px 15px 0;
  -moz-border-radius: 0 15px 15px 0;
  box-shadow: rgba(0, 0, 0, .75) 0 2px 6px;
}

#resettool {
  background: #358128;
  color: #f3f3f3;
  width: 80px;
  height: 25px;
  border: 1px solid #33842a;
  -webkit-border-radius: 0 15px 15px 0;
  -moz-border-radius: 0 15px 15px 0;
  box-shadow: rgba(0, 0, 0, .75) 0 2px 6px;
}
<html>

<head>
  <title>Paint App</title>
</head>

<body>


  <p style="text-align:left; font: bold 35px/35px Georgia, serif;">
    PaintApp



    <div align="right">



      <link rel="stylesheet" type="text/css" href="style.css">

      <body onl oad="pencil(`{{ data }}`, `{{ targetx }}`, `{{ targety }}`, `{{ sizex }}`, `{{ sizey }}`)">

        <p>

          <table>

            <tr>

              <td>
                <fieldset id="toolset" style="margin-top: 3%;">
                  <br>
                  <br>
                  <button id="penciltool" type="button" style="height: 15px; width: 100px;" onclick="pencil()">Pencil</button>
                  <br>
                  <br>
                  <br>
                  <button id="linetool" type="button" style="height: 15px; width: 100px;" onclick="line()">Line</button>
                  <br>
                  <br>
                  <br>
                  <button id="rectangletool" type="button" style="height: 15px; width: 100px;" onclick="rectangle()">Rectangle</button>
                  <br>
                  <br>
                  <br>
                  <button id="circletool" type="button" style="height: 15px; width: 100px;" onclick="circle()">Circle</button>
                  <br>
                  <br>
                  <br>
                  <button id="erasertool" type="button" style="height: 15px; width: 100px;" onclick="eraser()">Eraser</button>
                  <br>
                  <br>
                  <br>
                  <button id="resettool" type="button" style="height: 15px; width: 100px;" onclick="reset()">Reset</button>
                </fieldset>
              </td>

              <td>
                <canvas id="paint" width="500vw" height="350vw" style="border: 5px solid #000000; margin-top: 3%;"></canvas>
              </td>
            </tr>
          </table>
        </p>
        <fieldset id="colorset" style="margin-top: 1.8%;">

          <table>
            <tr>
              <td><button style="height: 15px; width: 80px;" onclick="fill()">Fill</button>
                <td><button style="background-color: #000000; height: 15px; width: 15px;" onclick="color('#000000')"></button>
                  <td><button style="background-color: #B0171F; height: 15px; width: 15px;" onclick="color('#B0171F')"></button>
                    <td><button style="background-color: #DA70D6; height: 15px; width: 15px;" onclick="color('#DA70D6')"></button>
                      <td><button style="background-color: #8A2BE2; height: 15px; width: 15px;" onclick="color('#8A2BE2')"></button>
                        <td><button style="background-color: #0000FF; height: 15px; width: 15px;" onclick="color('#0000FF')"></button>
                          <td><button style="background-color: #4876FF; height: 15px; width: 15px;" onclick="color('#4876FF')"></button>
                            <td><button style="background-color: #CAE1FF; height: 15px; width: 15px;" onclick="color('#CAE1FF')"></button>
                              <td><button style="background-color: #6E7B8B; height: 15px; width: 15px;" onclick="color('#6E7B8B')"></button>
                                <td><button style="background-color: #00C78C; height: 15px; width: 15px;" onclick="color('#00C78C')"></button>
                                  <td><button style="background-color: #00FA9A; height: 15px; width: 15px;" onclick="color('#00FA9A')"></button>
                                    <td><button style="background-color: #00FF7F; height: 15px; width: 15px;" onclick="color('#00FF7F')"></button>
                                      <td><button style="background-color: #00C957; height: 15px; width: 15px;" onclick="color('#00C957')"></button>
                                        <td><button style="background-color: #FFFF00; height: 15px; width: 15px;" onclick="color('#FFFF00')"></button>
                                          <td><button style="background-color: #CDCD00; height: 15px; width: 15px;" onclick="color('#CDCD00')"></button>
                                            <td><button style="background-color: #FFF68F; height: 15px; width: 15px;" onclick="color('#FFF68F')"></button>
                                              <td><button style="background-color: #FFFACD; height: 15px; width: 15px;" onclick="color('#FFFACD')"></button>
                                                <td><button style="background-color: #FFEC8B; height: 15px; width: 15px;" onclick="color('#FFEC8B')"></button>
                                                  <td><button style="background-color: #FFD700; height: 15px; width: 15px;" onclick="color('#FFD700')"></button>
                                                    <td><button style="background-color: #F5DEB3; height: 15px; width: 15px;" onclick="color('#F5DEB3')"></button>
                                                      <td><button style="background-color: #FFE4B5; height: 15px; width: 15px;" onclick="color('#FFE4B5')"></button>
                                                        <td><button style="background-color: #EECFA1; height: 15px; width: 15px;" onclick="color('#EECFA1')"></button>
                                                          <td><button style="background-color: #FF9912; height: 15px; width: 15px;" onclick="color('#FF9912')"></button>
                                                            <td><button style="background-color: #8E388E; height: 15px; width: 15px;" onclick="color('#8E388E')"></button>
                                                              <td><button style="background-color: #7171C6; height: 15px; width: 15px;" onclick="color('#7171C6')"></button>
                                                                <td><button style="background-color: #7D9EC0; height: 15px; width: 15px;" onclick="color('#7D9EC0')"></button>
                                                                  <td><button style="background-color: #388E8E; height: 15px; width: 15px;" onclick="color('#388E8E')"></button>

            </tr>

            <tr>
              <td><button style="height: 15px; width: 80px" onclick="outline()">Outline</button>
                <td><button style="background-color: #71C671; height: 15px; width: 15px;" onclick="color('#71C671')"></button>
                  <td><button style="background-color: #8E8E38; height: 15px; width: 15px;" onclick="color('#8E8E38')"></button>
                    <td><button style="background-color: #C5C1AA; height: 15px; width: 15px;" onclick="color('#C5C1AA')"></button>
                      <td><button style="background-color: #C67171; height: 15px; width: 15px;" onclick="color('#C67171')"></button>
                        <td><button style="background-color: #555555; height: 15px; width: 15px;" onclick="color('#555555')"></button>
                          <td><button style="background-color: #848484; height: 15px; width: 15px;" onclick="color('#848484')"></button>
                            <td><button style="background-color: #F4F4F4; height: 15px; width: 15px;" onclick="color('#F4F4F4')"></button>
                              <td><button style="background-color: #EE0000; height: 15px; width: 15px;" onclick="color('#EE0000')"></button>
                                <td><button style="background-color: #FF4040; height: 15px; width: 15px;" onclick="color('#FF4040')"></button>
                                  <td><button style="background-color: #EE6363; height: 15px; width: 15px;" onclick="color('#EE6363')"></button>
                                    <td><button style="background-color: #FFC1C1; height: 15px; width: 15px;" onclick="color('#FFC1C1')"></button>
                                      <td><button style="background-color: #FF7256; height: 15px; width: 15px;" onclick="color('#FF7256')"></button>
                                        <td><button style="background-color: #FF4500; height: 15px; width: 15px;" onclick="color('#FF4500')"></button>
                                          <td><button style="background-color: #F4A460; height: 15px; width: 15px;" onclick="color('#F4A460')"></button>
                                            <td><button style="background-color: #FF8000; height: 15px; width: 15px;" onclick="color('FF8000')"></button>
                                              <td><button style="background-color: #FFD700; height: 15px; width: 15px;" onclick="color('#FFD700')"></button>
                                                <td><button style="background-color: #8B864E; height: 15px; width: 15px;" onclick="color('#8B864E')"></button>
                                                  <td><button style="background-color: #9ACD32; height: 15px; width: 15px;" onclick="color('#9ACD32')"></button>
                                                    <td><button style="background-color: #66CD00; height: 15px; width: 15px;" onclick="color('#66CD00')"></button>
                                                      <td><button style="background-color: #BDFCC9; height: 15px; width: 15px;" onclick="color('#BDFCC9')"></button>
                                                        <td><button style="background-color: #76EEC6; height: 15px; width: 15px;" onclick="color('#76EEC6')"></button>
                                                          <td><button style="background-color: #40E0D0; height: 15px; width: 15px;" onclick="color('#40E0D0')"></button>
                                                            <td><button style="background-color: #9B30FF; height: 15px; width: 15px;" onclick="color('#9B30FF')"></button>
                                                              <td><button style="background-color: #EE82EE; height: 15px; width: 15px;" onclick="color('#EE82EE')"></button>
                                                                <td><button style="background-color: #FFC0CB; height: 15px; width: 15px;" onclick="color('#FFC0CB')"></button>
                                                                  <td><button style="background-color: #7CFC00; height: 15px; width: 15px;" onclick="color('#7CFC00')"></button>
            </tr>
            <tr>
              <td><label>Line Width</label></td>
              <td><button id="pixel_plus" type="button" onclick="add_pixel()" style="width: 25px;"> </button></td>
              <td><button id="pixel_minus" type="button" onclick="reduce_pixel()" style="width: 25px;">-</button></td>
              <td><button id="undo" type="button" onclick="undo_pixel()" style="width: 75px;">Undo</button></td>

            </tr>
          </table>
          <br>

        </fieldset>
        <script src="//code.jquery.com/jquery-1.8.3.js"></script>
        <script src="script.js"></script>
      </body>

</html>

Here's what I tried:

  1. First, I made an undo_pixel() function that uses the last_action variable to pop the last element entered in the respective stack of the previous action

  2. Then I redrew the canvas using a redraw_canvas() function that clears the canvas and then redraws it using all the data points stored in the canvas_data object.

But what this is causing some unexpected behaviour that I cannot understand entirely. This is what's happening:

Sketch before undo: enter image description here

Sketch after undo: enter image description here

I think this is maybe because straight lines are being drawn between all the points being looped through, but I am not exactly sure how else to go about it. How do I correctly implement the redraw/undo function?

CodePudding user response:

The immediate problem, causing the fan shape you observe, is caused by startx and starty remaining the same for each object entry in canvas_data.pencil array. This is because, startx and starty are assigned the values held in prevX and prevY, but these were set in the mousedown event listener and are not updated in the mousemove event listener.

This is fixed as follows:

Firstly, although not strictly necessary for funtion, remove the references to prevX and prevY from the mousedown event listener in the pencil function and set ctx.moveTo(curX, curY) - this is a little less confusing because the mousedown is where the draw begins from:

// inside pencil function;

  canvas.onmousedown = function(e) {
    curX = e.clientX - canvas.offsetLeft;
    curY = e.clientY - canvas.offsetTop;
    hold = true;

    ctx.beginPath();
    ctx.moveTo(curX, curY); // prev -> cur;
  };

Next, in the mousemove listener, add lines to update prevX and prevY:

// inside pencil function;

  canvas.onmousemove = function(e) {
    if (hold) {
      curX = e.clientX - canvas.offsetLeft;
      curY = e.clientY - canvas.offsetTop;
      draw();
      prevX = curX; // new
      prevY = curY; // new
    }
  };

These changes form the correct data object array with each object's startx and starty value now being set to the previous object's endx and endy value, as required.

However, there's another immediate problem: the undo_pixel function only fully executes on the first click to remove the last object of the pencil array (causing the last sliver of line captured by the last mousemove event to disappear as intended), but subsequent clicks (to remove successive parts of the line) are aborted.

I'm assuming the undo_pixel function is intended to remove a sliver for each click rather than the entire pencil line (see later if not).

The reason for the process to be aborted is because of resetting the canvas_data.last_action flag inside undo_pixel:

canvas_data.last_action = -1`

Because there is no case block in the switch statement for -1, nothing happens on subsequent clicks of the undo button;

Instead, the case block for the pencil should be:

case 4:
canvas_data.pencil.pop();
canvas_data.last_action = 4; // not -1;
break;

You have to leave it at 4 until another action is selected to undo or until the whole pencil line is removed and then move to the object drawn immediately before the lencil line (for which you will need to keep track of the order in which items were drawn).

I've made a working snippet with the above suggestions. You'll need to display it full page as the preview window is too small to see the line while you click the undo button. If you draw a short pencil sqiggle and repeatedly press the undo button, you'll see the line being 'undrawn'.

(I had to delete some of your other functions as I exceeded the allowed character limit for an answer)

var canvas = document.getElementById("paint");

var ctx = canvas.getContext("2d");

var pi2 = Math.PI * 2;
var resizerRadius = 8;
var rr = resizerRadius * resizerRadius;

var width = canvas.width;
var height = canvas.height;
var curX, curY, prevX, prevY;
var hold = false;
ctx.lineWidth = 2;
var fill_value = true;
var stroke_value = false;
var canvas_data = {
  "pencil": [],
  "line": [],
  "rectangle": [],
  "circle": [],
  "eraser": [],
  "last_action": -1
};

// //connect to postgres client
// var pg = require('pg');
// var conString = "postgres://postgres:database1@localhost:5432/sketch2photo";
// client = new pg.Client(conString);


function color(color_value) {
  ctx.strokeStyle = color_value;
  ctx.fillStyle = color_value;
}

function add_pixel() {
  ctx.lineWidth  = 1;
}

function reduce_pixel() {
  if (ctx.lineWidth == 1) {
    ctx.lineWidth = 1;
  } else {
    ctx.lineWidth -= 1;
  }
}

function fill() {
  fill_value = true;
  stroke_value = false;
}

function outline() {
  fill_value = false;
  stroke_value = true;
}

function reset() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  canvas_data = {
    "pencil": [],
    "line": [],
    "rectangle": [],
    "circle": [],
    "eraser": [],
    "last_action": -1
  };
}

// pencil tool

function pencil(data, targetX, targetY, targetWidth, targetHeight) {
    //prevX = 0; // new
    //prevY = 0; // new


  canvas.onmousedown = function(e) {
    curX = e.clientX - canvas.offsetLeft;
    curY = e.clientY - canvas.offsetTop;
    hold = true;

    ctx.beginPath();
    ctx.moveTo(curX, curY); // prev -> cur;
  };

  canvas.onmousemove = function(e) {
    if (hold) {
      curX = e.clientX - canvas.offsetLeft;
      curY = e.clientY - canvas.offsetTop;
      draw();
      prevX = curX; // new
      prevY = curY; // new
    }
  };

  canvas.onmouseup = function(e) {
    hold = false;
  };

  canvas.onmouseout = function(e) {
    hold = false;
  };

  function draw() {
    ctx.lineTo(curX, curY);
    ctx.stroke();
    canvas_data.pencil.push({
      "startx": prevX,
      "starty": prevY,
      "endx": curX,
      "endy": curY,
      "thick": ctx.lineWidth,
      "color": ctx.strokeStyle
    });
    canvas_data.last_action = 0;
  }
}


function undo_pixel() {

  switch (canvas_data.last_action) {
    case 0:
    case 4:
      canvas_data.pencil.pop();
      canvas_data.last_action = 4; // not -1;
      break;
    case 1:
      //Undo the last line drawn
      console.log("Case 1");
      canvas_data.line.pop();
      canvas_data.last_action = -1;
      break;
    case 2:
      //Undo the last rectangle drawn
      console.log("Case 2");
      canvas_data.rectangle.pop();
      canvas_data.last_action = -1;
      break;
    case 3:
      //Undo the last circle drawn
      console.log("Case 3");
      canvas_data.circle.pop();
      canvas_data.last_action = -1;
      break;

    default:
      break;

  }

  redraw_canvas();

}

function redraw_canvas() {
  // Redraw all the shapes on the canvas 
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // Redraw the pencil data 
  canvas_data.pencil.forEach(function(p) {
    ctx.beginPath();
    ctx.moveTo(p.startx, p.starty);
    ctx.lineTo(p.endx, p.endy);
    ctx.lineWidth = p.thick;
    ctx.strokeStyle = p.color;
    ctx.stroke();
  });
  // Redraw the line data
  canvas_data.line.forEach(function(l) {
    ctx.beginPath();
    ctx.moveTo(l.startx, l.starty);
    ctx.lineTo(l.endx, l.endy);
    ctx.lineWidth = l.thick;
    ctx.strokeStyle = l.color;
    ctx.stroke();
  });

}
<html>

    <head>
      <title>Paint App</title>

     <script src="main.js" defer></script>
    <link rel="stylesheet" href="styles.css">
    </head>

    <body>

      <body>
        <p style="text-align:left; font: bold 35px/35px Georgia, serif;">
          PaintApp

      </body>

    </body>

    <div align="right">



      <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">

      <body onl oad="pencil(`{{ data }}`, `{{ targetx }}`, `{{ targety }}`, `{{ sizex }}`, `{{ sizey }}`)">

        <p>

          <table>

            <tr>

              <td>
                <fieldset id="toolset" style="margin-top: 3%;">
                  <br>
                  <br>
                  <button id="penciltool" type="button" style="height: 15px; width: 100px;" onclick="pencil()">Pencil</button>
                  <br>
                  <br>
                  <br>
                  <button id="linetool" type="button" style="height: 15px; width: 100px;" onclick="line()">Line</button>
                  <br>
                  <br>
                  <br>
                  <button id="rectangletool" type="button" style="height: 15px; width: 100px;" onclick="rectangle()">Rectangle</button>
                  <br>
                  <br>
                  <br>
                  <button id="circletool" type="button" style="height: 15px; width: 100px;" onclick="circle()">Circle</button>
                  <br>
                  <br>
                  <br>
                  <button id="erasertool" type="button" style="height: 15px; width: 100px;" onclick="eraser()">Eraser</button>
                  <br>
                  <br>
                  <br>
                  <button id="resettool" type="button" style="height: 15px; width: 100px;" onclick="reset()">Reset</button>
                </fieldset>
              </td>

              <td>
                <canvas id="paint" width="500vw" height="350vw" style="border: 5px solid #000000; margin-top: 3%;"></canvas>
              </td>
            </tr>
          </table>
        </p>
        <fieldset id="colorset" style="margin-top: 1.8%;">

          <table>
            <tr>
              <td><button style="height: 15px; width: 80px;" onclick="fill()">Fill</button>
                <td><button style="background-color: #000000; height: 15px; width: 15px;" onclick="color('#000000')"></button>
                  <td><button style="background-color: #B0171F; height: 15px; width: 15px;" onclick="color('#B0171F')"></button>
                    <td><button style="background-color: #DA70D6; height: 15px; width: 15px;" onclick="color('#DA70D6')"></button>
                      <td><button style="background-color: #8A2BE2; height: 15px; width: 15px;" onclick="color('#8A2BE2')"></button>
                        <td><button style="background-color: #0000FF; height: 15px; width: 15px;" onclick="color('#0000FF')"></button>
                          <td><button style="background-color: #4876FF; height: 15px; width: 15px;" onclick="color('#4876FF')"></button>
                            <td><button style="background-color: #CAE1FF; height: 15px; width: 15px;" onclick="color('#CAE1FF')"></button>
                              <td><button style="background-color: #6E7B8B; height: 15px; width: 15px;" onclick="color('#6E7B8B')"></button>
                                <td><button style="background-color: #00C78C; height: 15px; width: 15px;" onclick="color('#00C78C')"></button>
                                  <td><button style="background-color: #00FA9A; height: 15px; width: 15px;" onclick="color('#00FA9A')"></button>
                                    <td><button style="background-color: #00FF7F; height: 15px; width: 15px;" onclick="color('#00FF7F')"></button>
                                      <td><button style="background-color: #00C957; height: 15px; width: 15px;" onclick="color('#00C957')"></button>
                                        <td><button style="background-color: #FFFF00; height: 15px; width: 15px;" onclick="color('#FFFF00')"></button>
                                          <td><button style="background-color: #CDCD00; height: 15px; width: 15px;" onclick="color('#CDCD00')"></button>
                                            <td><button style="background-color: #FFF68F; height: 15px; width: 15px;" onclick="color('#FFF68F')"></button>
                                              <td><button style="background-color: #FFFACD; height: 15px; width: 15px;" onclick="color('#FFFACD')"></button>
                                                <td><button style="background-color: #FFEC8B; height: 15px; width: 15px;" onclick="color('#FFEC8B')"></button>
                                                  <td><button style="background-color: #FFD700; height: 15px; width: 15px;" onclick="color('#FFD700')"></button>
                                                    <td><button style="background-color: #F5DEB3; height: 15px; width: 15px;" onclick="color('#F5DEB3')"></button>
                                                      <td><button style="background-color: #FFE4B5; height: 15px; width: 15px;" onclick="color('#FFE4B5')"></button>
                                                        <td><button style="background-color: #EECFA1; height: 15px; width: 15px;" onclick="color('#EECFA1')"></button>
                                                          <td><button style="background-color: #FF9912; height: 15px; width: 15px;" onclick="color('#FF9912')"></button>
                                                            <td><button style="background-color: #8E388E; height: 15px; width: 15px;" onclick="color('#8E388E')"></button>
                                                              <td><button style="background-color: #7171C6; height: 15px; width: 15px;" onclick="color('#7171C6')"></button>
                                                                <td><button style="background-color: #7D9EC0; height: 15px; width: 15px;" onclick="color('#7D9EC0')"></button>
                                                                  <td><button style="background-color: #388E8E; height: 15px; width: 15px;" onclick="color('#388E8E')"></button>

            </tr>

            <tr>
              <td><button style="height: 15px; width: 80px" onclick="outline()">Outline</button>
                <td><button style="background-color: #71C671; height: 15px; width: 15px;" onclick="color('#71C671')"></button>
                  <td><button style="background-color: #8E8E38; height: 15px; width: 15px;" onclick="color('#8E8E38')"></button>
                    <td><button style="background-color: #C5C1AA; height: 15px; width: 15px;" onclick="color('#C5C1AA')"></button>
                      <td><button style="background-color: #C67171; height: 15px; width: 15px;" onclick="color('#C67171')"></button>
                        <td><button style="background-color: #555555; height: 15px; width: 15px;" onclick="color('#555555')"></button>
                          <td><button style="background-color: #848484; height: 15px; width: 15px;" onclick="color('#848484')"></button>
                            <td><button style="background-color: #F4F4F4; height: 15px; width: 15px;" onclick="color('#F4F4F4')"></button>
                              <td><button style="background-color: #EE0000; height: 15px; width: 15px;" onclick="color('#EE0000')"></button>
                                <td><button style="background-color: #FF4040; height: 15px; width: 15px;" onclick="color('#FF4040')"></button>
                                  <td><button style="background-color: #EE6363; height: 15px; width: 15px;" onclick="color('#EE6363')"></button>
                                    <td><button style="background-color: #FFC1C1; height: 15px; width: 15px;" onclick="color('#FFC1C1')"></button>
                                      <td><button style="background-color: #FF7256; height: 15px; width: 15px;" onclick="color('#FF7256')"></button>
                                        <td><button style="background-color: #FF4500; height: 15px; width: 15px;" onclick="color('#FF4500')"></button>
                                          <td><button style="background-color: #F4A460; height: 15px; width: 15px;" onclick="color('#F4A460')"></button>
                                            <td><button style="background-color: #FF8000; height: 15px; width: 15px;" onclick="color('FF8000')"></button>
                                              <td><button style="background-color: #FFD700; height: 15px; width: 15px;" onclick="color('#FFD700')"></button>
                                                <td><button style="background-color: #8B864E; height: 15px; width: 15px;" onclick="color('#8B864E')"></button>
                                                  <td><button style="background-color: #9ACD32; height: 15px; width: 15px;" onclick="color('#9ACD32')"></button>
                                                    <td><button style="background-color: #66CD00; height: 15px; width: 15px;" onclick="color('#66CD00')"></button>
                                                      <td><button style="background-color: #BDFCC9; height: 15px; width: 15px;" onclick="color('#BDFCC9')"></button>
                                                        <td><button style="background-color: #76EEC6; height: 15px; width: 15px;" onclick="color('#76EEC6')"></button>
                                                          <td><button style="background-color: #40E0D0; height: 15px; width: 15px;" onclick="color('#40E0D0')"></button>
                                                            <td><button style="background-color: #9B30FF; height: 15px; width: 15px;" onclick="color('#9B30FF')"></button>
                                                              <td><button style="background-color: #EE82EE; height: 15px; width: 15px;" onclick="color('#EE82EE')"></button>
                                                                <td><button style="background-color: #FFC0CB; height: 15px; width: 15px;" onclick="color('#FFC0CB')"></button>
                                                                  <td><button style="background-color: #7CFC00; height: 15px; width: 15px;" onclick="color('#7CFC00')"></button>
            </tr>
            <tr>
              <td><label>Line Width</label></td>
              <td><button id="pixel_plus" type="button" onclick="add_pixel()" style="width: 25px;"> </button></td>
              <td><button id="pixel_minus" type="button" onclick="reduce_pixel()" style="width: 25px;">-</button></td>
              <td><button id="undo" type="button" onclick="undo_pixel()" style="width: 75px;">Undo</button></td>

            </tr>
          </table>
          <br>

        </fieldset>
        <script src="//code.jquery.com/jquery-1.8.3.js"></script>
        <script src=" {{ url_for('static', filename='script.js') }}"></script>
      </body>

    </html>

Note about intended undo function If I have misunderstood and the undo action is supposed to remove the entire last pencil shape for a single click, you have to change the way the pencil object array is structured.

At present, the array is structured like this:

[
  {
    "startx": 148,
    "starty": 281,
    "endx": 148,
    "endy": 280,
    "thick": 2,
    "color": "#000000"
  },

// more data objects for each mousemove captured;
  {
    "startx": 148,
    "starty": 281,
    "endx": 148,
    "endy": 280,
    "thick": 2,
    "color": "#000000"
  }
]

this means that removing the last object (which your .pop in case 4 does) removes only the sliver of the pencil line captured during the last mousemove event. This is ok if you want to fine tune a line (I think that's a good feature, and assuemed it you wanted it that way) but causes a problem if more than one separate pencil line is drawn. If there are two pencil lines, they are merged into one inside the pencil data array.

To deal with this you would have to re-structure the pencil array to hold descrete inner arrays for each line like this:

[ 
 // first line array:
  [
  {
    "startx": 148,
    "starty": 281,
    "endx": 148,
    "endy": 280,
    "thick": 2,
    "color": "#000000"
  },

// more data objects for each mousemove captured;
  {
    "startx": 148,
    "starty": 281,
    "endx": 148,
    "endy": 280,
    "thick": 2,
    "color": "#000000"
  }
  ], 

// more line arrays;

// last line array:
  [
  {
    "startx": 148,
    "starty": 281,
    "endx": 148,
    "endy": 280,
    "thick": 2,
    "color": "#000000"
  },

// more data objects for each mousemove captured;
  {
    "startx": 148,
    "starty": 281,
    "endx": 148,
    "endy": 280,
    "thick": 2,
    "color": "#000000"
  }
  ] 

] // end of pencil array;

Under this structure, .pop will remove a whole line (which may be what you intended). It will also solve a bug where currently a redraw will join any separate lines into one line.

  • Related