On this article, I’m going to clarify how one can convert animations created utilizing GSAP into animated GIFs utilizing modern-gif.

Right here’s a sneak peek of 1 I made earlier. 👇

Two panels, reading "screw you guys I'm making GIFs", with a South Park character moving from one panel to the next

On the hyperlinks beneath, you’ll discover a dwell preview and all of the code I’ll be referencing all through this text:

There are two “pages” within the repo. index accommodates all of the code for the GIF seen above, and simple is a place to begin for the steps coated on this put up.

Desk of Contents

The right way to convert GSAP Animations into GIFs

The strategy I’m utilizing to transform a GSAP animation right into a GIF entails capturing SVG information on every “replace” of the Tween and writing it to an HTML canvas. After the Tween completes I’m then capable of convert SVG information into Rasterized picture information which can be utilized by modern-gif to create every body of an animated GIF.

Getting Began

Right here’s the code I’ve used within the easy instance, and it’s what I’ll be utilizing to clarify every of the steps required to create an animated GIF from a GSAP animation:

<html lang='en'>
<head>
  <meta charset='utf-8' />
  <title>Easy</title>
  <script>
    const canvas = doc.getElementById('canvas');
    const ctx = canvas.getContext('2nd');

    let animationFrames = [];
    let canvasFrames = [];

    gsap.timeline({
      onUpdate: () => {},
      onComplete: () => {},
    })
    .fromTo('#rect', { x: -50 }, { length: 2, x: 350, ease: 'energy.ease2' });
  </script>
</head>
<physique>
  <important>
    <part>
      <svg
        id='svg'
        xmlns='http://www.w3.org/2000/svg'
        viewBox='0 0 400 200'
        width={400}
        top={200}
        model={{ border: '1px strong purple' }}
      >
        <rect id='rect' x='0' y='75' width='50' top='50' fill='purple'></rect>
      </svg>
      <canvas id='canvas' model={{ border: '1px strong blue' }} width={400} top={200}></canvas>
      <img id='picture' width={400} top={200} model={{ border: '1px strong inexperienced' }} />
      <a id='hyperlink' obtain='easy.gif'>Obtain</a>
    </part>
  </important>
  <script src='https://unpkg.com/modern-gif'></script>
  <script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js'></script>
</physique>
</html>

There are a few issues I’d like to clarify in regards to the above code.

Inline Script

On the high of the file I create a reference to the canvas component within the HTML (beneath) and outline a brand new reference to the canvas context referred to as ctx. This may enable me to reference the canvas component and write information to it.

There are two arrays outlined to carry the captured information (I’ll clarify the place every is utilized in a later step):

  • animationFrames
  • canvasFrames

And final, however not least, an occasion of a GSAP Timeline and Tween that animates an SVG rect component within the HTML (beneath).

HTML

  • The HTML accommodates an svg component with an ID of svg with a purple rect component with an ID of rect. The rect is the component I’ll be animating.
  • Under the svg component is a canvas component. That is the place I’ll write the captured SVG information to be used afterward.
  • Under the canvas component is an img component. That is the place the ultimate animated GIF might be displayed.
  • Lastly, there’s an a component which can be utilized to “obtain” the GIF.

Script parts

The 2 script parts on the backside are for the modern-gif library and GSAP library. Each should be included within the web page so you should utilize them.

Capturing SVG Knowledge

Find the GSAP Timeline and make the next modifications:

gsap.timeline({
  onUpdate: () => {
+    const xml = new XMLSerializer().serializeToString(svg);
+    const src = `information:picture/svg+xml;base64,${btoa(xml)}`;
+    animationFrames.push(src);
  },
  onComplete: () => {
+    console.log(animationFrames);
  },
})
.fromTo('#rect', { x: -50 }, { length: 2, x: 350, ease: 'energy.ease2' });

The above code serializes the HTML svg component and converts the info to an svg+xml;base64 string. At this level, the “picture information” isn’t fairly what I would like, however by changing it to a string I can retailer it within the animationFrame array to be used afterward.

In case you’ve added the console.log within the onComplete perform, it is best to see one thing much like the picture beneath within the console of your browser.

screenshot of svg+xml;base64 strings stored in an array and displayed in the browser's console

Convert SVG Knowledge to Rasterized Knowledge

gsap.timeline({
  onUpdate: () => {
    const xml = new XMLSerializer().serializeToString(svg);
    const src = `information:picture/svg+xml;base64,${btoa(xml)}`;
    animationFrames.push(src);
  },
  onComplete: () => {
-    console.log(animationFrames);
+    let inc = 0;
+    const renderSvgDataToCanvas = () => {
+      const virtualImage = new Picture();
+      virtualImage.src = animationFrames[inc];
+      virtualImage.onload = () => {
+        ctx.clearRect(0, 0, 400, 200);
+        ctx.drawImage(virtualImage, 0, 0, 400, 200);
+        canvasFrames.push(canvas.toDataURL('picture/jpeg'));
+        inc++;
+        if (inc < animationFrames.size - 1) {
+          renderSvgDataToCanvas();
+        } else {
+          console.log(canvasFrames);
+        }
+      };
+    };
+    renderSvgDataToCanvas();
  },
})
.fromTo('#rect', { x: -50 }, { length: 2, x: 350, ease: 'energy.ease2' });

This step is barely extra concerned and requires that I carry out an motion for every index of the animationFrames array.

By utilizing a recursive perform, renderSvgDataToCanvas, I can use the picture information from the animationFrames array, write it to the canvas. Then, by utilizing canvas.toDataURL('picture/jpeg') I can retailer rasterized information of every body of the animation within the canvasFrames array.

In case you’ve added the console.log within the onComplete perform, it is best to see one thing much like the beneath within the console of your browser. This time, nonetheless, be aware the MIME sort of the info: as an alternative of svg+xml, it’s picture/jpeg. That is essential for what I have to do subsequent.

rasterized data

Convert Rasterized Knowledge to GIF

That is the final step and entails passing every index of the canvasFrames array onto modern-gif.

gsap.timeline({
onUpdate: () => {
  const xml = new XMLSerializer().serializeToString(svg);
  const src = `information:picture/svg+xml;base64,${btoa(xml)}`;
  animationFrames.push(src);
},
onComplete: () => {
  let inc = 0;
  const renderSvgDataToCanvas = () => {
    const virtualImage = new Picture();
    virtualImage.src = animationFrames[inc];
    virtualImage.onload = () => {
      ctx.clearRect(0, 0, 400, 200);
      ctx.drawImage(virtualImage, 0, 0, 400, 200);
      canvasFrames.push(canvas.toDataURL('picture/jpeg'));
      inc++;
      if (inc < animationFrames.size - 1) {
        renderSvgDataToCanvas();
      } else {
-          console.log(canvasFrames);
+          generateGif();
      }
    };
  };
+    const generateGif = async () => {
+      const gif = await modernGif.encode({
+        width: 400,
+        top: 200,
+        frames: canvasFrames.map((body) => {
+          return { imageData: body, delay: 0 };
+        }),
+      });
+      const frames = await gif;
+      const blob = new Blob([frames], { sort: 'picture/gif' });
+      const src = URL.createObjectURL(blob);

+      const picture = doc.getElementById('picture');
+      const hyperlink = doc.getElementById('hyperlink');

+      picture.src = src;
+      hyperlink.href = src;
+    };
    renderSvgDataToCanvas();
  },
})
.fromTo('#rect', { x: -50 }, { length: 2, x: 350, ease: 'energy.ease2' });

Utilizing modernGif.encode you’ll be able to go an array of knowledge onto frames and outline a delay for every body, I’ve chosen so as to add a delay of 0 seconds.

The subsequent a part of the code offers with changing the modernGif.ecode information and changing it to “one more” MIME sort, this time picture/gif.

As soon as I’ve a ultimate “blob” of knowledge that represents my animated GIF I convert it to a URL after which set the src and href of the picture and hyperlink parts so I can see and obtain the GIF within the browser.

Converting to GIF

Body Charge

You may discover the ultimate GIF runs fairly slowly, it is because animations that run within the browser will sometimes play again at 60 frames per second (fps), whereas GIFs sometimes run at a a lot slower body fee, 12 or 24fps.

To “drop” some frames of the animation I take advantage of an array filter and JavaScript remainder operator to find out if the index is divisible by a sure quantity, in my case, I selected 6. Indexes that aren’t divisible by 6 are filtered out of the array. The ensuing animated GIF, whereas just a little clunky, will play again a lot quicker.

const generateGif = async () => {
  const gif = await modernGif.encode({
    width: 400,
    top: 200,
    frames: canvasFrames
+       .filter((_, index) => index % 6 === 0)
      .map((body) => {
        return { imageData: body, delay: 0 };
      }),
    });
  const frames = await gif;
  const blob = new Blob([frames], { sort: 'picture/gif' });
  const src = URL.createObjectURL(blob);

  const picture = doc.getElementById('picture');
  const hyperlink = doc.getElementById('hyperlink');

  picture.src = src;
  hyperlink.href = src;
};

And that’s how one can go from GSAP SVG animation to animated GIF by way of the HTML canvas!

When you’ve got any questions on something I’ve described on this put up be at liberty to seek out me on Twitter/X: @PaulieScanlon.