refactore directory structure
11
.config/awesome/rubato/.editorconfig
Normal file
|
@ -0,0 +1,11 @@
|
|||
root = true
|
||||
|
||||
[*.lua]
|
||||
charset = utf-8
|
||||
intent_style = tab
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
max_line_length = 120
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
21
.config/awesome/rubato/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 andOrlando
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
400
.config/awesome/rubato/README.md
Normal file
|
@ -0,0 +1,400 @@
|
|||
# rubato
|
||||
|
||||
- [Background and explanation](#background)
|
||||
- [How to actually use it](#usage)
|
||||
- [But why though?](#why)
|
||||
- [Arguments and Methods](#arguments-methods)
|
||||
- [Custom Easing Functions](#easing)
|
||||
- [Installation](#install)
|
||||
- [Why the name?](#name)
|
||||
- [Todo](#todo)
|
||||
|
||||
Basically like [awestore](https://github.com/K4rakara/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:
|
||||
|
||||
<img src="https://render.githubusercontent.com/render/math?math=\color{blue}m=\frac{d %2B ib(F_i(1)-1)}{i(F_i(1)-1) %2B o(F_o(1)-1) %2B t}" height=50>
|
||||
|
||||
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 two main 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:
|
||||
|
||||
```lua
|
||||
timed = rubato.timed {
|
||||
intro = 0.1,
|
||||
duration = 0.5,
|
||||
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 would print out a bunch of values (15 by default) which
|
||||
--I would normally copy and paste here but my stdout is broken
|
||||
--on awesome rn so just pretend there are a bunch of floats here
|
||||
|
||||
--and this'll send it back from 1 to 0, printing out another 15 #s
|
||||
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:
|
||||
|
||||
```lua
|
||||
timed = rubato.timed {
|
||||
intro = 0.1,
|
||||
duration = 0.3
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
The above is very subtly eased. A somewhat more pronounced easing would look more like this:
|
||||
|
||||
```lua
|
||||
timed = rubato.timed {
|
||||
intro = 0.5,
|
||||
duration = 1,
|
||||
easing = rubato.quadratic --quadratic slope, not 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:
|
||||
|
||||

|
||||
|
||||
<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](#easing)'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.
|
||||
- `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`\*)
|
||||
- `prop_intro`: when `true`, `intro`, `outro` and `inter` represent proportional
|
||||
values; 0.5 would be half the duration. (def. `false`)
|
||||
- `easing`: the easing table (def. `interpolate.linear`)
|
||||
- `easing_outro`: the outro easing table (def. as `easing`)
|
||||
- `easing_inter`: the "intermittent" easing function, 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
|
||||
:slightly_frowning_face:) (def. `false`)
|
||||
- `override_dt`: will cap rate to the fastest that awesome can possibly handle.
|
||||
This may result in frame-skipping. By setting it to false, it may make
|
||||
animations slower (def. `true`)
|
||||
- `awestore_compat`: make api even *more* similar to awestore's (def. `false`)
|
||||
- `log`: it would print additional logs, but there aren't any logs to print right
|
||||
now so it kinda just sits there (def. `false`)
|
||||
|
||||
All of these values (except awestore_compat and subscribed) are mutable and changing them will
|
||||
change how the animation looks. I do not suggest changing `pos`, however, unless you change the
|
||||
position of what's going to be animated in some other way
|
||||
|
||||
\*with the caviat that if the outro being the same as the intro would result in an error, it would go
|
||||
for the largest allowable outro time. Ex: duration = 1, intro = 0.6, then outro will default to 0.4.
|
||||
|
||||
Useful properties:
|
||||
- `target`: when set, sets the target and starts the animation, otherwise returns the target
|
||||
- `state`: immutable, returns true if an animation is in progress
|
||||
|
||||
Methods are as follows:
|
||||
- `timed:subscribe(func)`: subscribe a function to be ran every refresh of the animation
|
||||
- `timed:unsubscribe(func)`: unsubscribe a function
|
||||
- `timed:fire()`: run all subscribed functions at current position
|
||||
- `timed:abort()`: stop the animation
|
||||
- `timed:restart()`: restart the animaiton from it's approximate initial state (if a value is
|
||||
changed during the animation it will remain changed after calling restart)
|
||||
|
||||
Awestore compatibility functions (`awestore_compat` must be true):
|
||||
- `timed:set(target_new)`: sets the position the animation should go to, effectively the same
|
||||
as setting target
|
||||
- `timed:initial()`: returns the intiial position
|
||||
- `timed:last()`: returns the target position, effectively the same as `timed.target`
|
||||
|
||||
Awestore compatibility properties:
|
||||
- `timed.started`: subscribable table which is called when the animation starts or is interrupted
|
||||
+ `timed.started:subscribe(func)`: subscribes a function
|
||||
+ `timed.started:unsubscribe(func)`: unsubscribes a function
|
||||
+ `timed.started:fire()`: runs all subscribed functions
|
||||
- `timed.ended`: subscribable table which is called when the animation ends
|
||||
+ `timed.ended:subscribe(func)`: subscribes a function
|
||||
+ `timed.ended:unsubscribe(func)`: unsubscribes a function
|
||||
+ `timed.ended:fire()`: runs all subscribed functions
|
||||
|
||||
**builtin easing functions**
|
||||
- `easing.zero`: linear easing, zero slope
|
||||
- `easing.linear`: linear slope, quadratic easing
|
||||
- `easing.quadratic`: quadratic slope, cubic easing
|
||||
- `easing.bouncy`: the bouncy thing as shown in the example
|
||||
|
||||
**functions for setting default values**
|
||||
- `rubato.set_def_rate(rate)`: set default rate for all interpolators, takes an `int`
|
||||
- `rubato.set_override_dt(value))`: set default for override_dt for all interpolators, takes a
|
||||
`bool`
|
||||
|
||||
<h1 id="easing">Custom Easing Functions</h1>
|
||||
|
||||
To make a custom easing function, it's pretty easy. You just need a table with two values:
|
||||
|
||||
- `easing`, which is the function of the slope curve you want. So if you want quadratic easing
|
||||
you'd take the derivative, which would result in linear easing. **Important:** `f(0)=0` and
|
||||
`f(1)=1` must be true for it to look nice.
|
||||
- `F`, which is basically just the value of the antiderivative of the easing function at `x=1`.
|
||||
This is the antiderivative of the scaled function (such that (0, 0) and (1, 1) are in the
|
||||
function), however, so be wary of that.
|
||||
|
||||
In practice, creating your own easing would look like this:
|
||||
|
||||
1. Go to [easings.net](https://easings.net)
|
||||
|
||||
For the sake of this tutorial, we'll do an extremely complex (hellish? easing, "ease in elastic"
|
||||
For the sake of this tutorial, we'll do both an easy easing and a complex one. The easy easing will
|
||||
be the beautifully simple and quite frankly obvious quadratic. The much worse easing will be "ease
|
||||
in elastic."
|
||||
|
||||
2. Find the necessary information
|
||||
|
||||
For quadratic we already know the function: `y=x^2`. I don't even need to use latex it's that easy.
|
||||
|
||||
For ease in elastic, we use the function given [here](https://easings.net/#easeInElastic):
|
||||
|
||||
<img src="https://render.githubusercontent.com/render/math?math=\color{blue}f(x)=-2^{10 \, x - 10}\times \sin\left(-\frac{43}{6} \, \pi %2B \frac{20}{3} \, \pi x\right))">
|
||||
|
||||
3. Take the derivative
|
||||
|
||||
Quadratic: `y=2x`, easy as that.
|
||||
|
||||
**Important:** Look. Computers aren't the greatest at indefinite mathematics. As such, it's
|
||||
possible that, like myself, you will have a hard time getting the correct derivative if it's as
|
||||
complicated as these here. Don't be discouraged, however! Sagemath (making sure not to factor
|
||||
anything) could correctly do out this math, even if I had a bit of a scare realizing that when I
|
||||
was factoring it I was just being saved by `override_simulate` being accidentally set to true.
|
||||
|
||||
Anyways, use sagemath and jupyter notebook. I don't know if all sagemaths come with it
|
||||
preinstalled, but nix makes it so easy that all I have to do is `sage -n jupyter` and it'll open it
|
||||
right up. `%display latex` in jupiter makes it look pretty, whereas `%display ascii_art` will make
|
||||
it look *presentable* in tui sagemath.
|
||||
|
||||
The derivative (via sagemath) is as follows:
|
||||
|
||||
<img src="https://render.githubusercontent.com/render/math?math=\color{blue}f^\prime (x)=-\frac{20}{3} \, \pi 2^{10 \, x - 10} \cos\left(-\frac{43}{6} \, \pi %2B \frac{20}{3} \, \pi x\right) - 10 \cdot 2^{10 \, x - 10} \log\left(2\right) \sin\left(-\frac{43}{6} \, \pi %2B \frac{20}{3} \, \pi x\right)">
|
||||
|
||||
4. Double check that `f'(0)=0`
|
||||
|
||||
Quadratic: `2*0 = 0` so we're good
|
||||
|
||||
Ease in elastic not so much, however:
|
||||
|
||||
<img src="https://render.githubusercontent.com/render/math?math=\color{blue}f^\prime (0)=\frac{5}{1536} \, \sqrt{3} \pi - \frac{5}{1024} \, \log\left(2\right)">
|
||||
We'll subtract this value from `f(x)` so that our new `f(x)`, let's say `f_2(x)` has a point at
|
||||
(0, 0).
|
||||
|
||||
5. Double check that `f_2(1)=1`
|
||||
|
||||
Quadratic: No, actually. This means we have to do a wee bit of work: `f(1)=2`, so to counteract this,
|
||||
we'll create a new (and final) function that we can call `f_e` (easing function) by dividing `f(x)`
|
||||
by `f(1)`. In practice this looks like this:
|
||||
|
||||
```
|
||||
f(1)=2, f(x)/f(1) = 2x / 2 = x, f_e(x)=x
|
||||
```
|
||||
|
||||
Easy as that!
|
||||
|
||||
Or so you thought. Now let's check the same for ease in elastic:
|
||||
|
||||
<img src="https://render.githubusercontent.com/render/math?math=\color{blue}f_2(1)=-\frac{5}{1536} \, \sqrt{3} \pi %2B \frac{10245}{1024} \, \log\left(2\right)">
|
||||
|
||||
Hence the need for sagemath. Once we divide the two we get our final easing function, this:
|
||||
|
||||
<img src="https://render.githubusercontent.com/render/math?math=\color{blue}f_e(x)=\frac{4096 \, \pi 2^{10 \, x - 10} \cos\left(-\frac{43}{6} \, \pi %2B \frac{20}{3} \, \pi x\right) %2B 6144 \cdot 2^{10 \, x - 10} \log\left(2\right) \sin\left(-\frac{43}{6} \, \pi %2B \frac{20}{3} \, \pi x\right) %2B 2 \, \sqrt{3} \pi - 3 \, \log\left(2\right)}{2 \, \sqrt{3} \pi - 6147 \, \log\left(2\right)}">
|
||||
|
||||
What on god's green earth is that. Well whatever, at least it works (?).
|
||||
|
||||
6. Finally, we get the definite integral from 0 to 1 of our `f(x)`
|
||||
|
||||
For `f(x)=x` we can do that in our heads, it's just `1/2`.
|
||||
|
||||
For ease in elastic not so much. You can do this with sagemath and eventually get this:
|
||||
|
||||
<img src="https://render.githubusercontent.com/render/math?math=\color{blue}\frac{20 \, \sqrt{3} \pi - 30 \, \log\left(2\right) - 6147}{10 \, {\left(2 \, \sqrt{3} \pi - 6147 \, \log\left(2\right)\right)}}">
|
||||
|
||||
So this all looked pretty daunting probably, and to be honest it took me hours of either not using
|
||||
sage (I tried with wolfram alpha for a good hour) or using sage incorrectly (it took three months
|
||||
to realize that this entire section of the readme was wrong and that using `factor` made it
|
||||
incorrect), but now that I have this easy little code snippet you can use for sage it shouldn't be
|
||||
as much of a hastle for you.
|
||||
|
||||
```python
|
||||
from sage.symbolic.integration.integral import definite_integral
|
||||
function('f')
|
||||
f(x)='''your function goes here'''
|
||||
f(x)=derivative(f(x), x)
|
||||
f(x)=f(x)-f(0)
|
||||
f(x)=f(x)/f(1)
|
||||
print(f(x)) # easing
|
||||
print(definite_integral(f(x), x, 0, 1)) # F
|
||||
```
|
||||
|
||||
So the thing with using `factor` is that, while on some weird other version of sage I was geting a
|
||||
bunch of 0.49999s which wouldn't round to .5, the result was straight up wrong. So I advise against
|
||||
it, and if you can't do the derivative then sucks to suck I guess (just lmk in an issue or
|
||||
something and I'll try my very best).
|
||||
|
||||
4. Now we just have to translate this into an actual lua table.
|
||||
|
||||
Quadratic, as usual, is easy.
|
||||
```lua
|
||||
local quadratic = {
|
||||
F = 1/2 -- F = 1/2
|
||||
easing = function(t) return t end -- f(x) = x
|
||||
}
|
||||
```
|
||||
|
||||
Ease in elastic, as usual, is not. At one point I had the willpower to try and optimize operations,
|
||||
but I really don't want to simplify those equations and I can't trust `factor`, so for now it stays
|
||||
as is. If it irks you, make a pull request and save us both.
|
||||
|
||||
```lua
|
||||
local bouncy = {
|
||||
F = (20*math.sqrt(3)*math.pi-30*math.log(2)-6147) /
|
||||
(10*(2*math.sqrt(3)*math.pi-6147*math.log(2))),
|
||||
easing = function(t) return
|
||||
(4096*math.pi*math.pow(2, 10*t-10)*math.cos(20/3*math.pi*t-43/6*math.pi)
|
||||
+6144*math.pow(2, 10*t-10)*math.log(2)*math.sin(20/3*math.pi*t-43/6*math.pi)
|
||||
+2*math.sqrt(3)*math.pi-3*math.log(2)) /
|
||||
(2*math.pi*math.sqrt(3)-6147*math.log(2))
|
||||
end
|
||||
}
|
||||
|
||||
-- how it would actually look in a timed object
|
||||
timed = rubato.timed {
|
||||
intro = 0, --we'll use this as an outro, since it's weird as an intro
|
||||
outro = 0.7,
|
||||
duration = 1,
|
||||
easing = bouncy
|
||||
}
|
||||
```
|
||||
|
||||
We did it! Now to check whether or not it actually works
|
||||
|
||||

|
||||
|
||||
While you can't see its full glory in 25 fps gif form, it really is pretty cool. Furthermore, if it
|
||||
works with *that* function, it'll probably work with anything. As long as you have the correct
|
||||
antiderivative and it's properly scaled, you can probably use any (real, differentiable) function
|
||||
under the sun.
|
||||
|
||||
Note that if it's not properly scaled, this can be worked around (if you're lazy and don't care
|
||||
about a bit of a performance decrease). You can set `override_simulaton` to true. However, it is
|
||||
possible that it will not perform exactly as you expected if you do this so do your best to just
|
||||
find the derivative and antiderivative of the derivative.
|
||||
|
||||
<h1 id="install">Installation</h1>
|
||||
|
||||
So actually telling people how to install this is important, isn't it
|
||||
|
||||
It supports luarocks, so that'll cut it if you want a really really easy install, but it'll install
|
||||
it in some faraway lua bin where you'll probably leave it forever if you either stop using rubato or
|
||||
stop using awesome. However, it's certainly the easiest way to go about it. I personally don't like
|
||||
doing this much because it adds it globally and I'm only gonna be using this with awesome, but it's
|
||||
a really easy install.
|
||||
|
||||
```
|
||||
luarocks install rubato
|
||||
```
|
||||
|
||||
Otherwise, somewhere in your awesome directory, (I use `~/.config/awesome/lib`) you can run this
|
||||
command:
|
||||
|
||||
```
|
||||
git clone https://github.com/andOrlando/rubato.git
|
||||
```
|
||||
|
||||
Then, whenever you actually want to use rubato, do this at the start of the lua file: `local rubato
|
||||
= require "lib.rubato"`
|
||||
|
||||
<h1 id="name">Why the name?</h1>
|
||||
|
||||
Because I play piano so this kinda links up with other stuff I do, and rubato really well fits the
|
||||
project. In music, it means "push and pull of tempo" basically, which really is what easing is all
|
||||
about in the first place. Plus, it'll be the first of my projects without garbage names
|
||||
("minesweperSweeper," "Latin Learning").
|
||||
|
||||
<h1 id="todo">Todo</h1>
|
||||
|
||||
- [ ] add `target` function, which rather than a set time has a set distance.
|
||||
- [x] improve intro and outro arguments (asserts, default values, proportional intros/outros)
|
||||
- [x] get a better name... (I have a cool name now!)
|
||||
- [x] make readme cooler
|
||||
- [x] have better documentation and add to luarocks
|
||||
- [ ] remove gears dependency
|
||||
- [ ] only apply corrective coefficient to plateau
|
||||
- [ ] Do `prop_intro` more intelligently so it doesn't have to do so many comparisons
|
||||
- [ ] Make things like `abort` more useful
|
||||
- [ ] Merge that pr by @Kasper so instant works
|
||||
- [ ] Add a manager (this proceeds the above todo thing)
|
49
.config/awesome/rubato/easing.lua
Normal file
|
@ -0,0 +1,49 @@
|
|||
--- Linear easing (in quotes).
|
||||
local linear = {
|
||||
F = 0.5,
|
||||
easing = function(t) return t end
|
||||
}
|
||||
|
||||
--- Sublinear (?) easing.
|
||||
local zero = {
|
||||
F = 1,
|
||||
easing = function() return 1 end
|
||||
}
|
||||
|
||||
--- Quadratic easing.
|
||||
local quadratic = {
|
||||
F = 1/3,
|
||||
easing = function(t) return t * t end
|
||||
}
|
||||
|
||||
--bouncy constants
|
||||
local b_cs = {
|
||||
c1 = 6 * math.pi - 3 * math.sqrt(3) * math.log(2),
|
||||
c2 = math.sqrt(3) * math.pi,
|
||||
c3 = 6 * math.sqrt(3) * math.log(2),
|
||||
c4 = 6 * math.pi - 6147 * math.sqrt(3) * math.log(2),
|
||||
c5 = 46 * math.pi / 6
|
||||
}
|
||||
|
||||
-- Okay look. It works. It's not terribly slow because computers can do math
|
||||
-- quick. The other one decidedly does not work (thanks sagemath, I trusted
|
||||
-- you...) so this will have to do. I may try to fix it up at some point, I may
|
||||
-- just leave it be and laugh to myself whenever I see this. As they say, if
|
||||
-- As they say, if you want something fixed that badly, make a pull request lol
|
||||
local bouncy = {
|
||||
F = (20*math.sqrt(3)*math.pi-30*math.log(2)-6147) /
|
||||
(10*(2*math.sqrt(3)*math.pi-6147*math.log(2))),
|
||||
easing = function(t) return
|
||||
(4096*math.pi*math.pow(2, 10*t-10)*math.cos(20/3*math.pi*t-43/6*math.pi)
|
||||
+6144*math.pow(2, 10*t-10)*math.log(2)*math.sin(20/3*math.pi*t-43/6*math.pi)
|
||||
+2*math.sqrt(3)*math.pi-3*math.log(2)) /
|
||||
(2*math.pi*math.sqrt(3)-6147*math.log(2))
|
||||
end
|
||||
}
|
||||
|
||||
return {
|
||||
linear = linear,
|
||||
zero = zero,
|
||||
quadratic = quadratic,
|
||||
bouncy = bouncy
|
||||
}
|
BIN
.config/awesome/rubato/images/beautiful.gif
Normal file
After Width: | Height: | Size: 809 KiB |
BIN
.config/awesome/rubato/images/connected_graph.png
Normal file
After Width: | Height: | Size: 7 KiB |
BIN
.config/awesome/rubato/images/disconnected_graph.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
.config/awesome/rubato/images/forwards_forwards_graph.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
.config/awesome/rubato/images/normal_graph.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
.config/awesome/rubato/images/quadratic_easing.gif
Normal file
After Width: | Height: | Size: 248 KiB |
BIN
.config/awesome/rubato/images/slope_graph.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
.config/awesome/rubato/images/trapezoid_easing.gif
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
.config/awesome/rubato/images/triangleish.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
14
.config/awesome/rubato/init.lua
Normal file
|
@ -0,0 +1,14 @@
|
|||
RUBATO_DEF_RATE = 30
|
||||
RUBATO_OVERRIDE_DT = true
|
||||
RUBATO_DIR = (...):match("(.-)[^%.]+$").."rubato."
|
||||
|
||||
return {
|
||||
--Overarching functions to set defaults
|
||||
set_def_rate = function(rate) RUBATO_DEF_RATE = rate end,
|
||||
set_override_dt = function(value) RUBATO_OVERRIDE_DT = value end,
|
||||
|
||||
--Modules
|
||||
timed = require(RUBATO_DIR.."timed"),
|
||||
easing = require(RUBATO_DIR.."easing"),
|
||||
subscribable = require(RUBATO_DIR.."subscribable"),
|
||||
}
|
25
.config/awesome/rubato/rubato-1.1-1.rockspec
Normal file
|
@ -0,0 +1,25 @@
|
|||
package = "rubato"
|
||||
version = "1.1-1"
|
||||
source = {
|
||||
url = "git+https://github.com/andOrlando/rubato.git"
|
||||
}
|
||||
description = {
|
||||
detailed = [[
|
||||
Create smooth animations based off of a slope curve for near perfect interruptions. Similar to awestore, but solely dedicated to interpolation. Also has a cool name. Check out the README on github for more informaiton. Has (basically) complete compatibility with awestore.
|
||||
|
||||
Requires either gears or to be ran from awesomeWM
|
||||
]],
|
||||
homepage = "https://github.com/andOrlando/rubato",
|
||||
license = "MIT"
|
||||
}
|
||||
dependencies = {
|
||||
"gears"
|
||||
}
|
||||
build = {
|
||||
type = "builtin",
|
||||
modules = {
|
||||
easing = "easing.lua",
|
||||
timed = "timed.lua",
|
||||
subscribable = "subscribable.lua"
|
||||
}
|
||||
}
|
32
.config/awesome/rubato/subscribable.lua
Normal file
|
@ -0,0 +1,32 @@
|
|||
-- Kidna copying awesotre's stores on a surface level for added compatibility
|
||||
local function subscribable(args)
|
||||
local obj = args or {}
|
||||
local subscribed = {}
|
||||
|
||||
-- Subscrubes a function to the object so that it's called when `fire` is
|
||||
-- Calls subscribe_callback if it exists as well
|
||||
function obj:subscribe(func)
|
||||
local id = tostring(func):gsub("function: ", "")
|
||||
subscribed[id] = func
|
||||
|
||||
if self.subscribe_callback then self.subscribe_callback(func) end
|
||||
end
|
||||
|
||||
-- Unsubscribes a function and calls unsubscribe_callback if it exists
|
||||
function obj:unsubscribe(func)
|
||||
if not func then
|
||||
subscribed = {}
|
||||
else
|
||||
local id = tostring(func):gsub("function: ", "")
|
||||
subscribed[id] = nil
|
||||
end
|
||||
|
||||
if self.unsubscribe_callback then self.unsubscribe_callback(func) end
|
||||
end
|
||||
|
||||
function obj:fire(...) for _, func in pairs(subscribed) do func(...) end end
|
||||
|
||||
return obj
|
||||
end
|
||||
|
||||
return subscribable
|
328
.config/awesome/rubato/timed.lua
Normal file
|
@ -0,0 +1,328 @@
|
|||
local easing = require(RUBATO_DIR.."easing")
|
||||
local subscribable = require(RUBATO_DIR.."subscribable")
|
||||
local gears = require "gears"
|
||||
|
||||
--- Get the slope (this took me forever to find).
|
||||
-- i is intro duration
|
||||
-- o is outro duration
|
||||
-- t is total duration
|
||||
-- d is distance to cover
|
||||
-- F_1 is the value of the antiderivate at 1: F_1(1)
|
||||
-- F_2 is the value of the outro antiderivative at 1: F_2(1)
|
||||
-- b is the y-intercept
|
||||
-- m is the slope
|
||||
-- @see timed
|
||||
local function get_slope(i, o, t, d, F_1, F_2, b)
|
||||
return (d + i * b * (F_1 - 1)) / (i * (F_1 - 1) + o * (F_2 - 1) + t)
|
||||
end
|
||||
|
||||
--- Get the dx based off of a bunch of factors
|
||||
-- @see timed
|
||||
local function get_dx(time, duration, intro, intro_e, outro, outro_e, m, b)
|
||||
-- Intro math. Scales by difference between initial slope and target slope
|
||||
if time <= intro then
|
||||
return intro_e(time / intro) * (m - b) + b
|
||||
|
||||
-- Outro math
|
||||
elseif (duration - time) <= outro then
|
||||
return outro_e((duration - time) / outro) * m
|
||||
|
||||
-- Otherwise (it's in the plateau)
|
||||
else return m end
|
||||
end
|
||||
|
||||
--weak table for memoizing results
|
||||
local simulate_easing_mem = {}
|
||||
setmetatable(simulate_easing_mem, {__mode="kv"})
|
||||
|
||||
--- Simulates the easing to get the result to find an error coefficient
|
||||
-- Uses the coefficient to adjust dx so that it's guaranteed to hit the target
|
||||
-- This must be called when the sign of the target slope is changing
|
||||
-- @see timed
|
||||
local function simulate_easing(pos, duration, intro, intro_e, outro, outro_e, m, b, dt)
|
||||
local ps_time = 0
|
||||
local ps_pos = pos
|
||||
local dx
|
||||
|
||||
|
||||
-- Key for cacheing results
|
||||
local key = string.format("%f %f %f %s %f %s %f %f",
|
||||
pos, duration,
|
||||
intro, tostring(intro_e),
|
||||
outro, tostring(outro_e),
|
||||
m, b)
|
||||
|
||||
-- Short circuits if it's already done the calculation
|
||||
if simulate_easing_mem[key] then return simulate_easing_mem[key] end
|
||||
|
||||
-- Effectively runs the exact same code to find what the target will be
|
||||
while duration - ps_time >= dt / 2 do
|
||||
--increment time
|
||||
ps_time = ps_time + dt
|
||||
|
||||
--get dx, but use the pseudotime as to not mess with stuff
|
||||
dx = get_dx(ps_time, duration,
|
||||
intro, intro_e,
|
||||
outro, outro_e,
|
||||
m, b)
|
||||
|
||||
--increment pos by dx
|
||||
ps_pos = ps_pos + dx * dt
|
||||
end
|
||||
|
||||
simulate_easing_mem[key] = ps_pos
|
||||
return ps_pos
|
||||
end
|
||||
|
||||
--- INTERPOLATE. bam. it still ends in a period. But this one is timed.
|
||||
-- So documentation isn't super necessary here since it's all on the README and idk how to do
|
||||
-- documentation correctly, so please see the README or read the code to better understand how
|
||||
-- it works
|
||||
local function timed(args)
|
||||
|
||||
local obj = subscribable()
|
||||
|
||||
--set up default arguments
|
||||
obj.duration = args.duration or 1
|
||||
obj.pos = args.pos or 0
|
||||
|
||||
obj.prop_intro = args.prop_intro or false
|
||||
|
||||
obj.intro = args.intro or 0.2
|
||||
obj.inter = args.inter or args.intro
|
||||
|
||||
--set args.outro nicely based off how large args.intro is
|
||||
if obj.intro > (obj.prop_intro and 0.5 or obj.duration) and not args.outro then
|
||||
obj.outro = math.max((args.prop_intro and 1 or args.duration - args.intro), 0)
|
||||
|
||||
elseif not args.outro then obj.outro = obj.intro
|
||||
else obj.outro = args.outro end
|
||||
|
||||
--assert that these values are valid
|
||||
assert(obj.intro + obj.outro <= obj.duration or obj.prop_intro, "Intro and Outro must be less than or equal to total duration")
|
||||
assert(obj.intro + obj.outro <= 1 or not obj.prop_intro, "Proportional Intro and Outro must be less than or equal to 1")
|
||||
|
||||
obj.easing = args.easing or easing.linear
|
||||
obj.easing_outro = args.easing_outro or obj.easing
|
||||
obj.easing_inter = args.easing_inter or obj.easing
|
||||
|
||||
--dev interface changes
|
||||
obj.log = args.log or function() end
|
||||
obj.awestore_compat = args.awestore_compat or false
|
||||
|
||||
--animation logic changes
|
||||
obj.override_simulate = args.override_simulate or false
|
||||
--[[ rapid_set is allowed by awestore but I don't like it, so it's bound to awestore_compat if not explicitly set
|
||||
override_dt doesn't work well with big animations or scratchpads (blame awesome not me) (probably) so that too is
|
||||
is tied to awestore_compat if not explicitly set, then to the default value ]]
|
||||
obj.rapid_set = args.rapid_set == nil and obj.awestore_compat or args.rapid_set
|
||||
obj.override_dt = args.override_dt == nil and (not obj.awestore_compat and RUBATO_OVERRIDE_DT) or args.override_dt
|
||||
|
||||
-- hidden properties
|
||||
obj._props = {
|
||||
target = obj.pos,
|
||||
rate = args.rate or RUBATO_DEF_RATE
|
||||
}
|
||||
|
||||
-- awestore compatibility
|
||||
if obj.awestore_compat then
|
||||
obj._initial = obj.pos
|
||||
obj._last = 0
|
||||
|
||||
function obj:initial() return obj._initial end
|
||||
function obj:last() return obj._last end
|
||||
|
||||
obj.started = subscribable()
|
||||
obj.ended = subscribable()
|
||||
|
||||
end
|
||||
|
||||
-- Variables used in calculation, defined once bcz less operations
|
||||
local time = 0 -- current time
|
||||
local dt = 1 / obj._props.rate -- change in time
|
||||
local dx = 0 -- value of slope at current time
|
||||
local m -- slope
|
||||
local b -- y-intercept
|
||||
local is_inter --whether or not it's in an intermittent state
|
||||
|
||||
local last_frame_time --the time at the last frame
|
||||
local frame_time = dt --duration of the last frame (placeholder)
|
||||
|
||||
-- Variables used in simulation
|
||||
local ps_pos -- pseudoposition
|
||||
local coef -- corrective coefficient TODO: apply to plateau
|
||||
|
||||
-- The timer that does all the animating
|
||||
local timer = gears.timer { timeout = dt }
|
||||
timer:connect_signal("timeout", function()
|
||||
|
||||
-- Find the correct dt if it's not already correct
|
||||
if (obj.override_dt and last_frame_time) then
|
||||
frame_time = os.clock() - last_frame_time
|
||||
|
||||
--[[ if frame time is bigger than dt, we must readjust dt by the difference
|
||||
between dt and the last frame. Basically, dt + (frame_time - dt) which just
|
||||
results in frame_time ]]
|
||||
if (frame_time > timer.timeout) then dt = frame_time
|
||||
else dt = timer.timeout end
|
||||
end
|
||||
|
||||
-- for the next timeout event
|
||||
if (obj.override_dt) then last_frame_time = os.clock() end
|
||||
|
||||
--increment time
|
||||
time = time + dt
|
||||
|
||||
--get dx
|
||||
dx = get_dx(time, obj.duration,
|
||||
(is_inter and obj.inter or obj.intro) * (obj.prop_intro and obj.duration or 1),
|
||||
is_inter and obj.easing_inter.easing or obj.easing.easing,
|
||||
obj.outro * (obj.prop_intro and obj.duration or 1),
|
||||
obj.easing_outro.easing,
|
||||
m, b)
|
||||
|
||||
--increment pos by dx
|
||||
--scale by dt and correct with coef if necessary
|
||||
obj.pos = obj.pos + dx * dt * coef
|
||||
|
||||
--sets up when to stop by time
|
||||
--weirdness is to try to get as close to duration as possible
|
||||
if obj.duration - time < dt / 2 then
|
||||
obj.pos = obj._props.target --snaps to target in case of small error
|
||||
time = obj.duration --snaps time to duration
|
||||
|
||||
is_inter = false --resets intermittent
|
||||
timer:stop() --stops itself
|
||||
|
||||
--run subscribed in functions
|
||||
obj:fire(obj.pos, obj.duration, dx)
|
||||
|
||||
-- awestore compatibility...
|
||||
if obj.awestore_compat then obj.ended:fire(obj.pos, obj.duration, dx) end
|
||||
|
||||
--otherwise it just fires normally
|
||||
else obj:fire(obj.pos, time, dx) end
|
||||
end)
|
||||
|
||||
|
||||
-- Set target and begin interpolation
|
||||
local function set(target_new)
|
||||
|
||||
--disallow setting it twice (because it makes it go wonky sometimes)
|
||||
if not obj.rapid_set and obj._props.target == target_new then return end
|
||||
|
||||
--animation values
|
||||
obj._props.target = target_new --sets target
|
||||
time = 0 --resets time
|
||||
coef = 1 --resets coefficient
|
||||
|
||||
--rate stuff
|
||||
--both of these values would ideally be timer.timeout so that's their default
|
||||
dt, frame_time = timer.timeout, timer.timeout
|
||||
last_frame_time = nil --is given a value after the first frame
|
||||
|
||||
-- does annoying awestore compatibility
|
||||
if obj.awestore_compat then
|
||||
obj._last = obj._props.target
|
||||
obj.started:fire(obj.pos, time, dx)
|
||||
end
|
||||
|
||||
-- if it's already started, reflect that in is_inter
|
||||
is_inter = timer.started
|
||||
|
||||
--set initial position if interrupting another animation
|
||||
b = timer.started and dx or 0
|
||||
|
||||
--get the slope of the plateau
|
||||
m = get_slope((is_inter and obj.inter or obj.intro) * (obj.prop_intro and obj.duration or 1),
|
||||
obj.outro * (obj.prop_intro and obj.duration or 1),
|
||||
obj.duration,
|
||||
obj._props.target - obj.pos,
|
||||
is_inter and obj.easing_inter.F or obj.easing.F,
|
||||
obj.easing_outro.F,
|
||||
b)
|
||||
|
||||
--if it will make a mistake (or override_simulate is true), fix it
|
||||
--it should only make a mistake when switching direction
|
||||
--b ~= zero protection so that I won't get any NaNs (because NaN ~= NaN)
|
||||
if obj.override_simulate or (b ~= 0 and b / math.abs(b) ~= m / math.abs(m)) then
|
||||
ps_pos = simulate_easing(obj.pos, obj.duration,
|
||||
(is_inter and obj.inter or obj.intro) * (obj.prop_intro and obj.duration or 1),
|
||||
is_inter and obj.easing_inter.easing or obj.easing.easing,
|
||||
obj.outro * (obj.prop_intro and obj.duration or 1),
|
||||
obj.easing_outro.easing,
|
||||
m, b, dt)
|
||||
|
||||
--get coefficient by calculating ratio of theoretical range : experimental range
|
||||
coef = (obj.pos - obj._props.target) / (obj.pos - ps_pos)
|
||||
if coef ~= coef then coef = 1 end --check for div by 0 resulting in NaN
|
||||
end
|
||||
|
||||
if not timer.started then timer:start() end
|
||||
|
||||
end
|
||||
|
||||
if obj.awestore_compat then function obj:set(target) set(target) end end
|
||||
|
||||
-- Functions for setting state
|
||||
-- Completely resets the timer
|
||||
function obj:reset()
|
||||
timer:stop()
|
||||
time = 0
|
||||
obj._props.target = obj.pos
|
||||
dx = 0
|
||||
m = nil
|
||||
b = nil
|
||||
is_inter = false
|
||||
coef = 1
|
||||
dt = timer.timeout
|
||||
end
|
||||
|
||||
-- Effectively pauses the timer
|
||||
function obj:abort()
|
||||
timer:stop()
|
||||
is_inter = false
|
||||
end
|
||||
|
||||
--subscribe stuff initially and add callback
|
||||
obj.subscribe_callback = function(func) func(obj.pos, time, dt) end
|
||||
if args.subscribed ~= nil then obj:subscribe(args.subscribed) end
|
||||
|
||||
-- Metatable for cooler api
|
||||
local mt = {}
|
||||
function mt:__index(key)
|
||||
-- Returns the state value
|
||||
if key == "state" then return timer.started
|
||||
|
||||
-- If it's in _props return it from props
|
||||
elseif self._props[key] then return self._props[key]
|
||||
|
||||
-- Otherwise just be nice
|
||||
else return rawget(self, key) end
|
||||
end
|
||||
function mt:__newindex(key, value)
|
||||
-- Don't allow for setting state
|
||||
if key == "state" then return
|
||||
|
||||
-- Changing target should call set
|
||||
elseif key == "target" then set(value) --set target
|
||||
|
||||
-- Changing rate should also update timeout
|
||||
elseif key == "rate" then
|
||||
self._props.rate = value
|
||||
timer.timeout = 1 / value
|
||||
|
||||
-- If it's in _props set it there
|
||||
elseif self._props[key] ~= nil then self._props[key] = value
|
||||
|
||||
-- Otherwise just set it normally
|
||||
else rawset(self, key, value) end
|
||||
end
|
||||
|
||||
setmetatable(obj, mt)
|
||||
|
||||
return obj
|
||||
|
||||
end
|
||||
|
||||
return timed
|