<div id="preview">
  <svg id="amoeba" width="64" height="64" viewBox="0 0 64 64">
    <g id="amoeba-body">
      <circle id="amoeba-body-start" cx="32" cy="32" r="8" fill="#f14ca3"/>
      <path id="amoeba-body-end" d="M35.26,9.77h0a3.88,3.88,0,0,0,5,1.34h0a3.89,3.89,0,0,1,5.65,3.26h0a3.88,3.88,0,0,0,3.68,3.68h0a3.89,3.89,0,0,1,3.26,5.65h0a3.88,3.88,0,0,0,1.34,5h0a3.89,3.89,0,0,1,0,6.52h0a3.88,3.88,0,0,0-1.34,5h0a3.89,3.89,0,0,1-3.26,5.65h0a3.88,3.88,0,0,0-3.68,3.68h0a3.89,3.89,0,0,1-5.65,3.26h0a3.88,3.88,0,0,0-5,1.34h0a3.89,3.89,0,0,1-6.52,0h0a3.88,3.88,0,0,0-5-1.34h0a3.89,3.89,0,0,1-5.65-3.26h0a3.88,3.88,0,0,0-3.68-3.68h0a3.89,3.89,0,0,1-3.26-5.65h0a3.88,3.88,0,0,0-1.34-5h0a3.89,3.89,0,0,1,0-6.52h0a3.88,3.88,0,0,0,1.34-5h0a3.89,3.89,0,0,1,3.26-5.65h0a3.88,3.88,0,0,0,3.68-3.68h0a3.89,3.89,0,0,1,5.65-3.26h0a3.88,3.88,0,0,0,5-1.34h0A3.89,3.89,0,0,1,35.26,9.77Z" fill="#f14ca3"/>
    </g>
    <path id="amoeba-ring" d="M18.59,14h0a3.9,3.9,0,0,0,4.51-2.6h0a3.88,3.88,0,0,1,6.29-1.69h0a3.89,3.89,0,0,0,5.2,0h0a3.88,3.88,0,0,1,6.29,1.69h0A3.9,3.9,0,0,0,45.41,14h0A3.88,3.88,0,0,1,50,18.59h0a3.9,3.9,0,0,0,2.6,4.51h0a3.88,3.88,0,0,1,1.69,6.29h0a3.89,3.89,0,0,0,0,5.2h0a3.88,3.88,0,0,1-1.69,6.29h0A3.9,3.9,0,0,0,50,45.41h0A3.88,3.88,0,0,1,45.41,50h0a3.9,3.9,0,0,0-4.51,2.6h0a3.88,3.88,0,0,1-6.29,1.69h0a3.89,3.89,0,0,0-5.2,0h0a3.88,3.88,0,0,1-6.29-1.69h0A3.9,3.9,0,0,0,18.59,50h0A3.88,3.88,0,0,1,14,45.41h0a3.9,3.9,0,0,0-2.6-4.51h0a3.88,3.88,0,0,1-1.69-6.29h0a3.89,3.89,0,0,0,0-5.2h0a3.88,3.88,0,0,1,1.69-6.29h0A3.9,3.9,0,0,0,14,18.59h0A3.88,3.88,0,0,1,18.59,14Z" fill="none" stroke="#E6B0FF" stroke-miterlimit="10" stroke-linecap="round"/>
    <g id="amoeba-eyes">
      <path d="M30,32c0-3.31-1.69-6-5-6s-7,2.69-7,6,1.69,6,5,6C28,38,30,35.31,30,32Z" fill="#fff"/>
      <circle cx="25" cy="32" r="2.48" fill="#456bd9"/>
      <path d="M34.17,33.42c-.78-3.22.22-6.23,3.44-7s7.43.95,8.22,4.17-.22,6.23-3.44,7C37.54,38.78,35,36.64,34.17,33.42Z" fill="#fff"/>
      <circle cx="39" cy="32" r="2.48" fill="#456bd9"/>
    </g>
  </svg>
</div>
<form id="export">
  <label>
    <abbr title="Frames Per Second">FPS</abbr>
    <input id="fps" type="number" value="24" required>
  </label>
  <label>
    Scale
    <input id="scale" type="number" value="2" required>
  </label>
  <button>Export Frames (ZIP)</button>
</form>
html {
  height: 100%;
}

body {
  background: hsl(224, 50%, 10%);
  color: #111;
  display: grid;
  grid-template-rows: minmax(10em, 1fr) min-content;
  min-height: 100%;
}

@media (orientation: landscape) and (min-width: 42em) {
  body {
    grid-template-columns: minmax(10em, 2fr) minmax(15em, 1fr);
    grid-template-rows: auto;
  }
}

#preview {
  position: relative;
}

#amoeba {
  height: auto;
  left: 50%;
  min-width: 64px;
  position: absolute;
  transform: translate(-50%, -50%);
  top: 50%;
  width: 50%;
  width: 66vmin;
}

#export {
  align-self: center;
  background: #fff;
  border-top-left-radius: 0.5em;
  border-top-right-radius: 0.5em;
  display: grid;
  grid-gap: 1em;
  grid-template-columns: 1fr 1fr;
  justify-self: center;
  padding: 1em;
}

@media (orientation: landscape) and (min-width: 42em) {
  #export {
    border-bottom-left-radius: 0.5em;
    border-top-right-radius: 0;
  }
}

#export label {
  align-items: center;
  display: grid;
  grid-gap: 0.2em;
}

#export input {
  min-width: 0;
  width: auto;
}

#export button {
  grid-column: span 2;
  white-space: nowrap;
}
/**
 * Animation
 *
 * Uses GSAP + MorphSVG + DrawSVG
 */

const timeline = new TimelineMax({
  repeat: -1,
  repeatDelay: 1
});

const duration = 2;

timeline.to(
  "#amoeba-ring",
  duration,
  {
    rotation: 90,
    transformOrigin: "center",
    ease: Power3.easeOut
  },
  "start"
);

timeline.to(
  "#amoeba-body",
  duration,
  {
    rotation: -90,
    transformOrigin: "center",
    ease: Power3.easeOut
  },
  "start"
);

timeline.from(
  "#amoeba-ring",
  duration * 0.8,
  {
    drawSVG: 0,
    ease: Power3.easeOut
  },
  "start"
);

timeline.from(
  "#amoeba-body-end",
  duration,
  {
    morphSVG: "#amoeba-body-start",
    ease: Back.easeOut.config(2)
  },
  "start"
);

timeline.from(
  "#amoeba-body",
  duration * 0.8,
  {
    // scale: 0.6,
    opacity: 0,
    transformOrigin: "center"
  },
  "start"
);

timeline.from(
  "#amoeba-eyes",
  duration * 0.8,
  {
    scale: 0.25,
    opacity: 0,
    transformOrigin: "center",
    ease: Back.easeOut.config(3)
  },
  "start"
);

/**
 * Generation
 *
 * Uses https://www.npmjs.com/package/save-svg-as-png
 * With https://stuk.github.io/jszip/
 * And https://github.com/eligrey/FileSaver.js/
 * With help from https://stackoverflow.com/a/40329190/5175805
 */

function generate(fps = 24, scale = 2) {
  // Get duration from our timeline
  const duration = timeline.duration();
  // Calculate the total number of frames
  const frames = Math.ceil(duration * fps);

  // Filenames
  let filePrefix = "amoeba-";
  let fileScale = scale === 1 ? "" : `@${scale}x`;
  let fileExtension = ".png";

  // Adjust actual scale based on current device ratio
  scale = scale / window.devicePixelRatio;

  // Create a ZIP file we'll add images to
  const zip = new JSZip();

  // Set up a resolved promise for our loop
  let step = Promise.resolve();

  // For every frame we need to generate…
  for (let i = 0; i <= frames; i++) {
    let position = duration / frames * i;
    let filename = `${filePrefix}${i}${fileScale}${fileExtension}`;
    // Begin this step when the previous finishes
    step = step.then(() => {
      timeline.pause(position);
      return svgAsPngUri(document.getElementById("amoeba"), { scale }).then(
        uri => {
          // Convert data URI to plain base64
          let imgDataIndex = uri.indexOf("base64,") + "base64,".length;
          let imgData = uri.substr(imgDataIndex);
          zip.file(filename, imgData, { base64: true });
        }
      );
    });
  }

  step.then(() => {
    zip.generateAsync({ type: "blob" }).then(blob => {
      saveAs(blob, `${filePrefix}frames.zip`);
    });
    // Resume animation
    timeline.play();
  });
}

/**
 * Form action
 */

document.getElementById("export").addEventListener("submit", event => {
  // Convert the input values to numbers
  const fps = parseFloat(document.getElementById("fps").value, 10);
  const scale = parseFloat(document.getElementById("scale").value, 10);
  // Prevent the form from taking us anywhere
  event.preventDefault();
  // Pass these values to our PNG/ZIP generator
  generate(fps, scale);
});
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.3/TweenMax.min.js
  2. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/DrawSVGPlugin.min.js
  3. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/MorphSVGPlugin.min.js
  4. https://unpkg.com/save-svg-as-png@1.4.14/lib/saveSvgAsPng.js
  5. https://cdnjs.cloudflare.com/ajax/libs/jszip/3.2.2/jszip.min.js
  6. https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js