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.
Related
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.
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();
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.
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";
}
}