javascript - Undo / redo not working properly and painting after zoom not working properly too -
i trying implement paint bucket tool undo , redo functionality. issue undo , redo working first time, when undo redo multiple times, code fails. can me figure issue out? zoom working, painting after zoom not work correctly. complete code. can copy paste , work @ end.
<!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 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 = 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 = 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 elements on canvas function redraw() { uc = 0; rc = 0; var locx, locy; // make sure required resources loaded before redrawing if (curloadresnum < totalloadresources) { return; // check if images loaded or not. } clearcanvas(); // draw current state of color layer canvas context.putimagedata(colorlayerdata, 0, 0); undoarr.push(context.getimagedata(0, 0, canvaswidth, canvasheight)); console.log(undoarr); redoarr = new array(); // draw background context.drawimage(backgroundimage, 0, 0, canvaswidth, canvasheight); // draw outline image on top of everything. move separate // canvas did not have redraw everyime. context.drawimage(outlineimage, 0, 0, drawingareawidth, drawingareaheight); } ; function matchoutlinecolor(r, g, b, a) { return (r + g + b < 100 && === 255); } ; function matchstartcolor(pixelpos, startr, startg, startb) { var r = outlinelayerdata.data[pixelpos], g = outlinelayerdata.data[pixelpos + 1], b = outlinelayerdata.data[pixelpos + 2], = outlinelayerdata.data[pixelpos + 3]; // if current pixel of outline image black if (matchoutlinecolor(r, g, b, a)) { return false; } r = colorlayerdata.data[pixelpos]; g = colorlayerdata.data[pixelpos + 1]; b = colorlayerdata.data[pixelpos + 2]; // if current pixel matches clicked color if (r === startr && g === startg && b === startb) { return true; } // if current pixel matches 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] = !== undefined ? : 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]; // current pixel position pixelpos = (y * canvaswidth + x) * 4; // go long color matches , inside 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 long color matches , in inside 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 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 stack pixelstack.push([x + 1, y]); reachright = true; } } else if (reachright) { reachright = false; } } pixelpos += canvaswidth * 4; } } } ; // start painting paint bucket tool starting pixel specified startx , 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], = colorlayerdata.data[pixelpos + 3]; if (r === curcolor.r && g === curcolor.g && b === curcolor.b) { // return because trying fill 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 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 run locally. please run on 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>
canvas sizes & state history
canvas size
if have ever had around in dom notice many element have both height , width attribute , height , width style attribute.
for canvas these have 2 different meanings. lets create canvas.
var canvas = document.createelement("canvas");
now canvas element width , height can set. defines number of pixels in canvas image (the resolution)
canvas.width = 500; canvas.height = 500;
by default when image (canvas image) displayed in dom displayed 1 one pixel size. means each pixel in image there 1 pixel on page.
you can change setting canvas style width , height
canvas.style.width = "1000px"; // note must add unit type "px" in case canvas.style.width = "1000px";
this not change canvas resolution, display size. each pixel in canvas takes 4 pixels on page.
this becomes problem when using mouse draw canvas mouse coordinates in screen pixels no longer match canvas resolution.
to fix this. , example op code. need rescale mouse coordinates match canvas resolution. has been added op mousedown event listener. first gets display width/height resolution width , height. normalises mouse coords dividing display width/height. brings mouse coords range of 0 <= mouse < 1 multiply canvas pixel coordinates. pixels need @ integer locations (whole numbers) must floor result.
// assuming mousex , mousey mouse coords. if(this.style.width){ // make sure there width in style // (assumes if width there height var w = number(this.style.width.replace("px","")); // warning not work if size not in pixels var h = number(this.style.height.replace("px","")); // convert height number var pixelw = this.width; // canvas resolution var pixelh = this.height; mousex = math.floor((mousex / w) * pixelw); // convert mouse coords pixel coords mousey = math.floor((mousey / h) * pixelh); }
that fix scaling problem. looking @ code, mess , should not searching nodetree each time, re getting context. surprised works, might jquery (i don't know never use it) or might rendering elsewhere.
state history
the current state of computer program conditions , data define current state.. when save saving state, , when load restore state.
history way of saving , loading states without messing around in file system. has few conventions stats stored stack. first in last out, has redo stack allows redo previous undos maintain correct state , because states dependent on previous states redo can redo associated states. hence if undo , draw invalidate existing redo states , should dumped.
also saved state, on disk, or undo stack must dissociated current state. if make changes current state not want changes effect saved state.
this think went wrong op, using colorlayerdata
fill (paint) when got undo or redo using referenced data remained in undo/redo buffers when painted changing data still in undo buffer.
history manager
this general purpose state manager , work undo/redo needs, have ensure gather current state single object.
to have written simple history manager. has 2 buffers stacks 1 undos , 1 redos. holds current state, recent state knows about.
when push history manager take current state knows , push undo stack, save current state, , invalidate redo data (making redo array length 0)
when undo push current state onto redo stack, pop state undo stack , put in current state, return current state.
when redo push current state onto undo stack, pop state redo stack , put in current state, return current state.
it important make copy of state returned state managers not inadvertently change data stored in buffers.
you may ask. "why cant state manager ensure data copy?" question not role of state manager, saves states , must no matter has save, nature unaware of meaning of data stores. way can used images, text, game states, anything, file system can, can not (should not) aware of meaning , know how create meaningful copies. data push state manager single referance (64bits long) pixel data or push each byte of pixel data, not know difference.
also op have added ui control state manager. allows display current state ie disables , enables undo redo buttons. important ui design provide feedback.
the code
you need make following changes code use history manager. can or use guide , write own. wrote before detected error. if error may need change.
// old code (from memory) colorlayerdata = undoarr.pop(); context.putimagedata(colorlayerdata, 0, 0); // fix same applies redo , makes copy rather use // reference still stored in undoe buff context.putimagedata(undoarr, 0, 0); // put undo onto canvas colorlayerdata = context.getimagedata(0, 0, canvaswidth, canvaheight);
remove code have undo/redo.
change undo/redo buttons @ top of page to, single function handle both events.
<button id = "undo-button" onclick="history('undo')">undo</button> <button id = "redo-button" onclick="history('redo')">redo</button>
add following 2 functions code
function history(command){ // handles undo/redo button events. var data; if(command === "redo"){ data = historymanager.redo(); // data redo }else if(command === "undo"){ data = historymanager.undo(); // data undo } if(data !== undefined){ // if data has been found setcolorlayer(data); // set data } } // sets colour layer , creates copy 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); }
in redraw function have replace stuff had undo , add line @ same spot. saves current state in history manager.
historymanager.push(context.getimagedata(0, 0, canvaswidth, canvasheight));
in start function have add ui elements state manager. , can ignored stat manager ignore them if not defined.
if(historymanager !== undefined){ // visual feedback , not required history manager function. historymanager.ui.assignundobutton(document.queryselector("#undo-button")); historymanager.ui.assignredobutton(document.queryselector("#redo-button")); }
and off course historymanager self. encapsulates data can not access internal state except via interface provides.
the historymanager (hm) api
hm.ui
ui manager updates , assigns button disabled/enabled stateshm.ui.assignundobutton(element)
set undo elementhm.ui.assignredobutton(element)
set redo elementnm.ui.update()
updates button states reflect current internal state. internal states automatically call needed if changing redo/undo buttons stats selfhm.reset()
resets history manager clearing stacks , current saved states. call when load or create new project.nm.push(data)
add provided data history.nm.undo()
previous history state , return data stored. if no data return undefined.nm.redo()
next history state , return data stored. if no data return undefined.
the self invoking function creates history manager, interface accessed via variable historymanager
var historymanager = (function (){ // anon private (closure) scope var ubuffer = []; // undo buff var rbuffer = []; // redo buff var currentstate = undefined; // holds current history state var undoelement = undefined; var redoelement = undefined; var manager = { ui : { // ui interface disable , 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; })();
that fix zoom problem , undo problem. best of luck project.
Comments
Post a Comment