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();
Related
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.
I was wondering if it was possible to get the width and height of an image without putting an image into page, i.e without creating an image tag that displays it.
trying to make a sprite class using this method.
function Sprite(src,frames) {
// Sprite class
this.img = new Image();
this.img.src = src;
this.frames = frames;
this.cframe = 1;
this.setDim = function() {
this.fullWidth = this.img.width;
this.fullHeight = this.img.height;
}
this.img.onload = this.setDim();
console.log(this.fullWidth);
return this;
}
however this.fullWidth returns undefined
and the below that ignores the onload returns 0
function Sprite(src,frames) {
// Sprite class
this.img = new Image();
this.img.src = src;
this.frames = frames;
this.cframe = 1;
this.fullWidth = this.img.width;
this.fullHeight;
this.setDim = function() {
this.fullWidth = this.img.naturalWidth;
this.fullHeight = this.img.height;
console.log(this.fullWidth)
}
console.log(this.fullWidth)
//this.img.onload = this.setDim();
return this;
}
I don't really want to use Jquery for this.
I have also tried this.img.natrualWidth (as you can see in the example above)
it also returns 0
Any advice would be great,
Thanks
Updated this to match #vihan1086 answer
function Sprite(src,frames) {
// Sprite class
this.img = new Image();
this.img.src = src;
this.frames = frames;
this.cframe = 1;
var self = this;
self.loaded = function () {};
this.setDim = function() {
self.fullWidth = this.width;
self.fullHeight = this.height;
self.frameWidth = this.width / self.frames;
self.frameHeight = this.height;
self.loaded.apply(self, []);
}
this.loaded = function() {
return this;
}
this.img.onload = this.setDim;
}
then use
sprite = new Sprite(sprite,5);
sprite.loaded = function() {
console.log(sprite.fullWidth);
}
Problem
var img = new Image();
img.src = '/my/src/to/file';
//this refers to the current function at this point
img.onload = function () {
//this is 'img' at this point not the function
}
Solution
this is not in scope so you would add:
var self = this;//Self and this are referring to the same thing, the function
img.onload = function () {
//this refers to image but self still refers to the function's this
self.width = this.width;
self.height = this.height;
}
console.log(this.width);//logs width
console.log(this.height);//logs height
This leaves async problems which can be solved using two methods
A
this.img.onload = this.setDim; //Noticed the dropped ()
B
self.loaded = function () {};
this.setDim = function () {
//...
self.loaded.apply(self, []);
}
then
var sprite = new Sprite(...);
sprite.loaded = function () {
console.log(this.fullHeight);
}
Explanation
img.onload() changes the scope of the code, resulting in your this referring to img. Now the weird part. We created a variable self which refers to this, this allows us to refer to this in a different scope by using self.
Other
img.onload is "async" which means it won't follow along with the rest of the code. This means the console.log() has run, but the img.onload hasn't. Be careful when working when this type of code (I wrote a few solutions in the update). You should wait until img.onload is finished before checking the values. I've worked on something like this before and I'll see if I can find what I did to address all these issues; if I can, I'll update this answer.
UPDATE: I wouldn't run the setDim function at first and let the user run it setDim() -> setDim. If you wish to load the dimensions at first, put a load function to your Sprite() which is run when the dimensions are retrieved.
In javascript the statements are executed asynchronously. To know more about this read this excellent article Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
In your case as #Juhana mentioned passing reference should get the issue resolved
(Using Firefox32, and Win7. But in other browsers I need it to work, too.)
I can't find a command to retrieve the content of the pattern-object that I set on the 2D-context.
Is there no direct way to get the value array and the width and height?
And if there is really no direct way, is there a workaround?
I could just use fillRect with the pattern on a hidden canvas and then reading out the canvas. But then, how to get the correct height and width?
Pattern properties
The only method exposed on the CanvasPattern object is to handle transformations:
interface CanvasPattern {
// opaque object
void setTransform(SVGMatrix transform);
};
This means all other properties has to be tracked externally.
Workaround 1 - manually keep track of properties
The workaround is to read the width and height from the image you used for the pattern, as well as mode and optionally transforms.
Just keep a reference to them for later:
var img = ...; // image source
var patternMode = "repeat"; // store repeat mode
var patternWidth = img.naturalWidth; // width and height of image
var patternHeight = img.naturalHeight; // = width and height of pattern
var pattern = ctx.createPattern(img, patternMode); // use to create pattern
Workaround 2 - create a custom object
You can create a custom object which wraps up the pattern creation process and exposes methods that can hold width, height etc.
Example
An object could look like this:
function PatternExt(ctx, image, mode) {
var ptn = ctx.createPattern(image, mode || "repeat");
this.setTransform = ptn.setTransform ? ptn.setTransform.bind(ptn) : null;
this.width = image.naturalWidth;
this.height = image.naturalHeight;
this.image = image;
this.mode = mode;
this.pattern = ptn;
}
Then it's just a matter of creating an instance almost the same way as with createPattern():
var p = new PatternExt(ctx, img, "repeat");
ctx.fillStyle = p.pattern;
To read information do:
var w = p.width;
var h = p.height;
...
Rename/extend as you want/need.
Demo for custom object
// load an image for pattern
var img = new Image();
img.onload = demo;
img.src = "http://i.imgur.com/HF5eJZS.gif";
function demo() {
var ctx = document.querySelector("canvas").getContext("2d"), p;
// create a pattern instance
p = new PatternExt(ctx, img, "repeat");
// use as fill-style
ctx.fillStyle = p.pattern;
ctx.fillRect(0, 0, 300, 150);
// show some properties
ctx.font = "24px sans-serif";
ctx.fillStyle = "#fff";
ctx.fillText([p.width, p.height, p.mode].join(), 10, 30);
}
function PatternExt(ctx, image, mode) {
var ptn = ctx.createPattern(image, mode || "repeat");
this.setTransform = ptn.setTransform ? ptn.setTransform.bind(ptn) : null;
this.width = image.naturalWidth;
this.height = image.naturalHeight;
this.image = image;
this.mode = mode;
this.pattern = ptn;
}
<canvas></canvas>
If your desired pattern is currently used as the fillStyle, then you can fetch it by fetching the fillStyle:
myPattern=context.fillStyle;
Otherwise you can't fetch your pattern object because the context keeps any pattern objects you've created as private properties.
So typically you keep a reference to your pattern until it's not needed anymore.
If you also need the original imageObject used to create your pattern then you typically save a reference to that image also.
// create an imageObject for use in your pattern
var myImageObject=new Image();
myImageObject.onload=start; // call start() when myImageObject is fully loaded
myImageObject.src="";
function start(){
// myImageObject has now been fully loaded so
// create your pattern and keep a reference to it
var myPattern = context.createPattern(myImageObject, 'repeat');
}
... and later when you need the pattern ...
// use your pattern object reference to apply the pattern as a fillStyle
context.fillStyle = myPattern;
... and later if you need the original image object
// get the original image object's size
var imgWidth=myImageObject.width;
var imgHeight=myImageObject.height;
// draw the original image object to the context -- or whatever you need it for
context.drawImage(myImageObject,50,50);
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.