Canvas Element In Closure Is Being Changed Unexpectedly - javascript

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.

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()

Using `jsgif` to build layers over animated GIFs

I'm looking to get some help or guidance on the use of the excellent libgif.js library.
My objective is to take a page that has an animated gif and a png of text with transparent background, and then show the 2 images overlaid such that the resulting image can be copied to the clipboard.
I've succeeded in doing this with a static image as a template
However, if I try this with a gif, it merges the animated gif with the text image, but freezes the gif.
I've familiarized myself with the libgif.js library and have succeeded in using it to build a canvas from an animated gif and have it remain animated.
However, the text image is not being displayed in the final canvas, and I'm a little lost as to how I might go about fixing this.
Is it obvious to anyone why the textImage is being properly sized and (somewhat) apparently placed on the canvas, but not displayed in the final result?
As a side question, does anyone know why the progress bar completes quickly at first and then progresses more slowly a second time?
The HTML is rather long, but the JS from the JSFiddle is shown below (for those not willing to click through to the link).
function doit() {
var isGIF = true; // always true for now TODO: better way to test if-gif than regex for ".gif"
var previewContainer = document.getElementById("previewContainer");
var textImage = document.getElementById("textImage");
var templateImage = document.getElementById("templateImage");
var w = document.getElementById("templateImage").width;
var h = document.getElementById("templateImage").height;
previewContainer.removeChild(previewContainer.children[1]);
if (isGIF) {
var gif = new SuperGif({
gif: templateImage,
progressbar_height: 5,
auto_play: true,
loop_mode: true,
draw_while_loading: true
});
gif.load();
var canvas = gif.get_canvas();
var context = canvas.getContext('2d');
context.drawImage(textImage, 0, 0, w, h);
previewContainer.replaceChild(canvas, previewContainer.children[0]);
}
}
Note: this solution was originally based on Arend's solution in the comments of this question from this JSFiddle.
You would have to tweak the library in order to get access to their rendering loop (like a frame_rendered event).
This way, you would be able to add whatever you want over the image the library drawn at every frame.
But since I'm too lazy to dig in there, here is a workaround :
Instead of appending the canvas returned by the library in the document, you can keep it offscreen, and draw it on an other, visible, canvas.
It is on this new canvas that you will also draw your textImage, in an rAF loop.
function doit() {
var previewContainer = document.getElementById("previewContainer");
var textImage = document.getElementById("textImage");
var templateImage = document.getElementById("templateImage");
var w = templateImage.width;
var h = templateImage.height;
previewContainer.removeChild(previewContainer.children[1]);
var gif = new SuperGif({
gif: templateImage,
progressbar_height: 5,
auto_play: true,
loop_mode: true,
draw_while_loading: true
});
gif.load();
var gif_canvas = gif.get_canvas(); // the lib canvas
// a copy of this canvas which will be appended to the doc
var canvas = gif_canvas.cloneNode();
var context = canvas.getContext('2d');
function anim(t) { // our animation loop
context.clearRect(0,0,canvas.width,canvas.height); // in case of transparency ?
context.drawImage(gif_canvas, 0, 0); // draw the gif frame
context.drawImage(textImage, 0, 0, w, h); // then the text
requestAnimationFrame(anim);
};
anim();
previewContainer.replaceChild(canvas, previewContainer.children[0]);
}
<script src="https://cdn.rawgit.com/buzzfeed/libgif-js/master/libgif.js"></script>
<div>
<input type="submit" id="doit" value="Do it!" onclick="doit()" />
</div>
<div id="previewContainer">
<img id="templateImage" src="https://i.imgur.com/chWt4Yg.gif" />
<img id="textImage" src="https://i.stack.imgur.com/CmErq.png" />
</div>

Scaling HTML5 Canvas Image

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);
});

how to find the mouse position x/y using phaser

I'm having problems, trying to display the mouse position x/y when they click on an image, im using one of the click on and image example that phaser provides.
here is the code
var game = new Phaser.Game(800, 500, Phaser.AUTO, 'phaser-example', { preload: preload, create: create });
var text;
var counter = 0;
function preload () {
// You can fill the preloader with as many assets as your game requires
// Here we are loading an image. The first parameter is the unique
// string by which we'll identify the image later in our code.
// The second parameter is the URL of the image (relative)
game.load.image('Happy-face', 'happy.png');
}
function create() {
// This creates a simple sprite that is using our loaded image and
// displays it on-screen and assign it to a variable
var image = game.add.sprite(game.world.centerX, game.world.centerY, 'Happy-face');
// Moves the image anchor to the middle, so it centers inside the game properly
image.anchor.set(0.5);
// Enables all kind of input actions on this image (click, etc)
image.inputEnabled = true;
this.position = new Phaser.Point();
text = game.add.text(250, 16, '', { fill: '#ffffff' });
image.events.onInputDown.add(listener, this);
}
function listener () {
counter++;
text.text = "Position x/y " + counter + "!";
}
if you want x and y position o f input
game.input.x;
game.input.y;
if you want for mouse specifically
game.input.mousePointer.x;
game.input.mousePointer.y;
the listener function will be like
function listener () {
counter++;
text.text = game.input.mousePointer.x +"/"+game.input.mousePointer.y + counter + "!";
}
Just to add that the listener function will be sent 2 parameters: sprite and pointer. So you can do:
function listener (sprite, pointer) {
var x = pointer.x;
var y = pointer.y;
...
}
This will be the most accurate method to use as it accounts for multi-touch devices, where-as accessing input.x/y directly doesn't, it only contains the most recent touch event coordinates (which in a mouse only environment is fine, but not anywhere else).

jQuery and Canvas loading behaviour

After being a long time lurker, this is my first post here! I've been RTFMing and searching everywhere for an answer to this question to no avail. I will try to be as informative as I can, hope you could help me.
This code is for my personal webpage.
I am trying to implement some sort of a modern click-map using HTML5 and jQuery.
In the website you would see the main image and a hidden canvas with the same size at the same coordinates with this picture drawn into it.
When the mouse hovers the main picture, it read the mouse pixel data (array of r,g,b,alpha) from the image drawn onto the canvas. When it sees the pixel color is black (in my case I only check the RED value, which in a black pixel would be 0) it knows the activate the relevant button.
(Originally, I got the idea from this article)
The reason I chose this method, is for the page to be responsive and dynamically change to fit different monitors and mobile devices. To achieve this, I call the DrawCanvas function every time the screen is re-sized, to redraw the canvas with the new dimensions.
Generally, this works OK. The thing is ,there seems to be an inconsistent behavior in Chrome and IE(9). When I initially open the page, I sometimes get no pixel data (0,0,0,0), until i re-size the browser. At first I figured there's some loading issues that are making this happen so I tried to hack it with setTimeout, it still doesn't work. I also tried to trigger the re-size event and call the drawCanvas function at document.ready, still didn't work.
What's bothering me is most, are the inconsistencies. Sometimes it works, sometimes is doesn't. Generally, it is more stable in chrome than in IE(9).
Here is the deprecated code:
<script type="text/javascript">
$(document).ready(function(){setTimeout(function() {
// Get main image object
var mapWrapper = document.getElementById('map_wrapper').getElementsByTagName('img').item(0);
// Create a hidden canvas the same size as the main image and append it to main div
var canvas = document.createElement('canvas');
canvas.height = mapWrapper.clientHeight;
canvas.width = mapWrapper.clientWidth;
canvas.fillStyle = 'rgb(255,255,255)';
canvas.style.display = 'none';
canvas.id = 'hiddencvs';
$('#map_wrapper').append(canvas);
// Draw the buttons image into the canvas
drawCanvas(null);
$("#map_wrapper").mousemove(function(e){
var canvas = document.getElementById('hiddencvs');
var context = canvas.getContext('2d');
var pos = findPos(this);
var x = e.pageX - pos.x;
var y = e.pageY - pos.y;
// Get pixel information array (red, green, blue, alpha)
var pixel = context.getImageData(x,y,1,1).data;
var red = pixel[0];
var main_img = document.getElementById('map_wrapper').getElementsByTagName('img').item(0);
if (red == 0)
{
...
}
else {
...
}
});
},3000);}); // End DOM Ready
function drawCanvas(e)
{
// Get context of hidden convas and set size according to main image
var cvs = document.getElementById('hiddencvs');
var ctx = cvs.getContext('2d');
var mapWrapper = document.getElementById('map_wrapper').getElementsByTagName('img').item(0);
cvs.width = mapWrapper.clientWidth;
cvs.height = mapWrapper.clientHeight;
// Create img element for buttons image
var img = document.createElement("img");
img.src = "img/main-page-buttons.png";
// Draw buttons image inside hidden canvas, strech it to canvas size
ctx.drawImage(img, 0, 0,cvs.width,cvs.height);
}
$(window).resize(function(e){
drawCanvas(e);
}
);
function findPos(obj)
{
...
}
</script>
I'd appreciate any help!
Thanks!
Ron.
You don't wait for the image to be loaded so, depending on the cache, you may draw an image or not in the canvas.
You should do this :
$(function(){
var img = document.createElement("img");
img.onload = function() {
var mapWrapper = document.getElementById('map_wrapper').getElementsByTagName('img').item(0);
...
// your whole code here !
...
}
img.src = "img/main-page-buttons.png";
});

Categories

Resources