How to add undo-functionality to HTML5 Canvas? - javascript

I have a Sketching app done in all HTML5 and Javascript, and I was wondering how I would create an Undo Button, so you could undo the last thing you drew. Any idea?

You have to store all modifications in a datastructure. Then you can delete the latest modification if the user wants to undo it. Then you repaint all drawing operations from your datastructure again.

On http://arthurclemens.github.io/Javascript-Undo-Manager/ I have a working example of undo with a canvas element. When you make a modification, you feed the undo manager the undo and redo methods. Tracking of the position in the undo stack is done automatically. Source code is at Github.

The other option, if you need to manipulate objects is to convert your canvas to SVG using a library that preserves the Canvas API preventing a rewrite.
At least one such library exists at this time (November 2011):
SVGKit
Once you have SVG, it is much easier to remove objects and much more without the need to redraw the entire canvas.

Here is a solution that works for me. I have tried it in the latest versions of Firefox and Chrome and it works really well in those two browsers.
var isFirefox = typeof InstallTrigger !== 'undefined';
var ctx = document.getElementById('myCanvas').getContext("2d");
var CanvasLogBook = function() {
this.index = 0;
this.logs = [];
this.logDrawing();
};
CanvasLogBook.prototype.sliceAndPush = function(imageObject) {
var array;
if (this.index == this.logs.length-1) {
this.logs.push(imageObject);
array = this.logs;
} else {
var tempArray = this.logs.slice(0, this.index+1);
tempArray.push(imageObject);
array = tempArray;
}
if (array.length > 1) {
this.index++;
}
return array;
};
CanvasLogBook.prototype.logDrawing = function() {
if (isFirefox) {
var image = new Image();
image.src = document.getElementById('myCanvas').toDataURL();
this.logs = this.sliceAndPush(image);
} else {
var imageData = document.getElementById('myCanvas').toDataURL();
this.logs = this.sliceAndPush(imageData);
}
};
CanvasLogBook.prototype.undo = function() {
ctx.clearRect(0, 0, $('#myCanvas').width(), $('#myCanvas').height());
if (this.index > 0) {
this.index--;
this.showLogAtIndex(this.index);
}
};
CanvasLogBook.prototype.redo = function() {
if (this.index < this.logs.length-1) {
ctx.clearRect(0, 0, $('#myCanvas').width(), $('#myCanvas').height());
this.index++;
this.showLogAtIndex(this.index);
}
};
CanvasLogBook.prototype.showLogAtIndex = function(index) {
ctx.clearRect(0, 0, $('#myCanvas').width(), $('#myCanvas').height());
if (isFirefox) {
var image = this.logs[index];
ctx.drawImage(image, 0, 0);
} else {
var image = new Image();
image.src = this.logs[index];
ctx.drawImage(image, 0, 0);
}
};
var canvasLogBook = new CanvasLogBook();
So every time you draw any thing you will there after run function canvasLogBook.logDrawing() to store a snapshot of the canvas and then you can call canvasLogBook.undo() to undo and canvasLogBook.redo() to redo.

Related

Black images from canvas.toDataURL()

I have created a web application where users can upload images from their filesystem but in few cases I only get black image data on the server side. I have never got this issue in my development environment on lan even with the original images causing the error. This failure occurs overall only sporadic but especially one user is affected. All users should work with the latest firefox. I have read most of the question about this topic and I dont think this is related to a security issue, the jpg/png setting or preserveDrawingBuffer. Because I have never got this problem on lan with a fast PC and the fact that often the last selected images are affected it seems to be a synchronization problem.
To avoid this I work with $.Deferred and Callbacks but may be I missed something. First I load and convert the selected images directly into a list of canvas. The upload is disabled until this is finished. During the upload I just loop through the canvas list and read the data (canvas.toDataURL()) with a $.Deferred and transfer it to the server via ajax.
Get the images to the page:
var file = document.getElementById('fileSelect');
if (file != undefined) {
file.onchange = function (e) {
btnUpload.SetEnabled(false);
loadImgs(e, function () {
btnUpload.SetEnabled(true);
});
};
}
function loadImgs(e, callback) {
for (var t = 0; t < e.target.files.length; t++) {
if ($('.CanvasClass').length <= 20) {
var myTmpImg = new Image();
var myName = e.target.files[t].name;
myTmpImg.src = URL.createObjectURL(e.target.files[t]);
myTmpImg.onload = function () {
var GrenzeX = 1024;
var GrenzeY = 768;
var myFactor = 1;
if (myTmpImg.naturalWidth > GrenzeX || myTmpImg.naturalHeight > GrenzeY) {
if ((GrenzeX / myTmpImg.naturalWidth) < (GrenzeY / myTmpImg.naturalHeight)) {
myFactor = GrenzeX / myTmpImg.naturalWidth;
} else {
myFactor = GrenzeY / myTmpImg.naturalHeight;
}
}
var myCanvas = document.createElement('canvas');
myCanvas.className = "CanvasClass";
myCanvas.height = this.naturalHeight * myFactor;
myCanvas.width = this.naturalWidth * myFactor;
var ctx = myCanvas.getContext("2d");
ctx.drawImage(this, 0, 0, myCanvas.width, myCanvas.height);
$("#myImages").append(myCanvas);
myMetaData.push(new newImgData('', '', myName));
}
}
}
if (callback && typeof callback == "function") {
callback();
}
}
Read and transfer the Data:
$('.CanvasClass').each(function () {
var fd = new FormData();
var base64Data = getBase64Image(this, false);
base64Data.then(function (result) {
//Create Ajax Request
});
});
function getBase64Image(myCanvas, modus) {
var deferred = $.Deferred();
var dataURL = myCanvas.toDataURL("image/jpeg", 0.9);
if (modus === true) {
deferred.resolve(dataURL);
} else {
deferred.resolve(dataURL.replace(/^data:image\/(png|jpeg);base64,/, ""));
}
return deferred.promise();
}
I can also exclude issues during the data transfer because I logged the size of the images on clientside before uploading and compared this to the data at serverside and it was the same.

Drawing application undo button

i have problem with undo button for my drawing application
<input id="undo" type="image" src="images/undo.ico" onclick="cUndo()" width="25" height="25">
var cPushArray = new Array();
var cStep = -1;
var ctx;
// ctx = document.getElementById('myCanvas').getContext("2d");
function cPush() {
cStep++;
if (cStep < cPushArray.length) { cPushArray.length = cStep; }
cPushArray.push(document.getElementById('myCanvas').toDataURL());
}
function cUndo() {
if (cStep > 0) {
cStep--;
var canvasPic = new Image();
canvasPic.src = cPushArray[cStep];
canvasPic.onload = function () { ctx.drawImage(canvasPic, 0, 0); }
}
}
But this doesn't work.Please help
First remark : As #markE underlines, saving with DataURL has a high memory cost. You might consider saving the draw commands + their arguments within an array instead.
Seek for tuts/Stack Overflow post on the topic, out of a few posts you should get some nice ideas.
Anyway, you can go with the dataURL solution in a first time to get your application working (with a limit of 20 undos or like to avoid memory explosion), then you can later improve the undo to reach a higher limit.
I updated my code to handle such a stack limit.
For your issue : onload should be hooked prior to setting the src, but anyway with a DataURL you are not async : the image is built at once, so no need to hook unload.
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext("2d");
var historic = [];
var maxHistoricLength = 20; // might be more or less, depending on canvas size...
function saveForUndo() {
historic.push(canvas.toDataURL());
if (historic.length === maxHistoricLength +1) historic.shift();
}
function canUndo() {
return (historic.length !== 0 );
}
function undo() {
if (!canUndo()) return;
var lastDataURL = historic.pop();
var tmpImage = new Image();
tmpImage.src = lastDataURL;
ctx.drawImage(tmpImage, 0, 0);
}

A function to preload images - need to draw them now, but how?

I'm dabbling with canvas. And I'm a little lost on something.
I have this function:
function preloadimages(arr) {
var newimages = []
var arr = (typeof arr != "object") ? [arr] : arr
for (var i = 0; i < arr.length; i++) {
newimages[i] = new Image()
newimages[i].src = arr[i]
}
}
And I call it like so:
preloadimages(['images/background.png', 'images/hero.png', 'images/monster.png']);
The only problem is, I don't know how to then draw them again later.
If I was preloading one image inside my js I would say:
var bgOk = false;
var bg = new Image();
bg.onload = function () {
bgOk = true;
};
bg.src = "images/background.png";
and then further down when I wanted it drawn I would say:
if (bgOk) {
context.drawImage(bg, 0, 0);
}
And that would be that. The problem is I have made a preloader class, I don't really know how now to call in just the image I want to draw now, or even how to implement the bgOk idea so that if it loaded ok, I can draw it, and if not, leave it alone.
Could someone advise me on this? I'm basically just trying to go more class based rather than the dirty great mess I normally have with a huge javascript file that is ugly and not as maintainable.
This seems to be a complicated problem, but in reality isn't as bad as it looks. If you want to use pre-existing code, or just want to look at something for ideas you can have a look at: http://thinkpixellab.com/pxloader/ This library was used in the HTML5 version of Cut The Rope.
A simple custom implementation could be something like the following:
function loadImages(arr, callback) {
this.images = {};
var loadedImageCount = 0;
// Make sure arr is actually an array and any other error checking
for (var i = 0; i < arr.length; i++){
var img = new Image();
img.onload = imageLoaded;
img.src = arr[i];
this.images[arr[i] = img;
}
function imageLoaded(e) {
loadedImageCount++;
if (loadedImageCount >= arr.length) {
callback();
}
}
}
And then you can call it like this:
var loader = loadImages(['path/to/img1', 'path/to/img2', 'path/to/img3'], function() {
ctx.drawImage(loader.images['path/to/img1']); // This would draw image 1 after all the images have been loaded
// Draw all of the loaded images
for (var i = 0; i < loader.images.length; i++) {
ctx.drawImage(loader.images[i]);
}
});
If you want more details on asset loading you can have a look at the asset loading section of Udacity's HTML5 Game Development course https://www.udacity.com/course/cs255
A function I use:
function ImageLoader(sources, callback)
{
var images = {};
var loadedImages = 0;
var numImages = 0;
// get num of sources
for (var src in sources) {
numImages++;
}
for (var src in sources) {
images[src] = new Image();
images[src].onload = function() {
if (++loadedImages >= numImages) {
callback(images);
}
};
images[src].src = sources[src];
}
}
You call it like so:
var sources = {
bg: path/to/img.png,
title: path/to/img/png
};
var _images = {};
isReady = ImageLoader(sources, function(images) {
_images = images;
});
And then to access your images
_images.bg;
Example: drawImage(_images.bg, 0, 0);

Paint canvas undo feature

These are the button functions:
$("#undo").click(function() {
Stack1.undo();
});
$("#redo").click(function() {
Stack1.redo();
});
This is the undo function:
function clearCanvas()
{
ctx.clearRect(0,0,canvasWidth,canvasHeight);
}
Stack1 = new Stack();
///////////////////
function Stack(firstImg , size) {
var drawStack = new Array();
var stackIndex = 0;
var stackTop = 0;
var stackFloor = 0;
var stackSize = size;
drawStack[0] = firstImg;
this.add = function() {
drawStack[++stackIndex%stackSize] = canvas.toDataURL("image/png");
if (stackIndex >= stackSize) stackFloor = (stackIndex +1) % stackSize ;
stackTop = stackIndex % stackSize;
}
this.undo = function () {
if (stackIndex%stackSize == stackFloor ) return;
clearCanvas();
var tmpImg = new Image();
tmpImg.src = drawStack[--stackIndex%stackSize];
ctx.drawImage(tmpImg, 0, 0);
}
this.redo = function () {
if (stackIndex%stackSize == stackTop) return;
clearCanvas();
var tmpImg = new Image();
tmpImg.src = drawStack[++stackIndex%stackSize];
ctx.drawImage(tmpImg, 0, 0);
}
}
Also I declare the array at the top:
var drawStack = [];
I also put this code before I draw each stroke in my mouse down method:
Stack1.add();
Here is my working example..draw 3 circles on screen then click undo, everything goes blank, then click it again and only 2 remain. Its close but I cannot figure out the last part.
You've made this more complicated than it needs to be. The pseudo code for how an undo function usually works goes:
currentState = 0
maxStates = 10
stateArray = []
initialize:
push the current state onto the top of stateArray
save:
if there are states in stateArray above the currentState
clear the states in stateArray above the current state
push the current state onto the top of stateArray
currentState++
if the size of stateArray exceeds maxStates
remove the oldest state from the bottom of stateArray
currentState--
undo:
if there are previous states in stateArray
currentState--
revert the canvas to stateArray[currentState]
redo:
if there are newer states in stateArray
currentState++
revert the canvas to stateArray[currentState]
As you can see:
You don't ever need the mod operator.
You don't need to keep track of the top and bottom of the stack, only the index of the current state, the number of states you want and the size of the stateArray.
You should clear the states in the stateArray above the current state when you add a new one for it to function the way people expect (unless you are going to implement a branching history which most applications do not).
Edit: I noticed a different problem in your code, you are trying to immediately draw the image to the canvas instead of waiting for it to load. At minimum your undo function should look like:
this.undo = function () {
if (stackIndex%stackSize == stackFloor) return;
var tmpImg = new Image();
tmpImg.src = drawStack[--stackIndex%stackSize];
tmpImg.onload = function() {
clearCanvas();
ctx.drawImage(this, 0, 0);
}
}
If your indexes are right, which I suspect they are not. You can look at an example implementation of the algorithm I described above in this fiddle.

html5 canvas code blanking out

I am in desperate need to fix a problem I have in html5 canvas. I am making an educational game, but after adding another photo and making the necessary code changes, but the canvas blanked out. I reverted the changes, but the problem persisted. I spent the past week or so trying to fix it, but I've run out of ideas :( Here is the relevent code (if anyone needs another part, just say so):
// Create the canvas
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = 1041;
canvas.height = 550;
document.body.appendChild(canvas);
// Background image
var bgReady = false;
var bgImage = new Image();
bgImage.onload = function () {
bgReady = true;
};
bgImage.src = "images/introbackground.png";
//.... rest of the photos load like this so I won't put it...
// Level 1 background 1 image
var bbg1Ready = false;
var bbg1Image = new Image();
bbg1Image.onload = function () {
bbg1Ready = false; //Exception since it shouldn't load at the beginning
};
bbg1Image.src = "images/pollutedbeach1.png";
// Game objects
var hero = {
speed: 200 // movement in pixels per second
};
var level1;
var level2;
var biolevel = false;
//...Code to make the hero picture move with arrow keys and to reset the game are skipped...
if (hero.x <= (level1.x + 345)
&& level1.x <= (hero.x + 32)
&& hero.y <= (level1.y +50)
&& level1.y <= (hero.y + 32)){
biolevel = true;
return biolevel;
}
var render = function () {
if (bgReady) {
ctx.drawImage(bgImage, 0, 0);
}
//...The same for all photos previously loaded...
if (biolevel == true){
level1Ready = false;
level2Ready = false;
bbg1Ready = true;
hero.speed = 125;
}
};
//...Then the main game loop to animate it...
Sorry It's kind of long, but I really need a solution. Thanks!

Categories

Resources