Interactive graphics and Threads

Continuous display

With the help of an external appication to manage windows, it's possible to use Luxor to create continuously changing graphics in a window.

The @play macro

This example uses the MiniFB package, which you can add using ] add MiniFB.

The file play.jl defines a simple macro, @play, which continuously evaluates and draws the graphics in a window. For example, this code:

using Luxorinclude(dirname(pathof(Luxor)) * "/play.jl")let θ = 0    @play 400 400 begin    #        background("black")        sethue("white")        rotate(θ)        hypotrochoid(200, 110, 37, :stroke)        θ += π/120        sleep(0.01)    #    endend

draws a continuously rotating hypotrochoid.

live hypo

Clock

This code also imports the @play macro.

The call to sleep reduces the CPU time, and allows other processes to run, but the millisecond animation will be less smooth as a result.

clock

using Luxor, Colors, Dates, ColorSchemesinclude(dirname(pathof(Luxor)) * "/play.jl")function clock(cscheme=ColorSchemes.leonardo)    @play 400 600 begin        # background        sethue(get(cscheme, .0))        paint()        # 24hour sector        fontsize(30)        sethue(get(cscheme, .2))        h = Dates.hour(now())        sector(O, 180, 200, π/2, π/2 + rescale(h, 0, 24, 0, 2pi), :fill)        @layer begin            fontsize(12)            sethue("white")            @. text(["0", "6", "12", "18"], polar(190, [i * π/2 for i in 1:4]),                halign=:center,                valign=:middle)        end        # minute sector        sethue(get(cscheme, .4))        m = Dates.minute(now())        sector(O, 160, 180, 3π/2, 3π/2 + rescale(m, 0, 60, 0, 2pi), :fill)        # second sector        sethue(get(cscheme, .6))        s = Dates.second(now())        sector(O, 140, 160, 3π/2, 3π/2 + rescale(s, 0, 60, 0, 2pi), :fill)        # millisecond indicator        @layer begin            setopacity(0.5)            sethue(get(cscheme, .8))            ms = Dates.value(Dates.Millisecond(Dates.now()))            circle(polar(120, 3π/2 + rescale(ms, 0, 1000, 0, 2pi)), 20, :fill)        end        # central text        fontface("JuliaMono-Black")        sethue(get(cscheme, 1.0))        text(Dates.format(Dates.now(), "HH:MM:SS"), halign=:center)        sleep(0.05)    endendclock(ColorSchemes.klimt)

Live coding with MiniFB

Here are some examples of how to use Luxor with MiniFB as the display window, without using the simple @play macro.

Interactivity

This example lets you type graphic commands at the REPL and see the results instantly displayed in a window.

First, run this code to connect a Luxor drawing to a MiniFB buffer:

using Luxorusing Colorsusing MiniFBfunction window_update_task(window, buffer, showFPS=false)    state = mfb_update(window, buffer)    updateCount = 0    startTime = floor(Int, time())    fps = "0"    while state == MiniFB.STATE_OK        if showFPS            elapsedTime = floor(Int, time()) - startTime            if elapsedTime > 1                fps = string(round(Int, updateCount / elapsedTime))                startTime = floor(Int, time())                updateCount = 0            end            @layer begin                setcolor("black")                circle(boxtopleft() + (15, 15), 15, :fill)                setcolor("white")                fontsize(20)                text(fps, boxtopleft() + (15, 15), halign=:center, valign=:middle)            end        end        state = mfb_update(window, buffer)        sleep(1.0 / 120.0)        updateCount += 1    end    println("\nWindow closed\n")endconst WIDTH = 800const HEIGHT = 600buffer = zeros(ARGB32, WIDTH, HEIGHT)d = Drawing(buffer)window = mfb_open_ex("MiniFB", WIDTH, HEIGHT, MiniFB.WF_RESIZABLE)@async window_update_task(window, buffer, true)

Now, the window will display the results of any expressions you type at the REPL:

live graphics 1

Live animations

If you want to do live animations in the window in a "while" loop, you need to call sleep() for a while to allow the window_update_task() to get some execution time.

In this example you can enter "q" and "return" in the REPL to stop the animation's while loop. Using "ctrl-c" to stop the animation could also stop the window update task by chance.

live graphics 2

Code for this example
mutable struct Ball    position::Point    velocity::Pointendfunction sticks(w, h)    channel = Channel(10)    #enter "q" and "return" to stop the while loop    @async while true        kb = readline(stdin)        if contains(kb, "q")            put!(channel, 1)            break        end    end    colors = [ rand(1:255), rand(1:255), rand(1:255) ]    newcolors  = [ rand(1:255), rand(1:255), rand(1:255) ]    c = ARGB(colors[1]/255, colors[2]/255, colors[3]/255, 1.0)    balls = [ Ball( rand(BoundingBox(Point(-w/2, -h/2), Point(w/2, h/2))), rand(BoundingBox(Point(-10, -10), Point(10, 10))) ) for _ in 1:2 ]    while true        background(0, 0, 0, 0.05)        if colors == newcolors            newcolors = [ rand(1:255), rand(1:255), rand(1:255) ]        end        for (index, (col, newcol)) in enumerate(zip(colors, newcolors))            if col != newcol                col > newcol ? col -= 1 : col += 1                colors[index] = col            end        end        c = ARGB(colors[1]/255, colors[2]/255, colors[3]/255, 1.0)        for ball in balls            if !(-w/2 < ball.position.x < w/2)                ball.velocity = Point(-ball.velocity.x, ball.velocity.y)            end            if !(-h/2 < ball.position.y < h/2)                ball.velocity = Point(ball.velocity.x, -ball.velocity.y)            end            ball.position = ball.position + ball.velocity        end        setcolor(c)        line(balls[1].position, balls[2].position, :stroke)        sleep(1.0/120.0)        if isready(channel)            break        end    endendorigin()sticks(WIDTH, HEIGHT)

Interactive graphics with multiple drawings

This next example shows how to work with multiple drawings. We'll create three windows, then combine (AND) the contents of the first two and display them in the third.

multiple drawings

First, we'll setup our display buffers and MiniFB windows, one for each Luxor drawing:

using MiniFB, Luxor, Colors, FixedPointNumbersWIDTH = 500HEIGHT = 500function window_update_task(window,buffer)    state = mfb_update(window,buffer)    while state == MiniFB.STATE_OK        state = mfb_update(window, buffer)        sleep(1.0/120.0)    end    println("\nWindow closed\n")endwindow1 = mfb_open_ex("1", WIDTH, HEIGHT, MiniFB.WF_RESIZABLE)buffer1 = zeros(ARGB32, WIDTH, HEIGHT)@async window_update_task(window1,buffer1)window2 = mfb_open_ex("2", WIDTH, HEIGHT, MiniFB.WF_RESIZABLE)buffer2 = zeros(ARGB32, WIDTH, HEIGHT)@async window_update_task(window2,buffer2)window3 = mfb_open_ex("3=1+2", WIDTH, HEIGHT, MiniFB.WF_RESIZABLE)buffer3 = zeros(ARGB32, WIDTH, HEIGHT)@async window_update_task(window3,buffer3)

Buffers 1, 2, and 3 are the buffers for the three MiniFB windows. They'll appear on your display.

Next we'll create three Luxor drawings that connect to these buffers.

d1 = Drawing(buffer1)Luxor._set_next_drawing_index()d2 = Drawing(buffer2)Luxor._set_next_drawing_index()d3 = Drawing(buffer3, "julia.png")

We now have three drawings which are continuously updated and visible in three separate windows. Let's start by drawing on drawing 1.

Luxor._set_drawing_index(1)origin()setopacity(0.4)foregroundcolors = Colors.diverging_palette(    rand(0:360),    rand(0:360),    200, s=0.99, b=0.8)gsave()for i in 1:500    sethue(foregroundcolors[rand(1:end)])    circle(Point(rand(-300:300), rand(-300:300)), 15, :fill)endgrestore()

Now let's switch to drawing 2 and draw the Julia logo:

Luxor.set_drawing_index(2)origin()setopacity(1.0)gsave()julialogo(centered=true, bodycolor=colorant"white")grestore()

Finally, we'll switch to drawing 3, and set its contents by ANDing the buffers of drawings 1 and 2:

Luxor.set_drawing_index(3)background("black")buffer3 .= reinterpret(ARGB{N0f8},    (reinterpret.(UInt32,buffer1) .&     reinterpret.(UInt32,buffer2)))

To finish, we'll set the opacity of each pixel to 1.0:

buffer3.=ARGB32.(RGB24.(buffer3))finish()preview()

Threads

Luxor is thread safe. To run the examples below, start Julia with more than 1 thread. To see how many threads you have available, ask Julia:

julia> Threads.nthreads()4

As a first example, we'll produce multiple PNG files in parallel:

using Luxor # hidetempdir = mktempdir(; cleanup=false)cd(tempdir)function make_drawings(i::Int)    println("Working on thread ", Threads.threadid())    w = 300    h = 300    filename = "sample" * string(i) * ".png"    Drawing(w, h, filename)    origin()    background("black")    setopacity(0.5)    fontsize(250)    sethue("grey40")    text(string(i), halign=:center, valign=:middle)    for k in 1:50        pg1 = polycross(rand(BoundingBox()), rand(60:120), 4, vertices=true)        pg2 = polycross(rand(BoundingBox()), rand(60:120), 4, vertices=true)        pg3 = polyintersect(pg1, pg2)        for p in pg3            randomhue()            poly(p, :fill)        end    end    finish()    returnendThreads.@threads for i = 1:(2*Threads.nthreads())    make_drawings(i)end
Working on thread 2
Working on thread 3
Working on thread 4
Working on thread 2
Working on thread 1
Working on thread 3
Working on thread 1
Working on thread 4

threads drawing

Advanced threads with live view

To demonstrate what is possible, we again show live graphics in MiniFB windows, but this time in different threads.

There are two ways to use threads with Luxor.

One way is to use a single thread for each window we want to show, i.e each window we spawn and the Luxor graphics inside is a different thread.

The other way to use threads is e.g. a single window, with several threads all drawing into the same buffer, which is shown in the single window. For this you need to utilize locks as shown in the second example below.

A thread for each window

First, here's an example where each window and its graphics is a single thread. No locks or channels are needed.

Let's start with the header and a helper function for our animation:

using ThreadPools# the low level Threads.@spawn macro can not be used, because threads are scheduled# randomly into available threadids. If a thread is spawned into an already running# threadid, the former thread # is closed by the scheduler. So we use the better# ThreadPools.spawnbg to spawn the threads.using MiniFB, Luxor, Colors, FixedPointNumbersmutable struct Ball    position::Point    velocity::Pointendfunction step_ball(ball, w, h, r)    if ball.position.x - r < -w / 2 || ball.position.x + r > w / 2        ball.velocity = Point(-ball.velocity.x, ball.velocity.y)    end    if ball.position.y - r < -h / 2 || ball.position.y + r > h / 2        ball.velocity = Point(ball.velocity.x, -ball.velocity.y)    end    ball.position = ball.position + ball.velocity    return ballend

Open and run the first window in its own thread:

function window_ball()    w = 500    h = 500    r = 50    buffer = zeros(RGB24, w, h)    ball = Ball(Point(0, 0), rand(BoundingBox(Point(-10, -10), Point(10, 10))))    Drawing(buffer)    origin()    window = mfb_open_ex("Ball", w, h, MiniFB.WF_RESIZABLE)    state = MiniFB.STATE_OK    while state == MiniFB.STATE_OK        ball = step_ball(ball, w, h, r)        background(0.0, 0.0, 0.0, 1.0)        setcolor("red")        circle(ball.position.x, ball.position.y, r, :fill)        state = mfb_update(window, buffer)        sleep(1.0/120.0)    end    println("\nWindow closed\n")endspawnbg(window_ball)

Open and run a second window in its own thread:

function window_stick()    w = 500    h = 500    r = 0    buffer = zeros(RGB24, w, h)    balls = [Ball(rand(BoundingBox(Point(-w/2, -h/2),        Point(w/2, h/2))),        rand(BoundingBox(Point(-10, -10), Point(10, 10)))) for _ in 1:2]    Drawing(buffer)    origin()    background(0.0,0.0,0.0,1.0)    window = mfb_open_ex("Sticks", w, h, MiniFB.WF_RESIZABLE)    state = MiniFB.STATE_OK    while state == MiniFB.STATE_OK        background(0.0, 0.0, 0.0, 0.05)        setcolor("green")        for ball in balls            ball=step_ball(ball, w, h, r)        end        line(balls[1].position, balls[2].position, :stroke)        state=mfb_update(window, buffer)        sleep(1.0/120.0)    end    println("\nWindow closed\n")endspawnbg(window_stick)

If you have threads left you can start another thread with a third window:

spawnbg(window_stick)

If you run out of threadids, the command spawnbg(window_stick) will block the REPL until a thread is freed, e.g. by closing one of the windows.

A single window with graphics of several threads

The next example shows a single window/buffer where several threads are drawing into it. This needs some extra caution by utilizing locks, because every thread uses the same drawing buffer. Therefore all drawing commands needs to be synchronized with lock/unlock:

using ThreadPoolsusing MiniFB, Luxor, Colorsstruct Window    c::ReentrantLock    w::Int    h::Int    d::Drawing    buffer::Matrix{ARGB32}    function Window(w,h)        c = ReentrantLock()        b = zeros(ARGB32, w, h)        d = Drawing(b)        origin()        new(c,w,h,d,b)    endendfunction window_update_task(win::Window, showFPS=true)    w = win.w    h = win.h    updateCount = 0    startTime = floor(Int, time())    fps = "0"    sb = zeros(ARGB32, 105, 55)    window = mfb_open_ex("MiniFB", w, h, MiniFB.WF_RESIZABLE)    state=MiniFB.STATE_OK    set_drawing = true    while state == MiniFB.STATE_OK        lock(win.c)        if set_drawing            currentdrawing(win.d)            set_drawing = false        end        if showFPS            elapsedTime=floor(Int,time())-startTime            if elapsedTime > 1                fps=string(round(Int,updateCount/elapsedTime))                startTime=floor(Int,time())                updateCount=0            end            sb.=win.buffer[1:105, 1:55]            @layer begin                (dx,dy) = Point(0.0, 0.0) - getworldposition(Point(0.0, 0.0);centered=false)                setcolor(1.0, 0, 0, 0.5)                fontsize(50)                text(fps, Point(5+dx, 5+dy), halign=:left, valign = :top)            end        end        state=mfb_update(window,win.buffer)        if showFPS            win.buffer[1:105, 1:55].=sb        end        background(0, 0, 0, 0.05)        unlock(win.c)        sleep(1.0/120.0)        updateCount+=1    end    println("\nWindow closed\n")end

That's all we need to define a window with a fps display. Let's define it and run it as a new thread:

win = Window(800, 600)    # the window definition# we need a function without parameter, that's what we create here:let window=win   global t_window_update_task   function t_window_update_task()       window_update_task(window)   endend# run the task as a new thread:spawnbg(t_window_update_task)

The window with a FPS display shows up. It runs in its own thread so we can still use the REPL to create new threads which draw into this window. The most important special code here is:

currentdrawing(win.d)

We start a new thread for every drawing and because the Luxor drawings of different threads are separated from each other to ensure thread safety, we overwrite the drawing in each thread with the drawing started with

win=Window(800, 600)

and stored in the win-object. See the constructor in struct Window.

Let's show it:

mutable struct Ball    position::Point    velocity::Pointendfunction stick(win)    w = win.w    h = win.h    colors = [rand(1:255), rand(1:255), rand(1:255)]    newcolors = [rand(1:255), rand(1:255), rand(1:255)]    c = ARGB(colors[1] / 255, colors[2] / 255, colors[3] / 255, 1.0)    balls = [Ball(        rand(BoundingBox(Point(-w / 2, -h / 2), Point(w / 2, h / 2))),        rand(BoundingBox(Point(-10, -10), Point(10, 10))))            for _ in 1:2]    set_drawing = true    while true        if colors == newcolors            newcolors = [rand(1:255), rand(1:255), rand(1:255)]        end        for (index, (col, newcol)) in enumerate(zip(colors, newcolors))            if col != newcol                col > newcol ? col -= 1 : col += 1                colors[index] = col            end        end        c = ARGB(colors[1] / 255, colors[2] / 255, colors[3] / 255, 1.0)        for ball in balls            if !(-w / 2 < ball.position.x < w / 2)                ball.velocity = Point(-ball.velocity.x, ball.velocity.y)            end            if !(-h / 2 < ball.position.y < h / 2)                ball.velocity = Point(ball.velocity.x, -ball.velocity.y)            end            ball.position = ball.position + ball.velocity        end        lock(win.c)        if set_drawing            currentdrawing(win.d)            set_drawing = false        end        setcolor(c)        line(balls[1].position, balls[2].position, :stroke)        unlock(win.c)        sleep(1.0 / 60.0)    endend# again a function for calling without parameter:let window=win   global t_stick   function t_stick()       stick(window)   endend# run the thread and let the sticks fly:spawnbg(t_stick)

Because our threads are synchronized via lock/unlock we can spawn low-level threads as much as we want and our machine allows:

function spawn_many()    tid=1:Threads.nthreads()    for i in tid        t=Task(t_stick)        ccall(:jl_set_task_tid, Cvoid, (Any, Cint), t, i-1)        schedule(t)    endend

Now you can repeat it as much as you dare:

spawn_many()