I love small projects for helping me learn, especially programming. I’m still learning Julia, and have found myself wanting more “little silly things” I can digest and learn from. A lot of the projects I see in Julia are big mathematical models, and I’m just not ready to dive that deep yet.

This series of tweets caught my eye, partly because of the cool animation, but also the bite-sized amount of information it was conveying - that interpolation in Julia can be specified so easily, thanks in large part to the multiple dispatch design of the language.

“Surely I could get those 7 lines of code to run” I thought.

Entering the code into VScode was straightforward enough, no problems there. I could define the interpolation function

interpolate(a, b) = t -> ((1.0-t)*a + t*b)

however extending the * and + methods did require me to import Base:* and import Base:+ which I think I knew but had forgotten.

+(f::Function, g::Function) = x -> f(x) + g(x)
*(t::Number, g::Function) = x -> t * g(x)

Defining the secondary and tertiary interpolations, also straightforward

bz1(p1, p2) = interpolate(p1, p2)
bz2(p1, p2, p3) = interpolate(bz1(p1, p2), bz1(p2, p3))
bz3(p1, p2, p3, p4) = interpolate(bz2(p1, p2, p3), bz2(p2, p3, p4))

Now the tricky part - evaluating some of these. I knew that a and b represent points, but how to do that here? They’re not single numbers, but coordinates. I tried a Tuple as (1, 2) but that doesn’t seem to work. I do need to remember that interpolate is itself a function of t, so that needs to be specified as well. If I try to interpolate halfway between two “points” with Tuples

interpolate((0,1), (1,2))(0.5)
ERROR: MethodError: no method matching *(::Float64, ::Tuple{Int64,Int64})
Closest candidates are:
  *(::Any, ::Any, ::Any, ::Any...) at operators.jl:538
  *(::Float64, ::Float64) at float.jl:405
  *(::AbstractFloat, ::Bool) at bool.jl:112

Okay, how about Arrays?

interpolate([0,1], [1,2])(0.5)
2-element Array{Float64,1}:
 0.5
 1.5

Huzzah!

After that, it’s a matter of generating the points specified by

bz3(p1, p2, p3, p4)(t)(t)(t)

for various values of t. I did that with a map and joined the results back into a single Array

dots = map(i -> bz3(p1, p2, p3, p4)(i)(i)(i),collect(0:0.1:1))
dots = hcat(dots...)
dots
2×11 Array{Float64,2}:
 0.5  0.47535  0.5368  0.66245  …  1.36905  1.4872  1.53815  1.5
 1.0  1.3124   1.5312  1.6588      1.3052   1.0128  0.6436   0.2

That was, I’d say, a success.

Drunk with confidence, I wanted to try to reproduce the animation from the tweet, so I dug into the documentation. It didn’t seem too bad, and I think I’ve managed to reproduce it pretty well

anim = @animate for t in collect(vcat(0:0.01:1,1:-0.01:0))
    a = bz3(p1, p2, p3, p4)(t)(t)(t);
    b1 = bz2(p1, p2, p3)(t)(t);
    b2 = bz2(p2, p3, p4)(t)(t);
    c1 = bz1(p1, p2)(t);
    c2 = bz1(p2, p3)(t);
    c3 = bz1(p3, p4)(t);
    stars = hcat(p1, p2, p3, p4);
    diamond1 = hcat(c1, c2);
    diamond2 = hcat(c2, c3);
    square = hcat(b1, b2);
    plot(xlim = (-0.1,2.5), ylim = (-0.1,2.5), legend = false)
    scatter!(dots[1,:], dots[2,:], markersize = 2)
    plot!(diamond1[1,:], diamond1[2,:], markersize = 10, markershape = :diamond, color = :green)
    plot!(diamond2[1,:], diamond2[2,:], markersize = 10, markershape = :diamond, color = :green)
    plot!(square[1,:], square[2,:], markersize = 10, markershape = :square, color = :blue)
    plot!(stars[1,:], stars[2,:], markersize = 10, markershape = :star, color = :purple)
    scatter!(Tuple(a), markersize = 10, markershape = :circle, markercolor = :red)
end

gif(anim, fps = 24)

Moving the points around, I get a new version all of my own

I’m very happy with how these turned out, and I’ve learned a lot! A gist of the code to make these is hosted here: https://gist.github.com/jonocarroll/27f9b57332424ea50ec2970e74d8e3b3

If there are better ways to do any of the steps (there surely are) please feel free to let me know!

Was this fun? You Bezier ass!