Convert anime.js timelines to CSS animations

Pure Nim score 15/100 · last commit 2022-05-12 · 1 stars · tests present · no docs generated

Summary

Latest Version Unknown
License Unknown
CI Status Failing
Stars 1
Forks 0
Open Issues 0
Last Commit 2022-05-12
Downloads 0
Last Indexed 2026-09-06 07:36

Installation

nimble install Convert anime.js timelines to CSS animations
choosenim install Convert anime.js timelines to CSS animations
git clone https://gitlab.com/cameralibre/convert-anime-to-css

OS Compatibility

Platform Linux macOS Windows FreeBSD OpenBSD NetBSD Android iOS WASM Embedded
Convert anime.js timelines to CSS animations - - - - - - -

README

Converting an Anime.js timeline to CSS animations

Use this script to create self-contained looping SVG files ⁠— like very high-quality, lightweight GIFs ⁠— which can be the source of an <img> element, and therefore can be used in places where you're not able to use inline SVG or javascript, such as Gitlab/Github Markdown:

a complex CSS animation looping like a GIF

This SVG was originally animated with anime.js (triangle-circle_anime.svg). Although it looks simple, it's a 250-line timeline with many simultaneous transforms, different custom easings on certain keyframes, squashing and stretching, some duplication & staggering to create a motion blur effect, and tight interaction of the two shapes. It took quite a bit of iteration to get right, so the flexibility of an anime.js timeline was very helpful. I then converted it to CSS animations with this script, so it can be embedded here, within a markdown document. It would be extremely difficult to write something like this from scratch in CSS.

<img> elements cannot access any external resources or even run an inline <script>, so if you want a looping SVG animation in your <img>, it has to be animated with CSS, or ye olde SMIL.

I've written complex CSS animations before, and it's a horrible experience - I much prefer the straightforward and flexible approach to animation writing that anime.js timelines offer.

So this script enables writing animations with anime.js, but publishing as either anime or CSS. You paste the script into the <script> element in your SVG document which contains the anime timeline, save and open the SVG in a browser, and it will be converted.

Can I use this with HTML, rather than SVG?

Um, maybe..? I'm writing with SVG files in mind, but with a little tweaking it should also work on anime.js timelines in HTML documents. Of course, you can't use animated HTML in an <img> either, but maybe you want the performance advantages that CSS offers over JS animations, or you want complex animations in an environment where you can't load scripts (but can alter CSS).

What does it do?

  • gets (or assigns) the id of each target (animated element)
  • lists all keyframes, animated property and value changes for each target
  • converts easings to cubic-bezier format, and applies them per-keyframe where needed.
  • converts relative time values to percentages of total duration
  • anime can animate multiple transforms on one element independently ⁠— CSS can't. So a wrapping group will be created around the original element for each conflicting transform, and a new animation created for that group.

  • formats the data as CSS animations, and adds it to the document

  • deletes all script elements from the document

It doesn't account for everything that you may be able to do with an anime.js timeline, and many of those things wouldn't work in CSS anyway (interaction, animating non-presentation attributes like viewBox, using function-based values...) So if your end goal is CSS animations, make sure you stick within the limitations of CSS when you're writing.

Using the script

There's still a small amount of manual work required for now:

Prepare the SVG file

  • Paste the the contents of 'convert-anime-to-css.js' at the bottom of the <script> section of the SVG.
  • if your anime timeline is not called tl, replace the reference to tl in the script with your timeline name:
const animeData = getAnimeJsData(tl)
  • Your animation target elements in the SVG should have an id. But if they don't, one will be generated for them. If you would like the ids to be more meaningful than the generated ids, add them manually before running the script.

Convert timeline to CSS

Now you can open the SVG in a browser, and it will be converted to CSS. Right-click -> Save Page As and choose a new name for the file (eg. Original-Name_css.svg) If you're lucky, that's it!

Manual fixes

  • if there's anything funny happening with the animation, you may need to change the order of some transform properties (translate, rotate, scale, and skew), i.e.:
  10% { transform: scale(2) translate(20px, 40px); }

is quite different to:

  10% { transform: translate(20px, 40px) scale(2); }

(CSS applies these transforms in order, from right-to-left)

Final steps - embedding fonts

  • if there is any text in the animation, you will likely want to embed the font file, depending on how you are going to use the SVG. (An SVG in an <img> element cannot access external resources like Google fonts).
  • Here's some help with embedding the font: Creating Embeddable Fonts as Data URIs by Amelia Bellamy-Royds.

  • Once you've got an embeddable font, make a second <style></style> section, just before the closing </svg> tag, and paste the contents of your-embeddable-font.css into it.

  • With the font now embedded, you can delete any import statements you may have, eg. to Google fonts:

    @import url('https://fonts.googleapis.com/css2?family=Dosis&display=swap');

How the script works

The core difference between an anime timeline and CSS animations is that an anime.js timeline generally lists every change chronologically, eg:

.add({
  // animate this,
})
.add({
  // then this,
})
.add({
  // then this.
})

Whereas CSS animations are organised by the animation target - we can compare an anime.js timeline with its CSS equivalent to make the distinction clearer:

anime.js

const tl = anime.timeline({})

tl.add({
    targets: 'rect, circle',
    opacity: [0, 1] // both elements fade in
  })
  .add({
    targets: 'rect',
    translateX: [0, 10] // rect moves L -> R
  })
  .add({
    targets: 'circle',
    translateX: [0, 10] // circle moves L -> R
  })
  .add({
    targets: 'rect',
    translateX: [10, 0] // rect moves back R -> L
  })
  .add({
    targets: 'rect, circle',
    opacity: [1, 0] // both fade out
  })

CSS

rect { animation: 2.5s rect-animation; }

@keyframes rect-animation {
  0% { opacity: 0; }
  20% { opacity: 1; transform: translateX(0); }
  40% { transform: translateX(10); }
  60% { transform: translateX(10); }
  80% { opacity: 1; transform: translateX(0); }
  100% { opacity: 0; }
}


circle { animation: 2.5s circle-animation; }

@keyframes circle-animation {
  0% { opacity: 0; }
  20% { opacity: 1; }
  40% { transform: translateX(0); }
  60% { transform: translateX(10); }
  80% { opacity: 1; }
  100% { opacity: 0; }
}

CSS gets way harder to read as an animation gets complex, and can be a real shit to sync up nicely.

The anime timeline object

You can log an anime.js timeline object to the console and see that it contains children, one for each of its .add({}) chronological sections, which seems to be referred to as an anim object in anime.js.

An anim looks kinda like this:

{
  "update": null,
  "begin": null,
  "loopBegin": null,
  "change": null,
  "changeComplete": null,
  "loopComplete": null,
  "complete": null,
  "loop": 1,
  "direction": "normal",
  "autoplay": false,
  "timelineOffset": 1000,
  "id": 1,  // an internal id that anime assigns to this animation section
  "children": [],
  "animatables": [
    {
      "target": {}, // DOM node of target, eg. <circle r="1" cy="3" cx="3" style="opacity: 0; transform: translateX(10px);">
      "id": 0,
      "total": 1,
      "transforms": {
        "list": { "translateX": "10px" }, // a Map object
        "last": "translateX"
      }
    }
  ],
  "animations": [
    {
      "type": "css",
      "property": "opacity",
      "animatable": { // anim.animations[i].animatables is exactly the same as anim.animatables[i] from earlier ↑     
      },
      "tweens": [
        {
          "value": [0, 1],
          "delay": 0,
          "endDelay": 0,
          // "easing": function (r) - this is a private function that anime.js runs, so I'm using a lookup table of different easings' fingerprints to work out which one it is
          "duration": 300,
          "round": 0,
          "from": {
            "original": "0",
            "numbers": [0],
            "strings": []
          },
          "to": {
            "original": "1",
            "numbers": [1],
            "strings": []
          },
          "start": 0,
          "end": 300,
          "isPath": false,
          "isPathTargetInsideSVG": false,
          "isColor": false
        }
      ],
      "duration": 300,
      "delay": 0,
      "endDelay": 0,
      "currentValue": 1
    },
    {
      "type": "transform",
      "property": "translateX",
      "animatable": { // anim.animations[i].animatables is exactly the same as anim.animatables[i] from earlier ↑     
      },
      "tweens": [
        {
          "value": [0, 10],
          "delay": 0,
          "endDelay": 0,
          "duration": 300,
          "round": 0,
          "from": {
            "original": "0px",
            "numbers": [0],
            "strings": ["","px"]
          },
          "to": {
            "original": "10px",
            "numbers": [10],
            "strings": ["","px"]
          },
          "start": 0,
          "end": 300,
          "isPath": false,
          "isPathTargetInsideSVG": false,
          "isColor": false
        }
      ],
      "duration": 300,
      "delay": 0,
      "endDelay": 0,
      "currentValue": "10px"
    }
  ],
  "duration": 300,
  "delay": 0,
  "endDelay": 0,
  "finished": Promise { <state>: "pending" },
  "passThrough": false,
  "currentTime": 300,
  "progress": 100,
  "paused": true,
  "began": true,
  "loopBegan": true,
  "changeBegan": false,
  "completed": true,
  "changeCompleted": true,
  "reversePlayback": false,
  "reversed": false,
  "remaining": 0
}

So my general approach for the script is: 1. Grab the data that I need from each anim object in the timeline 2. Perform calculations on that data where necessary 3. Put the data into an object with the same general shape as a CSS animation declaration 4. Do a whole lot of nested looping and string manipulation to format the object as CSS 5. Insert the CSS animations into a new <style> section of the document 6. Delete all <script> elements