Scaling HTML5 Canvas Image - javascript

Building a web page on which I am trying to set an image as the background of the main canvas. The actual image is 1600x805 and I am trying to code the application so that it will scale the image either up or down, according to the dimensions of the user's screen. In Prime.js I have an object that sets the properties of the application's canvas element located in index.html. Here is the code for that object:
function Prime(w,h){
if(!(function(){
return Modernizr.canvas;
})){ alert('Error'); return false; };
this.context = null;
this.self = this;
this.globalCanvasMain.w = w;
this.globalCanvasMain.h = h;
this.globalCanvasMain.set(this.self);
this.background.setBg();
}
Prime.prototype = {
constructor: Prime,
self: this,
globalCanvasMain: {
x: 0,
y: 0,
set: function(ref){
ref.context = document.getElementById('mainCanvas').getContext('2d');
$("#mainCanvas").parent().css('position', 'relative');
$("#mainCanvas").css({left: this.x, top: this.y, position: 'absolute'});
$("#mainCanvas").width(this.w).height(this.h);
}
},
background: {
bg: null,
setBg: function(){
this.bg = new Image();
this.bg.src = 'res/background.jpg';
}
},
drawAll: function(){
this.context.drawImage(this.background.bg, 0,0, this.background.bg.width,this.background.bg.height,
this.globalCanvasMain.x,this.globalCanvasMain.y, this.globalCanvasMain.w,this.globalCanvasMain.h);
}
};
The primary interface through which external objects like this one will interact with the elements in index.html is home.js. Here's what happens there:
$(document).ready(function(){
var prime = new Prime(window.innerWidth,window.innerHeight);
setInterval(prime.drawAll(), 25);
});
For some reason, my call to the context's drawImage function clips only the top left corner from the image and scales it up to the size of the user's screen. Why can I not see the rest of the image?

The problem is that the image has probably not finished loading by the time you call setInterval. If the image is not properly loaded and decoded then drawImage will abort its operation:
If the image isn't yet fully decoded, then nothing is drawn
You need to make sure the image has loaded before attempting to draw it. Do this using the image's onload handler. This operation is asynchronous so it means you also need to deal with either a callback (or a promise):
In the background object you need to supply a callback for the image loading, for example:
...
background: {
bg: null,
setBg: function(callback) {
this.bg = new Image();
this.bg.onload = callback; // supply onload handler before src
this.bg.src = 'res/background.jpg';
}
},
...
Now when the background is set wait for the callback before continue to drawAll() (though, you never seem to set a background which means drawImage tries to draw null):
$(document).ready(function(){
var prime = new Prime(window.innerWidth, window.innerHeight);
// supply a callback function reference:
prime.background.setBg(callbackImageSet);
// image has loaded, now you can draw the background:
function callbackImageSet() {
setInterval(function() {
prime.drawAll();
}, 25);
};
If you want to draw the complete image scaled to fit the canvas you can simplify the call, just supply the new size (and this.globalCanvasMain.x/y doesn't seem to be defined? ):
drawAll: function(){
this.context.drawImage(this.background.bg, 0,0,
this.globalCanvasMain.w,
this.globalCanvasMain.h);
}
I would recommend you to use requestAnimationFrame to draw the image as this will sync with the monitor update.
Also remember to provide callbacks for onerror/onabort on the image object.

There is a problem with the setInterval function. You are not providing proper function reference. The code
setInterval(prime.drawAll(), 25);
execute prime.drawAll only once, and as the result only little part of the image which is being loaded at this moment, is rendered.
Correct code should be:
$(document).ready(function(){
var prime = new Prime(window.innerWidth, window.innerHeight);
setInterval(function() {
prime.drawAll();
}, 25);
});

Related

Dom doesn't update right away when using convertToBlob on an Offscreen canvas

I have an application that needs to generate a couple thousand images. The way that I'm doing that is with a set of preloaded pngs (acting as transparent layers) and an offscreen canvas. I draw the images onto the canvas, convert it to a blob, and then write the image to a div using a custom class called Images.
I want to show a loading bar and clear old images, first, but there is a 3-5 second delay before the dom updates even though the "empty()" and "show()" code is at the beginning of the click request.
Is there something I'm doing wrong with regard to the asynchrony or promises that is causing the dom to not update immediately?
Here's some of the code:
// Generate Images
$("#generate_images").click(function(){
// Show loading bar
$("#progress_bar .progress-bar").css("width", "0%");
$("#progress_bar").show(0);
// Show loading spinner
$("#loading").show(0);
$("#images").empty();
console.log("Generating Images");
$.each(images, function(id, image){
// Sort traits (png layers)
var traits = Object.values(image.traits).sort((a, b) => {
return a.z_index - b.z_index;
});
images[id].layers = [];
$.each(traits, function(trait_idx, trait){
images[id].layers.push(preloaded_images[trait.variant_id])
});
});
// Create canvas
var canvas = new OffscreenCanvas(1200, 1200);
var context = canvas.getContext("2d");
console.log("Generating canvases.");
$.each(images, function(id, image){
// Clear the canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// Draw each image layer
$.each(images.layers, function(src, layer){
context.drawImage(layer, 0, 0);
});
// Add imageData to screen
canvas.convertToBlob().then(function(blob) {
// Do something with the blob like render to the screen
});
});
});
Any thoughts on making this more efficient would also be appreciated.
Generating a thousand images is not something a browser is necessarily meant to do, but you need to give the DOM a chance to update so add a timeout
Collect what you want to do in an array
Do not loop, but instead do
function generate() {
if (arrayCounter >= array.length) return
canvas.convertToBlob().then(function(blob) {
if (success) {
arrayCounter++
generate()
}
}
}
collect()
generate()

Canvas Element In Closure Is Being Changed Unexpectedly

I am working on creating a webpage that will allow users to add color filters to images they upload. In order to do this I wish to keep an original copy of the image stored in a canvas. Then whenever a user changes one of the color values, using a range slider I want to copy the original image into a new canvas edit it and then plot the new image.
The reason I don't want to apply changes directly to the original image is that they can be difficult to reverse. For example if the user set the blue channel of an image to 0 then unless I have the original image the user would not be able to undo that effect.
My current solution was to create a closure holding a canvas element. This closure is initialized when the user uploads their photo. The code for my closure is below
var scaleCanvas = (function() {
var scaledCvs = document.createElement('canvas'); //Creates a canvas object
var scaledCtx = scaledCvs.getContext("2d"); //Gets canvas context
// All the functions in this section can access scaledCanvas
return {
init: function(cvs) {
scaledCvs = cvs;
console.log("Image Stored")
}, //close init
getImg: function() {
return (scaledCvs);
} //Close getImg
}; //close return
})();
Once the canvas is initialized it can be accessed using the scaleCanvas.getImg() function. An example of this would be during my function adjustFilters() which is called by a range slider element as below
<div class="slidecontainer">
<label>Blue Channel</label>
<input type="range" min="0" max="150" value="100" class="slider" id="blue" onchange="adjustFilters();"/>
</div>
Once the blue slider is changed I load the canvas that I want to use to display the image. Then I load my original image using scaleCanvas.getImg(). Then I apply the effects I am interested in. Then I draw the image. What is strange though is that it appears the scaledCvs variable in my closure is being changed. I say this because if I select a value for "blue" as 0 then it is impossible for me to recover the original blue channel. This should not happen unless the scaledCvs in my closure is being altered somehow.
function adjustFilters() {
var canvas = document.getElementById("dispImg"); //gets the canvas element
canvas.style.display = "block"; //Makes the canvas visible
var context = canvas.getContext("2d"); //Using 2D context
// look up the size the canvas is being displayed at
const width = canvas.clientWidth;
const height = canvas.clientHeight;
var imgCV = scaleCanvas.getImg(); //Gets the original canvas image
var imgCtx = imgCV.getContext('2d');
var blue = document.getElementById("blue").value / 100; //Gets value of the blue slider
// Adjusts blue channel
adjustColor(imgCtx, imgCV.height, imgCV.width, "blue", blue)
// Draws the Image
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(imgCV, 0, 0);
}
I am looking for suggestions on how to get my code to operate as expected. i.e I need the variable scaledCvs in my closure to not be adjusted except for when the init function is called.
Thank You
The reason for the observed behavior is that canvas elements are passed by reference therefore
init: function(cvs) {
scaledCvs = cvs;
console.log("Image Stored")
}, //close init
Does not create a copy of cvs it just creates a reference to it. In order to create a copy of the actual canvas you would need to follow the protocol below
init: function(cvs) {
//Create a Copy of the provided canvas (cvs)
scaledCvs.width=cvs.width;
scaledCvs.height=cvs.height;
scaledCtx.drawImage(cvs,0,0);
}, //close init
For the entire code to work properly you would also need to modify getImage() so that it is not passed a reference of scaledCvs. The final closure definition would be
var scaleCanvas = (function() {
var scaledCvs=document.createElement('canvas'); //Creates a canvas object
var scaledCtx=scaledCvs.getContext("2d"); //Gets canvas context
// All the functions in this section can access scaledCanvas
return {
init: function(cvs) {
//Create a Copy of the provided canvas (cvs)
scaledCvs.width=cvs.width;
scaledCvs.height=cvs.height;
scaledCtx.drawImage(cvs,0,0);
}, //close init
getImg: function() {
//Since canvas are passed by reference in order to protect the saved canvas I have to create a copy before passing it out
var cpCanvas = document.createElement('canvas');
var context = cpCanvas.getContext("2d");
cpCanvas.height = scaledCvs.height;
cpCanvas.width = scaledCvs.width;
context.drawImage(scaledCvs,0,0);
return(cpCanvas);
} //Close getImg
}; //close return
})();
All other code can remain the same.

Issue with image load recursive chain on slow network/mobile

So basically I have a page with a few sections. Each sections contains 5-30 image icons that are fairly small in size but large enough that I want to manipulate the load order of them.
I'm using a library called collagePlus which allows me to give it a list of elements which it will collage into a nice image grid. The idea here is to start at the first section of images, load the images, display the grid, then move on to the next section of images all the way to the end. Once we reach the end I pass a callback which initializes a gallery library I am using called fancybox which simply makes all the images interactive when clicked(but does not modify the icons state/styles).
var fancyCollage = new function() { /* A mixed usage of fancybox.js and collagePlus.js */
var collageOpts = {
'targetHeight': 200,
'fadeSpeed': 2000,
'allowPartialLastRow': true
};
// This is just for the case that the browser window is resized
var resizeTimer = null;
$(window).bind('resize', function() {
resetCollage(); // resize all collages
});
// Here we apply the actual CollagePlus plugin
var collage = function(elems) {
if (!elems)
elems = $('.Collage');
elems.removeWhitespace().collagePlus(collageOpts);
};
var resetCollage = function(elems) {
// hide all the images until we resize them
$('.Collage .Image_Wrapper').css("opacity", 0);
// set a timer to re-apply the plugin
if (resizeTimer) clearTimeout(resizeTimer);
resizeTimer = setTimeout(function() {
collage(elems);
}, 200);
};
var setFancyBox = function() {
$(".covers").fancybox({/*options*/});
};
this.init = function(opts) {
if (opts != null) {
if (opts.height) {
collageOpts.targetHeight = opts.height;
}
}
$(document).ready(function() {
// some recursive functional funk
// basically goes through each section then each image in each section and loads the image and recurses onto the next image or section
function loadImage(images, imgIndex, sections, sectIndex, callback) {
if (sectIndex == sections.length) {
return callback();
}
if (imgIndex == images.length) {
var c = sections.eq(sectIndex);
collage(c);
images = sections.eq(sectIndex + 1).find("img.preload");
return loadImage(images, 0, sections, sectIndex + 1, callback);
}
var src = images.eq(imgIndex).data("src");
var img = new Image();
img.onload = img.onerror = function() {
images[imgIndex].src = src; // once the image is loaded set the UI element's source
loadImage(images, imgIndex + 1, sections, sectIndex, callback)
};
img.src = src; // load the image in the background
}
var firstImgList = $(".Collage").eq(0).find("img.preload");
loadImage(firstImgList, 0, $(".Collage"), 0, setFancyBox);
});
}
}
From my galleries I then call the init function.
It seems like my recursive chain being triggered by img.onload or img.onerror is not working properly if the images take a while to load(on slow networks or mobile). I'm not sure what I'm missing here so if anyone can chip in that would be great!
If it isn't clear what is going wrong from the code I posted you can see a live example here: https://www.yuvalboss.com/albums/olympic-traverse-august-2017
It works quite well on my desktop, but on my Nexus 5x it does not work and seems like the finally few collage calls are not happening. I've spent too long on this now so opening this up to see if I can get some help. Thanks everyone!
Whooooo I figured it out!
Was getting this issue which I'm still unsure about what it means
[Violation] Forced reflow while executing JavaScript took 43ms
Moved this into the callback that happens only once all images are loaded
$(window).bind('resize', function() {
resetCollage(); // resize all collages
});
For some reason it was getting called early even if the browser never resized causing collage to get called when no elements existed yet.
If anyone has any informative input as to why I was getting this js violation would be great to know so I can make a better fix but for now this works :):):)

javascript canvas - check if images created with toDataURL() are loaded

I'm loading an image into my albImg array.
in my loop i then do this:
for(var j = 1; j < albImg.length; j++){
if(albImg[j].complete == true && albImg[j].width > 0){
loadedimages++;
}
}
to make sure all my images are loaded.
I then call my flipImg() function like this:
if(loadedimages == albImg.length-1){
flipImg();
}
I then flip an image and
ctx2.save();
ctx2.scale(-1, 1);
for (var i = RszSpriteCount; i < sprites.length; i++) {
ctx2.drawImage(albImg[sprites[i][0]], sprites[i][1], sprites[i][2], sprites[i][3], sprites[i][4], 0 - (sprites[i][1] + sprites[i][3]), sprites[i][2], sprites[i][3], sprites[i][4]);
}
ctx2.restore();
var flipSz = albImg.length;
albImg[flipSz] = new Image();
albImg[flipSz].src = cnv2.toDataURL();
Here's where my problem begins.
The new image I created - albImg[5] - can't be displayed until it is loaded.
But it is created as if it already is loaded.
That is to say that:
albImg[5].width is already set (to 750) before I can display it.
albImg[5].complete is set to true, before I can display it.
albImg[5].onload = ctx.drawImage(albImg[5], 0, 0); will try to draw the image before it is loaded.
How can I check if my flipped image really is loaded before I display it? in Javascript?
(due to circumstances I'm not using jQuery for this)
Please help.
Your main error is in how you do set the onload event handler :
albImg[5].onload = ctx.drawImage(albImg[5], 0, 0)
will set the return value of drawImage() (undefined) to the onload listener.
What you want is
albImg[5].onload = e => ctx.drawImage(albImg[5], 0, 0);
or
albImg[5].onload = function(){ ctx.drawImage(this, 0, 0) };
For the complete and width properties set to true, it's because while the loading of an Image is always async, in your case, the image is probably already HTTP cached.
Since the HTTP loading and the javascript execution are not executed on the same thread, it is possible that the Image actually loaded before the browser returns its properties.
But even then, the onload event will fire (best to set it before the src though).
var cacher = new Image();
cacher.onload = function(){
var img = new Image();
img.onload = function(){
console.log('"onload" fires asynchronously even when cached');
};
img.src = c.toDataURL();
console.log('cached : ', img.complete, img.naturalWidth);
}
cacher.src = c.toDataURL();
console.log('before cache :', cacher.complete, cacher.naturalWidth);
<canvas id="c"></canvas>
So when dealing with an new Image (not one in the html markup), always simply listen to its onload event.
Now, with the few information you gave in your question, it would seem that you don't even need these images, nor to deal with any of their loadings (except for the sprites of course), since you can directly and synchronously call ctx.drawImage(CanvasElement, x, y).
const ctx = c.getContext('2d');
ctx.moveTo(0, 0);
ctx.lineTo(300, 75);
ctx.lineTo(0, 150);
ctx.fillStyle = 'rgba(120,120,30, .35)';
ctx.fill();
const flipped = c.cloneNode(); // create an offscreen canvas
const f_ctx = flipped.getContext('2d');
f_ctx.setTransform(-1, 0,0,1, c.width, 0);// flip it
f_ctx.drawImage(c,0,0);// draw the original image
// now draw this flipped version on the original one just like an Image.
ctx.drawImage(flipped, 0,0);
// again in 3s
setTimeout(()=>ctx.drawImage(flipped, 150,0), 3000);
<canvas id="c"></canvas>

Variable values aren't available for use after image.onload?

I'm using Canvas to perform a couple of things on an image which is loaded/drawn using image.onload & context.drawImage combo. I'm calculating the bounding size for scaling the images using a simple function which returns the values. I need those values for use at a later point in my code, but no matter what I do, I'm not able to assign the values to a variable. I'm also not able to access my Canvas's styleheight/stylewidth attributes after I assign it the calculated dimensions.
Here's a pseudos ample of my code
$(document).ready(function(){
//Canvas access, context reference etc here.
//Since I'm assigning styles to the canvas on the fly, the canvas has no ht/wdt yet
var dimes = '';
var image = new Image();
image.onload = function(){
//Apply original image height/width as canvas height/width 'attributes'
//(so that I can save the original sized image)
//Check if the image is larger than the parent container
//Calculate bounds if not
//Apply calculated dimensions as 'style' height/width to the canvas, so that the image fits
dimes = scaleImage(...);
//Works!
console.log(dimes);
//Rest all code
}
image.src = '...';
//Blank!!!
console.log(dimes);
//These all blank as well!!!
jQuery('#mycanvas').height() / width() / css('height') / css('width');
document.getElementById(canvas).style.height / .style.width / height / width;
});
I need to access the calculated dimensions for a 'reset' kind of function, that resets my canvas with the drawn image to the calculated size.
As #apsillers noted, the console.log(dimes) code is being executed after you simply define the image.onload() event handler.
If you want to access dimes outside of image.onload(), you'll need to ensure it's being executed after the image loads... e.g. as a response to a button click.
Put the var dimes = ""; before the $(document).ready() to make it a global variable.
Then if you need to access dimes in an event handler, it's ready for you:
$(document).ready(function() {
var image = new Image();
var dimes = "";
image.onload = function() {
dimes = scaleImage(...);
};
$(button).click(function() {
if (dimes === "") {
// image is not yet loaded
} else {
console.log(dimes);
}
});
});
Of course, dimes will now only be accessible inside this first $(document).ready() event handler. If you add another one (which you can certainly do in jQuery), you'll need to use the $.fn.data() jQuery object method to store dimes:
$(document).ready(function() {
var image;
$(document).data("dimes", ""); // initializes it
image.onload = function() {
$(document).data("dimes", scaleImage(...));
};
});
// some other code
$(document).ready(function() {
$("#myButton").click(function() {
var dimes = $(document).data("dimes");
if (dimes === "") {
// image not yet loaded
} else {
console.log(dimes);
}
});
});
Your img.onload function can run only after the JavaScript execution thread stops being busy, i.e., after your ready function completes. Thus, your console.log(dimes) call is running before your onload function.
Put any code that needs to use dimes inside of the onload function. Otherwise, the code the needs to use dimes might run before the onload handler fires.
http://jsfiddle.net/KxTny/1/
$(document).ready(function(){
var dimes = 0;
var width = 20;
var height = 30;
pass(dimes, width, height);
});​
function pass(dimes, width, height) {
alert(dimes);
alert(height);
alert(width);
}

Categories

Resources