撤消/重做不正常,缩放后绘画也不能正常工作

我正在尝试使用撤消和重做function实现一个油漆桶工具。 问题是undo和redo第一次正常工作,但是当我多次撤消重做时,代码失败了。 任何人都可以帮我解决这个问题吗? 此外,缩放function正常,但缩放后绘画无法正常工作。 这是我的完整代码。 你可以复制粘贴,它将在你的最后工作。

   Painitng  body { width: 100%; height: auto; text-align: center; } .colorpick { widh: 100%; height: atuo; } .pick { display: inline-block; width: 30px; height: 30px; margin: 5px; cursor: pointer; } canvas { border: 2px solid silver; }        
var colorYellow = { r: 255, g: 207, b: 51 }; var context; var canvasWidth = 500; var canvasHeight = 500; var myColor = colorYellow; var curColor = myColor; var outlineImage = new Image(); var backgroundImage = new Image(); var drawingAreaX = 0; var drawingAreaY = 0; var drawingAreaWidth = 500; var drawingAreaHeight = 500; var colorLayerData; var outlineLayerData; var totalLoadResources = 2; var curLoadResNum = 0; var undoarr = new Array(); var redoarr = new Array(); var uc = 0; var rc = 0; // Clears the canvas. function clearCanvas() { context.clearRect(0, 0, context.canvas.width, context.canvas.height); } function undo() { if (undoarr.length <= 0) return; if (uc==0) { redoarr.push(undoarr.pop()); uc = 1; } var a = undoarr.pop(); colorLayerData = a; redoarr.push(a); clearCanvas(); context.putImageData(a, 0, 0); context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight); context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight); console.log(undoarr); } function redo() { if (redoarr.length <= 0) return; if (rc==0) { undoarr.push(redoarr.pop()); rc = 1; } var a = redoarr.pop(); colorLayerData = a; undoarr.push(a); clearCanvas(); context.putImageData(a, 0, 0); context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight); context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight); console.log(redoarr); } // Draw the elements on the canvas function redraw() { uc = 0; rc = 0; var locX, locY; // Make sure required resources are loaded before redrawing if (curLoadResNum < totalLoadResources) { return; // To check if images are loaded successfully or not. } clearCanvas(); // Draw the current state of the color layer to the canvas context.putImageData(colorLayerData, 0, 0); undoarr.push(context.getImageData(0, 0, canvasWidth, canvasHeight)); console.log(undoarr); redoarr = new Array(); // Draw the background context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight); // Draw the outline image on top of everything. We could move this to a separate // canvas so we did not have to redraw this everyime. context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight); } ; function matchOutlineColor(r, g, b, a) { return (r + g + b = drawingBoundTop && matchStartColor(pixelPos, startR, startG, startB)) { y -= 1; pixelPos -= canvasWidth * 4; } pixelPos += canvasWidth * 4; y += 1; reachLeft = false; reachRight = false; // Go down as long as the color matches and in inside the canvas while (y drawingBoundLeft) { if (matchStartColor(pixelPos - 4, startR, startG, startB)) { if (!reachLeft) { // Add pixel to stack pixelStack.push([x - 1, y]); reachLeft = true; } } else if (reachLeft) { reachLeft = false; } } if (x drawingAreaY && mouseY < drawingAreaY + drawingAreaHeight) && (mouseX <= drawingAreaX + drawingAreaWidth)) { paintAt(mouseX, mouseY); } }); } ; resourceLoaded = function () { curLoadResNum += 1; //if (curLoadResNum === totalLoadResources) { createMouseEvents(); redraw(); //} }; function start() { var canvas = document.createElement('canvas'); canvas.setAttribute('width', canvasWidth); canvas.setAttribute('height', canvasHeight); canvas.setAttribute('id', 'canvas'); document.getElementById('canvasDiv').appendChild(canvas); if (typeof G_vmlCanvasManager !== "undefined") { canvas = G_vmlCanvasManager.initElement(canvas); } context = canvas.getContext("2d"); backgroundImage.onload = resourceLoaded(); backgroundImage.src = "images/t1.png"; outlineImage.onload = function () { context.drawImage(outlineImage, drawingAreaX, drawingAreaY, drawingAreaWidth, drawingAreaHeight); try { outlineLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight); } catch (ex) { window.alert("Application cannot be run locally. Please run on a server."); return; } clearCanvas(); colorLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight); resourceLoaded(); }; outlineImage.src = "images/d.png"; } ; getColor = function () { }; $(document).ready(function () { start(); }); $('#zoomin').click(function () { if ($("#canvas").width()==500){ $("#canvas").width(750); $("#canvas").height(750); var ctx = canvas.getContext("2d"); ctx.drawImage(backgroundImage, 0, 0, 749, 749); ctx.drawImage(outlineImage, 0, 0, 749, 749); redraw(); } else if ($("#canvas").width()==750){ $("#canvas").width(1000); $("#canvas").height(1000); var ctx = canvas.getContext("2d"); ctx.drawImage(backgroundImage, 0, 0, 999, 999); ctx.drawImage(outlineImage, 0, 0, 999, 999); redraw(); } }); $('#zoomout').click(function () { if ($("#canvas").width() == 1000) { $("#canvas").width(750); $("#canvas").height(750); var ctx = canvas.getContext("2d"); ctx.drawImage(backgroundImage, 0, 0, 749, 749); ctx.drawImage(outlineImage, 0, 0, 749, 749); redraw(); } else if ($("#canvas").width() == 750) { $("#canvas").width(500); $("#canvas").height(500); var ctx = canvas.getContext("2d"); ctx.drawImage(backgroundImage, 0, 0, 499, 499); ctx.drawImage(outlineImage, 0, 0, 499, 499); redraw(); } });
function hello(e) { var rgb = e.replace(/^(rgb|rgba)\(/, '').replace(/\)$/, '').replace(/\s/g, '').split(','); myColor.r = parseInt(rgb[0]); myColor.g = parseInt(rgb[1]); myColor.b = parseInt(rgb[2]); curColor = myColor; console.log(curColor); }

canvas大小和州历史

canvas的尺寸

如果您曾经浏览过DOM,您会注意到许多元素都将高度和宽度作为属性,高度和宽度作为样式属性。

对于canvas,它们有两种不同的含义。 所以让我们创建一个canvas。

 var canvas = document.createElement("canvas"); 

现在可以设置canvas元素的宽度和高度。 这定义了canvas图像中的像素数(分辨率)

 canvas.width = 500; canvas.height = 500; 

默认情况下,当图像(canvas只是图像)显示在DOM中时,它以一对一的像素大小显示。 这意味着对于图像中的每个像素,页面上都有一个像素。

您可以通过设置canvas样式宽度和高度来更改此设置

 canvas.style.width = "1000px"; // Note you must add the unit type "px" in this case canvas.style.width = "1000px"; 

这不会改变canvas分辨率,只会改变显示大小。 现在,对于canvas中的每个像素,它在页面上占用4个像素。

当您使用鼠标绘制到canvas时,这会成为一个问题,因为鼠标坐标的屏幕像素不再与canvas分辨率相匹配。

解决这个问题。 并作为OP代码的一个例子。 您需要重新缩放鼠标坐标以匹配canvas分辨率。 这已添加到OP mousedown事件侦听器中。 它首先获得显示宽度/高度,然后是分辨率宽度和高度。 它通过除以显示宽度/高度来标准化鼠标坐标。 这使得鼠标坐标到0 <= mouse <1的范围,然后我们相乘以获得画布像素坐标。 由于像素需要位于整数位置(整数),因此您必须将结果置于底层。

 // assuming that the mouseX and mouseY are the mouse coords. if(this.style.width){ // make sure there is a width in the style // (assumes if width is there then height will be too var w = Number(this.style.width.replace("px","")); // warning this will not work if size is not in pixels var h = Number(this.style.height.replace("px","")); // convert the height to a number var pixelW = this.width; // get the canvas resolution var pixelH = this.height; mouseX = Math.floor((mouseX / w) * pixelW); // convert the mouse coords to pixel coords mouseY = Math.floor((mouseY / h) * pixelH); } 

这将解决您的缩放问题。 但是看看你的代码,它是一团糟,你不应该每次都在搜索nodetree,重新获取上下文。 我很惊讶它有效,但那可能是Jquery(我不知道,因为我从来没有使用它)或者可能是你在别处渲染。

国家历史

计算机程序的当前状态是定义当前状态的所有条件和数据。当您保存某个状态时保存状态,并在加载时恢复状态。

历史只是一种保存和加载状态的方法,而不会在文件系统中乱七八糟。 它有一些约定,表明统计数据存储为堆栈。 第一个是最后一个,它有一个重做堆栈,允许你重做以前的撤消,但保持正确的状态,因为状态依赖于以前的状态,重做只能从关联状态重做。 因此,如果您撤消然后绘制某些内容,则会使任何现有重做状态无效,并且应该将它们转储。

保存的状态,无论是磁盘还是撤消堆栈,都必须与当前状态分离。 如果您对当前状态进行了更改,则不希望这些更改影响已保存的状态。

我认为这是你出错的地方OP,因为你在使用colorLayerData填充(绘画)时你有一个撤消或重做你在哪里使用撤消/重做缓冲区中保留的引用数据因此当你绘制时你实际上正在改变数据仍在撤消缓冲区中。

历史经理

这是一个通用状态管理器,可以满足任何撤消/重做需求,您所要做的就是确保将当前状态收集到一个对象中。

为了帮助我写一个简单的历史经理。 它有两个缓冲区作为堆栈,一个用于undos,另一个用于redos。 它还保持当前状态,这是它所知道的最新状态。

当您推送到历史记录管理器时,它将获取它所知道的当前状态并将其推送到撤消堆栈,保存当前状态,并使任何重做数据无效(使重做数组长度为0)

撤消时,它会将当前状态推送到重做堆栈,从撤消堆栈弹出状态并将其置于当前状态,然后它将返回当前状态。

重做时,它会将当前状态推送到撤消堆栈,从重做堆栈弹出状态并将其置于当前状态,然后它将返回当前状态。

从状态管理器返回状态的副本非常重要,这样您就不会无意中更改缓冲区中存储的数据。

你可能会问。 “为什么州政府经理不能确保数据是副本?” 一个很好的问题,但这不是一个州经理的角色,它保存了州,它必须这样做,无论它需要保存什么,它本质上完全没有意识到它存储的数据的含义。 这样它就可以用于图像,文本,游戏状态,任何东西,就像文件系统一样,它不能(不应该)意识到其含义,因此知道如何创建有意义的副本。 您推送到状态管理器的数据只是像素数据的单个参考(64位长),或者您可以推送像素数据的每个字节,它不知道差异。

另外OP我已经向状态管理器添加了一些UI控件。 这允许它显示其当前状态即禁用并启用撤消重做按钮。 它对于提供反馈的良好UI设计始终很重要。

代码

您需要对代码进行以下所有更改才能使用历史记录管理器。 您可以这样做,或者只是将其作为指南并编写自己的指南。 我在检测到你的错误之前写了这个。 如果这是唯一的错误,那么您可能只需要更改。

  // your old code (from memory) colorLayerData = undoArr.pop(); context.putImageData(colorLayerData, 0, 0); // the fix same applies to redo and just makes a copy rather than use // the reference that is still stored in the undoe buff context.putImageData(undoArr, 0, 0); // put the undo onto the canvas colorLayerData = context.getImageData(0, 0, canvasWidth, canvaHeight); 

删除撤消/重做的所有代码。

将页面顶部的撤消/重做按钮更改为,使用单个function来处理这两个事件。

    

将以下两个函数添加到代码中

  function history(command){ // handles undo/redo button events. var data; if(command === "redo"){ data = historyManager.redo(); // get data for redo }else if(command === "undo"){ data = historyManager.undo(); // get data for undo } if(data !== undefined){ // if data has been found setColorLayer(data); // set the data } } // sets colour layer and creates copy into colorLayerData function setColorLayer(data){ context.putImageData(data, 0, 0); colorLayerData = context.getImageData(0, 0, canvasWidth, canvasHeight); context.drawImage(backgroundImage, 0, 0, canvasWidth, canvasHeight); context.drawImage(outlineImage, 0, 0, drawingAreaWidth, drawingAreaHeight); } 

在重绘function中,您可以替换撤消所需的内容,并在同一位置添加此行。 这会将当前状态保存在历史记录管理器中。

  historyManager.push(context.getImageData(0, 0, canvasWidth, canvasHeight)); 

在start函数中,您必须将UI元素添加到状态管理器。 这取决于您并且可以忽略,如果未定义,则统计管理器将忽略它们。

  if(historyManager !== undefined){ // only for visual feedback and not required for the history manager to function. historyManager.UI.assignUndoButton(document.querySelector("#undo-button")); historyManager.UI.assignRedoButton(document.querySelector("#redo-button")); } 

而且当然是历史经理自己的。 它封装了数据,因此除了通过接口提供外,您无法访问其内部状态。

historyManager(hM)API

  • hM.UI ui管理器只是更新并分配按钮禁用/启用状态
  • hM.UI.assignUndoButton(element)设置undo元素
  • hM.UI.assignRedoButton(element)设置重做元素
  • nM.UI.update()更新按钮状态以反映当前的内部状态。 所有内部状态都会自动调用此选项,因此只有在您更改自己的重做/撤消按钮统计信息时才需要
  • hM.reset()重置历史管理器清除所有堆栈和当前保存的状态。 加载或创建新项目时调用此方法。
  • nM.push(data)将提供的数据添加到历史记录中。
  • nM.undo()获取先前的历史状态并返回存储的数据。 如果没有数据,那么这将返回undefined。
  • nM.redo()获取下一个历史状态并返回存储的数据。 如果没有数据,那么这将返回undefined。

自调用函数创建历史管理器,通过变量historyManager访问接口

 var historyManager = (function (){ // Anon for private (closure) scope var uBuffer = []; // this is undo buff var rBuffer = []; // this is redo buff var currentState = undefined; // this holds the current history state var undoElement = undefined; var redoElement = undefined; var manager = { UI : { // UI interface just for disable and enabling redo undo buttons assignUndoButton : function(element){ undoElement = element; this.update(); }, assignRedoButton : function(element){ redoElement = element; this.update(); }, update : function(){ if(redoElement !== undefined){ redoElement.disabled = (rBuffer.length === 0); } if(undoElement !== undefined){ undoElement.disabled = (uBuffer.length === 0); } } }, reset : function(){ uBuffer.length = 0; rBuffer.length = 0; currentState = undefined; this.UI.update(); }, push : function(data){ if(currentState !== undefined){ uBuffer.push(currentState); } currentState = data; rBuffer.length = 0; this.UI.update(); }, undo : function(){ if(uBuffer.length > 0){ if(currentState !== undefined){ rBuffer.push(currentState); } currentState = uBuffer.pop(); } this.UI.update(); return currentState; // return data or unfefined }, redo : function(){ if(rBuffer.length > 0){ if(currentState !== undefined){ uBuffer.push(currentState); } currentState = rBuffer.pop(); } this.UI.update(); return currentState; }, } return manager; })(); 

这将解决您的缩放问题和撤消问题。 祝你的项目好运。

此函数matchOutlineColor 4个代表RGBA颜色的数字。

红色,绿色,蓝色,Alpha(颜色有多透明)

RGBA颜色范围为0-255,因此从0(无颜色)到255(全彩色),白色为rgba(255,255,255,255) ,黑色为rgba(0,0,0,255) ,透明为rgba(0,0,0) ,0)

此代码不会检查颜色是否为黑色,只是加在一起的红色+绿色+黄色至少小于100(总共750个)。 我怀疑该function检查颜色是否为深色。

例如,这将全部通过:

 
Dark RED
Dark GREEN
Dark BLUE