I am making a paint app in HTML using JavaScript and Flask-Python. Currently, I am able to draw lots of pencil drawings and shapes like rectangles/circles without any problem. The following functionality that I am trying to implement for this application is a undo function.
I store the strokes and the canvas drawing data in a JS object in the following manner:
canvas_data = { "pencil": [], "line": [], "rectangle": [], "circle": [], "eraser": [], "last_action": -1 };
All of the key names should be self-explanatory except last_action. I use this last_action variable to know the last category used by the user so that I can later use this information to implement the undo function.
var canvas = document.getElementById("paint");
var ctx = canvas.getContext("2d");
var pi2 = Math.PI * 2;
var resizerRadius = 8;
var rr = resizerRadius * resizerRadius;
var width = canvas.width;
var height = canvas.height;
var curX, curY, prevX, prevY;
var hold = false;
ctx.lineWidth = 2;
var fill_value = true;
var stroke_value = false;
var canvas_data = {
"pencil": [],
"line": [],
"rectangle": [],
"circle": [],
"eraser": [],
"last_action": -1
};
// //connect to postgres client
// var pg = require('pg');
// var conString = "postgres://postgres:database1@localhost:5432/sketch2photo";
// client = new pg.Client(conString);
function color(color_value) {
ctx.strokeStyle = color_value;
ctx.fillStyle = color_value;
}
function add_pixel() {
ctx.lineWidth += 1;
}
function reduce_pixel() {
if (ctx.lineWidth == 1) {
ctx.lineWidth = 1;
} else {
ctx.lineWidth -= 1;
}
}
function fill() {
fill_value = true;
stroke_value = false;
}
function outline() {
fill_value = false;
stroke_value = true;
}
function reset() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
canvas_data = {
"pencil": [],
"line": [],
"rectangle": [],
"circle": [],
"eraser": [],
"last_action": -1
};
}
// pencil tool
function pencil(data, targetX, targetY, targetWidth, targetHeight) {
canvas.onmousedown = function(e) {
curX = e.clientX - canvas.offsetLeft;
curY = e.clientY - canvas.offsetTop;
hold = true;
prevX = curX;
prevY = curY;
ctx.beginPath();
ctx.moveTo(prevX, prevY);
};
canvas.onmousemove = function(e) {
if (hold) {
curX = e.clientX - canvas.offsetLeft;
curY = e.clientY - canvas.offsetTop;
draw();
}
};
canvas.onmouseup = function(e) {
hold = false;
};
canvas.onmouseout = function(e) {
hold = false;
};
function draw() {
ctx.lineTo(curX, curY);
ctx.stroke();
canvas_data.pencil.push({
"startx": prevX,
"starty": prevY,
"endx": curX,
"endy": curY,
"thick": ctx.lineWidth,
"color": ctx.strokeStyle
});
canvas_data.last_action = 0;
}
}
// line tool
function line() {
canvas.onmousedown = function(e) {
img = ctx.getImageData(0, 0, width, height);
prevX = e.clientX - canvas.offsetLeft;
prevY = e.clientY - canvas.offsetTop;
hold = true;
};
canvas.onmousemove = function linemove(e) {
if (hold) {
ctx.putImageData(img, 0, 0);
curX = e.clientX - canvas.offsetLeft;
curY = e.clientY - canvas.offsetTop;
ctx.beginPath();
ctx.moveTo(prevX, prevY);
ctx.lineTo(curX, curY);
ctx.stroke();
canvas_data.line.push({
"startx": prevX,
"starty": prevY,
"endx": curX,
"endY": curY,
"thick": ctx.lineWidth,
"color": ctx.strokeStyle
});
ctx.closePath();
canvas_data.last_action = 1;
}
};
canvas.onmouseup = function(e) {
hold = false;
};
canvas.onmouseout = function(e) {
hold = false;
};
}
// rectangle tool
function rectangle() {
canvas.onmousedown = function(e) {
img = ctx.getImageData(0, 0, width, height);
prevX = e.clientX - canvas.offsetLeft;
prevY = e.clientY - canvas.offsetTop;
hold = true;
};
canvas.onmousemove = function(e) {
if (hold) {
ctx.putImageData(img, 0, 0);
curX = e.clientX - canvas.offsetLeft - prevX;
curY = e.clientY - canvas.offsetTop - prevY;
ctx.strokeRect(prevX, prevY, curX, curY);
if (fill_value) {
ctx.fillRect(prevX, prevY, curX, curY);
}
canvas_data.rectangle.push({
"startx": prevX,
"starty": prevY,
"width": curX,
"height": curY,
"thick": ctx.lineWidth,
"stroke": stroke_value,
"stroke_color": ctx.strokeStyle,
"fill": fill_value,
"fill_color": ctx.fillStyle
});
canvas_data.last_action = 2;
}
};
canvas.onmouseup = function(e) {
hold = false;
};
canvas.onmouseout = function(e) {
hold = false;
};
}
// circle tool
function circle() {
canvas.onmousedown = function(e) {
img = ctx.getImageData(0, 0, width, height);
prevX = e.clientX - canvas.offsetLeft;
prevY = e.clientY - canvas.offsetTop;
hold = true;
};
canvas.onmousemove = function(e) {
if (hold) {
ctx.putImageData(img, 0, 0);
curX = e.clientX - canvas.offsetLeft;
curY = e.clientY - canvas.offsetTop;
ctx.beginPath();
ctx.arc(Math.abs(curX + prevX) / 2, Math.abs(curY + prevY) / 2, Math.sqrt(Math.pow(curX - prevX, 2) + Math.pow(curY - prevY, 2)) / 2, 0, Math.PI * 2, true);
ctx.closePath();
ctx.stroke();
if (fill_value) {
ctx.fill();
}
canvas_data.circle.push({
"startx": prevX,
"starty": prevY,
"radius": curX - prevX,
"thick": ctx.lineWidth,
"stroke": stroke_value,
"stroke_color": ctx.strokeStyle,
"fill": fill_value,
"fill_color": ctx.fillStyle
});
canvas_data.last_action = 3;
}
};
canvas.onmouseup = function(e) {
hold = false;
};
canvas.onmouseout = function(e) {
hold = false;
};
}
// eraser tool
function eraser() {
canvas.onmousedown = function(e) {
curX = e.clientX - canvas.offsetLeft;
curY = e.clientY - canvas.offsetTop;
hold = true;
prevX = curX;
prevY = curY;
ctx.beginPath();
ctx.moveTo(prevX, prevY);
};
canvas.onmousemove = function(e) {
if (hold) {
curX = e.clientX - canvas.offsetLeft;
curY = e.clientY - canvas.offsetTop;
draw();
}
};
canvas.onmouseup = function(e) {
hold = false;
};
canvas.onmouseout = function(e) {
hold = false;
};
function draw() {
ctx.lineTo(curX, curY);
var curr_strokeStyle = ctx.strokeStyle;
ctx.strokeStyle = "#ffffff";
ctx.stroke();
canvas_data.pencil.push({
"startx": prevX,
"starty": prevY,
"endx": curX,
"endy": curY,
"thick": ctx.lineWidth,
"color": ctx.strokeStyle
});
canvas_data.last_action = 4;
ctx.strokeStyle = curr_strokeStyle;
}
}
// Function to undo the last action by the user
function undo_pixel() {
// Print that function has been called
console.log("undo_pixel() called");
// Print the last action that was performed
console.log(canvas_data.last_action);
switch (canvas_data.last_action) {
case 0:
case 4:
console.log("Case 0 or 4");
canvas_data.pencil.pop();
canvas_data.last_action = -1;
break;
case 1:
//Undo the last line drawn
console.log("Case 1");
canvas_data.line.pop();
canvas_data.last_action = -1;
break;
case 2:
//Undo the last rectangle drawn
console.log("Case 2");
canvas_data.rectangle.pop();
canvas_data.last_action = -1;
break;
case 3:
//Undo the last circle drawn
console.log("Case 3");
canvas_data.circle.pop();
canvas_data.last_action = -1;
break;
default:
break;
}
// Redraw the canvas
redraw_canvas();
}
// Function to redraw all the shapes on the canvas
function redraw_canvas() {
// Redraw all the shapes on the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Redraw the pencil data
canvas_data.pencil.forEach(function(p) {
ctx.beginPath();
ctx.moveTo(p.startx, p.starty);
ctx.lineTo(p.endx, p.endy);
ctx.lineWidth = p.thick;
ctx.strokeStyle = p.color;
ctx.stroke();
});
// Redraw the line data
canvas_data.line.forEach(function(l) {
ctx.beginPath();
ctx.moveTo(l.startx, l.starty);
ctx.lineTo(l.endx, l.endy);
ctx.lineWidth = l.thick;
ctx.strokeStyle = l.color;
ctx.stroke();
});
// Redraw the rectangle data
canvas_data.rectangle.forEach(function(r) {
ctx.beginPath();
ctx.rect(r.startx, r.starty, r.width, r.height);
ctx.lineWidth = r.thick;
ctx.strokeStyle = r.color;
if (r.fill) {
ctx.fillStyle = r.fill_color;
ctx.fillRect(startx, starty, width, height);
}
ctx.stroke();
});
// Redraw the circle data
canvas_data.circle.forEach(function(c) {
// "startx": prevX, "starty": prevY, "radius": curX - prevX, "thick": ctx.lineWidth, "stroke": stroke_value, "stroke_color": ctx.strokeStyle, "fill": fill_value, "fill_color": ctx.fillStyle
ctx.beginPath();
ctx.arc(c.startx, c.starty, c.radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
if (c.fill) {
ctx.fillStyle = c.fill_color;
ctx.fill();
}
});
}
$("#paint1").mousedown(function(e) {
handleMouseDown(e);
});
$("#paint1").mouseup(function(e) {
handleMouseUp(e);
});
$("#paint1").mouseout(function(e) {
handleMouseOut(e);
});
$("#paint1").mousemove(function(e) {
handleMouseMove(e);
});
html {
min-width: 1500px;
position: relative;
}
#toolset {
width: 100px;
height: 340px;
position: absolute;
left: 0px;
top: 50px;
background: #35d128;
}
#paint {
position: absolute;
left: 130px;
top: 50px;
}
#colorset {
position: absolute;
left: 0px;
top: 450px;
width: 300px;
}
#title {
position: absolute;
left: 500px;
}
#penciltool {
background: #358128;
color: #f3f3f3;
width: 80px;
height: 25px;
border: 1px solid #33842a;
-webkit-border-radius: 0 15px 15px 0;
-moz-border-radius: 0 15px 15px 0;
box-shadow: rgba(0, 0, 0, .75) 0 2px 6px;
}
#linetool {
background: #358128;
color: #f3f3f3;
width: 80px;
height: 25px;
border: 1px solid #33842a;
-webkit-border-radius: 0 15px 15px 0;
-moz-border-radius: 0 15px 15px 0;
box-shadow: rgba(0, 0, 0, .75) 0 2px 6px;
}
#rectangletool {
background: #358128;
color: #f3f3f3;
width: 80px;
height: 25px;
border: 1px solid #33842a;
-webkit-border-radius: 0 15px 15px 0;
-moz-border-radius: 0 15px 15px 0;
box-shadow: rgba(0, 0, 0, .75) 0 2px 6px;
}
#circletool {
background: #358128;
color: #f3f3f3;
width: 80px;
height: 25px;
border: 1px solid #33842a;
-webkit-border-radius: 0 15px 15px 0;
-moz-border-radius: 0 15px 15px 0;
box-shadow: rgba(0, 0, 0, .75) 0 2px 6px;
}
#erasertool {
background: #358128;
color: #f3f3f3;
width: 80px;
height: 25px;
border: 1px solid #33842a;
-webkit-border-radius: 0 15px 15px 0;
-moz-border-radius: 0 15px 15px 0;
box-shadow: rgba(0, 0, 0, .75) 0 2px 6px;
}
#resettool {
background: #358128;
color: #f3f3f3;
width: 80px;
height: 25px;
border: 1px solid #33842a;
-webkit-border-radius: 0 15px 15px 0;
-moz-border-radius: 0 15px 15px 0;
box-shadow: rgba(0, 0, 0, .75) 0 2px 6px;
}
<html>
<head>
<title>Paint App</title>
</head>
<body>
<p style="text-align:left; font: bold 35px/35px Georgia, serif;">
PaintApp
<div align="right">
<link rel="stylesheet" type="text/css" href="style.css">
<body onload="pencil(`{{ data }}`, `{{ targetx }}`, `{{ targety }}`, `{{ sizex }}`, `{{ sizey }}`)">
<p>
<table>
<tr>
<td>
<fieldset id="toolset" style="margin-top: 3%;">
<br>
<br>
<button id="penciltool" type="button" style="height: 15px; width: 100px;" onclick="pencil()">Pencil</button>
<br>
<br>
<br>
<button id="linetool" type="button" style="height: 15px; width: 100px;" onclick="line()">Line</button>
<br>
<br>
<br>
<button id="rectangletool" type="button" style="height: 15px; width: 100px;" onclick="rectangle()">Rectangle</button>
<br>
<br>
<br>
<button id="circletool" type="button" style="height: 15px; width: 100px;" onclick="circle()">Circle</button>
<br>
<br>
<br>
<button id="erasertool" type="button" style="height: 15px; width: 100px;" onclick="eraser()">Eraser</button>
<br>
<br>
<br>
<button id="resettool" type="button" style="height: 15px; width: 100px;" onclick="reset()">Reset</button>
</fieldset>
</td>
<td>
<canvas id="paint" width="500vw" height="350vw" style="border: 5px solid #000000; margin-top: 3%;"></canvas>
</td>
</tr>
</table>
</p>
<fieldset id="colorset" style="margin-top: 1.8%;">
<table>
<tr>
<td><button style="height: 15px; width: 80px;" onclick="fill()">Fill</button>
<td><button style="background-color: #000000; height: 15px; width: 15px;" onclick="color('#000000')"></button>
<td><button style="background-color: #B0171F; height: 15px; width: 15px;" onclick="color('#B0171F')"></button>
<td><button style="background-color: #DA70D6; height: 15px; width: 15px;" onclick="color('#DA70D6')"></button>
<td><button style="background-color: #8A2BE2; height: 15px; width: 15px;" onclick="color('#8A2BE2')"></button>
<td><button style="background-color: #0000FF; height: 15px; width: 15px;" onclick="color('#0000FF')"></button>
<td><button style="background-color: #4876FF; height: 15px; width: 15px;" onclick="color('#4876FF')"></button>
<td><button style="background-color: #CAE1FF; height: 15px; width: 15px;" onclick="color('#CAE1FF')"></button>
<td><button style="background-color: #6E7B8B; height: 15px; width: 15px;" onclick="color('#6E7B8B')"></button>
<td><button style="background-color: #00C78C; height: 15px; width: 15px;" onclick="color('#00C78C')"></button>
<td><button style="background-color: #00FA9A; height: 15px; width: 15px;" onclick="color('#00FA9A')"></button>
<td><button style="background-color: #00FF7F; height: 15px; width: 15px;" onclick="color('#00FF7F')"></button>
<td><button style="background-color: #00C957; height: 15px; width: 15px;" onclick="color('#00C957')"></button>
<td><button style="background-color: #FFFF00; height: 15px; width: 15px;" onclick="color('#FFFF00')"></button>
<td><button style="background-color: #CDCD00; height: 15px; width: 15px;" onclick="color('#CDCD00')"></button>
<td><button style="background-color: #FFF68F; height: 15px; width: 15px;" onclick="color('#FFF68F')"></button>
<td><button style="background-color: #FFFACD; height: 15px; width: 15px;" onclick="color('#FFFACD')"></button>
<td><button style="background-color: #FFEC8B; height: 15px; width: 15px;" onclick="color('#FFEC8B')"></button>
<td><button style="background-color: #FFD700; height: 15px; width: 15px;" onclick="color('#FFD700')"></button>
<td><button style="background-color: #F5DEB3; height: 15px; width: 15px;" onclick="color('#F5DEB3')"></button>
<td><button style="background-color: #FFE4B5; height: 15px; width: 15px;" onclick="color('#FFE4B5')"></button>
<td><button style="background-color: #EECFA1; height: 15px; width: 15px;" onclick="color('#EECFA1')"></button>
<td><button style="background-color: #FF9912; height: 15px; width: 15px;" onclick="color('#FF9912')"></button>
<td><button style="background-color: #8E388E; height: 15px; width: 15px;" onclick="color('#8E388E')"></button>
<td><button style="background-color: #7171C6; height: 15px; width: 15px;" onclick="color('#7171C6')"></button>
<td><button style="background-color: #7D9EC0; height: 15px; width: 15px;" onclick="color('#7D9EC0')"></button>
<td><button style="background-color: #388E8E; height: 15px; width: 15px;" onclick="color('#388E8E')"></button>
</tr>
<tr>
<td><button style="height: 15px; width: 80px" onclick="outline()">Outline</button>
<td><button style="background-color: #71C671; height: 15px; width: 15px;" onclick="color('#71C671')"></button>
<td><button style="background-color: #8E8E38; height: 15px; width: 15px;" onclick="color('#8E8E38')"></button>
<td><button style="background-color: #C5C1AA; height: 15px; width: 15px;" onclick="color('#C5C1AA')"></button>
<td><button style="background-color: #C67171; height: 15px; width: 15px;" onclick="color('#C67171')"></button>
<td><button style="background-color: #555555; height: 15px; width: 15px;" onclick="color('#555555')"></button>
<td><button style="background-color: #848484; height: 15px; width: 15px;" onclick="color('#848484')"></button>
<td><button style="background-color: #F4F4F4; height: 15px; width: 15px;" onclick="color('#F4F4F4')"></button>
<td><button style="background-color: #EE0000; height: 15px; width: 15px;" onclick="color('#EE0000')"></button>
<td><button style="background-color: #FF4040; height: 15px; width: 15px;" onclick="color('#FF4040')"></button>
<td><button style="background-color: #EE6363; height: 15px; width: 15px;" onclick="color('#EE6363')"></button>
<td><button style="background-color: #FFC1C1; height: 15px; width: 15px;" onclick="color('#FFC1C1')"></button>
<td><button style="background-color: #FF7256; height: 15px; width: 15px;" onclick="color('#FF7256')"></button>
<td><button style="background-color: #FF4500; height: 15px; width: 15px;" onclick="color('#FF4500')"></button>
<td><button style="background-color: #F4A460; height: 15px; width: 15px;" onclick="color('#F4A460')"></button>
<td><button style="background-color: #FF8000; height: 15px; width: 15px;" onclick="color('FF8000')"></button>
<td><button style="background-color: #FFD700; height: 15px; width: 15px;" onclick="color('#FFD700')"></button>
<td><button style="background-color: #8B864E; height: 15px; width: 15px;" onclick="color('#8B864E')"></button>
<td><button style="background-color: #9ACD32; height: 15px; width: 15px;" onclick="color('#9ACD32')"></button>
<td><button style="background-color: #66CD00; height: 15px; width: 15px;" onclick="color('#66CD00')"></button>
<td><button style="background-color: #BDFCC9; height: 15px; width: 15px;" onclick="color('#BDFCC9')"></button>
<td><button style="background-color: #76EEC6; height: 15px; width: 15px;" onclick="color('#76EEC6')"></button>
<td><button style="background-color: #40E0D0; height: 15px; width: 15px;" onclick="color('#40E0D0')"></button>
<td><button style="background-color: #9B30FF; height: 15px; width: 15px;" onclick="color('#9B30FF')"></button>
<td><button style="background-color: #EE82EE; height: 15px; width: 15px;" onclick="color('#EE82EE')"></button>
<td><button style="background-color: #FFC0CB; height: 15px; width: 15px;" onclick="color('#FFC0CB')"></button>
<td><button style="background-color: #7CFC00; height: 15px; width: 15px;" onclick="color('#7CFC00')"></button>
</tr>
<tr>
<td><label>Line Width</label></td>
<td><button id="pixel_plus" type="button" onclick="add_pixel()" style="width: 25px;">+</button></td>
<td><button id="pixel_minus" type="button" onclick="reduce_pixel()" style="width: 25px;">-</button></td>
<td><button id="undo" type="button" onclick="undo_pixel()" style="width: 75px;">Undo</button></td>
</tr>
</table>
<br>
</fieldset>
<script src="//code.jquery.com/jquery-1.8.3.js"></script>
<script src="script.js"></script>
</body>
</html>
Here's what I tried:
First, I made an
undo_pixel()function that uses thelast_actionvariable to pop the last element entered in the respective stack of the previous actionThen I redrew the canvas using a
redraw_canvas()function that clears the canvas and then redraws it using all the data points stored in thecanvas_dataobject.
But what this is causing some unexpected behaviour that I cannot understand entirely. This is what's happening:
I think this is maybe because straight lines are being drawn between all the points being looped through, but I am not exactly sure how else to go about it. How do I correctly implement the redraw/undo function?


{{ ... }}), which probably cannot work in a Stack Snippet . Also your HTML has twobodyelements, which make it invalid. When clicking "Undo", there is an error raised concerningstartx. You should first fix that error. Finally, undo will never work correctly when you don't have a stack of all actions. Having a reference to the last action only will make it impossible to undo two actions.