javascriptjqueryhtml5-canvasgifsprite-sheet

how to convert frames of GIF into spritesheet


I am trying to merge multiple frames from a GIF to convert them into spritesheet. I am somehow able to extract frames using libgif.js here is my code. The Canvas in which i want to put all my images is empty and is at the end i dont know what is wrong with it.

<img hidden src="https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif" rel:auto_play="0"
    rel:rubbable="0" />

  <div id="frames">
  </div>
  <canvas id="myCanvas" style="border:1px solid #d3d3d3;">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://unpkg.com/jsgif@1.0.2/libgif.js"></script>
<script>
  $(document).ready(function () {
    $('img').each(function (idx, img_tag) {
      var total = 0;
      if (/^.+\.gif$/.test($(img_tag).prop("src"))) {
        var rub = new SuperGif({ gif: img_tag, progressbar_height: 0 });
        rub.load(function () {
          for (var i = 0; i < rub.get_length(); i++) {
            total += 1;
            rub.move_to(i);
            var canvas = cloneCanvas(rub.get_canvas());

            $("#frames").append('<img id = "' + i + '"src= "' + canvas + '">');

          }
          for (var i = 0; i <= total; i++) {
            id = i.toString();
            var img = document.getElementById(id);
            window.onload = function () {

              var c = document.getElementById("myCanvas");
              var ctx = c.getContext("2d");

              ctx.drawImage(img, 10, 10);
            }
          }
        });
      }
    });
  });

  function cloneCanvas(oldCanvas) {
    var img = oldCanvas.toDataURL("image/png");
    return img;
  }
</script>

Solution

  • The main issue in your solution is that you are looping through all inserted frame-images and using window.onload with the expectation to draw images on the canvas. However, you are assigning windows.onload on the image elements after the images have already been attached to the dom in the previous (for i = 0) iteration, through $("#frames").append(.... This means that your onload handlers are getting registered after the event has already fired.

    I rearranged your code into the snippet below to make sure that onload is registered before appending the image-frames into the document.

    In this solution here I created the "spritesheet" by putting the image-frames vertically one-after-the-other. Tweak the solution accordingly to create your desired "spritesheet layout".

    The tricky part is that you need to set a width and height for the canvas in a dynamic fashion, which means you have to wait for all image-frames to be loaded into the document and meassure their collective widths and heights. The algorithm should be:

    1. Collect all widths and heights of image-frames onload
    2. Calculate the total canvas.width and canvas.height depending on your desired layout (in my vertical-one-ofter-the-other solution this means to use the maximal image width as canvas width and the sum of all image heights as height. If you need a different spritesheet layout then you need to adapt this logic differently)
    3. Draw all the images on the canvas, which by this point has enough width and height to fit all your gif-frame-images.

    In my solution I did not implement this algorith, I rather hard-coded the canvas width and height after manually calculating the max-img-width and total-img-height for your particular gif example.

    $(document).ready(function () {
        const c = document.getElementById("myCanvas");
        const ctx = c.getContext("2d");
        let canvasWidth = 0;
        let canvasHeight = 0;
        $('img').each(function (idx, img_tag) {
          var total = 0;
          if (/^.+\.gif$/.test($(img_tag).prop("src"))) {
            var rub = new SuperGif({ gif: img_tag, progressbar_height: 0 });
            rub.load(function () {
              for (let i = 0; i < rub.get_length(); i++) {
                total += 1;
                rub.move_to(i);
                var canvas = cloneCanvas(rub.get_canvas());
    
                const img = $('<img id = "' + i + '"src= "' + canvas + '">');
                img.on('load', function() {
                  // draw the image
                  ctx.drawImage(this, 0, canvasHeight);
                  // extend canvas width, if newly loaded image exceeds current width
                  canvasWidth = $(this).width() > canvasWidth ? $(this).width() : canvasWidth;
                  // add canvas height by the newly loade image height value
                  canvasHeight += $(this).height();
                  // resize sprite-canvas to host the new image
                  canvas.width = canvasWidth;
                  canvas.height = canvasHeight;
                });
                $("#frames").append(img);
              }
            });
          }
        });
      });
    
      function cloneCanvas(oldCanvas) {
        var img = oldCanvas.toDataURL("image/png");
        return img;
      }
    Gif:
    <img hidden src="https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif" rel:auto_play="0"
        rel:rubbable="0" />
    <hr />
    Frames:
    <div id="frames">
    </div>
    <hr />
    Canvas:
    <hr />
    <canvas id="myCanvas" style="border:1px solid #d3d3d3;" width="400" height="17600">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://unpkg.com/jsgif@1.0.2/libgif.js"></script>