如何使用JavaScript实现HTMLcanvas的撤销功能?

How to implement undo function for HTML canvas using JavaScript?

我正在使用 JavaScript 和 Flask-Python 在 HTML 中制作一个绘画应用程序。目前,我可以毫无问题地画出许多铅笔画和 rectangles/circles 之类的形状。我尝试为此应用程序实现的以下功能是 undo 函数。

我按以下方式将笔画和 canvas 绘图数据存储在 JS 对象中:

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

除了 last_action 之外,所有键名都应该是不言自明的。我使用这个 last_action 变量来了解用户最后使用的类别,以便我以后可以使用此信息来实现撤消功能。

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 onload="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>

这是我尝试过的:

  1. 首先,我创建了一个 undo_pixel() 函数,该函数使用 last_action 变量弹出上一个操作的相应堆栈中输入的最后一个元素

  2. 然后我使用清除 canvas 的 redraw_canvas() 函数重新绘制 canvas,然后使用 [=22] 中存储的所有数据点重新绘制它=]对象。

但这导致了一些我无法完全理解的意外行为。这是正在发生的事情:

撤消前草图:

撤消后的草图:

我认为这可能是因为在所有被循环的点之间绘制了直线,但我不确定还有什么办法可以解决这个问题。如何正确实现redraw/undo功能?

导致您观察到的扇形的直接问题是由 startxstartycanvas_data.pencil 数组中的每个对象条目保持相同引起的。这是因为,startxstarty 被分配了 prevXprevY 中保存的值,但这些值是在 mousedown 事件侦听器中设置的并且不会更新在 mousemove 事件侦听器中。

固定如下:

首先,虽然对于功能来说不是绝对必要的,但从铅笔函数的 mousedown 事件侦听器中删除对 prevXprevY 的引用并设置 ctx.moveTo(curX, curY) -这有点不那么令人困惑,因为 mousedown 是绘制开始的地方:

// 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;
  };

接下来,在 mousemove 侦听器中,添加行以更新 prevXprevY:

// 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
    }
  };

这些更改形成了正确的数据对象数组,每个对象的 startxstarty 值现在根据需要设置为前一个对象的 endxendy 值.

但是,还有另一个紧迫的问题:undo_pixel 函数仅在第一次单击以删除铅笔数组的最后一个对象时完全执行(导致最后一个 mousemove 事件按预期消失),但随后的点击(删除该行的连续部分)被中止。

我假设 undo_pixel 函数旨在为每次点击删除一条条子而不是整条铅笔线(如果不是,请稍后查看)。

进程中止的原因是因为重置了undo_pixel中的canvas_data.last_action标志:

canvas_data.last_action = -1`

因为 -1 的 switch 语句中没有 case 块,所以在随后单击 undo 按钮时没有任何反应;

相反,铅笔盒应为:

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

您必须将其保留在 4,直到选择另一个操作来撤消或直到整个铅笔线被删除,然后移动到紧接在 lencil 线之前绘制的对象(为此您需要跟踪绘制项目的顺序)。

我根据上述建议制作了一个工作片段。您需要将其显示为整页,因为预览 window 太小,在您单击撤消按钮时看不到该行。如果你画一个短铅笔波浪形并反复按撤消按钮,你会看到这条线是 'undrawn'.

(我不得不删除你的一些其他功能,因为我超出了允许的答案字符限制)

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 onload="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>

关于预期撤消功能的注意事项 如果我误解了 undo 操作应该通过单击删除整个最后一个铅笔形状,则您必须更改 pencil 对象数组的结构方式。

目前数组结构如下:

[
  {
    "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"
  }
]

这意味着删除最后一个对象(您的 .popcase 4 中所做的)只会删除在最后一个 mousemove 事件期间捕获的铅笔线的条子。如果您想微调一条线(我认为这是一个很好的功能,并假设您想要那样),这没问题,但如果绘制了不止一条单独的铅笔线,则会导致问题。如果有两条铅笔线,它们将在 pencil 数据数组中合并为一条。

要解决这个问题,您必须 re-structure pencil 数组来为每一行保存离散的内部数组,如下所示:

[ 
 // 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;

在此结构下,.pop 将删除整行(这可能是您想要的)。它还将解决当前重绘会将任何单独的行合并为一行的错误。