Making masks with Canvas globalCompositeOperation for jigsaw puzzle game backend - javascript

I'm trying to make a backend for publishing simple jigsaw-puzzle games. The game uses 12 premade shapes as masks for making the 12 puzzle pieces. I found the excellent Canvas global CompositeOperation tutorial, and tested it.
The application I'm making is using ajax to send each finished piece to the serverside .php-script. The user loads an image (600x400) using SSE and the app moves the original image inside tempCanvas according to the values of the arrays arr_x and arr_y It's supposed to happen in a for-loop:
function drawPieces(){
var canvas = document.getElementById("myCanvas");
var context =canvas.getContext("2d");
var tempCanvas = document.getElementById("tempCanvas");
var tempContext = tempCanvas.getContext("2d");
var img_mask = new Image();
var w;
var h;
var cvd0,cvd1,cvd2,cvd3,cvd4,cvd5,cvd6,cvd7,cvd8,cvd9,cvd10,cvd11;
var arr_data = [cvd0,cvd1,cvd2,cvd3,cvd4,cvd5,cvd6,cvd7,cvd8,cvd9,cvd10,cvd11];
var arr_x = [0,-136,-289,-414,0,-115,-270,-415,0,-118,-288,-415];
var arr_y = [0,0,0,0,-113,-111,-111,-124,-244,-256,-243,-241];
var img_bg = new Image(600, 400);
for (var i = 0; i<arr_x.length; i++) {
img_bg = original;
// get the mask
img_mask.src = "img/mask"+i+".png";
w = img_mask.width;
h = img_mask.height;
console.log("w = ", img_mask.width, " h = ", img_mask.height);
tempCanvas.width = w;
tempCanvas.height = h;
// Her lages maska
tempContext.drawImage(img_mask,0,0);
tempContext.globalCompositeOperation = 'source-in';
tempContext.drawImage(img_bg,arr_x[i],arr_y[i]);
myCanvas.width = w;
myCanvas.height = h;
// Draws tempCanvas on to myCanvas
console.log("tempCanvas: ", tempCanvas, " img_mask.src = ", img_mask.src);
context.drawImage(tempCanvas, 0, 0);
arr_data[i] = myCanvas;
sendData(arr_data[i], [i]);
};
}
Sending the image data to the server:
function sendData(cvd, index){
var imageData = cvd.toDataURL("image/png");
var ajax = new XMLHttpRequest();
ajax.open("POST", "testsave.php", false);
// ajax.onreadystatechange=function(){
// console.log("index = ", index)
// };
ajax.setRequestHeader('Content-Type', 'application/upload');
ajax.send(imageData+"<split>"+index);
};
I got a button to start drawPieces. But I get a number of issues. Firefox throws an error:
InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable: context.drawImage(tempCanvas, 0, 0);
But if I click the button again the loop is run and I get 12 pieces in my folder. (Using xammp for now). But the pieces are cut wrong! The app doesn't seem to load the correct mask each time the loop runs.
So I tested it without using a loop by having 12 different functions where each function is calling the next one. It worked with one function, but started messing up the masks when I added more:
function drawPiece0(){
img_bg = original;
img_mask.src = "img/mask0.png";
tempCanvas.width = 185;
tempCanvas.height = 145;
// Her lages maska
tempContext.drawImage(img_mask,0,0);
tempContext.globalCompositeOperation = 'source-in';
tempContext.drawImage(img_bg,0,0);
myCanvas.width = 185;
myCanvas.height = 145;
// Tegner tempCanvas over på myCanvas
context.drawImage(tempCanvas, 0, 0);
cvd0 = myCanvas;
sendData(cvd0, 0);
// drawPiece1();
};
Something is absolutely wrong in my setup, but I can't figure out what it is. Someone help me please!
By the way, here is my .php-script too:
<?php
if (isset($GLOBALS["HTTP_RAW_POST_DATA"]))
{
// Get the data
$imageData=$GLOBALS['HTTP_RAW_POST_DATA'];
$parts = explode("<split>", $imageData);
$imageData = $parts[0];
$index= $parts[1];
// Remove the headers (data:,) part.
// A real application should use them according to needs such as to check image type
$filteredData=substr($imageData, strpos($imageData, ",")+1);
// Need to decode before saving since the data we received is already base64 encoded
$unencodedData=base64_decode($filteredData);
// Save file.
$fp = fopen( "pieces/".$index.".png", "wb" );
fwrite( $fp, $unencodedData);
fclose( $fp );
}
?>

Problem
The problem is that image loading is asynchronous which means they load in the background while your code is continuing.
That means the images won't be ready (loaded and decoded) when you try to use them with drawImage resulting in the error. The error is indirect here though as w and h do not get valid values for the canvas which means canvas will be 0 width and 0 height which will trigger the actual error when attempted drawn.
The reason why it "works" the second time is because an image exists in the browser's cache and may be able to provide the image before it's used.
Another problem is that in your loop you are overwriting the image variable so only the last image will be used when loaded.
Solution
The solution is to make or use an image loader before you start the loop. It's easy to make one but for simplicity I will use the YAIL loader in this example (disclaimer: author ibid) but any kind of loader will do as long as you use a callback for the images:
function drawPieces(){
var canvas = document.getElementById("myCanvas");
var context =canvas.getContext("2d");
var tempCanvas = document.getElementById("tempCanvas");
var tempContext = tempCanvas.getContext("2d");
var img_mask;
var w;
var h;
var cvd0,cvd1,cvd2,cvd3,cvd4,cvd5,cvd6,cvd7,cvd8,cvd9,cvd10,cvd11;
var arr_data = [cvd0,cvd1,cvd2,cvd3,cvd4,cvd5,cvd6,cvd7,cvd8,cvd9,cvd10,cvd11];
var arr_x = [0,-136,-289,-414,0,-115,-270,-415,0,-118,-288,-415];
var arr_y = [0,0,0,0,-113,-111,-111,-124,-244,-256,-243,-241];
var img_bg;
// using some image(s) loader
var loader = new YAIL({done: draw});
for (var i = 0; i<arr_x.length; i++)
loader.add("img/mask"+i+".png");
loader.load(); // start loading images
// this is called when images has loaded
function draw(e) {
for (var i = 0; i<arr_x.length; i++) {
//img_bg = original; ??
// get the mask
var img_mask = e.images[i]; // loaded images in an array
w = img_mask.width; // now you will have a valid
h = img_mask.height; // dimension here
console.log("w = ", img_mask.width, " h = ", img_mask.height);
tempCanvas.width = w;
tempCanvas.height = h;
// Her lages maska
tempContext.drawImage(img_mask,0,0);
tempContext.globalCompositeOperation = 'source-in';
tempContext.drawImage(img_bg,arr_x[i],arr_y[i]);
myCanvas.width = w;
myCanvas.height = h;
// Draws tempCanvas on to myCanvas
console.log("tempCanvas: ", tempCanvas, " img_mask.src = ", img_mask.src);
context.drawImage(tempCanvas, 0, 0);
arr_data[i] = myCanvas;
sendData(arr_data[i], [i]);
}
};
}
Note: loading images will make your code asynchronous in nature. If the code depends on some other function right after the pieces has been drawn then you must invoke that function from within the inner one after the loop has finished.
Hope this helps. If the pieces still doesn't show correctly please set up a fiddle with the images uploaded to f.ex. imgur.com so we can dig deeper into the problem.
Hope this helps!

Related

Can't seem to get an old image off the canvas

I'm importing an image (that has to come in portrait) onto a canvas object, rotating it (because it's actually landscape) and running some OCR on the base64 from the canvas. That all works fine. However when I put a new image onto the canvas, the old one is retained and never replaced. I've tried clearRect, even gone to the extent of creating a new dom element each time and destroying it when I have everything I need (which is still in the code below) but I just cannot get the first image to clear.
Here's the relevant bit of the code
function onSuccess(imageURI) {
//CREATE NEW CANVAS ELEMENT
g = document.createElement('canvas');
g.setAttribute("id", "thePic");
g.style.overflow = "visible";
document.body.appendChild(g);
const canvas = document.getElementById('thePic'),
context = canvas.getContext('2d');
make_base();
function make_base()
{
base_image = new Image();
base_image.src = imageURI;
base_image.onload = function(){
const uriheight = base_image.naturalHeight;
const uriwidth = base_image.naturalWidth;
console.log(uriwidth + " " + uriheight);
context.canvas.width = uriheight;
context.canvas.height = uriheight;
context.drawImage(base_image, 0, 0);
//ROTATE THE IMAGE 90
context.clearRect(0,0,canvas.width,canvas.height);
context.save();
context.translate(canvas.width/2,canvas.height/2);
context.rotate(90*Math.PI/180);
context.drawImage(base_image,-base_image.width/1.2,-base_image.width/1.2);
context.restore();
var theCanvas = document.getElementById('thePic');
var rotImg = theCanvas.toDataURL();
var rotImg = rotImg.substring(21);
textocr.recText(4, rotImg, onSuccess, onFail);
function onSuccess(recognizedText) {
var recdata = recognizedText.lines.linetext;
var blockData = recognizedText.blocks.blocktext;
context.clearRect(0, 0, 10000,10000);
base_image = "";
rotImg = "";
document.getElementById('thePic').outerHTML = "";
document.getElementById('cgh').innerHTML = "";
}
}
}
Any advice much appreciated.
So it turned out to not be the canvas at all, it was image caching in a previous function that give the image to the canvas.
Thanks to everyone who took the time to have a squiz.

Javascript img to base64 don't load

I develop Photoshop extension that sends images to the server.
Edit: Extensions in photoshop build from html file that define the GUI, js file that basically is the same as any js file, but it's can also launch photoshop function and it is execute from photoshop.
I need to send the images from the file system of the user (from C:\path\to\images)
To encode the images I converted them to dataURL (base64).
The problem occurs in the first time that I convert the images to dataURL. But in the second time and so, it manages to convert the images and everything is fine. In the first time the image doesn't loaded.
I have a folder where the images are and I want to upload the pictures from there, I used a loop that runs on photos and set them into <img src=path> to and then it converts them based 64 via <canvas>.
My code:
function convertLayersToBase64(imgHeight, imgWidth){
var img = new Image();
images = [];
for (var i=0; i<=imagesLength; i++){
path = folder + "layer " + i +".png";
img.src = path;
var canvas = document.createElement("canvas");
canvas.height = imgHeight;
canvas.width = imgWidth;
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
var dataURL = canvas.toDataURL();
images.push( dataURL );
}
return images;
}
I tried to delay the conversion by delay:
function delay(time) {
var d1 = new Date();
var d2 = new Date();
while (d2.valueOf() < d1.valueOf() + time) {
d2 = new Date();
}
}
JQuery when ready:
$(function(){
images.push(getBase64Image());
});
Img.complete
while(!img.complete)
continue;
(In the last example the code stuck in loop)
To put the function in:
img.onload = function(){
//the function here..
//when I use this method it succeed to convert
//only the last image.
}
Nothing worked..
I tried everything, please tell me what to change and how to fix that.
Edit: It's seem to me that the only way to load an image it's when the code
The function onload is an asynchronous action. You cannot just return images as the last statement within your convertLayersToBase64 function. You should either use promises, or a more simple approach would be to use a callback function.
function convertLayersToBase64(imgHeight, imgWidth, callback){
var images = [];
for (var i = 0; i <= imagesLength; i++) {
var img = new Image();
img.onload = function() {
var canvas = document.createElement("canvas");
canvas.height = imgHeight;
canvas.width = imgWidth;
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(this, 0, 0);
var dataURL = canvas.toDataURL();
images.push(dataURL);
if(images.length === imagesLength) {
callback(images);
}
}
path = folder + "layer " + i +".png";
img.src = path;
}
}
You would call this like:
convertLayersToBase64(200, 200, function(images) {
console.log('hii, i got images', images);
});
This is obviously without any form of error check, or even best practice guidelines, but I'll leave it up to you to implement that.

How can i save an image in my computer that has been filtered with CSS [duplicate]

I'm building a new website that will let users apply filters to images (just like Instagram). I will use -webkit-filter for that.
The user must be able to save the filtered image. There is any way I can do that using JavaScript?
You can't save images directly, but you can render them in Canvas, then save from there.
See: Save HTML5 canvas with images as an image
There is no direct/straight forward method to export an image with CSS Filter.
Follow the below steps for Saving/Exporting an Image with -webkit-filter applied on it:
1. Render the image to a canvas:
var canvas = document.createElement('canvas');
canvas.id="canvasPhoto";
canvas.width = imageContaainer.width;
canvas.height = imageContaainer.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(imageContaainer, 0, 0, canvas.width, canvas.height);
Get the ImageData from canvas and apply the filter. Eg: I will apply grayscale filter to the ImageData below:
function grayscale(ctx) {
var pixels = ctx.getImageData(0,0,canvas.width, canvas.height);
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
var r = d[i];
var g = d[i+1];
var b = d[i+2];
var v = 0.2126*r + 0.7152*g + 0.0722*b;
d[i] = d[i+1] = d[i+2] = v
}
context.putImageData(pixels, 0, 0);
}
Add an event and use the below code to trigger download
function download(canvas) {
var data = canvas.toDataURL("image/png");
if (!window.open(data))
{
document.location.href = data;
}
}

JavaScript - Uncaught TypeError: Type error

I am trying to break a spritesheet and well, it's not putting it into a 2D array like it should. At the end of my code, near the console.log, it spits out ImageData...but when it tries to putImageData... I get Uncaught TypeError: Type error
Help?
and here's my code:
$(document).ready(function () {
console.log("Client setup...");
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
canvas.tabIndex = 0;
canvas.focus();
console.log("Client focused.");
var imageObj = new Image();
imageObj.src = "./images/all_tiles.png";
imageObj.onload = function() {
context.drawImage(imageObj, imageWidth, imageHeight);
};
var allLoaded = 0;
var tilesX = ImageWidth / tileWidth;
var tilesY = ImageHeight / tileHeight;
var totalTiles = tilesX * tilesY;
var tileData = [totalTiles]; // Array with reserved size
for(var i=0; i<tilesY; i++)
{
for(var j=0; j<tilesX; j++)
{
// Store the image data of each tile in the array.
tileData.push(context.getImageData(j*tileWidth, i*tileHeight, tileWidth, tileHeight));
allLoaded++;
}
}
if (allLoaded == totalTiles) {
console.log("All done: " + allLoaded);
console.log(tileData[1]);
context.putImageData(tileData[0], 0 ,0); // Sample paint
}
});
error happens at context.putImageData(tileData[0], 0 ,0); // Sample paint
Here is example fiddle of code above http://jsfiddle.net/47XUA/
Your problem is that the first element of tileData is not an ImageData object, it's a plain number! When you initialized your array you used:
var tileData = [totalTiles];
This means that tileData[0] is equal to the value of totalTiles, which is the number 483. You are providing a number instead of an ImageData object to putImageData, and the browser is throwing an error in confusion. If you look at any other element in the rest of your array, you'll see it is successfully being populated with ImageData objects as you expect; only the first element in the array is a number.
You probably meant to do:
var tileData = new Array(totalTiles);
which would create an array with the specified length property, instead of setting the first value in the array to a number. Personally, I would run some performance tests to see if that is even useful; you may be just as well using var tileData = [];
A type error is caused when a variable or object passed into a statement is not of a type that is expected for that command and can’t be converted into something that is valid.
See here for a full definition of the error.
So most likely that context or tileData is in an incorrect format or is null or undefined
It might be a racing condition. You are using drawImage only when the image ./images/all_tiles.png has been loaded. But you are using the image data on the context before the onload fired. Try to move everything which is after the onload handler into the onload handler.
The problem here is that the imageObj.onload() function is being called AFTER the rest of the script is executed.
This means that queries to getImageData() are returning undefined, as there is no image data in the canvas when called.
A simple way to fix this is to encapuslate the remaining script in the imageObj.onload function, like so:
$(document).ready(function () {
console.log("Client setup...");
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
canvas.tabIndex = 0;
canvas.focus();
console.log("Client focused.");
var imageObj = new Image();
imageObj.src = "./images/all_tiles.png";
imageObj.onload = function() {
context.drawImage(imageObj, 0, 0);
var allLoaded = 0;
var tilesX = imageWidth / tileWidth;
var tilesY = imageHeight / tileHeight;
var totalTiles = tilesX * tilesY;
var tileData = new Array(); // Array with NO reserved size
for(var i=0; i<tilesY; i++)
{
for(var j=0; j<tilesX; j++)
{
// Store the image data of each tile in the array.
tileData.push(context.getImageData(j*tileWidth, i*tileHeight, tileWidth, tileHeight));
allLoaded++;
}
}
context.putImageData(tileData[0], 0 ,0); // Sample paint
}; // End 'imageObj.onload()' scope.
}); // End document ready function scope.
This ensures that any calls to getImageData() are made after context.drawImage()
You may also have a problem here that you are not drawing the image to cover the canvas. Change the line:
context.drawImage(imageObj, imageWidth, imageHeight);
to
context.drawImage(imageObj, 0, 0);
EDIT: You also had ImageWidth and imageWidth (same for height). Rename these.
EDIT: Reserving the Array size here with new Array(totalTiles) was causing an issue. When we pushed new tile images to the array, it pushed to the end of the pre-allocated array (starting at tileData[totalTiles]). This is easily fixed by removing the size argument. Otherwise instead of using push() we could copy the image data into the array in a loop, eg: tileData[n] = getImageData(...), starting at tileData[0].
I'm using Chrome.
context.drawImage(spritesheet.image, 0, 0, 4608, 768);// Works
context.drawImage(spritesheet.image, 768, 0, 768, 768, 4608, 768); // Doesn't work: Uncaught TypeError: Type error
Thank you!

Rotating and Caching in canvas bug

I am making a game and in it I would like to have the ability to use cached canvas's instead of rotating the image every frame. I think I made a function for adding images that makes sense to me, but I am getting an error every frame that tells me that the canvas object is no longer available.
INVALID_STATE_ERR: DOM Exception 11: An attempt was made to use an
object that is not, or is no longer, usable.
You might also need to know that I am using object.set(); to go ahead and add that image to a renderArray. That may be affecting whether the canvas object is still avaliable?
Here is the function that returns a cached canvas, (I took it from a post on this website :D)
rotateAndCache = function(image, angle){
var offscreenCanvas = document.createElement('canvas');
var offscreenCtx = offscreenCanvas.getContext('2d');
var size = Math.max(image.width, image.height);
offscreenCanvas.width = size;
offscreenCanvas.height = size;
offscreenCtx.translate(size/2, size/2);
offscreenCtx.rotate(angle + Math.PI/2);
offscreenCtx.drawImage(image, -(image.width/2), -(image.height/2));
return offscreenCanvas;
}
And here is some more marked up code:
var game = {
render:function(){
//gets called every frame
context.clearRect(0, 0, canvas.width, canvas.height);
for(i = 0; i < game.renderArray.length; i++){
switch(game.renderArray[i].type){
case "image":
context.save();
context.translate(game.renderArray[i].x, game.renderArray[i].y);
context.rotate(Math.PI*game.renderArray[i].rotation/180);
context.translate(-game.renderArray[i].x, -game.renderArray[i].y);
context.drawImage(game.renderArray[i].image, game.renderArray[i].x, game.renderArray[i].y);
context.restore();
break;
}
if(game.renderArray[i].remove == true){
game.renderArray.splice(i,1);
if(i > 1){
i--;
}else{
break;
}
}
}
},
size:function(width, height){
canvas.height = height;
canvas.width = width;
return height + "," + width;
},
renderArray:new Array(),
//initialize the renderArray
image:function(src, angle){
if(angle != undefined){
//if the argument 'angle' was given
this.tmp = new Image();
this.tmp.src = src;
//sets 'this.image' (peach.image) to the canvas. It then should get rendered in the next frame, but apparently it doesn't work...
this.image = rotateAndCache(this.tmp, angle);
}else{
this.image = new Image();
this.image.src = src;
}
this.x = 0;
this.y = 0;
this.rotation = 0;
this.destroy = function(){
this.remove = true;
return "destroyed";
};
this.remove = false;
this.type = "image";
this.set = function(){
game.renderArray.push(this);
}
}
};
var canvas, context, peach;
$(document).ready(function(){
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
//make the variable peach a new game.image with src of meme.jpg and an angle of 20.
peach = new game.image('meme.jpg', 20);
peach.set();
game.size(700,500);
animLoop();
});
If you want, here is this project hosted on my site:
http://keirp.com/zap
There are no errors on your page, at least not anymore or not that I can see.
It's quite possible that the problem is an image that is not done loading. For instance that error will happen if you try to make a canvas pattern out of a not-yet-finished-loading image. Use something like pxloader or your own image loading function to make sure all the images are complete before you start drawing.
Anyway, it's nigh impossible to figure out what was or is happening since your code isn't actually giving any errors (anymore).

Categories

Resources