2

I have a web page that links to several pure CSS animation libraries, and I may add more in future. I want to be able to randomly select an animation from all available ones. I can't figure out how to get a list of the keyframe names in JavaScript so that I can pick one and use it. My typical CSS element looks like animation-name: <<keyframe name>>;

Can someone share the code to do this?

Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
Mark Kortink
  • 1,770
  • 4
  • 21
  • 36

2 Answers2

3

The following script will get all the animations in all CSS sheets and store them into an array.


function getAnimationList(){
// Return a list of all of the animation keyframes in all style sheets.
    var ss = document.styleSheets;
    var anims = [];
    // loop through all the style sheets
    for (var s = 0; s < ss.length; s++) {
        if (ss[s].cssRules) {
            // loop through all the rules
            for (var r = ss[s].cssRules.length - 1; r >= 0; r--) {
                var rule = ss[s].cssRules[r];
                if ((rule.type === window.CSSRule.KEYFRAMES_RULE || rule.type === window.CSSRule.WEBKIT_KEYFRAMES_RULE)) {
                    anims.push(rule);
                }
            }
        }
    }
    return anims;
};

// Write all the animation (keyframe) names to the console.
animList = getAnimationList();
if (animList.length == 0){
    console.log('>>> No Animations');
} else {
    console.log('>>> Number of animations is ' + animList.length);
    for (var a = 0; a < animList.length; a++) {
        console.log('>>> ' + animList[a].name);
    };
};

One problem I did encounter is that many browsers have now implemented cross-domain constraints that include CSS files. If you want to search some CSS style sheets on another domain your browser may object. Refer to this as a starting point.

Also note that the reason I decrement through the rules is explained here.

Mark Kortink
  • 1,770
  • 4
  • 21
  • 36
  • Since you put all rules in an array anyway instead of returning only the first you find, I don't think you need to iterate backwards. – Bergi Dec 12 '20 at 22:56
  • @Bergi good point, agree, worth noting though if you want to use the array to name search. – Mark Kortink Dec 12 '20 at 23:17
0

I wanted to get a list of key-frames in Javascript, to be able to use directly with the Element.animate() API, so I found this question, and was able to improve the getAnimationList() function to work with css @keyframes, and here is a sample that works with animate.css :

var effects=getAnimationList(1);
function getAnimationList(offset){
  // Returns a list of all animation keyframes in all style sheets
  var ss =document.styleSheets, anims={}, step= offset?'offset':'step', rName, trOrig;
  for(var s=0;s<ss.length;s++){// loop through all style sheets
    if(ss[s].cssRules){// loop through all the rules
      for(var r=0;r<ss[s].cssRules.length;r++){
        var rule = ss[s].cssRules[r];
        if( rule.type === window.CSSRule.KEYFRAMES_RULE || rule.type === window.CSSRule.WEBKIT_KEYFRAMES_RULE ){
          if(rule.cssRules && !rule.cssText.startsWith('@-webkit-')){
            anims[rule.name]??=[];
            for(var k=0;k<rule.cssRules.length;k++){// add all keyframes
              var steps=rule.cssRules[k].keyText.split(', ').map(v =>
                parseFloat(v)/(String(v).includes("%")?100:1) ),
                cssArr= Object.entries(rule.cssRules[k].style).filter( x =>
                x[1]!="" && !x[0].startsWith('webkit') && isNaN(parseInt(x[0])) );
              steps.forEach( sk => anims[rule.name].push( Object.fromEntries( [[step,sk]].concat(cssArr) ) ) );
            }//get ordered frames and merge those with the same offset
            anims[rule.name]= anims[rule.name].reduce( (a,b)=>{
              var obj=a.filter(x=>x[step]==b[step])[0];
              if(!obj){a.push(b);}
              else{ for(var k in b){ obj[k]=b[k];} } //bottom rules overwrite top ones like in CSS
              return a;
            }, [] ).sort( (a,b) => a[step]-b[step] );
          }
        }else if( (rule.type === window.CSSRule.STYLE_RULE || rule.type === window.CSSRule.WEBKIT_STYLE_RULE) && rule.selectorText.startsWith('.animate__') ){
          rName=rule.selectorText.split('.animate__')[1];
          trOrig=rule.style.transformOrigin;//using the right transform-origin
          if(anims[rName] && trOrig){ anims[rName].map(x=>x.transformOrigin =trOrig);}
        }
      }
    }
  } return anims;
}
function fillSelectOpt(sel,vals){
  vals.forEach((x,i)=>{
    var opt=document.createElement('option');
    opt.value=x; opt.innerText=x; sel.append(opt);
  });
}
fillSelectOpt(effSel,Object.keys(effects));

animate.onclick=function(e){
  document.getElementById('box').animate( effects[effSel.value],
    {duration:1200, fill:'backwards', delay:100, iterations:1} );
};
effSel.onchange= function(e){animate.onclick(e);}
#box{
  padding: 20px; margin: 50px auto;
  background: #c791d5; color: #f7edf9;
  border: 2px solid#8512a3; border-radius: 5px;
  width: 100px; height: 20px;
  text-align: center; font: bold 20px Arial;
}
body{padding:70px 50px; text-align: center;}
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" rel="stylesheet" crossorigin="anonymous" />

Select effect : <select id="effSel"></select>
<button id="animate">Animate</button>
<div id="box">myElement</div>

The variable effects holds all found animation names, stored as arrays of keyframe objects with their own css properties, ordered by ascending offset (a requirement for Element.animate(), unlike css).


Note : Because of CORS issues, you must have your css file in the same domain as your html file, or at least the server must deliver your css file with "*" origin (which is the case here with "cdnjs.cloudflare.com"), and include it like :

<link rel="stylesheet" href="https://..." crossorigin="anonymous" />

In the worst case, just copy/paste your css rules in the same html file, if you just want to get the css rules and use them with JS somewhere else. You can adjust getAnimationList() to work with any other css files, and the offset parameter is just to avoid confusion with the same css keyword, because Element.animate() also uses offset (from 0 to 1) in its keyframes, which acts like the "xx%" in css rules (from == 0 == 0% and to == 1 == 100%).

Note 2 : I was using this form of the API :

element.animate({ offset:[vals], transform:[vals], opacity:[vals] }, opts );

where "vals" is a concatenation of css values in the same respective order, but because not all keyframes have all properties, this broke some animations (like "hinge"), so it's safer to stick with the "normal" form :

element.animate( [ {keyframe1}, {keyframe2}, ..], opts ).

Youssef
  • 132
  • 2
  • 2
  • 10