如何使用数组添加撤消列表?

How do I use array to add an undo-list?

我正在尝试在 JavaScript 中制作一个绘画程序,我想包括一个撤消功能(不是橡皮擦)。如何将所有事件添加到一个数组中,然后才能将它们一一删除?

我有一个工具下拉列表(目前只有四个工具可用)。我添加了一个带有 id 的撤消按钮。我已经尝试了几个小时(实际上是几天)来找出如何做到这一点。我找到了一些例子,我想我必须同时使用 push 和空数组才能更进一步?

这是工具选择和按钮的代码

<label>
  Object type:
    <select id="selectTool">
        <option value="line">Linje</option>
        <option value="pencil">Blyant</option>
        <option value="rect">Rektangel</option>
        <option value="circle">Sirkel</option>
        <option value="oval">Oval</option>
        <option value="polygon">Polygon</option>
    </select>

  Shape drawn:
    <select id="shapeDrawn">
        <option value=""></option>
    </select>   

  <input type="button" id="cmbDelete" value="Undo last action">

</label>

撤消功能可能是这样的,但是这个功能

var shapes = [];
shapes.push(newShape);


 function cmbDeleteClick(){
  if(shapes.length > 0){
    var selectedShapeIndex = selectShape.selectedIndex;
    shapes.splice(selectedShapeIndex,1);
    selectShape.options.remove(selectedShapeIndex);
    selectShape.selectedIndex = selectShape.options.length - 1;
  }
    cmbDelete = document.getElementById("cmbDelete");
    cmbDelete.addEventListener("click",cmbDeleteClick, false);
    fillSelectShapeTypes();
    drawCanvas(); 
}

理想情况下,在 canvas 上绘制的所有内容都会添加到下拉菜单中,并且可以通过单击按钮将其删除(撤消)。这是代码 JS Bin

的 "working" 版本

您当前的实现不使用 shapes 数组,并且无法在创建后重新绘制它们。

所以最简单的方法是将每个动作存储为位图。 所以你需要一个位图数组,我们称之为:

var history = [];

绘制完某些内容后,我们会创建当前 canvas 的快照并将其存储在该数组中:

history.push(contextTmp.getImageData(0,0,canvasTmp.width,canvasTmp.height))

当您想撤消操作时,弹出历史记录并在 canvas 上绘制最后一个位图:

function cmbDeleteClick(){
    history.pop()
    contextTmp.putImageData(history[history.length-1],0,0)
}

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Paint</title>
 <style type="text/css">
  #content { position: relative; }
  #cvs { border: 1px solid #c00; }
  #cvsTmp { position: absolute; top: 1px; left: 1px; }
    </style>
</head>
<body>
<p>
  
<label>
Object type:
 <select id="selectTool">
  <option value="line">Linje</option>
  <option value="pencil">Blyant</option>
  <option value="rect">Rektangel</option>
  <option value="circle">Sirkel</option>
  <option value="oval">Oval</option>
  <option value="polygon">Polygon</option>
 </select>
 
Shape drawn:
 <select id="shapeDrawn">
  <option value=""></option>
 </select> 
 
History:
 <select id="historySelect">
 </select> 
  
<input type="button" id="cmbDelete" value="Undo last action">

</label>

</p>
 
<div id="content">
 <canvas id="cvs" width="1024" height="512"></canvas>
</div>

<script type="text/javascript">

 
if(window.addEventListener) {
window.addEventListener('load', function () {
  var canvas;
  var context;
  var canvasTmp;
  var contextTmp;

  var tool;
  var toolDefault = 'line';
 
  var cmbDelete = null;
  var shapes = [];
  var history = [];
  var historySelect;

// Canvas and temp. canvas
 
function init () {
    canvasTmp = document.getElementById('cvs');
   if (!canvasTmp) {
    return;
   } if (!canvasTmp.getContext) {
      return;
    }
    
    historySelect = document.getElementById('historySelect')
    historySelect.addEventListener('change', ()=>{
      restoreHistoryAction(historySelect.value)
    })

    contextTmp = canvasTmp.getContext('2d');
   if (!contextTmp) {
    return;
   }

    // Add the temporary canvas.
    var content = canvasTmp.parentNode;
    canvas = document.createElement('canvas');
    if (!canvas) {
      return;
    }

    canvas.id     = 'cvsTmp';
    canvas.width  = canvasTmp.width;
    canvas.height = canvasTmp.height;
    content.appendChild(canvas);

    context = canvas.getContext('2d');
  

    // Get the tool select input.
    var toolSelect = document.getElementById('selectTool');
    if (!toolSelect) {
      return;
    }
    toolSelect.addEventListener('change', ev_tool_change, false);

    // Activate the default tool.
    if (tools[toolDefault]) {
      tool = new tools[toolDefault]();
      toolSelect.value = toolDefault;
    }

    // Attach the mousedown, mousemove and mouseup event listeners.
    canvas.addEventListener('mousedown', evMouse, false);
    canvas.addEventListener('mousemove', evMouse, false);
    canvas.addEventListener('mouseup',   evMouse, false);
  
    drawCanvas()
  }

function evMouse (ev) {
    if (ev.layerX || ev.layerX == 0) {
      ev._x = ev.layerX;
      ev._y = ev.layerY;
    }
 var evHandler = tool[ev.type];
 if (evHandler) {
  evHandler(ev);
 }
}
 
  // The event handler for any changes made to the tool selector.
  function toolChange (ev) {
    if (tools[this.value]) {
      tool = new tools[this.value]();
    }
  } 
 
 
  // Updates Canvas on interval timeout
function drawCanvas() {
 contextTmp.drawImage(canvas, 0, 0);
    history.push(contextTmp.getImageData(0,0,canvasTmp.width,canvasTmp.height))
    updateHistorySelection()
 context.clearRect(0, 0, canvas.width, canvas.height);
}
 
  function ev_tool_change (ev) {
    if (tools[this.value]) {
      tool = new tools[this.value]();
    }
  }

  // Get excact position for mouse coordinates in canvas
  function mouseAction (ev) {
    if (ev.layerX || ev.layerX == 0) {
      ev._x = ev.layerX;
      ev._y = ev.layerY;
    }

    // Call the event handler of the tool.
    var func = tool[ev.type];
    if (func) {
      func(ev);
    }
  }


  function selectShapeChange(){
    drawCanvas();
  }
 
 

 
 
 var tools = {};

  // The drawing pencil.
  tools.pencil = function () {
    var tool = this;
    this.started = false;

    this.mousedown = function (ev) {
        context.beginPath();
        context.moveTo(ev._x, ev._y);
        tool.started = true;
    };

    this.mousemove = function (ev) {
      if (tool.started) {
        context.lineTo(ev._x, ev._y);
        context.stroke();
      }
    };

    this.mouseup = function (ev) {
      if (tool.started) {
        tool.mousemove(ev);
        tool.started = false;
        drawCanvas();
      }
    };
  };
 
// The rectangle tool.
  tools.rect = function () {
    var tool = this;
    this.started = false;

    this.mousedown = function (ev) {
      tool.started = true;
      tool.x0 = ev._x;
      tool.y0 = ev._y;
    };

    this.mousemove = function (ev) {
      if (!tool.started) {
        return;
      }

      var x = Math.min(ev._x,  tool.x0),
          y = Math.min(ev._y,  tool.y0),
          w = Math.abs(ev._x - tool.x0),
          h = Math.abs(ev._y - tool.y0);

      context.clearRect(0, 0, canvas.width, canvas.height);

      if (!w || !h) {
        return;
      }
      context.fillRect(x, y, w, h);
  context.fillStyle = 'hsl(' + 360 * Math.random() + ', 50%, 50%)';
    };

    this.mouseup = function (ev) {
      if (tool.started) {
        tool.mousemove(ev);
        tool.started = false;
        drawCanvas();
      }
    };
  };

  // The line tool.
  tools.line = function () {
    var tool = this;
    this.started = false;

    this.mousedown = function (ev) {
      tool.started = true;
      tool.x0 = ev._x;
      tool.y0 = ev._y;
    };

    this.mousemove = function (ev) {
      if (!tool.started) {
        return;
      }

      context.clearRect(0, 0, canvas.width, canvas.height);

      context.beginPath();
      context.moveTo(tool.x0, tool.y0);
      context.lineTo(ev._x,   ev._y);
      context.stroke();
      context.closePath();
    };

    this.mouseup = function (ev) {
      if (tool.started) {
        tool.mousemove(ev);
        tool.started = false;
        drawCanvas();
      }
    };
  };
 
// Circle tool
  tools.circle = function () {
    var tool = this;
    this.started = false;

    this.mousedown = function (ev) {
      tool.started = true;
      tool.x0 = ev._x;
      tool.y0 = ev._y;
    };

    this.mousemove = function (ev) {
      if (!tool.started) {
        return;
      }

context.clearRect(0, 0, canvas.width, canvas.height);

var radius = Math.max(
Math.abs(ev._x - tool.x0),
Math.abs(ev._y - tool.y0)
) / 2;

var x = Math.min(ev._x, tool.x0) + radius;
var y = Math.min(ev._y, tool.y0) + radius;

context.beginPath();
context.arc(x, y, radius, 0, Math.PI*2, false);
// context.arc(x, y, 5, 0, Math.PI*2, false);
context.stroke();
context.closePath();

};

    this.mouseup = function (ev) {
      if (tool.started) {
        tool.mousemove(ev);
        tool.started = false;
        drawCanvas();
      }
 };
  };

// Ellipse/oval tool
 
// Polygon tool

// Undo button

 function cmbDeleteClick(){
    if(history.length<=1)
      return
      
    history.pop()
    contextTmp.putImageData(history[history.length-1],0,0)
    updateHistorySelection()
  }
  
  function updateHistorySelection(){
    historySelect.innerHTML = ''

    history.forEach((entry,index)=>{
      let option = document.createElement('option')
      option.value = index
      option.textContent = index===0 ? 'Beginning' : 'Action '+index
      historySelect.appendChild(option)
    })

    historySelect.selectedIndex = history.length-1
  }

  function restoreHistoryAction(index){
    contextTmp.putImageData(history[index],0,0)
  }
  
  cmbDelete = document.getElementById("cmbDelete");
  cmbDelete.addEventListener("click",cmbDeleteClick, false);

  init();

}, false); } 
 

</script>
</body>
</html>

虽然这不是很有效。它将为每个动作存储 canvas 的完整位图,因此非常耗费内存。最好让绘图工具实际创建一个形状实例,可以调用它根据需要在 canvas 上重绘。

您需要做的是在进行每次更改之前跟踪绘画的完整状态,以便您可以恢复它。所以你有一个撤消数组,每当你修改canvas,右之前你做修改,你把当前的canvas状态推到数组上(canvas.toDataURL 将非常有助于封装整个图像状态)。之后再做修改。

undo时,可以pop undo数组的最后一个元素,也就是最后一次改变之前canvas的数据URL,然后重新设置canvas 到那个图像。所以像这样:

function undoLastChange() {
  const canvas = document.getElementById('canvas_ID');
  const ctx = canvas.getContext('2d');
  const img = new Image();
  img.onload = () => {
    ctx.drawImage(img, 0, 0);
  };
  img.src = undoArray.pop();
}