Javis.jl examples series: The chase problem


Creation date: 2021-08-09

Tags: javis, animation, julia

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 😉

Powered by Buttondown.

The Problem

Okay 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.

Animation

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.

a square

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.

The dogs in their starting positions

Now we're going into the complicated part of this. How do we move the dogs?

Action! 🎬

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
Moving the docs to the right...

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
Note
The 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
Note
The O is the letter O which stands for the origin and is the same as Point(0,0).
Just all at the origin

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 /home/ole/Julia/Javis/src/layers.jl:183
  get_position(::Point) at /home/ole/Julia/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:

Our chasing dogs

For this animation:

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.

Note
Here we can use the frames starting from 1 as the dogs are already evaluated. This is the case as we define the object after the 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.

final chase animation

Solution

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 😉

Full code

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 😄

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.

anim_translate

Animate the translation of the attached object (see act!).

Example

Background(1:100, ground)
obj = Object((args...) -> circle(O, 50, :fill), Point(100, 0))
act!(obj, Action(1:50, anim_translate(10, 10)))

Options

  • anim_translate(x::Real, y::Real) define by how much the object should be translated. The end point will be current_pos + Point(x,y)
  • anim_translate(tp::Point) define direction and length of the translation vector by using Point
  • anim_translate(fp::Union{Object,Point}, tp::Union{Object,Point}) define the from and to point of a translation. It will be translated by tp - fp.
    • Object can be used to move to the position of another object
mod1(x, y)

Modulus after flooring division, returning a value r such that mod(r, y) == mod(x, y) in the range $(0, y]$ for positive y and in the range $[y,0)$ for negative y.

With integer arguments and positive y, this is equal to mod(x, 1:y), and hence natural for 1-based indexing. By comparison, mod(x, y) == mod(x, 0:y-1) is natural for computations with offsets or strides.

See also mod, fld1, fldmod1.

Examples

julia> mod1(4, 2)
2

julia> mod1.(-5:5, 3)'
1×11 adjoint(::Vector{Int64}) with eltype Int64:
 1  2  3  1  2  3  1  2  3  1  2

julia> mod1.([-0.1, 0, 0.1, 1, 2, 2.9, 3, 3.1]', 3)
1×8 Matrix{Float64}:
 2.9  3.0  0.1  1.0  2.0  2.9  3.0  0.1
change(s::Symbol, [val(s)])

Changes the keyword s of the parent Object from vals[1] to vals[2] in an animated way if vals is given as a Pair otherwise it sets the keyword s to val.

Arguments

  • s::Symbol Change the keyword with the name s
  • vals::Pair If vals is given i.e 0 => 25 it will be animated from 0 to 25.
    • The default is to use 0 => 1 or use the value given by the animation
    defined in the Action

Example

Background(1:100, ground)
obj = Object((args...; radius = 25, color="red") -> object(O, radius, color), Point(100, 0))
act!(obj, Action(1:50, change(:radius, 25 => 0)))
act!(Action(51:100, change(:radius, 0 => 25)))
act!(Action(51:100, change(:color, "blue")))



Want to be updated? Consider subscribing and receiving a mail whenever a new post comes out.

Powered by Buttondown.


Subscribe to RSS