Issue with saving canvas data to an array - javascript

I am creating a web application using HTML5 canvas to draw images ( like paint web ), I try to implement the "undo" (ctrl+Z) and "redo" features and here I am facing a strange problem with an array of canvas elements.
Sometimes, when I hit ctrl+Z to undo, a blank image appears, however the data is in the array and I point to the correct element (because when I play with undo/redo I manage to have the corrects images in the right order).
If you can have a look at the following code I would be grateful, I've spent a lot of time already and I'm not able to locate the problem... :-(
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] = cvs.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];
cvsCtx.drawImage(tmpImg, 0, 0);
}
this.redo = function () {
if (stackIndex%stackSize == stackTop) return;
clearCanvas();
var tmpImg = new Image();
tmpImg.src = drawStack[++stackIndex%stackSize];
cvsCtx.drawImage(tmpImg, 0, 0);
}
}
Any solution or workaround, I will take, thank you very much !!

I have implemented undo/redo functionality a few times and, while I cannot post the code for licensing reasons, I can give you some psuedocode that should demonstrate how simple undo/redo actually is:
first, you need two arrays. call these "undo" and "redo".
Every time the state changes, push that state to the undo stack.
When the user presses ctrl-z (undo), pop the last saved state from the undo stack. push this state to the redo queue, and also make it the current state.
When the user presses ctrl-y (redo), pop the last saved state from the redo queue.
If either of the arrays begins to fill up past the # of states you want to save, use shift to discard the oldest state.
For references on push, pop, and shift, see the MDN documentation.
Also, you will probably find yourself wishing arrays had peek, so here it is:
Array.prototype.peek = function () {
var theArray = this;
var temp = theArray.pop();
if (temp !== undefined) {
theArray.push(temp);
}
return temp;
};
edit: there was a bug in my snippet, calling .peek() on an empty array would push undefined.

Related

JavaScript: initialize different image if an error occurs

I have a folder with the contents like the following (giving a minimal example)
ex1_1.png
ex1_2.png
ex2_1.png
page.html
and I've written the following JavaScript code. The HTML file has two canvas elements that are designed to draw ex1_1.png and ex2_1.png and each canvas element has an associated "Next" button. If the first one is clicked it erases the first canvas element and draws ex_2.png. What I want is for the Next button to cycle through all my images, going back to the start when the last image is exceeded. The following JavaScript accomplishes this, except for the part where it cycles back. When it reaches the image with source ex1_3.png (which doesn't exist in the folder), I get a crash, but on the draw() command--which tells me that for whatever reason, it's not cycling the source back to ex1_1.png before attempting to draw.
To the best of my ability to debug this, something is going wrong with the img.onerror part, or how its implemented with the global variable window.indicator. When I cycle through using the next button, the indicator shows true then false if I print the value from within the img.onerror function. But if I print from within the next() function, it never shows false. This sounds like some kind of an issue with the window.indicator keeping its value globally.
// Variable to indicate whether the most recently generated image
// was valid.
window.indicator = true;
// Give the file base-name and index as stored in the local address.
// Return the corresponding Image() object.
function initImg(name, ind) {
var img = new Image();
// The file is local, the image is always of the form
// baseName_i.png
window.indicator = true;
img.src = name + "_" + ind + ".png";
img.onerror = function() {
// Find the appropriate canvas state element, and
// update its state back to 1.
for (i = 0; i < canStates.length; i++) {
var n = canStates[i][0].getAttribute("id");
n = n.split("_")[1];
if (name == canStates[i][0].getAttribute("id")) {
canStates[i][1] = 1;
}
window.indicator = false;
}
};
return img;
}
// Give the canvas context and image objects. Draw the image to
// the context, no return value.
function draw(ctx, img) {
// Check that the image is loaded before writing. Keep
// checking every 50 milliseconds.
if (!img.complete) {
setTimeout( function() {
draw(ctx, img);
}, 50);
}
// Clear the current image and draw the new.
ctx.clearRect(0,0, 200,200);
ctx.drawImage(img, 0,0, 200,200);
}
// Give the string canvas id and string base-name, create the
// canvas object and draw the first image to the canvas.
function slideShow(canId, name) {
var can = document.getElementById(canId);
can.width = 300;
can.height = 300;
var ctx = can.getContext('2d');
var img = initImg(name, 1);
draw(ctx, img);
}
// Next button function. Give the name of the canvas, draw the
// next image or cycle to the start.
function next(button) {
var name = button.getAttribute("name");
// Find the appropriate canvas element.
for (i = 0; i < canStates.length; i++) {
var id = canStates[i][0].getAttribute("id");
id = id.split("_")[1];
if (id == name) {
// Use the states to produce an image, and update the
// states.
canStates[i][1] += 1;
var img = initImg(name, canStates[i][1]);
if (!window.indicator) {
img = initImg(name, 1);
}
// Draw to the canvas.
draw(canStates[i][0].getContext('2d'),img);
}
}
}
// Create a global variable tracking all states of "Next" buttons.
// Stored as a list, each element is a list, the left coordinate is
// a canvas and the right coordinate is its state (image index).
// Also initialize all slide shows.
// The variable r stores the canvases and states, initialized
// outside the function in order to pass-by-reference so as to act
// as a global variable.
var canStates = new Array();
window.onload = function() {
var cans = document.getElementsByTagName("canvas");
for (i=0; i < cans.length; i++) {
var c = cans[i];
var n = c.getAttribute("id").split("_")[1];
slideShow("can_"+n, n);
}
for (i = 0; i < cans.length; i++) {
canStates[i] = [cans[i],1];
}
}
I could switch strategies completely here. I've heard that PHP is a decent way to server-side look at the files in a directory, and I could use that, but I don't know how to make a PHP script execute when a browser is loaded, or how to take its results and hand them over to the JavaScript file.

ExtJS how to set icon of button to image in memory (not on disk)?

Long story short in another portion of the program I make canvases, convert them to DataURLs, then pass them over to the following portion to use as the icon image of the buttons. Whenever I set this.icon = "/path/to/image.jpg", it pulls it correctly, but since these images are not on disk, I am unsure how to
arrowHandler: function (arrow) {
var list = [];
var library = Ext.getCmp("library");
var buttons = Ext.getCmp("numbered").menu.buttons; //where the dataURLs are pushed in another portion of the program
function btn(num) {
var image = new Image;
image.src = buttons[num].dataURL;
this.xtype = "button";
this.height = 50;
this.width = 50;
this.icon = image; //where putting an actual path works correctly, but this code doesn't
this.num = num;
this.handler = function (btn) {
btn.up("button").menu.Style = this.num;
btn.up("button").fireEvent("selected", this.num);
};
}
for (var i = 0; i <= 0; i++)
library.items.items.push(new btn(i));
},
I am aware the loop is only going thru index 0 - it's like that purposefully for testing.
SOLUTION
The selected correct answer did provide the right way to set the icon with a DataURI, but it wasn't the fix to my issue. Turns out instead of doing
library.items.items.push(new btn(i));
I needed to be doing
library.add(new btn(i));
The error I kept encountering with pushing was "c.render() is not a function". I mention that solely to make it hopefully searchable for anyone else who encounters that error.
Should be the same as data uri, you'll have to convert it first.
https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL
var dataURL = canvas.toDataURL();
Here is a button fiddle:
https://fiddle.sencha.com/#view/editor&fiddle/1og6

Phaserio Dynamic Sprite Creation

I am using PhaserIO in conjunction with Meteor to create a multiplayer html5 game and have run into a snag I cannot seem to figure out in a networking prototype I was making. First, the relevant code (also available as a gist):
if(Meteor.isClient) {
var game,
mainState,
mainStateInitialized,
characterData;
Template.game.game = function() {
characterData = Character.find().fetch();
if(!mainStateInitialized)
{
game = new Phaser.Game(500, 600, Phaser.AUTO, 'gameScreen');
createMainState()
}
}
}
function createMainState()
{
mainState = {
sprites: null,
playerLastFrame: characterData.length,
playerCurrentFrame: null,
charData: characterData,
preload: function() {
this.sprites = game.add.group();
game.stage.disableVisibilityChange = true;
game.load.image('hello', 'resources/hello.png');
},
create: function() {
$.each(characterData, function(index) {
var sprite = mainState.sprites.create(this.x, this.y, 'hello');
sprite.angle = this.angle;
});
},
update: function() {
this.charData = characterData;
this.playersCurrentFrame = this.charData.length;
if(this.playersLastFrame > this.playersCurrentFrame) {
//todo: remove player that left
}
else if(this.playersLastFrame < this.playersCurrentFrame) {
for(var i = playersLastFrame; i < playersCurrentFrame; i++) {
var thisData = this.charData[i],
sprite = null;
sprite = mainState.sprites.create(thisData.x, thisData.y, 'hello');
sprite.angle = thisData.angle;
}
}
for(var j = 0; j < mainState.sprites.length; j++) {
mainState.sprites.getAt(j).angle = this.charData[j].angle;
}
playersLastFrame = this.charData.length;
}
}
game.state.add('main', mainState);
game.state.start('main');
mainStateInitialized = true;
}
The idea of this prototype is to have a sprite shown in the canvas for each account in the DB. The main features I am testing are:
Dynamically adding sprites/player data seamlessly (as all proper multiplayer online games should be capable of. This will eventually pave the way for a proper join/leave system)
And to mess with creating efficient packets.
Right now, I am running into an issue with dynamically creating a new sprite when a player creates a new account. About 75% of the time, when a player makes a new account nothing happens. Meteor correctly pushes down the character data, which I can query, and mainState.sprites correctly shows the sprite data. However, nothing is rendered on the canvas.
The other 25% of the time, it works fine. In addition, if I have the code break-pointed it works 100% of the time as far as I can tell.
So, something intermittent is obviously occurring here but I can't figure out what the issue is. Is there something I am missing when adding a sprite during the update loop? Is there a better way to approach this?
I have my code on Nitrous.io, so I can run a localhost instance for you to hit if it would help in solving the problem.
The issue was I was not setting playersLastFrame = playersCurrentFrame.
I feel silly now considering this is a basic loop/compare structure. last = current at end of loop.
Sigh : (.

Appending and Preloading Images with JavaScript

I'm in the process of creating a site that preloads several large gifs. Due to the size of the images. I need them all to be loaded before displayed to the user. In the past I have done this numerous times using something basic like this:
var image = new Image();
image.onload = function () { document.appendChild(image); }
image.src = '/myimage.jpg';
However, i'm loading a group of images from an array, which contains the image source url. It should show a loading message and once they have all loaded it show perform a callback and hide the loading message etc.
The code I've been using is below:
var images = ['image1.gif', 'image2.gif', 'image3.gif'];
function preload_images (target, callback) {
// get feedback container
var feedback = document.getElementById('feedback');
// show feedback (loading message)
feedback.style.display = 'block';
// set target
var target = document.getElementById(target);
// clear html of target incase they refresh (tmp fix)
target.innerHTML = '';
// internal counter var
var counter = 0;
// image containers attach to window
var img = new Array();
// loop images
if (images.length > 0) {
for (var i in images) {
// new image object
img[i] = new Image();
// when ready peform certain actions.
img[i].onload = (function (value) {
// append to container
target.appendChild(img[value]);
// hide all images apart from the first image
if (value > 0) {
hide(img[value]);
}
// increment counter
++counter;
// on counter at correct value use callback!
if (counter == images.length) {
// hide feedback (loading message)
feedback.style.display = 'none';
if (callback) {
callback(); // when ready do callback!
}
}
})(i);
// give image alt name
img[i].alt = 'My Image ' + i;
// give image id
img[i].id = 'my_image_' + i
// preload src
img[i].src = images[i];
}//end loop
}//endif length
}//end preload image
It's really weird, I'm sure it should just work, but it doesn't even show my loading message. It just goes straight to the callback.. I'm sure it must be something simple, I've been busy and looking at it for ages and finding it a tad hard to narrow down.
I've been looking over stackoverflow and people have had similar problems and I've tried the solutions without much luck.
Any help would be greatly appreciated! I'll post more code if needed.
Cheers!
If I'm not totally wrong the problem is with you assignment to
// when ready peform certain actions.
img[i].onload = (function (value) {...})(i);
here you instantly call and execute the function and return undefined to the onload attribute, what can not be called when the image is loaded.
What you can do to have access to the value 'i' when the image is loaded you can try something like the following:
onload = (function(val){
var temp = val;
return function(){
i = temp;
//your code here
}
})(i);
this should store the value in temp and will return a callable function which should be able to access this value.
I did not test that if it is working and there maybe a better solution, but this one came to my mind :)
Try this for your onload callback:
img[i].onload = function(event) {
target.appendChild(this);
if (img.indexOf(this) > 0) {
hide(this);
}
// ...
};
Hope you can get it working! It's bed time for me though.
Edit: You'll probably have to do something about img.indexOf(this)... just realized you are using associative array for img. In your original code, I don't think comparing value to 0 is logical in that case, since value is a string. Perhaps you shouldn't use an associative array?

Image Preloading Canvas in Chrome

I have, to the best of my knowledge, preloaded all of my images successfully. In FireFox, everything renders just fine, but it refuses to show up in Chrome. I've already run into other random quirks with the webkit browsers, and I assume this is just one of them. Here's the applicable code (I apologize for the length, just being verbose. Most of it is quite straightforward, I assure you).
initResources() is called from the $(document).ready() function, where canvas is also properly initialized.
The specific issue appears to be the gameboard. The X's and O's (it's tictactoe...) appear fine, but the gameboard refuses to appear on the canvas in Chrome alone. Works in all other browsers. Chrome's developer tools insists they're being loaded via Network tab, console is showing everything it should. Thank you.
[EDIT: Just now failed in Firefox as well. May have been a cache issue, at which point I'm even more confused]
var canvas, context, playerPiece, aiPiece;
var x = new Image();
var o = new Image();
var gameBoard = new Image();
var numResources = 3; // Total number of resources to wait for loading
var curResources = 0;
function initResources() {
// Get all our resources ready to go
x.height = 130;
x.width = 130;
x.onload = isLoaded();
x.src = "images/gamepieceX.png";
o.height = 130;
o.width = 130;
o.onload = isLoaded();// = curResources++;
o.src = "images/gamepieceO.png";
gameBoard.height = 500;
gameBoard.width = 500;
gameBoard.onload = isLoaded(); //= curResources++;
gameBoard.src = "images/gameBoard.png";
}
function isLoaded() {
curResources++;
console.log("Loaded resource! Total: " + curResources);
if(curResources == numResources) {
console.log("All loaded up! Moving on!");
gameSetup();
}
}
function gameSetup() {
// Draw our board and assign a piece
console.log("Setting up game!");
playerPiece = x;
aiPiece = o;
// Draw gameboard
context.drawImage(gameBoard, 0, 0);
console.log(gameBoard);
canvas.addEventListener ("mousedown", mouseClicked, false);
}
I was calling the functions inline instead of for a callback for .onload on each image. This was firing off the game initialization before the image was loaded. Thanks to those who commented, but I saw this only moments after posting here :)
Here's a link to a chrome bug that might have something to do with this.
https://code.google.com/p/chromium/issues/detail?id=245296
Instead of using
new Image()
create an image element the old fashioned way
document.createElement("img")

Categories

Resources