Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Drawing images to canvas returns InvalidStateError for every new image drawn, then succeeds

Situation

I have a grid-like unordered list with 144 (90px x 90px) images (12x12) that may be rotated. My end goal is to take the 144 image grid and save it as 1 image.

Current Solution

My current solution has me following these steps:

  1. Create a canvas that is one image width x 12 wide and one image height x 12 high. This is to represent the end product image.
  2. Loop through the list items(images), extracting the image src from the item and drawing it onto its own canvas that is the size of the image.
  3. Rotate the new small canvas however it's image has been rotated on the grid.
  4. Draw the new small canvas onto the end-result canvas at the x and y of the current pointer.

Things to note

As I loop through the images, I keep track of a pointer (where I am currently on the canvas). I do this by maintaining a row and a col number. They represent the current row and column of images I am drawing. I use them, multiplied by the width and height of a single image, to get the exact x and y coordinates on the canvas to draw the next image.

Current Problem

When I call the function to create, draw and generate the base64 of the canvas, I receive the following error message: "InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable.". If this error was occurring 100% of the time, I'd assume its because the image I'm drawing to the canvas, either isn't loaded yet or is not being loaded at all, but, I only receive this error once for every new image I load. For example, if I have a 144 image grid, that is 2 different images each drawn 72 times, I will receive InvalidStateError twice and then the third time I call the function, it will succeed.

Current Code

Please keep in mind this is simply spike code to test saving the image, I am aware some refactoring is required.

generateThumbnail: function(){
  var builder = this,
      canvas = document.createElement('canvas'),
      content,
      row = 0,
      col = 0;

  // width is single image width (90) x number of tiles wide (usually 12)
  canvas.width = 90 * builder.grid[0];
  // height is single image height (90) x number of tiles high (usually 12)
  canvas.height = 90 * builder.grid[1];
  // get 2d context of new canvas
  context = canvas.getContext("2d");

  // loop through all of the images on the grid
  $.each($(".pattern-grid li"), function(i, tile) {
    var $tile = $(tile),
        image = new Image(),
        src = $tile.find("img").attr("src"),
        width,
        height,
        buffer,
        bufferctx,
        x,
        y;

    // set crossOrigin of image to anonymous as these images are loaded via CORS
    image.crossOrigin = "Anonymous";
  
    // increase row number by 1 if it has reached the end of the row and its not the first image being drawn
    if(i % builder.grid[0] == 0 && i != 0){
      row++;
    }
    // Set Column to 0 if it is a new row, otherwise increase column by 1 (unless it is the first image being drawn)
    if(col == builder.grid[0]-1){
      col = 0;
    }else if(i != 0){
      col++;
    }

    // determine if there was no image drawn at this location
    if(src != undefined){
      image.src = src;
      // get the width and height the image, to be used for the small canvas and where to draw it
      width = image.width;
      height = image.height;
      // create a new buffer canvas to draw the image to, this will be used to apply any rotations that may exist
      buffer = document.createElement("canvas");
      //set width and height of the buffer to the current images width and height
      buffer.width = width;
      buffer.height = height;
      bufferctx = buffer.getContext("2d");
      //Determine x and y coordinates to draw the small canvas using row and column numbers
      x = col*width;
      y = row*height;
      //Save current state of buffer canvas
      bufferctx.save();
      //translate and then rotate the buffer canvas by the image's rotation
      bufferctx.translate(width/2, height/2);
      bufferctx.rotate($tile.find("img").data("rotation")*Math.PI/180);
      bufferctx.translate(width/2*-1, height/2*-1);
      //draw image to buffer canvas and restore its context
      bufferctx.drawImage(image, 0, 0);
      bufferctx.restore();
      //draw the buffer canvas to the main canvas at predetermined x and y
      context.drawImage(buffer, x, y, width, height);
    }
  });
  return canvas.toDataURL();
}
like image 510
Bryan Ashley Avatar asked Oct 28 '13 14:10

Bryan Ashley


1 Answers

I was able to use @abiessu's suggestion with an onload, paired with a closure to save state of the function. My solution, that works, is:

generateThumbnail: function(){
  var builder = this,
      canvas = document.createElement('canvas'),
      content,
      row = 0,
      col = 0;
  // width is single image width (90) x number of tiles wide (usually 12)
  canvas.width = 90 * builder.grid[0];
  // height is single image height (90) x number of tiles high (usually 12)
  canvas.height = 90 * builder.grid[1];
  context = canvas.getContext("2d");
  // loop through all of the images on the grid
  $.each($(".pattern-grid li"), function(i, tile) {
    var $tile = $(tile),
        image = new Image(),
        src = $tile.find("img").attr("src");
     // set crossOrigin of image to anonymous as these images are loaded via CORS
    image.crossOrigin = "Anonymous";
    // increase row number by 1 if it has reached the end of the row and its not the first image being drawn
    if(i % builder.grid[0] == 0 && i != 0){
      row++;
    }
    // increase row number by 1 if it has reached the end of the row and its not the first image being drawn
    if(col == builder.grid[0]-1){
      col = 0;
    }else if(i != 0){
      col++;
    }
    image.onload = function(row, col){
      return function(){
        // determine if there was no image drawn at this location
        if(src != undefined){
          var width = image.width,
              height = image.height,
              buffer = document.createElement("canvas"),
              bufferctx,
              x,
              y;
          buffer.width = width;
          buffer.height = height;
          bufferctx = buffer.getContext("2d");
          x = col*width;
          y = row*height;
          bufferctx.save();
          bufferctx.translate(width/2, height/2);
          bufferctx.rotate($tile.find("img").data("rotation")*Math.PI/180);
          bufferctx.translate(width/2*-1, height/2*-1);
          bufferctx.drawImage(image, 0, 0);
          bufferctx.restore();
          context.drawImage(buffer, x, y, width, height);
        }
      }
    }(row, col);
    image.src = $tile.find("img").attr("src");
  });
  window.canvas = canvas;
}
like image 51
Bryan Ashley Avatar answered Oct 15 '22 22:10

Bryan Ashley