Creation date: 2021-08-09
Yesterday I heard the third episode of the 3b1b podcast which is an interview of the famous Steven Strogatz. There he mentions two interesting puzzles at the beginning of the conversation. The first is about a geometry problem and the second one is what I want to "tackle" (visualize) in this blog post today.
Before I explain the problem I would like to link you to the JuliaCon video of the presentation that Jacob Zelko and I made to present the latest state of Javis .
Mostly due to the excellent work of Arsh Sharma we now have quite some more functionality in Javis than the last time I've wrote about it back in June.
We have layers now as well as easy functions and macros to define objects without using the weird anonymous syntax functionality. I would like to visualize interesting problems or concepts on this blog and talk about the code to create those in the next couple of weeks.
If you want to keep getting updated please join the newsletter such that you don't miss one of those 😉
Free Patreon newsletterOkay let's start now with the problem formulation:
Let's assume we have a square and in each of the corners there is a dog. Each dog is trying to chase a dog which is their clockwise neighbor. How long does it take each dog to catch its neighbor?
Now they don't just run along the side of the edge as that would be quite boring 😄 They try to use the fastest way possible and continuously update the direction they are running in. Furthermore the speed they are running is a constant value.
Now I want to visualize the dogs and the path they are taking. Of course as I'm one of the creators of Javis I want to use that tool 😄
Let's start with drawing the square.
using Javis
function ground(args...)
background("black")
sethue("white")
end
function main()
video = Video(1000, 1000)
nframes = 300
square_len = 800
Background(1:nframes, ground)
Object(1:nframes, JRect(Point(-square_len/2, -square_len/2), square_len, square_len; color="white"))
render(video; pathname = "chase.gif")
end
This might partly look familiar if you have seen Javis code before but as I intend this series for newcomers I want to go over everything in as much detail as needed. For this I keep the animations short in general. Nevertheless I use quite some advanced and sometimes undocumented features of Javis so I'm sure there is something to learn for everyone.
Now I first of all need to have using Javis
at some point and I like to have a main
function to avoid having everything in the global scope.
I define the video
with 1000x1000 pixels and then the number of frames and the size of the square.
For defining the background of the animation I have the ground
function which takes in some arguments which aren't relevant which is the reason why I choose args...
here as the parameters. I define a black background and the default color to being white (even though I never use it 😄).
Next up I define my object which should draw the square. In v0.6.1 of Javis some new convenience functions were introduced including JRect which draws a rectangle. We define the frames of the object which is the full number of frames here and then the upper left corner position of our square as well as the width and height. Additionally we define the color of the square with color
as we want to have only the square outline we don't need to add an action
keyword. If we would like to fill it we could have used action = :fill
after defining the color.
Okay let's place the dogs in each corner.
For this we add the following lines before the render
function:
dog_colors = Colors.JULIA_LOGO_COLORS
dog_positions = [
Point(-square_len / 2, -square_len / 2),
Point(square_len / 2, -square_len / 2),
Point(square_len / 2, square_len / 2),
Point(-square_len / 2, square_len / 2),
]
dogs = [
Object(
1:nframes,
JCircle(dog_positions[i], 15; action = :fill, color = dog_colors[i]),
) for i in 1:4
]
We start with defining the colors of the circles that we'll use to represent the dogs. I used the four colors of the Julialang logo for this. You need to also add using Colors
at the top of the file.
Then we define the corners first and then we create four objects. Each of them is defined using the function JCircle which takes in the center of the circle and the radius plus some keyword arguments like before. We do the creation of the objects inside a list comprehension. In that we iterate i
between 1 and 4 and can use that to access both the position of the dog as well as the color.
Now we're going into the complicated part of this. How do we move the dogs?
Well each dog wants to move into the direction of the neighboring dog and that with a certain constant speed. In various tutorials of Javis we only tackle simple movements like translate by a fixed value or rotate. We showed how to rotate around another object in the first tutorial which at least uses the pos
function that I'll also use in a moment. However calculating the vector and then moving along that changing direction isn't that simple with the basic functionality provided by Javis. That said there is a way which one can use when one understands how Javis works from the ground up.
That is something I want to show you here. Once you understand this simple example you'll be able to create much more powerful animations yourself. Why isn't that documented then? Well... I would like to create an easier process for the user but at least will link to this post in the docs 😉
Alright let's check out a simple action first maybe.
for i in 1:4
act!(dogs[i], Action(1:nframes, anim_translate(100, 0)))
end
Now the dogs are moving very slowly to the right. We do this by applying an Action
to each of the dogs which is here defined for all frames as well and
anim_translate
just tells in which direction the dogs should move. Each dog will end up at dogs_positions .+ Point(100, 0)
.
Now anim_translate(100, 0)
is in the backend just a function which calls an anonymous function. This means we can define our own action as well as long as it has the anonymous function style.
Let me show you what I mean. We add the following two functions:
function chase(dogs, from, to)
(args...) -> _chase(dogs, from, to)
end
function _chase(dogs, from, to)
println("$from chases $to")
end
and change our act! from before to:
act!(dogs[i], Action(1:nframes, chase(dogs, i, mod1(i+1, 4))))
When we call main()
we will get an output of repeated:
1 chases 2
2 chases 3
3 chases 4
4 chases 1
mod1
function is very useful when working with modulo in the 1 index based language Julia. It basically also just wraps around but instead of going from 0 to 3 it goes from 1 to 4 which then can be used to index our array at a later stage.The functionality that we need now to actually visualize the chasing positions is what is provided by the
change
method under the hood. We want to change the center of the dogs objects.
Therefore we need to call dogs[from].change_keywords[:center] = new_pos
and need to compute the new position of the dog. And yes I hear you all: This should be a documented in the official documentation.
Okay now let's set the new_pos
to the origin just to see that it works:
function _chase(dogs, from, to)
dogs[from].change_keywords[:center] = O
end
O
is the letter O
which stands for the origin and is the same as Point(0,0)
.Well that isn't really an animation is it? Let's compute the actual value of the dogs position. For this we also introduce the variable speed
which I set to speed = 3
right after defining the square_len
.
Then we replace the chase
function again:
function _chase(dogs, from, to, speed)
animal = dogs[from]
chases = dogs[to]
diff = pos(chases) - pos(animal)
diff /= sqrt(diff.x^2 + diff.y^2)
new_pos = pos(animal) + speed * diff
animal.change_keywords[:center] = new_pos
end
So we take the current position of the dog we want to chase as well as the position of the current dog. Then compute the difference and normalize that and calculate the new position by adding it to the current position while also combining the "vector" diff
with the speed.
The chase
function for the extra parameter speed
:
function chase(dogs, from, to, speed)
(args...) -> _chase(dogs, from, to, speed)
end
as well as passing speed
into the chase
function:
act!(dogs[i], Action(1:nframes, chase(dogs, i, mod1(i+1, 4), speed)))
Unfortunately we get:
ERROR: MethodError: no method matching get_position(::Nothing)
Closest candidates are:
get_position(::Javis.Layer) at /Users/olek/Code/Javis/src/layers.jl:183
get_position(::Point) at /Users/olek/Code/Javis/src/object_values.jl:17
The problem here is that the dogs don't have their initial position calculated as this would draw the dogs directly. We also compute the action before we draw the dog so before we call the object itself. This way the pos
calls in our chase
function return nothing.
We could either check if pos
returns something and if it does we do our calculation or as I decided: We simply set them to the corner in frame one and let them chase starting at frame 2.
Therefore we use:
act!(dogs[i], Action(2:nframes, chase(dogs, i, mod1(i+1, 4), speed)))
This creates the hardest part of a lovely animation:
For this animation:
Had to visualize the problem chase problem mentioned by @stevenstrogatz in the @3blue1brown podcast.
— OpenSourcES (@opensourcesblog) August 8, 2021
As a true #julialang fan with the @JuliaLanguage colors ;) pic.twitter.com/Vfb3anUbBB
we want to do a bit more.
We first of all remove the square by commenting out the Object JRect line.
Then we create the path which is also done in our first tutorial.
Inside our act!
for loop we'll add:
Object(1:nframes, (args...) -> path!(dogs_paths[i], pos(dogs[i]), dog_colors[i]))
and then we need to define the path!
function as well as the dogs_paths
vector.
dogs
objects and it's an object itself and not an action acting on the dogs.The dogs_paths
vector will hold the path each of the dogs took and is initialized with:
dogs_paths = [Point[] for _ in 1:4]
Our path function is simply copied from the tutorial:
function path!(points, cpos, color)
sethue(color)
push!(points, cpos) # add pos to points
circle.(points, 2, :fill) # draws a circle for each point using broadcasting
end
It adds the position to the vector of points and used the broadcast function to draw a small circle at each position.
Well you can still work out the solution to the original problem on your own 😉 I don't want to spoil anything and was much more interested in animating the problem at this stage.
Thanks for reading and share it with your friends if you like and think about subscribing to the newsletter or directly to Patreon to get everthing 2 days earlier 😉
using Colors
using Javis
function ground(args...)
background("black")
sethue("white")
end
function chase(dogs, from, to, speed)
(args...) -> _chase(dogs, from, to, speed)
end
function _chase(dogs, from, to, speed)
animal = dogs[from]
chases = dogs[to]
diff = pos(chases) - pos(animal)
diff /= sqrt(diff.x^2 + diff.y^2)
new_pos = pos(animal) + speed * diff
animal.change_keywords[:center] = new_pos
end
function path!(points, cpos, color)
sethue(color)
push!(points, cpos) # add pos to points
circle.(points, 2, :fill) # draws a circle for each point using broadcasting
end
function main()
video = Video(1000, 1000)
nframes = 300
speed = 3
square_len = 800
Background(1:nframes, ground)
# Object(1:nframes, JRect(Point(-square_len/2, -square_len/2), square_len, square_len; color="white"))
dog_colors = Colors.JULIA_LOGO_COLORS
dogs_paths = [Point[] for _ in 1:4]
dog_positions = [
Point(-square_len / 2, -square_len / 2),
Point(square_len / 2, -square_len / 2),
Point(square_len / 2, square_len / 2),
Point(-square_len / 2, square_len / 2),
]
dogs = [
Object(
1:nframes,
JCircle(dog_positions[i], 15; action = :fill, color = dog_colors[i]),
) for i in 1:4
]
for i in 1:4
act!(dogs[i], Action(2:nframes, chase(dogs, i, mod1(i+1, 4), speed)))
Object(1:nframes, (args...) -> path!(dogs_paths[i], pos(dogs[i]), dog_colors[i]))
end
render(video; pathname = "chase.gif")
end
Thanks to my 12 patrons!
Special special thanks to my >4$ patrons. The ones I thought couldn't be found 😄
Anonymous
Kangpyo
Gurvesh Sanghera
Szymon Bęczkowski
Colin Phillips
Jérémie Knuesel
For a donation of a single dollar per month you get early access to these posts. Your support will increase the time I can spend on working on this blog.
There is also a special tier if you want to get some help for your own project. You can checkout my mentoring post if you're interested in that and feel free to write me an E-mail if you have questions: o.kroeger <at> opensourc.es
I'll keep you updated on Twitter OpenSourcES.