How to add an infinite number of images to the html5 canvas? - javascript

I want to let a user drag and drop as many images as they wish onto an html5 canvas. From tutorials online I gather its something like:
var canvas = document.getElementById("canvas"),
context = canvas.getContext("2d"),
imgs = arr.map(function callback(currentValue, index, array) {
img = img = document.createElement("img");
}[, thisArg])
mouseDown = false,
brushColor = "rgb(0, 0, 0)",
hasText = true,
clearCanvas = function () {
if (hasText) {
context.clearRect(0, 0, canvas.width, canvas.height);
hasText = false;
}
};
for (var i=0;i<imgs.length;i++){
imgs[i].addEventListener("load",function(){
context.drawImage(img,0,0);
})
}
I know line 4 is completely wrong..., but usually people seem to create their images as variables and then change the source with the dragged image. Since I want as many images as the user wishes to add, that would be an array without any size?
Furthermore, Since this code gets called once, on page load, this array cannot be appended to later (like when the user decides to add another image). So maybe this entire paradigm is incorrect in this case?
Can someone advise me on how to go about this? Thanks!
EDIT:
proposed solution:
imgs_arr = [];
var canvas = document.getElementById("canvas"),
context = canvas.getContext("2d"),
imgs = imgs_arr.map(function callback(src) {
var img = document.createElement("img");
img.src = src;
return img;
});
mouseDown = false,
brushColor = "rgb(0, 0, 0)",
hasText = true,
clearCanvas = function () {
if (hasText) {
context.clearRect(0, 0, canvas.width, canvas.height);
hasText = false;
}
};
// Adding instructions
context.fillText("Drop an image onto the canvas", 240, 200);
context.fillText("Click a spot to set as brush color", 240, 220);
function drawImage(element,index,array){
element.addEventListener("load",function(){
clearCanvas()
context.drawImage(element,0,0)
})
}
imgs_arr.forEach(drawImage);
Currently throws : Cannot read property 'appendChild' of null(anonymous function) because my imgs_arr is blank - no one has added images yet.

Assuming your arr is an array of image URLs, your map should look like this:
imgs = arr.map(function callback(src) {
var img = document.createElement("img");
img.src = src;
return img;
});
I renamed currentValue to src to make sense as to what it is, and I removed the extra parameters, but that was all technically optional.
The important parts are:
Create a new img element and assign it to a variable.
Assign the URL to the src of that `img.
Return the image.
The map() function gathers up all the values returned from the callback and turns those into an array. That'll give you an array of Image objects (which are the JS form of the HTML <img> element).
The rest looks more or less correct (though you really should use ; instead of , for ending your lines to avoid possible weird things).
Also, for adding more after it is initialized, you should just have to push() a new Image onto the array and then redraw it.

Related

Why can't I add more than one set of object entries when using onload in image pre loader? (runnable code included)

There is something wrong with my preloader. I want to:
have an array of objects that have a key for the image name, and then the url for the image as the value
create each image object one by one, and then when it is loaded push that into an object that has the name as the key and the actual image object as the value.
I have been working on it all day, and it nearly works. The problem is I can't seem to have more than one object in my images object at a time!
Is this whole idea wrong? I need the images preloaded and accessible for the game. I will add a function to start the game once they are all loaded later, but for now I am stuck. I searched all day for other peoples solutions, and they all seem to relate to loading images into the browser cache rather than having them ready to draw using ctx.drawImage or they are totally over engineered with many multiple layers of callbacks and unwanted functionality.
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let images = {};
function preloadImages(arrayOfNames) {
const img = new Image();
for (let value of arrayOfNames) {
for (let key in value) {
let name = key;
img.src = value[key];
console.log(name); // alerts key
console.log(img.src); // alerts key's value
img.onload = function addToImagesObject() {
images[name] = img; // WHY does the tweet overwrite the zombie and not just add to the images so we have 2 entries?
ctx.drawImage(images[name], 0, 0);
console.log(images);
};
}
}
}
const arrayOfNames = [
{ zombie: 'https://via.placeholder.com/200x100' },
{ tweet: 'https://via.placeholder.com/350x150' },
];
preloadImages(arrayOfNames);
<canvas id="gameCanvas" width="650" height="650">Sorry, your browser can't display
canvas
</canvas>
You are using the same Image object for your loops. That means each iteration you are overwriting the src and the onload function of that same object. So the last iteration is the one that is going to be used.
So your "tweet" property isn't overwriting your "zombie" one. The zombie one never existed, because it was never created in the first place. The onload function that would have created it never ran as it was overwritten before the browser had time to load the image and call it.
Simply use a different image object in each loop iteration. This will make it so you have separate images and event calls for each value.
for (let key in value) {
const img = new Image();
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let images = {};
function preloadImages(arrayOfNames) {
for (let value of arrayOfNames) {
for (let key in value) {
//each value should get it's own image object
const img = new Image();
let name = key;
img.src = value[key];
console.log(name); // alerts key
console.log(img.src); // alerts key's value
img.onload = function addToImagesObject() {
images[name] = img; // WHY does the tweet overwrite the zombie and not just add to the images so we have 2 entries?
ctx.drawImage(images[name], 0, 0);
console.log(images);
};
}
}
}
const arrayOfNames = [
{ zombie: 'https://via.placeholder.com/200x100' },
{ tweet: 'https://via.placeholder.com/350x150' },
];
preloadImages(arrayOfNames);
<canvas id="gameCanvas" width="650" height="650">Sorry, your browser can't display
canvas
</canvas>

Syncing canvas drawing / reading

So I have this little static class which does some image transformation:
// static class ImageFactory
var ImageFactory = (function () {
var ImageFactory = {};
ImageFactory.flip = function (image) {
return invert(image, false, true);
};
ImageFactory.mirror = function (image) {
return invert(image, true, false);
};
// private
function invert(image, isMirror, isFlip) {
var canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
var context = canvas.getContext("2d");
context.translate(isMirror ? canvas.width : 0, isFlip ? canvas.height : 0);
context.scale(isMirror ? -1 : 1, isFlip ? - 1 : 1);
context.drawImage(image, 0, 0);
return canvas;
}
return ImageFactory;
})();
The problem is: sometimes it produces 'blank' (wholly transparent) images instead of flips and mirrors, both in Chrome and Firefox. I suspect it has something to do with asynchronous operations which sometimes don't get done in time. According to some literature canvas drawing should be treated synchronously by browsers, but this issue tells me the other way.
Anyway, is there a safe way to draw to a secondary canvas and then use that canvas as an image to draw on the main canvas?
This is the code which should ensure no ImageFactory method is called before all input images are ready:
function load() {
for (var i = 0; i < images.length; ++i) {
var image = images[i];
if (!image.complete) {
var timeout = setTimeout(onTimeout, LOADING_TIMEOUT);
return;
}
}
scene = sceneFactory();
loop();
function onTimeout() {
load();
}
}
Where images is just an array containing all images in the DOM. Only after loop() is called I have some calls to the ImageFactory methods.
Im not sure exactly what may cause the problem , but i guess it is caused by the image not being ready when the function gets called.
Try calling the image mirroring function after the image has finished loading.
Use image onload callback to check if image has finished loading and is ready to be used.
You may also try drawing the canvas again after a second or so, just to make sure it gets drawn properly.

Modifying an object that's been returned

Edit: Thanks Teemu for the answer it helped a lot
I'm not sure if the question is phrased quite correctly for my problem... Anyways, I am trying to create a getImage function for use in my html5 game. I want the API to be designed so that a boolean prerender argument can be given. I'm having trouble with implementing it so that the prerendering bit can modify the returned image.
The code that uses the function looks like this:
var resources = {
image: getImage('path/to/my/image.png'),
prerendered: getImage('my/prerendered/image.png', true)
};
And the code for the getImage function looks like this:
var getImage = function (source, prerender) {
var img = new Image(); // store as empty image
img.onload = function () {
if (prerender) { // all my prerendering code
var can = document.createElement('canvas');
var ctx = can.getContext('2d');
can.width = img.width;
can.height = img.height;
ctx.drawImage(img, img.width, img.height);
// right here is where I attempt to modify img
// but since it's already been returned, I'm not changing the returned value - I think
img = can;
}
img.ready = true; // a property my game uses to check if the resource is ready
};
img.src = source; // load that badboy
return img;
};
One solution I tried was to wrap img in an object and just have it be a property e.g
var wrapper = {img: new Image()};
But this seems ugly, and I think there's a better solution
If I've understood your question correctly, you want to return a canvas instead of img in case of prerender is true. If so, you can do it for example like this:
var getImage = function (source, prerender) {
var img = new Image(),
can, ctx;
img.onload = function () {
if (prerender) {
// prerender, can & ctx are defined in the outer scope
// img still refers to the originally-created image, hence this will work
can.width = img.width;
can.height = img.height;
ctx.drawImage(img, img.width, img.height);
}
img.ready = true;
};
if (prerender) {
can = document.createElement('canvas');
ctx = can.getContext('2d');
}
img.src = source;
return can || img; // returns can if it's defined, img otherwise
};
I believe you can convert the canvas back to image using canvas.toDataURL & update the source?
So instead of :
img = can;
Perhaps :
img.src = can.toDataURL();

Function to automate image loading using canvas

In one of my projects, I successfully used the following code to render a png image in an html5 canvas using JavaScript.
var sizeIcon = new Image();
sizeIcon.draw = function() {
ctx.drawImage(sizeIcon, tooltip.x + 10, tooltip.y + 10);
};
sizeIcon.onload = sizeIcon.draw;
sizeIcon.src = "./images/icons/ruler.png";
tooltip.icons.push(sizeIcon);
Since I have multiple icons to load, I implemented the following function:
var imageLoader = function(x, y, src) {
var image = new Image();
image.draw = function() {
ctx.drawImage(image, x, y);
};
image.onload = image.draw;
image.src = src;
tooltip.icons.push(image);
};
imageLoader(tooltip.x + 10, tooltip.y + 10, "./images/icons/ruler.png");
With tooltip.icons being a globally accessible array.
This function does nothing (and does not produce any error nor warnings in the console). I also tried filling the tooltip.icons array directly using something like tooltip.icons[n] = new Image(); without success (where n = tooltip.icons.length). There is probably a part of the JavaScript scope that I don't understand.
You are basically risking invalidating your image object (as in not available) when you get to the callback handler as image loading is asynchronous and the function will (most likely) exit before the onload is called.
Try to do a little switch around such as this:
var imageLoader = function(x, y, src) {
var image = new Image();
function draw() {
// use *this* here instead of image
ctx.drawImage(this, x, y)
};
image.onload = draw;
image.src = src;
tooltip.icons.push(image);
};
Instead of the small hack here you could store the coordinates (and urls) in an array and iterate through that.

Canvas image drawing order

I'm making a simple engine for isometric games to use in the future.
The thing is i'm having trouble drawing on the canvas. Take for instance this code:
function pre_load() {
var imgs = ['1', '1000','1010','1011','1012','1013'];
var preload_image_object = new Image();
$.each(imgs,function(i,c){
preload_image_object.src='img/sprites/'+c.logo+'.png';
})
}
function GameWorld() {
//[...]
this.render = function() {
this.draw(1000, this.center);
this.draw(1013, this.center);
this.draw(1010, this.center);
}
this.draw = function(id, pixel_position) {
draw_position_x = pixel_position[0] - this.tile_center[0];
draw_position_y = pixel_position[1] - this.tile_center[1];
inst = this;
var img = new Image();
img.onload = function () {
inst.ctx.drawImage(img, draw_position_x, draw_position_y);
console.log('Drawn: ' + id);
}
img.src = "img/sprites/"+id+".png";
}
}
One might expect that things will be drawn in the order, id 1000, then id 1013, then id 1010 since the images are already loaded(even if not, currently i'm working on a local server).
But if I refresh the page several times, I'll get different results:
Case 1
Case 2
Case 3
(Any other permutation may happen)
The only solution I can think about is making the function "hang" until the onload event has been called and only then proceed to the next .draw() call. Would this be the right approach? How would I go about doing it? Is there a better solution?
Thanks!
Your intuition is correct...you need an image loader.
There are plenty of image loader patterns out there--here's one:
When all your images are fully loaded, the imgs[] array below has your sprites in proper order.
var imgURLs = ['1', '1000','1010','1011','1012','1013'];
var imgs=[];
var imgCount=0;
function pre_load(){
for(var i=0;i<imgURLs.length;i++){
var img=new Image();
imgs.push(img);
img.onload=function(){
if(++imgCount>=imgs.length){
// imgs[] now contains all your images in imgURLs[] order
render();
}
}
img.src= "img/sprites/"+imgURLs[i]+".png";
}
}

Categories