撤消/重做无法正常工作,缩放后绘画也无法正常工作
Undo / redo not working properly and painting after zoom not working properly too
我正在尝试实现具有撤消和重做功能的油漆桶工具。问题是撤消和重做第一次工作正常,但是当我多次撤消重做时,代码失败了。谁能帮我解决这个问题?缩放也有效,但缩放后绘画无法正常工作。这是我的完整代码。您只需复制粘贴即可。
<!DOCTYPE html>
<html>
<head>
<title>Painitng</title>
<style>
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;
}
</style>
</head>
<body>
<button id="zoomin">Zoom In</button>
<button id="zoomout">Zoom Out</button>
<button onclick="undo()">Undo</button>
<button onclick="redo()">Redo</button>
<div id="canvasDiv"></div>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js"></script>
<script type="text/javascript">
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 < 100 && a === 255);
}
;
function matchStartColor(pixelPos, startR, startG, startB) {
var r = outlineLayerData.data[pixelPos],
g = outlineLayerData.data[pixelPos + 1],
b = outlineLayerData.data[pixelPos + 2],
a = outlineLayerData.data[pixelPos + 3];
// If current pixel of the outline image is black
if (matchOutlineColor(r, g, b, a)) {
return false;
}
r = colorLayerData.data[pixelPos];
g = colorLayerData.data[pixelPos + 1];
b = colorLayerData.data[pixelPos + 2];
// If the current pixel matches the clicked color
if (r === startR && g === startG && b === startB) {
return true;
}
// If current pixel matches the new color
if (r === curColor.r && g === curColor.g && b === curColor.b) {
return false;
}
return true;
}
;
function colorPixel(pixelPos, r, g, b, a) {
colorLayerData.data[pixelPos] = r;
colorLayerData.data[pixelPos + 1] = g;
colorLayerData.data[pixelPos + 2] = b;
colorLayerData.data[pixelPos + 3] = a !== undefined ? a : 255;
}
;
function floodFill(startX, startY, startR, startG, startB) {
var newPos,
x,
y,
pixelPos,
reachLeft,
reachRight,
drawingBoundLeft = drawingAreaX,
drawingBoundTop = drawingAreaY,
drawingBoundRight = drawingAreaX + drawingAreaWidth - 1,
drawingBoundBottom = drawingAreaY + drawingAreaHeight - 1,
pixelStack = [[startX, startY]];
while (pixelStack.length) {
newPos = pixelStack.pop();
x = newPos[0];
y = newPos[1];
// Get current pixel position
pixelPos = (y * canvasWidth + x) * 4;
// Go up as long as the color matches and are inside the canvas
while (y >= 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 <= drawingBoundBottom && matchStartColor(pixelPos, startR, startG, startB)) {
y += 1;
colorPixel(pixelPos, curColor.r, curColor.g, curColor.b);
if (x > 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 < drawingBoundRight) {
if (matchStartColor(pixelPos + 4, startR, startG, startB)) {
if (!reachRight) {
// Add pixel to stack
pixelStack.push([x + 1, y]);
reachRight = true;
}
} else if (reachRight) {
reachRight = false;
}
}
pixelPos += canvasWidth * 4;
}
}
}
;
// Start painting with paint bucket tool starting from pixel specified by startX and startY
function paintAt(startX, startY) {
var pixelPos = (startY * canvasWidth + startX) * 4,
r = colorLayerData.data[pixelPos],
g = colorLayerData.data[pixelPos + 1],
b = colorLayerData.data[pixelPos + 2],
a = colorLayerData.data[pixelPos + 3];
if (r === curColor.r && g === curColor.g && b === curColor.b) {
// Return because trying to fill with the same color
return;
}
if (matchOutlineColor(r, g, b, a)) {
// Return because clicked outline
return;
}
floodFill(startX, startY, r, g, b);
redraw();
}
;
// Add mouse event listeners to the canvas
function createMouseEvents() {
$('#canvas').mousedown(function (e) {
// Mouse down location
var mouseX = e.pageX - this.offsetLeft,
mouseY = e.pageY - this.offsetTop;
if ((mouseY > 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 () {
};
</script>
<script type="text/javascript"> $(document).ready(function () {
start();
});</script>
<script language="javascript">
$('#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();
}
});
</script>
<div class="colorpick">
<div class="pick" style="background-color:rgb(150, 0, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 0, 152);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 151, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 0, 5);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 255, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 255, 255);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 0, 255);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 150, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 0, 150);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 255, 150);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(150, 0, 255);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 150, 255);" onclick="hello(this.style.backgroundColor);"></div>
</div>
<script>
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);
}
</script>
</body>
</html>
这个函数matchOutlineColor
接受代表RGBA颜色的4个数字。
Red, Green, Blue, Alpha (how transparent the color is)
RGBA 颜色范围从 0-255,因此从 0(无颜色)到 255(全色),白色为 rgba(255,255,255,255),黑色为 rgba(0,0,0,255) 透明为 rgba(0,0,0,0).
此代码不检查颜色是否为黑色,只是检查红色 + 绿色 + 黄色相加至少少于 100(总共 750)。我怀疑该功能会检查颜色是否为深色。
例如这将全部通过 true:
<div style="background-color:rgba(99,0,0,255)">Dark RED</div>
<div style="background-color:rgba(0,99,0,255)">Dark GREEN</div>
<div style="background-color:rgba(0,0,99,255)">Dark BLUE</div>
如果要检查边框是否为黑色可以将函数更改为
function matchOutlineColorBlack(r, g, b, a) {
//Ensures red + green + blue is none
return (r + g + b == 0 && a === 255);
};
function matchOutlineColorWhite(r, g, b, a) {
//Checks that color is white (255+255+255=750)
return (r + g + b == 750 && a === 255);
};
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 事件侦听器中。它首先获取显示 width/height 然后是分辨率宽度和高度。它通过除以显示 width/height 来标准化鼠标坐标。这将鼠标坐标带到 0 <= 鼠标 < 1 的范围内,然后我们乘以得到 canvas 像素坐标。由于像素需要位于整数位置(整数),因此您必须对结果进行取整。
// 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);
}
这将解决您的缩放问题。但是看看你的代码,它是一团糟,你不应该每次都搜索节点树,重新获取上下文。我很惊讶它的工作原理,但这可能是 Jquery(我不知道,因为我从未使用过它)或者可能是你在其他地方渲染。
州历史
计算机程序的当前状态是定义当前状态的所有条件和数据。当您保存某些内容时,您正在保存状态,而当您加载时,您恢复状态。
历史记录只是一种保存和加载状态的方式,不会在文件系统中乱七八糟。它有一些约定,表示统计信息存储为堆栈。先入后出,它有一个重做堆栈,允许您重做以前的撤消但保持正确的状态,并且因为状态依赖于以前的状态,重做只能从关联的状态重做。因此,如果您撤消然后绘制一些东西,您会使任何现有的重做状态无效,并且它们应该被丢弃。
还有保存的状态,无论是在磁盘上,还是撤销堆栈,都必须与当前状态分离。如果您对当前状态进行更改,您不希望这些更改影响保存的状态。
我认为这就是您出错的地方 OP,因为您使用 colorLayerData
进行填充(绘制),当您使用 [=132] 中保留的引用数据进行撤消或重做时=] 缓冲区因此,当您绘制时,您实际上正在更改仍在撤消缓冲区中的数据。
历史管理器
这是一个通用状态管理器,可以满足任何 undo/redo 需求,您所要做的就是确保将当前状态收集到一个对象中。
为了帮助我写了一个简单的历史管理器。它有两个缓冲区作为堆栈,一个用于撤消,一个用于重做。它还保存当前状态,这是它知道的最新状态。
当你推送到历史管理器时,它会将它知道的当前状态推送到撤消堆栈,保存当前状态,并使任何重做数据无效(使重做数组长度为 0)
当你撤消时,它会将当前状态推入重做堆栈,从撤消堆栈中弹出一个状态并将其放入当前状态,然后它将return当前状态。
当你重做时,它会将当前状态推入撤消堆栈,从重做堆栈弹出一个状态并将其放入当前状态,然后它将return当前状态。
从状态管理器中复制状态 return 很重要,这样您就不会无意中更改缓冲区中存储的数据。
你可能会问。 "why cant the state manager ensure that the data is a copy?" 一个很好的问题,但这不是状态管理器的角色,它保存状态并且无论必须保存什么都必须这样做,它本质上完全不知道它存储的数据的含义。这样它就可以用于图像、文本、游戏状态,任何东西,就像文件系统一样,它不能(不应该)知道含义,因此知道如何创建有意义的副本。您推送到状态管理器的数据只是对像素数据的单个引用(64 位长),或者您可以推送像素数据的每个字节,它不知道区别。
还有 OP 我已经向状态管理器添加了一些 UI 控件。这允许它显示其当前状态,即禁用和启用撤消重做按钮。提供反馈对于良好的 UI 设计始终很重要。
代码
您需要对代码进行以下所有更改才能使用历史记录管理器。您可以这样做,或者只是将其用作 guide 并编写您自己的。我在检测到您的错误之前写了这篇文章。如果这是唯一的错误,那么您可能只需要更改即可。
// 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);
删除 undo/redo.
的所有代码
将页面顶部的 undo/redo 按钮更改为,使用一个函数来处理两个事件。
<button id = "undo-button" onclick="history('undo')">Undo</button>
<button id = "redo-button" onclick="history('redo')">Redo</button>
在你的代码中添加以下两个函数
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);
}
在重绘功能中,您已经替换了用于撤消的内容,并在同一位置添加了这一行。这会将当前状态保存在历史管理器中。
historyManager.push(context.getImageData(0, 0, canvasWidth, canvasHeight));
在启动函数中,您必须将 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 本身。它封装了数据,因此您无法访问其内部状态,除非通过提供的接口。
historyManager (hM)API
hM.UI
ui 经理刚刚更新和分配按钮
disabled/enabled 州
hM.UI.assignUndoButton(element)
设置撤消元素
hM.UI.assignRedoButton(element)
设置重做元素
nM.UI.update()
更新按钮状态以反映当前
内部状态。所有内部状态都自动调用这个所以
仅当您更改 redo/undo 按钮统计您自己时才需要
hM.reset()
重置历史管理器清除所有堆栈和当前保存的状态。加载或创建新项目时调用此方法。
nM.push(data)
将提供的数据添加到历史记录中。
nM.undo()
获取之前的历史状态和return 存储的数据。如果没有数据,那么这将 return 未定义。
nM.redo()
获取下一个历史状态和return存储的数据。如果没有数据,那么这将 return 未定义。
自调用函数创建历史管理器,接口通过变量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;
})();
这将解决您的缩放问题和撤消问题。祝你的项目好运。
我正在尝试实现具有撤消和重做功能的油漆桶工具。问题是撤消和重做第一次工作正常,但是当我多次撤消重做时,代码失败了。谁能帮我解决这个问题?缩放也有效,但缩放后绘画无法正常工作。这是我的完整代码。您只需复制粘贴即可。
<!DOCTYPE html>
<html>
<head>
<title>Painitng</title>
<style>
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;
}
</style>
</head>
<body>
<button id="zoomin">Zoom In</button>
<button id="zoomout">Zoom Out</button>
<button onclick="undo()">Undo</button>
<button onclick="redo()">Redo</button>
<div id="canvasDiv"></div>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js"></script>
<script type="text/javascript">
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 < 100 && a === 255);
}
;
function matchStartColor(pixelPos, startR, startG, startB) {
var r = outlineLayerData.data[pixelPos],
g = outlineLayerData.data[pixelPos + 1],
b = outlineLayerData.data[pixelPos + 2],
a = outlineLayerData.data[pixelPos + 3];
// If current pixel of the outline image is black
if (matchOutlineColor(r, g, b, a)) {
return false;
}
r = colorLayerData.data[pixelPos];
g = colorLayerData.data[pixelPos + 1];
b = colorLayerData.data[pixelPos + 2];
// If the current pixel matches the clicked color
if (r === startR && g === startG && b === startB) {
return true;
}
// If current pixel matches the new color
if (r === curColor.r && g === curColor.g && b === curColor.b) {
return false;
}
return true;
}
;
function colorPixel(pixelPos, r, g, b, a) {
colorLayerData.data[pixelPos] = r;
colorLayerData.data[pixelPos + 1] = g;
colorLayerData.data[pixelPos + 2] = b;
colorLayerData.data[pixelPos + 3] = a !== undefined ? a : 255;
}
;
function floodFill(startX, startY, startR, startG, startB) {
var newPos,
x,
y,
pixelPos,
reachLeft,
reachRight,
drawingBoundLeft = drawingAreaX,
drawingBoundTop = drawingAreaY,
drawingBoundRight = drawingAreaX + drawingAreaWidth - 1,
drawingBoundBottom = drawingAreaY + drawingAreaHeight - 1,
pixelStack = [[startX, startY]];
while (pixelStack.length) {
newPos = pixelStack.pop();
x = newPos[0];
y = newPos[1];
// Get current pixel position
pixelPos = (y * canvasWidth + x) * 4;
// Go up as long as the color matches and are inside the canvas
while (y >= 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 <= drawingBoundBottom && matchStartColor(pixelPos, startR, startG, startB)) {
y += 1;
colorPixel(pixelPos, curColor.r, curColor.g, curColor.b);
if (x > 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 < drawingBoundRight) {
if (matchStartColor(pixelPos + 4, startR, startG, startB)) {
if (!reachRight) {
// Add pixel to stack
pixelStack.push([x + 1, y]);
reachRight = true;
}
} else if (reachRight) {
reachRight = false;
}
}
pixelPos += canvasWidth * 4;
}
}
}
;
// Start painting with paint bucket tool starting from pixel specified by startX and startY
function paintAt(startX, startY) {
var pixelPos = (startY * canvasWidth + startX) * 4,
r = colorLayerData.data[pixelPos],
g = colorLayerData.data[pixelPos + 1],
b = colorLayerData.data[pixelPos + 2],
a = colorLayerData.data[pixelPos + 3];
if (r === curColor.r && g === curColor.g && b === curColor.b) {
// Return because trying to fill with the same color
return;
}
if (matchOutlineColor(r, g, b, a)) {
// Return because clicked outline
return;
}
floodFill(startX, startY, r, g, b);
redraw();
}
;
// Add mouse event listeners to the canvas
function createMouseEvents() {
$('#canvas').mousedown(function (e) {
// Mouse down location
var mouseX = e.pageX - this.offsetLeft,
mouseY = e.pageY - this.offsetTop;
if ((mouseY > 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 () {
};
</script>
<script type="text/javascript"> $(document).ready(function () {
start();
});</script>
<script language="javascript">
$('#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();
}
});
</script>
<div class="colorpick">
<div class="pick" style="background-color:rgb(150, 0, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 0, 152);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 151, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 0, 5);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 255, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 255, 255);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 0, 255);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 150, 0);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(255, 0, 150);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 255, 150);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(150, 0, 255);" onclick="hello(this.style.backgroundColor);"></div>
<div class="pick" style="background-color:rgb(0, 150, 255);" onclick="hello(this.style.backgroundColor);"></div>
</div>
<script>
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);
}
</script>
</body>
</html>
这个函数matchOutlineColor
接受代表RGBA颜色的4个数字。
Red, Green, Blue, Alpha (how transparent the color is)
RGBA 颜色范围从 0-255,因此从 0(无颜色)到 255(全色),白色为 rgba(255,255,255,255),黑色为 rgba(0,0,0,255) 透明为 rgba(0,0,0,0).
此代码不检查颜色是否为黑色,只是检查红色 + 绿色 + 黄色相加至少少于 100(总共 750)。我怀疑该功能会检查颜色是否为深色。
例如这将全部通过 true:
<div style="background-color:rgba(99,0,0,255)">Dark RED</div>
<div style="background-color:rgba(0,99,0,255)">Dark GREEN</div>
<div style="background-color:rgba(0,0,99,255)">Dark BLUE</div>
如果要检查边框是否为黑色可以将函数更改为
function matchOutlineColorBlack(r, g, b, a) {
//Ensures red + green + blue is none
return (r + g + b == 0 && a === 255);
};
function matchOutlineColorWhite(r, g, b, a) {
//Checks that color is white (255+255+255=750)
return (r + g + b == 750 && a === 255);
};
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 事件侦听器中。它首先获取显示 width/height 然后是分辨率宽度和高度。它通过除以显示 width/height 来标准化鼠标坐标。这将鼠标坐标带到 0 <= 鼠标 < 1 的范围内,然后我们乘以得到 canvas 像素坐标。由于像素需要位于整数位置(整数),因此您必须对结果进行取整。
// 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);
}
这将解决您的缩放问题。但是看看你的代码,它是一团糟,你不应该每次都搜索节点树,重新获取上下文。我很惊讶它的工作原理,但这可能是 Jquery(我不知道,因为我从未使用过它)或者可能是你在其他地方渲染。
州历史
计算机程序的当前状态是定义当前状态的所有条件和数据。当您保存某些内容时,您正在保存状态,而当您加载时,您恢复状态。
历史记录只是一种保存和加载状态的方式,不会在文件系统中乱七八糟。它有一些约定,表示统计信息存储为堆栈。先入后出,它有一个重做堆栈,允许您重做以前的撤消但保持正确的状态,并且因为状态依赖于以前的状态,重做只能从关联的状态重做。因此,如果您撤消然后绘制一些东西,您会使任何现有的重做状态无效,并且它们应该被丢弃。
还有保存的状态,无论是在磁盘上,还是撤销堆栈,都必须与当前状态分离。如果您对当前状态进行更改,您不希望这些更改影响保存的状态。
我认为这就是您出错的地方 OP,因为您使用 colorLayerData
进行填充(绘制),当您使用 [=132] 中保留的引用数据进行撤消或重做时=] 缓冲区因此,当您绘制时,您实际上正在更改仍在撤消缓冲区中的数据。
历史管理器
这是一个通用状态管理器,可以满足任何 undo/redo 需求,您所要做的就是确保将当前状态收集到一个对象中。
为了帮助我写了一个简单的历史管理器。它有两个缓冲区作为堆栈,一个用于撤消,一个用于重做。它还保存当前状态,这是它知道的最新状态。
当你推送到历史管理器时,它会将它知道的当前状态推送到撤消堆栈,保存当前状态,并使任何重做数据无效(使重做数组长度为 0)
当你撤消时,它会将当前状态推入重做堆栈,从撤消堆栈中弹出一个状态并将其放入当前状态,然后它将return当前状态。
当你重做时,它会将当前状态推入撤消堆栈,从重做堆栈弹出一个状态并将其放入当前状态,然后它将return当前状态。
从状态管理器中复制状态 return 很重要,这样您就不会无意中更改缓冲区中存储的数据。
你可能会问。 "why cant the state manager ensure that the data is a copy?" 一个很好的问题,但这不是状态管理器的角色,它保存状态并且无论必须保存什么都必须这样做,它本质上完全不知道它存储的数据的含义。这样它就可以用于图像、文本、游戏状态,任何东西,就像文件系统一样,它不能(不应该)知道含义,因此知道如何创建有意义的副本。您推送到状态管理器的数据只是对像素数据的单个引用(64 位长),或者您可以推送像素数据的每个字节,它不知道区别。
还有 OP 我已经向状态管理器添加了一些 UI 控件。这允许它显示其当前状态,即禁用和启用撤消重做按钮。提供反馈对于良好的 UI 设计始终很重要。
代码
您需要对代码进行以下所有更改才能使用历史记录管理器。您可以这样做,或者只是将其用作 guide 并编写您自己的。我在检测到您的错误之前写了这篇文章。如果这是唯一的错误,那么您可能只需要更改即可。
// 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);
删除 undo/redo.
的所有代码将页面顶部的 undo/redo 按钮更改为,使用一个函数来处理两个事件。
<button id = "undo-button" onclick="history('undo')">Undo</button>
<button id = "redo-button" onclick="history('redo')">Redo</button>
在你的代码中添加以下两个函数
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);
}
在重绘功能中,您已经替换了用于撤消的内容,并在同一位置添加了这一行。这会将当前状态保存在历史管理器中。
historyManager.push(context.getImageData(0, 0, canvasWidth, canvasHeight));
在启动函数中,您必须将 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 本身。它封装了数据,因此您无法访问其内部状态,除非通过提供的接口。
historyManager (hM)API
hM.UI
ui 经理刚刚更新和分配按钮 disabled/enabled 州hM.UI.assignUndoButton(element)
设置撤消元素hM.UI.assignRedoButton(element)
设置重做元素nM.UI.update()
更新按钮状态以反映当前 内部状态。所有内部状态都自动调用这个所以 仅当您更改 redo/undo 按钮统计您自己时才需要hM.reset()
重置历史管理器清除所有堆栈和当前保存的状态。加载或创建新项目时调用此方法。nM.push(data)
将提供的数据添加到历史记录中。nM.undo()
获取之前的历史状态和return 存储的数据。如果没有数据,那么这将 return 未定义。nM.redo()
获取下一个历史状态和return存储的数据。如果没有数据,那么这将 return 未定义。
自调用函数创建历史管理器,接口通过变量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;
})();
这将解决您的缩放问题和撤消问题。祝你的项目好运。