Creation date: 2020-08-04
I'm back! Sorry that I didn't blog for more than two weeks. I might have gotten a bit too much into the streaming world and programmed more than blogging about it. This post starts with some housekeeping and then explains the newest project I'm working on. Feel free to jump to the point you are most interested in.
The last post here was about visualizing kinda random numbers :D Visualizing digits. It might be the one that brought me to building Javis but more on that later.
I'm currently mostly working on two projects:
ConstraintSolver.jl where I try to build a constraint solver from scratch in Julia. I started with some refactoring lately and streamed about it via Twitch. The videos are saved on YouTube. The refactoring is still going and I probably will write a post about that once it's done just to have a better structure of the outcome. Streaming is unstructured and hard to watch later I suppose. I'm still trying to figure out the best way to combine my possibilities to teach julia.
The new project is Javis.jl which I explain in the next section.
Sometimes I work on Juniper.jl as it is my small baby. It's my first project in Julia and the most used one I've built up til now. I mostly do bugfixes for it at the moment but might come back to it to add some new awesome features.
Okay what is Javis?
Javis stands for Julia Mathematical visualizations and animations. (Currently at least ๐)
A small side note maybe: The Juniper package I was talking about was called MINLPBnB.jl
which is the worst name possible, so I might have gotten better at naming things. I have to say it's very easy to see what the package does: Mixed Iteger Non-Linear Programming using Branch and Bound. Well, some folks told me it should have a proper name... We debated to call it Bacon.jl
and I can't remember what it stands for.
... The thread apparently it was for "Branch Optimization Nonlinear". Just saw that I suggested BOREDOM as well. Oh man fun times...
Any way I liked Javis
this time and just tried to find out what it might stand for.
To answer the actual question:
Javis tries to make it as easy and extensible as possible to create small animations like this one:
I'll show the code for it later. ๐ In general I want to have a simple way to animate all the ideas that might get into my head. Just some examples
Visualizing linear algebra concepts easily
Animate puzzles for my ConstraintSolver
Explore math concepts
Normally I try to build things from scratch (in a high level programming language ๐). This time I'm more aware than ever that I'm standing on the shoulders of giants.
It is of course built with Julia which should be no surprise. BTW a new version (v1.5) is official now ๐
Besides that it is highly dependent on the awesome package Luxor.jl which is used for all the drawing onto a canvas. It provides simple functions like line
, circle
and so on but also gave rise to the way we do animations.
Luxor.jl is itself built using Cairo.jl which is a wrapper around the C library Cairo.
If you're interested in 2D graphics, you should definitely check out the awesome Luxor Tutorial.
You might be interested in the current state of Javis.jl as the documentation is very sparse (or non-existent for stable
) and the README doesn't show anything especially entertaining ๐
Me an my fellow Julian and streamer Jacob Zelko started this project about two weeks ago. Furthermore, I was away for a week so everything is very new.
The basic functionality
\(\LaTeX\) text which is not possible in Luxor.jl
Translation and rotation of objects
Objects depending on others (quite basic as of now)
Very basic morphing between two shapes
Before I give a view into the future you might want to see some code, right?
I think we should start with the very very basics before I show you the 40 lines of code.
using Javis, Luxor
using Javis
as we started to reexport everything that Luxor does and change some methods to work in our favor ๐We normally need both of the packages as we want to do some animations (Javis
) and want to do some drawing (Luxor
).
I don't like to work in the global scope so let's create a function:
function dancing()
video = Video(500, 500)
javis(video,
[
...
]
,tempdirectory="test/current/images", deletetemp=true, creategif=true, pathname="test/current/dancing_circles.gif")
end
deletetemp
and creategif
don't exist anymore.It has quite a few parameters to create a simple gif
out of it, so I think this needs improvement but that's for another day.
First of all we create our video object which is nothing more than a struct. The object sets the width and height in pixel of the video and stores information in between but that is of no concern to the user.
Then we have our javis
function which is the main function of our project. It takes at least two arguments: the video
and a list of actions that shall be performed during the video. The ...
is just a placeholder for now.
We have a list of keyword arguments:
tempdirectory
stores the images (yes we create an image for each frame and combine them later)
deletetemp
means that all images are destroyed after we created the gif
creategif
yes we want to create a gif and not only having a list of images
pathname
the gif should be stored here
Okay now to Action
.
Let's create our first "Animation":
function ground(args...)
background("white")
sethue("black")
end
function dancing()
video = Video(500, 500)
javis(video,
[
BackgroundAction(1:70, ground)
]
,tempdirectory="test/current/images", deletetemp=true, creategif=true, pathname="test/current/dancing_circles.gif")
end
The ground
function sets the background to white and the paint brush color to black.
Why do I need the args...
? I'm glad you ask. Each user function gets three arguments video, action, frame
where the first is the Video
struct, the second is the Action
struct and frame
is just the current frame number. They are irrelevant for the background such that we don't need to write them down explicitly. "Unfortunately" we need to write args...
such that Julia actually knows that we have a method that accepts those three arguments. The ...
basically stands for as many arguments as you want.
sethue
is the same as setcolor
but doesn't mess with the opacityMore interesting is the BackgroundAction(1:70, ground)
which has just two arguments here:
frames
a range of frames it is applied to.
func
the function that is called for each frame
I hope it is clear what you can expect from the 70 frames we just created ๐
Okay it's time to actually draw something with adding another action:
...
BackgroundAction(1:70, ground),
Action(1:70, (args...)->line(O, Point(25, 25), :stroke), Translation(O, Point(0, 100)))
...
This time we define an Action
instead of a BackgroundAction
which basically means: Everything in this action stays in the action and is not exposed to the outside world. You might wanna call it the Vegas of actions. Sorry for the bad joke...
There are some new parts in this:
An anonymous function with the ->
syntax.
The line
function from Luxor which takes in two points and an action
O
is the same as Point(0, 0)
and stands for origin (which is in the center of the canvas)
:stroke
means that we want to actually draw the line which is not the default and might be confusing
Translation
from the origin to the point (0,100)
Some more information before you have a look at the spoiler.
The canvas has its origin in the center as mentioned but it also has the y-axis going down instead of up. I'm not sure whether we want to change that for Javis.
So the Translation
is animated by interpolation between the start and end translation. In the future we will make it possible to use easing functions to change this behavior.
Additionally, the Translation
translates the plane such that drawing the line from (0,0)
to (25, 25)
results in drawing it from (0, 100)
to (25, 125)
in the last frame.
I hope that it makes sense so far.
Okay how do I draw circle that is moving down at the same rate by defining a new Action
?
...
BackgroundAction(1:70, ground),
Action(1:70, (args...)->line(O, Point(25, 25), :stroke), Translation(O, Point(0, 100))),
Action(1:70, (args...)->circle(Point(-25, 0), 10, :fill), Translation(O, Point(0, 100)))
...
Simple, right?
Okay this is the boring way of doing it but let's have a look at the circle
function first.
The circle
function takes a Point
a radius
and an action. For the action I chose :fill
instead of :stroke
this time.
For a bit more fun we want to define the first action a bit differently using a named function.
function draw_line(args...)
line(O, Point(25, 25), :stroke)
return Transformation(O, 0.0)
end
and then:
...
BackgroundAction(1:70, ground),
Action(1:70, :line, draw_line, Translation(O, Point(0, 100))),
Action(1:70, (args...)->circle(pos(:line) .+ Point(-25, 0), 10, :fill))
...
This adds quite some more complexity but hopefully it's not too hard to understand and it is a very powerful tool.
Action(1:70, :line, draw_line, Translation(O, Point(0, 100))),
is not an anonymous function anymore but it also has a name :line
as the second argument.
Furthermore, the function now return something:
return Transformation(O, 0.0)
It does return the same position (the origin, tail of the line) every time. The second argument is the angle
of the transformation which is set to 0.0
as we don't use it. There is already a PR to be able to use return O
instead ๐
return Point(x,y)
now instead of using a Transformation
.Now in the circle
action:
Action(1:70, (args...)->circle(pos(:line) .+ Point(-25, 0), 10, :fill))
we have access to the position of the line with pos(:line)
and we removed the Translation
.
The pos(:line)
is actually not the origin all the time as it had its own Translation
. Javis "assumes" that you're interested in the actual canvas position of :line
and not the one that is returned directly. Internally the position is therefore converted into the global coordinate system.
This is an important concept so let me try to explain it in different words ๐
You can use some kind of variable which is a Symbol
like :line
to be able to save whatever you return in that function.
If you return a Point
or a Transformation
it will be automatically converted to global coordinates.
You can access the saved position with pos(:line)
or whichever Symbol
you have used.
Okay yeah I think I can finally explain the code for the animation I showed in the beginning:
function dc()
p1 = Point(100,0)
p2 = Point(100,80)
path_of_blue = Point[]
path_of_red = Point[]
video = Video(500, 500)
javis(video, [
BackgroundAction(1:70, ground),
Action(1:70, :red_ball, (args...)->circ(p1, "red"), Rotation(0.0, 2ฯ)),
Action(1:70, :blue_ball, (args...)->circ(p2, "blue"), Rotation(2ฯ, 0.0, :red_ball)),
Action(1:70, (video, args...)->path!(path_of_red, pos(:red_ball), "red")),
Action(1:70, (video, args...)->path!(path_of_blue, pos(:blue_ball), "blue")),
Action(1:70, (args...)->rad(pos(:red_ball), pos(:blue_ball), "black"))
], tempdirectory="test/current/images", deletetemp=true, creategif=true, pathname="test/current/dancing_circles.gif")
return video
end
You don't need to define the frames every time anymore. It's possible to define it for BackgroundAction
and define later actions like:
Action(:red_ball, (args...)->circ(p1, "red"), Rotation(0.0, 2ฯ))
without defining frames. Then it will use the same frames as the previous action.
First of all I just defined some points at the beginning to change them in an easier way.
The functions itself are not that interesting:
circ
just creates a circle with some default radius and sets the color with sethue
and returns the center.
path!
takes a list of points and a new point and draws them as circles
rad
draws a line from the first to the second point with some color
function circ(p=O, color="black")
sethue(color)
circle(p, 25, :fill)
return Transformation(p, 0.0)
end
function path!(points, pos, color)
sethue(color)
push!(points, pos)
circle.(points, 2, :fill)
end
function rad(p1, p2, color)
sethue(color)
line(p1,p2, :stroke)
end
Let's get back to the actions:
BackgroundAction(1:70, ground),
Action(1:70, :red_ball, (args...)->circ(p1, "red"), Rotation(0.0, 2ฯ)),
Action(1:70, :blue_ball, (args...)->circ(p2, "blue"), Rotation(2ฯ, 0.0, :red_ball)),
Instead of using Translation
we now use Rotation
which takes in either two or three arguments.
start rotation
end rotation
center of the rotation (default is O
)
Here we use :red_ball
as the center of rotation for the :blue_ball
.
The red ball rotates around the origin with a radius of 100
as it starts at Point(100, 0)
The blue ball rotates around the red ball with a radius of \(\sqrt{100^2+80^2} \approx 128\) as it has the initial start position of p2 = Point(100, 80)
is rotated around the start position of p1 = Point(100, 0)
Action(1:70, (video, args...)->path!(path_of_red, pos(:red_ball), "red")),
Action(1:70, (video, args...)->path!(path_of_blue, pos(:blue_ball), "blue")),
with path!
function path!(points, pos, color)
sethue(color)
push!(points, pos)
circle.(points, 2, :fill)
end
just adds the new position of either the red or blue ball with pos
to a list of points and draws the path by just drawing a circle for each position of points
.
This is done with the broadcast dot
notation circle.
.
The last action:
Action(1:70, (args...)->rad(pos(:red_ball), pos(:blue_ball), "black"))
just draw the line between the red and blue ball to see that the radius is really not changing.
There are some things that are on the list:
ability to draw vectors for a small linear algebra intro video
do some nice animations of matrices like the highly requested feature: transposing a matrix
I'm really interested in doing nice morphing between shapes
Make it easier to define frames
Easing functions for more interesting animations
...
There is so much more to do! If you want to get involved: Just reach out.
Even after I've published the poster just a few days ago there are a lot of new changes. You might just wanna check out the repository from time to time or watch it. (You might get quite a few E-mails). GitHub: Javis.jl
See you next time :)
Thanks for reading and special thanks to my 10 patrons!
List of patrons paying more than 4$ per month:
Site Wang
Gurvesh Sanghera
Szymon Bฤczkowski
Logan Kilpatrick
Currently I get more than 20$ per month using Patreon and PayPal.
For a donation of a single dollar per month you get early access to the posts. Try it out at the start of a month and if you don't enjoy it just cancel your subscription before pay day (end of month).
I'll keep you updated on Twitter OpenSourcES as well as my more personal one: Twitter Wikunia_de