How to Iterate Keyframe Percentages Less CSS

How to iterate keyframe percentages Less CSS

It seems like the Less compiler does not evaluate functions when directly used as a selector. Solution would be to make use of a temporary variable like in either of the below snippets:

.loop (@n, @index: 0) when (@index <= @n) { /* note the <= as we need 100% frame also */
@keyframeSel: percentage(@index/@n); /* note the lack of * 100 as Less already does it */
@{keyframeSel}{
prop: value;
}
.loop(@n, (@index + 1)); // Next iteration.
}
@keyframes anim {
.loop(20); // Launch the loop.
}

or

.loop (@n, @index: 0) when (@index <= @n) {
@keyframeSel: @index/@n * 100%;
@{keyframeSel}{
prop: value;
}
.loop(@n, (@index + 1)); // Next iteration.
}
@keyframes anim {
.loop(20); // Launch the loop.
}

Expressing CSS3 @keyframes using seconds instead of percentages

Don't forget you can run multiple animations on the same element, and that you can set their duration, delay and all other animation-... rules independently.

E.g, you can split all your keyframes to single-key @keyframes rules.

Then it's easy to control when they'll kick in and to chain them.

div {
width: 120px;
height: 120px;
background-color: violet;
animation-fill-mode: forwards;
animation-name: orange, yellow, green, cyan, blue, violet;
animation-delay: 0s, 3s, 6s, 9s, 12s, 15s, 18s;
animation-duration: 3s; /* same for all */
}

@keyframes orange {
to { background-color: orange; }
}
@keyframes yellow {
to { background-color: yellow; }
}
@keyframes green {
to { background-color: green; }
}
@keyframes cyan {
to { background-color: cyan; }
}
@keyframes blue {
to { background-color: blue; }
}
@keyframes violet {
to { background-color: violet; }
}
<div></div>

How to calculate percentages in LESS CSS?

According to the LESS CSS website, you need to change the order of your equation

The output is pretty much what you expect—LESS understands the difference between colors and units. If a unit is used in an operation, like in:

@var: 1px + 5;

LESS will use that unit for the final output—6px in this case.

It should be:

width: 100%*(140/620);

SCSS stop keyframe SVG circle animation based on percentage

The total length of a path in SVG can be found using the getTotalLength method. In your case you could also use the formula for the perimeter of a circle (2*Math.PI*r).

Anyway you need to know the length of the path you want to animate which in this case is 445.

stroke-dasharray: 445;
stroke-dashoffset: 445;

If you want to stop the animation at 50% this means you have to stop it at 445 / 2 = 222.5

 @keyframes stroke {
to {
stroke-dashoffset: 222.5;
}
}

Next come the demo. I hope it helps.

svg {  border: 1px solid;}
.circle--static circle { stroke-dasharray: 4; animation: stroke 2s ease-out forwards;}
.circle--animated { top: 0; left: 0; /*position: absolute;*/}.circle--animated circle { stroke-dasharray: 445; stroke-dashoffset: 445; animation: stroke 6s ease-out forwards; animation-iteration-count: infinite;}@keyframes stroke { to { stroke-dashoffset: 222.5; }}@keyframes fadeIn { to { opacity: 1; }}
<svg height="180" width="180" class="circle--static">  <circle cx="100" cy="100" r="71" stroke="#cde9db" stroke-width="6" fill-opacity="0" /></svg>

<svg height="180" width="180" class="circle--animated"> <circle id="kk" cx="100" cy="100" r="71" stroke="#68c087" stroke-width="10" fill-opacity="0" /></svg>

Get/set current @keyframes percentage/change keyframes

I achieved roughly what I wanted using pure javascript with my CSS3.

For my experiment to come up with a way to do these objectives, I created a basic CSS3 animation rotating a circle around a small circular path. My goal was to, when a button was clicked, change the origin of the animation to the new location

To achieve the first goal of getting the percentage value of the animation I simply approximated the current percentage using the following setInterval, which displays the approximated current percent. This runs every 40 milliseconds to correspond with the duration of the animation (milliseconds / 100)

var result = document.getElementById('result'), currentPercent = 0;
var showPercent = window.setInterval(function() {
if(currentPercent < 100)
{
currentPercent += 1;
}
else {
currentPercent = 0;
}
result.innerHTML = currentPercent;
}, 40);

Note on this solution:

  • It is not perfect due because the counter keeps running when another tab is clicked but the animation stops, so they become un-synchronized
  • It is also faulty when the button is clicked long after the last click. Evidently the setInterval runs a little bit longer than the animation, so they become less and less synced each iteration
  • I looked everywhere for a more perfect solution but have been unable to come up with one as of yet

To achieve the second goal of setting a new definition for an animation's % value it took a bit of a more complex solution. After pouring through dozens of articles and web pages (the important ones listed below), I managed to come up with the following solution:

var circle = document.getElementById('circle'), 
button = document.getElementById('button');
var result = document.getElementById('result'),
totalCurrentPercent = 0,
currentPercent = 0;
var showPercent = window.setInterval(function() {
if(currentPercent < 100)
{
currentPercent += 1;
}
else {
currentPercent = 0;
}
result.innerHTML = currentPercent;
}, 40);
function findKeyframesRule(rule) {
var ss = document.styleSheets;
for (var i = 0; i < ss.length; ++i) {
for (var j = 0; j < ss[i].cssRules.length; ++j) {
if (ss[i].cssRules[j].type == window.CSSRule.WEBKIT_KEYFRAMES_RULE && ss[i].cssRules[j].name == rule) { return ss[i].cssRules[j]; }
}
}
return null;
}
function change(anim) {
var keyframes = findKeyframesRule(anim),
length = keyframes.cssRules.length;
var keyframeString = [];
for(var i = 0; i < length; i ++)
{
keyframeString.push(keyframes[i].keyText);
}
var keys = keyframeString.map(function(str) {
return str.replace('%', '');
});
totalCurrentPercent += currentPercent;
if(totalCurrentPercent > 100)
{
totalCurrentPercent -= 100;
}
var closest = getClosest(keys);
var position = keys.indexOf(closest),
firstPercent = keys[position];
for(var i = 0, j = keyframeString.length; i < j; i ++)
{
keyframes.deleteRule(keyframeString[i]);
}
var multiplier = firstPercent * 3.6;
keyframes.insertRule("0% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 0) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 0) + "deg); background-color:red; }");
keyframes.insertRule("13% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 45) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 45) + "deg); }");
keyframes.insertRule("25% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 90) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 90) + "deg); }");
keyframes.insertRule("38% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 135) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 135) + "deg); }");
keyframes.insertRule("50% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 180) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 180) + "deg); }");
keyframes.insertRule("63% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 225) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 225) + "deg); }");
keyframes.insertRule("75% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 270) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 270) + "deg); }");
keyframes.insertRule("88% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 315) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 315) + "deg); }");
keyframes.insertRule("100% { -webkit-transform: translate(100px,100px) rotate(" + (multiplier + 360) + "deg) translate(-100px,-100px) rotate(" + (multiplier + 360) + "deg); }");
circle.style.display = "inherit";
circle.style.webkitAnimationName = anim;
window.clearInterval(showPercent);
currentPercent = 0;
showPercent = self.setInterval(function() {
if(currentPercent < 100)
{
currentPercent += 1;
}
else {
currentPercent = 0;
}
result.innerHTML = currentPercent;
}, 40);
}
button.onclick = function() {
circle.style.webkitAnimationName = "none";
circle.style.display = "none";
setTimeout(function () {
change("rotate");
}, 0);
}
function getClosest(keyframe) {
var curr = keyframe[0];
var diff = Math.abs (totalCurrentPercent - curr);
for (var val = 0; val < keyframe.length; val++) {
var newdiff = Math.abs(totalCurrentPercent - keyframe[val]);
if (newdiff < diff) {
diff = newdiff;
curr = keyframe[val];
}
}
return curr;
}

Check out the experiment itself here including comments describing what each part of the javascript is doing

Notes on this solution:

  • I tried to make the change function as non-hard-coded as possible
  • It works alright for approximating the current @keyvalue percentage
  • The transition from one animation to the other is only as smooth as however close the nearest % value of the animation is to the current % of the animation, so adding more % definitions to the animation would make it even more smooth

In the process of trying to come up with a solution for the problem, I found these useful resources:

  • RussellUresti's answer in this SO post and corresponding example was quite influential and greatly aided my final solution
  • To get the closest value based on an input and array values, I used paxdiablo's method in this SO post (thank you)
  • This plugin, while I didn't use it myself, seemed to achieve a very similar (though seemingly not quite as customizable) effect in jQuery

---EDIT---

If you are just using CSS transitions or you can change your animation to just use transitions instead, you can use this simpler approach. You can pause the transition by copying the attributes changed by the transition, setting the attributes to those changed attributes, and then removing the class that animates it. Of course if you are using the same object to animate/pause you will need to animate the first click then pause it the next click. You can easily do the pure javascript equivalent as well

Note: the !important in the CSS changed attribute is necessary unless you have a more leveled selector for the animation class than the jQuery selector, aka something like div #defaultID.animationClass { as opposed to just #defaultID.animationClass {. Since #defaultID and #defaultID.animationClass are both one level, this example requires the !important

--Another Edit--

For more information on this topic, check out my post on CSS-Tricks

Can I loop this keyframes animation somehow with Sass?

If you just want the animation to loop add this to style:

animation-iteration-count:infinite;

Or replace the animation name with:

  animation: slidySlidesFadeInOut 180s infinite;

And this does not require SASS, although it is completely valid in SASS.

How can I use calculated values as keyframes?

TL;DR

You can't. Use a preprocessor like sass. SASS is cool, CSS is not.

The Problem

CSS works as a templating system. You can declare a selector which then selects an element on which you are able to change it's props, right?

Yeah.. not really. You can't do whatever you like. The available selectors as of now are pretty strict in terms of dynamics. So to overcome this weakness, they introduced variables (below there's sample on how you'd use them).

:root {
--bgColor: #fafafa;
}

/*
some styling
...
*/

.my-class {
background-color: var(--bgColor);
}

Well.. not the solution you're looking for, because it only works on properties (eg. background-color, font-size, width etc.) and will be evaluated at runtime. Same goes for calc().

The Solution

Since we cannot use dynamic variables or calculated values as selectors or as part of a selector, you cannot implement this in pure CSS. However, you can use a preprocessor such as sass or less to be able to use many features including string interpolation -> #{myCalculatedValue}%

CSS preprocessors transpiles the features they've provided for you into valid CSS code before the browser runs it so basically every variable you've declared (including string interpolation, but excluding CSS vars since it wasn't provided by the preprocessor) will be transpiled into a static value which CSS can use (eg. 1/0.6% will evaluate to 1.67%). It doesn't really matter what you're doing as long as it transpiles. You can also mix the two. Example: in sass you can use CSS variables as well as SCSS variables.

Requirements

This is great. We have dynamic values, we have loops, we have functions. All we need is a compiler or transpiler that turns our scss, sass or less code into real CSS for the browsers to be able to read it and apply.

If you're using a frontend framework (such as Angular, React or Vue), you don't have to do anything besides initializing the project with scss styling.

Otherwise you'll need to have something like node-sass or sass, but you'll have to transpile it manually before you start your server.

node-sass is the best IMO eg. if you use server-side rendering, but there's several sass transpiler on npm.

EDIT: You can find several transpilers on npm.

BONUS TIP: If you're using VSCode, there's an extension called Live Sass Compiler(ritwickdey.live-sass) which, as the name suggests, will compile your sass files to css files automatically (same can be achieved with the --watch tag if you're using a transpiler).



Related Topics



Leave a reply



Submit