javascript 使用 Canvas 撤消/重做绘画程序
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/17150610/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Undo/Redo for paint program using Canvas
提问by Steven Barrett
I need to implement a undo/redo system for my paint program: http://www.taffatech.com/Paint.html
我需要为我的绘画程序实现一个撤消/重做系统:http: //www.taffatech.com/Paint.html
My idea that I came up with is to have 2 arrays stacks, one for undo and 1 for redo. Anytime you draw and release the mouse it saves the canvas image to the undo array stack by push. if you draw something else and release it will do the same. However if you click undo it will pop the top image of undo array and print that to canvas and then push it onto the redo stack.
我想出的想法是有 2 个数组堆栈,一个用于撤消,1 个用于重做。每当您绘制并释放鼠标时,它都会通过推送将画布图像保存到撤消数组堆栈中。如果你画别的东西然后释放它也会做同样的事情。但是,如果您单击撤消,它将弹出撤消数组的顶部图像并将其打印到画布,然后将其推送到重做堆栈上。
redo when clicked will pop from itself and push to undo. the top of undo will be printed after each mouse off.
单击时重做将从自身弹出并推送以撤消。每次鼠标关闭后都会打印撤销的顶部。
Is this the right way or is there a better one?
这是正确的方法还是有更好的方法?
回答by markE
A word of warning!
一句警告!
Saving the whole canvas as an image for undo/redo is memory intensive and a performance killer.
将整个画布保存为用于撤消/重做的图像是内存密集型和性能杀手。
However, your idea of progressively saving the user's drawings in an array is still a good idea.
但是,您将用户的绘图逐步保存在数组中的想法仍然是一个好主意。
Instead of saving the whole canvas as an image, just create an array of points to record every mousemove the user makes as they are drawing. This is your “drawing array” that can be used to fully redraw your canvas.
无需将整个画布保存为图像,只需创建一个点数组来记录用户在绘图时所做的每一次鼠标移动。这是您的“绘图数组”,可用于完全重绘画布。
Whenever the user drags the mouse they are creating a polyline (a group of connected line segments). When the user drags to create a line, save that mousemove point to your drawing array and extend their polyline to the current mousemove position.
每当用户拖动鼠标时,他们正在创建多段线(一组连接的线段)。当用户拖动以创建一条线时,将该鼠标移动点保存到您的绘图数组并将其多段线扩展到当前鼠标移动位置。
function handleMouseMove(e) {
// calc where the mouse is on the canvas
mouseX = parseInt(e.clientX - offsetX);
mouseY = parseInt(e.clientY - offsetY);
// if the mouse is being dragged (mouse button is down)
// then keep drawing a polyline to this new mouse position
if (isMouseDown) {
// extend the polyline
ctx.lineTo(mouseX, mouseY);
ctx.stroke();
// save this x/y because we might be drawing from here
// on the next mousemove
lastX = mouseX;
lastY = mouseY;
// Command pattern stuff: Save the mouse position and
// the size/color of the brush to the "undo" array
points.push({
x: mouseX,
y: mouseY,
size: brushSize,
color: brushColor,
mode: "draw"
});
}
}
If the user wants to “undo”, just pop the last point off the drawing array:
如果用户想要“撤消”,只需弹出绘图数组的最后一个点:
function undoLastPoint() {
// remove the last drawn point from the drawing array
var lastPoint=points.pop();
// add the "undone" point to a separate redo array
redoStack.unshift(lastPoint);
// redraw all the remaining points
redrawAll();
}
Redo is logically more tricky.
重做在逻辑上更棘手。
The simplest Redo is when the user can only redo immediately after an undo. Save each “undo” point in your separate “redo” array. Then if the user wants to redo, you can just add the redo bits back to the to the main array.
最简单的重做是用户只能在撤消后立即重做。将每个“撤消”点保存在单独的“重做”数组中。然后,如果用户想要重做,您只需将重做位添加回主数组。
The complication is if you let the user “redo” after they have done more drawing.
复杂的是,如果您让用户在完成更多绘图后“重做”。
For example, you could end up with a dog with 2 tails: a newly drawn tail and a second “redo” tail !
例如,您最终可能会得到一条有两条尾巴的狗:一条新绘制的尾巴和第二条“重做”尾巴!
So if you allow redo's after additional drawing, you would need a way to keep the user from being confused during redo. Matt Greer's idea of “layering” redos is one good way. Just alter that idea by saving the redo points, not the entire canvas image. Then the user could toggle the redo on/off to see if they would like to keep their redo.
因此,如果您在附加绘图后允许重做,则需要一种方法来防止用户在重做期间感到困惑。Matt Greer 的“分层”重做的想法是一种好方法。只需通过保存重做点来改变这个想法,而不是整个画布图像。然后用户可以打开/关闭重做以查看他们是否想要保留他们的重做。
Here is an example of using an undo array I created for a previous question: Drawing to canvas like in paint
这是使用我为上一个问题创建的撤消数组的示例: Drawing to canvas like in paint
Here is that code and a Fiddle: http://jsfiddle.net/m1erickson/AEYYq/
这是代码和小提琴:http: //jsfiddle.net/m1erickson/AEYYq/
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<!--[if lt IE 9]><script type="text/javascript" src="../excanvas.js"></script><![endif]-->
<style>
body{ background-color: ivory; }
canvas{border:1px solid red;}
</style>
<script>
$(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var lastX;
var lastY;
var mouseX;
var mouseY;
var canvasOffset=$("#canvas").offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;
var isMouseDown=false;
var brushSize=20;
var brushColor="#ff0000";
var points=[];
function handleMouseDown(e){
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// Put your mousedown stuff here
ctx.beginPath();
if(ctx.lineWidth!=brushSize){ctx.lineWidth=brushSize;}
if(ctx.strokeStyle!=brushColor){ctx.strokeStyle=brushColor;}
ctx.moveTo(mouseX,mouseY);
points.push({x:mouseX,y:mouseY,size:brushSize,color:brushColor,mode:"begin"});
lastX=mouseX;
lastY=mouseY;
isMouseDown=true;
}
function handleMouseUp(e){
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// Put your mouseup stuff here
isMouseDown=false;
points.push({x:mouseX,y:mouseY,size:brushSize,color:brushColor,mode:"end"});
}
function handleMouseMove(e){
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// Put your mousemove stuff here
if(isMouseDown){
ctx.lineTo(mouseX,mouseY);
ctx.stroke();
lastX=mouseX;
lastY=mouseY;
// command pattern stuff
points.push({x:mouseX,y:mouseY,size:brushSize,color:brushColor,mode:"draw"});
}
}
function redrawAll(){
if(points.length==0){return;}
ctx.clearRect(0,0,canvas.width,canvas.height);
for(var i=0;i<points.length;i++){
var pt=points[i];
var begin=false;
if(ctx.lineWidth!=pt.size){
ctx.lineWidth=pt.size;
begin=true;
}
if(ctx.strokeStyle!=pt.color){
ctx.strokeStyle=pt.color;
begin=true;
}
if(pt.mode=="begin" || begin){
ctx.beginPath();
ctx.moveTo(pt.x,pt.y);
}
ctx.lineTo(pt.x,pt.y);
if(pt.mode=="end" || (i==points.length-1)){
ctx.stroke();
}
}
ctx.stroke();
}
function undoLast(){
points.pop();
redrawAll();
}
ctx.lineJoin = "round";
ctx.fillStyle=brushColor;
ctx.lineWidth=brushSize;
$("#brush5").click(function(){ brushSize=5; });
$("#brush10").click(function(){ brushSize=10; });
// Important! Brush colors must be defined in 6-digit hex format only
$("#brushRed").click(function(){ brushColor="#ff0000"; });
$("#brushBlue").click(function(){ brushColor="#0000ff"; });
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUp(e);});
// hold down the undo button to erase the last line segment
var interval;
$("#undo").mousedown(function() {
interval = setInterval(undoLast, 100);
}).mouseup(function() {
clearInterval(interval);
});
}); // end $(function(){});
</script>
</head>
<body>
<p>Drag to draw. Use buttons to change lineWidth/color</p>
<canvas id="canvas" width=300 height=300></canvas><br>
<button id="undo">Hold this button down to Undo</button><br><br>
<button id="brush5">5px Brush</button>
<button id="brush10">10px Brush</button>
<button id="brushRed">Red Brush</button>
<button id="brushBlue">Blue Brush</button>
</body>
</html>
回答by Matt Greer
That's the basic idea of what I did for my paint app; and it does work well, except this approach can be very memory intensive.
这就是我为我的绘画应用程序所做的工作的基本思想;它确实工作得很好,除了这种方法可能非常占用内存。
So a slight tweak that I did was only store undo/redo clips that are the size of the last action the user did. So if they just draw a tiny smidge of the canvas, you can store a tiny canvas that is a fraction of the full size, and save a lot of memory.
所以我做的一个小调整是只存储用户最后一个动作大小的撤消/重做剪辑。所以如果他们只是画画布的一小部分,你可以存储一个很小的画布,它是完整尺寸的一小部分,并节省大量内存。
My undo/redo system lives in Painter.js. I wrote this app two years ago, so my memory is a bit hazy but I can help explain things if you decide to decode what I did.
我的撤消/重做系统位于Painter.js 中。我两年前写了这个应用程序,所以我的记忆有点模糊,但如果你决定解码我所做的,我可以帮助解释事情。
回答by pedrorezende_
Try to implement the Command design pattern.
尝试实现命令设计模式。
There's another similar question here: Best design pattern for "undo" feature
这里还有另一个类似的问题:“撤消”功能的最佳设计模式