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";
}
}
Related
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>
So I have loaded in an image and can access the properties within this function locally such as ball.height and width, but I need to use these properties in other functions to do calculations with. How would I access these properties since they are only locally defined currently. I can't set it as global (or at least don't know how) since in order to get the image in the first place I have to load it in with the function.
function drawBall() {
var ball = new Image();
$(ball).on('load', function() {
ctx.drawImage(ball, xBall, yBall);
console.log(ball.height);
});
ball.src = 'ball.png';
}
function ballHit() {
// For example calculate if ball.height = canvas.height
}
Since the image load is a asynchronous task, you can use a callbackļ¼
function drawBall(callback) {
var ball = new Image();
$(ball).on('load', function() {
ctx.drawImage(ball, xBall, yBall);
console.log(ball.height);
callback(ball.height,ball.width);
});
ball.src = 'ball.png';
}
drawBall(ballHit);
Create a global variable globalBall and once the image is loaded you can assign the image from the function to the global object.
var globalBall = new Image();
function drawBall() {
var ball = new Image();
$(ball).on('load', function() {
ctx.drawImage(ball, xBall, yBall);
console.log(ball.height);
});
ball.src = 'ball.png';
globalBall = ball;
}
function ballHit() {
if(globalball.height == canvas.height)
{
// Do your stuff
}
}
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.
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.
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.