Reputation: 1954
I have an HTML5 canvas that allows user to draw various shapes and a brush feature allowing the user to freehand. I am using the command pattern for these implementations. The problem I am facing is the "undo" functionality. It works perfectly fine for all the other commands however when it comes to the "brush" there seems to be an issue with it.
The way the brush works is it stores points with each drag of the mouse, once a new point is added the entire array of points are redrawn on the screen. Only when the user releases the mouse does the drawing stop. You can probably see the problem immediately, the older the points the more they get redrawn on the screen. This causes the line to look much darker in color than it actually is.
My solution is to only connect the last point n-1 with point n-2 however that literally only redraws the last 2 points. I do not understand fully how the canvas is working and why this method does not work YET redrawing overtop points seems to work...
Here is some code outlining the key parts.
BrushStrat.prototype.mousemove=function(event){
if(this.command!=null){
//add this point to the list
this.command.addPoint({x:event.canvasX, y:event.canvasY});
//redraw all points
this.command.draw(this.paint.context);
}
}
BrushCommand.prototype.draw=function(context){
if(this.points.length==0){
return;
}
context.beginPath();
context.strokeStyle = this.strokeStyle;
context.lineWidth=this.lineWidth;
//start from the first point
context.moveTo(this.points[0].x,this.points[0].y);
//redraw all subsequent points
for(var i=1;i<this.points.length;i++){
context.lineTo(this.points[i].x, this.points[i].y);
}
context.stroke();
}
Upvotes: 0
Views: 2201
Reputation: 105015
Freehand brush strokes using the Command Pattern
You made a good choice in implementing the “command pattern” to track your user’s freestyle brush strokes!
Every drawing between mouseDown and mouseUp is treated as a "drag group".
Every "drag group" is added to a master array (CommandStack[]).
Then you can easily UNDO the last drawing by simply removing the last group on CommandStack[].
This is what happens during a drag cycle by the user:
MouseDown:
MouseMove:
MouseUp:
Then you can simply and efficiently UNDO strokes:
Here is code and a Fiddle: http://jsfiddle.net/m1erickson/nUbzS/
<!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 strokeColor="red";
var strokeWidth=2;
var canMouseX;
var canMouseY;
var canvasOffset=$("#canvas").offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;
var isMouseDown=false;
// command pattern -- undo
var commandStack=new Array();
var newStart;
var newPoints=new Array();
function handleMouseDown(e){
canMouseX=parseInt(e.clientX-offsetX);
canMouseY=parseInt(e.clientY-offsetY);
$("#downlog").html("Down: "+ canMouseX + " / " + canMouseY);
// Put your mousedown stuff here
lastX=canMouseX;
lastY=canMouseY;
isMouseDown=true;
// command pattern stuff
newStart={x:canMouseX,y:canMouseY};
newPoints=new Array();
}
function handleMouseUp(e){
canMouseX=parseInt(e.clientX-offsetX);
canMouseY=parseInt(e.clientY-offsetY);
$("#uplog").html("Up: "+ canMouseX + " / " + canMouseY);
// Put your mouseup stuff here
isMouseDown=false;
// command pattern stuff
commandStack.push({moveTo:newStart,points:newPoints});
}
function handleMouseOut(e){
canMouseX=parseInt(e.clientX-offsetX);
canMouseY=parseInt(e.clientY-offsetY);
$("#outlog").html("Out: "+ canMouseX + " / " + canMouseY);
// Put your mouseOut stuff here
isMouseDown=false;
}
function handleMouseMove(e){
canMouseX=parseInt(e.clientX-offsetX);
canMouseY=parseInt(e.clientY-offsetY);
$("#movelog").html("Move: "+ canMouseX + " / " + canMouseY);
// Put your mousemove stuff here
if(isMouseDown){
ctx.beginPath();
ctx.moveTo(lastX,lastY);
ctx.lineTo(canMouseX,canMouseY);
ctx.stroke();
lastX=canMouseX;
lastY=canMouseY;
// command pattern stuff
newPoints.push({x:canMouseX,y:canMouseY});
}
}
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUp(e);});
$("#canvas").mouseout(function(e){handleMouseOut(e);});
$("#undo").click(function(e){ undoLast(); });
function undoLast(){
commandStack.pop();
redrawAll();
}
function redrawAll(){
// prep for commandStack redraws
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.save()
ctx.strokeStyle="blue";
ctx.beginPath();
// loop through the commandStack and draw all nodes
for(var s=0;s<commandStack.length;s++){
// move to the starting point of this node
var start=commandStack[s].moveTo;
ctx.moveTo(start.x,start.y);
// draw each line segment in this node
var pts=commandStack[s].points;
for(var p=0;p<pts.length;p++){
ctx.lineTo(pts[p].x,pts[p].y);
} // end for(p)
} // end for(s)
// actually draw the lines
ctx.stroke();
ctx.restore();
}
}); // end $(function(){});
</script>
</head>
<body>
<p id="downlog">Down</p>
<p id="movelog">Move</p>
<p id="uplog">Up</p>
<p id="outlog">Out</p>
<canvas id="canvas" width=300 height=300></canvas>
<button id="undo">Undo</button>
</body>
</html>
Upvotes: 2
Reputation: 9465
Clear the canvas, or atleast the drawing area, before redrawing each array point.
Edit Yeah sorry, I assumed you had a game loop. However, it's still a valid option: use two canvases for the drawing area. One for the "current" shape/squiggle being drawn (which you clear before drawing each point) and another persistent layer which has all the completed shapes/squiggles.
So to recap, when a user clicks and drags this shape is drawn to the current layer. When the user releases the mouse, the image is now "locked it" and transferred to the persistent layer.
Hope that makes sense.
Upvotes: 2