<div class="p-2" style="min-width: 700px;">
  <div class="flex items-center mb-2">
    <label class="w-1/6">Spritesheet</label>
    <input class="input is-small mr-2" style="width:500px" type="text" id="input" value="https://scontent.fhan2-1.fna.fbcdn.net/v/t39.1997-6/p480x480/73252790_526220981444667_4912786796657508352_n.png?_nc_cat=107&_nc_sid=0572db&_nc_oc=AQm9Rha5tZe_kub4gwl1QElwTN9Eyei-4Lha8jRE8CZC7wO-go1w72FJLMVUB4uj3kw&_nc_ht=scontent.fhan2-1.fna&oh=45fda801721ac2c098217e33eec8d156&oe=5EE07A61" />
  </div>
  
  <div class="flex mb-2">
    <div class="flex items-center w-1/3">
      <label class="w-1/2">Original Width</label>
      <input class="input is-small mr-2" style="width:50px" type="text" id="originalWidth" value="160" />
      <span class="text-gray-700">px</span>
    </div>
    <div class="flex items-center w-1/3">
      <label class="w-1/2">Original Height</label>
      <input class="input is-small mr-2" style="width:50px" type="text" id="originalHeight" />
      <span class="text-gray-700">px</span>
    </div>
  </div>
  
  <div class="flex mb-2">
    <div class="flex items-center w-1/3">
      <label class="w-1/2">Width</label>
      <input class="input is-small mr-2" style="width:50px" type="text" id="width" value="100" />
      <span class="text-gray-700">px</span>
    </div>
    <div class="flex items-center w-1/3">
      <label class="w-1/2">Height</label>
      <input class="input is-small mr-2" style="width:50px" type="text" id="height" />
      <span class="text-gray-700">px</span>
    </div>
  </div>
  
  <div class="flex items-center w-1/2 mb-2">
    <label class="w-1/3">Frame rate</label>
    <input class="input is-small mr-2" style="width:50px" type="text" id="fps" value="8" />
    <span class="text-gray-700">fps</span>
  </div>
  <div>
    <button class="button is-small" id="run">
      Make GIF
    </button>
  </div>
</div>
async function run() {
  const spriteSheetURL = document.getElementById('input').value;
  const originalWidth = Number(document.getElementById('originalWidth').value);
  const originalHeight = Number(document.getElementById('originalHeight').value) || originalWidth;
  const width = Number(document.getElementById('width').value);
  const height = Number(document.getElementById('height').value) || width;  
  const fps = Number(document.getElementById('fps').value) || 10;

  const sprites = await makeCanvases(spriteSheetURL, originalWidth, originalHeight, width, height);
  const blob = await renderGif(sprites, fps);

  const url = URL.createObjectURL(blob);
  const img = document.createElement('img');
  img.setAttribute('src', url);
  img.className = 'mb-2';
  
  const btn = document.createElement('button');
  btn.innerHTML = '<i class="far fa-save mr-1"></i>Save';
  btn.className = 'button is-small';
  btn.onclick = () => {
    const a = document.createElement('a');
    a.href = url;
    a.download = `${url.slice(url.lastIndexOf('/') + 1)}.gif`;
    a.click();
  };
  
  const item = document.createElement('div');

  const removeBtn = document.createElement('button');
  removeBtn.className = 'absolute';
  removeBtn.style.top = 0;
  removeBtn.style.right = 0;
  removeBtn.innerHTML = '<i class="far fa-times-circle text-gray-500"></i>'
  removeBtn.onclick = () => {
    document.body.removeChild(item);
    URL.revokeObjectURL(url)
  };
  
  item.className = 'inline-block text-center relative';
  item.appendChild(img)
  item.appendChild(btn);
  item.appendChild(removeBtn);
  document.body.appendChild(item);
}

async function renderGif(frames, fps) {
  const workerScript = await loadWorkerScript();

  const gif = new GIF({
    workers: 2,
    workerScript,
    quality: 1,
  });

  frames.forEach(frame => gif.addFrame(frame, {
    delay: 1000 / fps,
  }));

  return new Promise((resolve, reject) => {
    gif.on('finished', resolve);

    try {
      gif.render();
    } catch(e) {
      reject(e);
    }
  });
}

async function makeCanvases(spriteSheetUrl, originalWidth, originalHeight, canvasWidth, canvasHeight) {
  const spriteSheet = await loadImage(spriteSheetUrl);
  spriteSheet.crossOrigin = '';

  let x = 0;
  let y = 0;
  let sprites = [];

  while (y < spriteSheet.height) {
    x = 0;

    while (x < spriteSheet.width) {
      const canvas = document.createElement('canvas');
      canvas.width = canvasWidth;
      canvas.height = canvasHeight;

      const ctx = canvas.getContext('2d');
      ctx.drawImage(spriteSheet, x, y, originalWidth, originalHeight, 0, 0, canvasWidth, canvasHeight);

      const isEmpty = ctx.getImageData(0, 0, canvasWidth, canvasHeight).data.every(channel => channel === 0);

      if (!isEmpty) {
        ctx.fillStyle = '#fff';
        ctx.fillRect(0, 0, canvasWidth, canvasHeight);
        ctx.drawImage(spriteSheet, x, y, originalWidth, originalHeight, 0, 0, canvasWidth, canvasHeight);
        sprites.push(canvas);
      }

      x += originalWidth;
    }

    y += originalHeight;
  }

  return sprites;
}

function loadImage(imgUrl) {
  const img = new Image();
  img.crossOrigin = 'Anonymous';

  return new Promise((resolve, reject) => {
    img.onload = function() {
      resolve (this);
    };

    img.error = reject;

    img.src = imgUrl;
  })
}

async function loadWorkerScript() {
  const { data } = await axios.get('https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.worker.js', {
    responseType: 'blob',
  });

  const content = await data.text();

  const blob = new Blob([content], {
    type: 'application/javascript',
  });

  return URL.createObjectURL(blob);
}

document.getElementById('run').onclick = run;
View Compiled
Run Pen

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/1.4.6/tailwind.min.css
  2. https://cdnjs.cloudflare.com/ajax/libs/bulma/0.8.2/css/bulma.min.css
  3. https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.js
  2. https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js