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 states
  • hm.ui.assignundobutton(element) set undo element
  • hm.ui.assignredobutton(element) set redo element
  • nm.ui.update() updates button states reflect current internal state. internal states automatically call needed if changing redo/undo buttons stats self
  • hm.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

Popular posts from this blog

Ansible - ERROR! the field 'hosts' is required but was not set -

customize file_field button ruby on rails -

SoapUI on windows 10 - high DPI/4K scaling issue -