SkillAgentSearch skills...

Rubato

Create smooth animations with a slope curve for awesomeWM

Install / Use

/learn @andOrlando/Rubato
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

rubato

Basically like awestore but not really.

Join the cool curve crew

<!-- look colleges might see this and think its distasteful so I'm commenting it out for the moment <img src="https://cdn.discordapp.com/attachments/702548961780826212/879022533314216007/download.jpeg" height=160>--> <h1 id="background">Background and Explanation</h1>

The general premise of this is that I don't understand how awestore works. That and I really wanted to be able to have an interpolator that didn't have a set time. That being said, I haven't made an interpolator that doesn't have a set time yet, so I just have this instead. It has a similar function to awestore but the method in which you actually go about doing the easing is very different.

When creating an animation, the goal is to make it as smooth as humanly possible, but I was finding that with conventional methods, should the animation be interrupted with another call for animation, it would look jerky and inconsistent. You can see this jerkiness everywhere in websites made by professionals and it makes me very sad. I didn’t want that for my desktop so I used a bit of a different method.

This jerkiness is typically caused by discontinuous velocity graphs. One moment it’s slowing down, and the next it’s way too fast. This is caused by just lazily starting the animation anew when already in the process of animating. This kind of velocity graph looks like this:

<img src="images/disconnected_graph.png" alt="Disconnected Velocity Graph" height=160/>

Whereas rubato takes into account this initial velocity and restarts animation taking it into account. In the case of one wanting to interpolate from one point to another and then back, it would look like this:

<img src="images/connected_graph.png" alt="Connected Velocity Graph" height=160/>

<sub><sup>okay maybe my graph consistancy is trash, what can I do...</sup></sub>

These are what they would look like with forwards-and-back animations. A forwards-than-forwards animation would look more like this, just for reference:

<img src="images/forwards_forwards_graph.png" alt="Forwards ForwardsGraph" height=160/>

To ask one of you to give these graphs as inputs, however, would be really dumb. So instead we define an intro function and its duration, which in the figure above is the y=x portion, an outro function and its duration, which is the y=-x portion, and the rest is filled with constant velocity. The area under the curve for this must be equal to the position for this to end up at the correct position (antiderivative of velocity is position). If we know the area under the curve for the intro and outro functions, the only component we need to ensure that the antiderivative is equal to the position would be the height of the graph. We find that with this formula:

$$m=\frac{d + ib(F_i(1)-1)}{i(F_i(1)-1) + o(F_o(1)-1) + t}$$

where m is the height of the plateau, i is intro duration, F_i is the antiderivative of the intro easing function, o is outro duration, F_o is the antiderivative of the outro easing function, d is the total distance needed to be traveled, b is the initial slope, and t is the total duration.

We then simulate the antiderivative by adding v(t) (or the y-value at time t on the slope graph) to the current position 30 times per second (by default, but I recommend 60). There is some inaccuracy since it’s not a perfect antiderivative and there’s some weirdness when going from positive slopes to negative slopes that I don’t know how to intelligently fix (I have to simulate the antiderivative beforehand and multiply everything by a coefficient to prevent weird errors), but overall it results in good looking interruptions and I get a dopamine hit whenever I see it in action.

There are a couple small issues that I can’t/don’t know how to fix mathematically:

  • It’s not perfectly accurate (it is perfectly accurate as dt goes to zero) which I don’t think is possible to fix unless I stop simulating the antiderivative and actually calc out the function, which seems time inefficient
  • When going from a positive m to a negative m, or in other words going backwards after going forwards in the animation, it will always undershoot by some value. I don’t know what that value is, I don’t know where it comes from, I don’t know how to fix it except for lots and lots of time-consuming testing, but it’s there. To compensate for this, whenever there’s a situation in which this will happen, I simulate the animation beforehand and multiply the entire animation by a corrective coefficient to make it do what I want
  • Awesome is kinda slow at redrawing imaages, so 60 redraws per second is realistically probably not going to happen. If you were to, for example, set the redraws per second to 500 or some arbitrarily large value, if I did nothing to dt, it would take forever to complete an animaiton. So since I can't fix awesome, I just (by default but this is optional) limit the rate based on the time it takes for awesome to render the first frame of the animation (Thanks Kasper for pointing this out and showing me a solution).

So that’s how it works. I’d love any contributions anyone’s willing to give. I also have plans to create an interpolator without a set duration called target as opposed to timed when I have the time (or need it for my rice).

<h1 id="usage">How to actually use it</h1>

So to actually use it, just create the object, give it a couple parameters, give it some function to execute, and then run it by updating target! In practice it'd look like this:

timed = rubato.timed {
    duration = 1/2, --half a second
    intro = 1/6, --one third of duration
    override_dt = true, --better accuracy for testing
    subscribed = function(pos) print(pos) end
}

--you can also achieve the same effect as the `subscribed` parameter with this:
--timed:subscribe(function(pos) print(pos) end)

--target is initially 0 (unless you set pos otherwise)
timed.target = 1
-- Here it prints out this:
-- 0
-- 0
-- 0.02
-- 0.06
-- 0.12
-- 0.2
-- 0.3
-- 0.4
-- 0.5
-- 0.6
-- 0.7
-- 0.8
-- 0.88
-- 0.94
-- 0.98
-- 1
-- 1
-- First 0 is because when you initially subscribe a function
-- it calls that function at the current position, which is 0
-- Last zero is because it'll snap to the exact position in 
-- case of minor error which can come about from floating point
-- math or correcting for frameskips

--When called after it finishes printing, this would print out
--the same numbers but in reverse, sending it back from 1 to 0
timed.target = 0

If you're familiar with the awestore api and don't wanna use what I've got, you can use those methods instead if you set awestore_compat = true. It’s a drop-in replacement, so your old code should work perfectly with it. If it doesn’t, please make an issue and I’ll do my best to fix it. Please include the broken code so I can try it out myself.

So how do the animations actually look? Let’s check out what I (at one point) use(ed) for my workspaces:

timed = rubato.timed {
    intro = 0.1,
    duration = 0.3
}

Normal Easing

The above is very subtly eased. A somewhat more pronounced easing would look more like this:

timed = rubato.timed {
    intro = 0.5,
    duration = 1,
    easing = rubato.quadratic --quadratic slope, not easing
}

Quadratic Easing

The first animation’s velocity graph looks like a trapezoid, while the second looks like the graph shown below. Note the lack of a plateau and longer duration which gives the more pronounced easing:

More Quadratic Easing

<h1 id="why">But why though?</h1>

Why go through all this hassle? Why not just use awestore? That's a good question and to be fair you can use whatever interpolator you so choose. That being said, rubato is solely focused on animation, has mathematically perfect interruptions and I’ve been told it also looks smoother.

Furthermore, if you use rubato, you get to brag about how annoying it was to set up a monstrous derivative just to write a custom easing function, like the one shown in Custom Easing Function's example. That's a benefit, not a downside, I promise.

Also maybe hopefully the code should be almost digestible kinda maybe. I tried my best to comment and documentate, but I actually have no idea how to do lua docs or anything.

Also it has a cooler name

<h1 id="arguments-methods">Arguments and Methods</h1>

For rubato.timed:

Arguments (in the form of a table):

  • duration: the total duration of the animation
  • rate: the number of times per second the timer executes. Higher rates mean smoother animations and less error.
  • prop_intro: when true, intro, outro and inter represent proportional values; 0.5 would be half the duration. (def. false)
  • pos: the initial position of the animation (def. 0)
  • intro: the duration of the intro
  • outro: the duration of the outro (def. same as intro*)
  • inter: the duration of intermittent animations (def. same as intro*)
  • easing: the easing table (def. interpolate.linear)
  • easing_outro: the outro easing table (def. as easing)
  • easing_inter: the "intermittent" easing table, which defines which easing to use in the case of animation interruptions (def. same as easing)
  • subscribed: a function to subscribe at initialization (def. nil)
  • override_simulate: when true, will simulate everything instead of just when dx and b have opposite signs at the cost of having to do a little more work (and making my hard work on finding the formula for m worthless :sli
View on GitHub
GitHub Stars264
CategoryDevelopment
Updated16d ago
Forks7

Languages

Lua

Security Score

100/100

Audited on Mar 24, 2026

No findings