SVG to path sequence, that can be used by Luxor and Javis

Hey @BuddhiLW ! Co-creator of Javis.jl here! Let me CC @Wikunia

I know that when I did something similar like this here: https://youtu.be/ckvsc6ukdOc?t=15 What I did were the following steps:

  1. Load an image with Images.jl
  2. Apply a black and white color scheme to the image
  3. Apply a shelling function to create shell of core shapes
  4. Pass or threshold values in your resulting image to remove noise that would not be part of the outline you want
  5. Convert each point within your image that you want into the appropriate (x, y) coordinate tuples.
  6. Convert each of these points from the outline of your image into a Luxor Point object and store the points in an array.

If you want the code I used for this outlining process, here is the code:

using Javis
using FFTW
using FFTViews
using FileIO
using Images
using TravelingSalesmanHeuristics

function ground(args...)
    background("black")
    sethue("white")
end

function circ(; r = 10, vec = O, action = :stroke, color = "white")
    sethue("black")
    circle(O, r, action)
    # my_arrow(O, vec)
    return vec
end

function my_arrow(start_pos, end_pos)
    arrow(
        start_pos,
        end_pos;
        linewidth = distance(start_pos, end_pos) / 100,
        arrowheadlength = 7,
    )
    return end_pos
end

function draw_line(
    p1 = O,
    p2 = O;
    color = "white",
    action = :stroke,
    edge = "solid",
    linewidth = 3,
)
    sethue(color)
    setdash(edge)
    Luxor.setline(linewidth)
    line(p1, p2, action)
end

function draw_path!(path, pos, color)
    sethue(color)

    push!(path, pos)
    return draw_line.(path[2:end], path[1:(end - 1)]; color = color)
end

function get_points(img)
    findall(x -> x == 1, img) .|> x -> Point(x.I)
end

function texty()
	fontsize(36)
	text("Loading Asset: Jacob Zelko", Point(0, 290); halign = :center)
end

c2p(c::Complex) = Point(real(c), imag(c))

remap_idx(i::Int) = (-1)^i * floor(Int, i / 2)
remap_inv(n::Int) = 2n * sign(n) - 1 * (n > 0)

function animate_fourier(options)
    npoints = options.npoints
    nplay_frames = options.nplay_frames
    nruns = options.nruns
    nframes = nplay_frames + options.nend_frames

    # obtain points from julialogo
    points = get_points(load(File(format"PNG", "jacob_outline.png")))
    npoints = length(points)
    println("#points: $npoints")
    # solve tsp to reduce length of extra edges
    distmat = [distance(points[i], points[j]) for i = 1:npoints, j = 1:npoints]

    path, cost = solve_tsp(distmat; quality_factor = options.tsp_quality_factor)
    println("TSP cost: $cost")
    points = points[path] # tsp saves the last point again

    # optain the fft result and scale
    y = [p.x  - 372.6666666667 for p in points] 
    x = [p.y - 323.3333333333 / 1.25 for p in points] 

    fs = FFTView(fft(complex.(x, y)))
    # normalize the points as fs isn't normalized
    fs ./= npoints
    npoints = length(fs)

    video = Video(options.width, options.height)
    Background(1:nframes, ground)

    circles = Object[]

    for i = 1:npoints
        ridx = remap_idx(i)

        push!(circles, Object((args...) -> circ(; r = abs(fs[ridx]), vec = c2p(fs[ridx]))))

	if i > 1
	    # translate to the tip of the vector of the previous circle
	    act!(circles[i], Action(1:1, anim_translate(circles[i - 1])))
	end
	ridx = remap_idx(i)
	act!(circles[i], Action(1:nplay_frames, anim_rotate(0.0, ridx * 2π * nruns)))
    end

    trace_points = Point[]
    Object(1:nframes, (args...) -> draw_path!(trace_points, pos(circles[end]), "white"))

    loading = Object(1:nframes, (args...) -> texty())
    act!(loading, Action(1:400, sineio(), appear(:draw_text)))

    return render(video; pathname = joinpath(@__DIR__, options.filename))
    # return render(video; liveview = true)
end

function main()
    # hd_options = (
    # npoints = 3001, # rough number of points for the shape => number of circles
    # nplay_frames = 1200, # number of frames for the animation of fourier
    # nruns = 2, # how often it's drawn
    # nend_frames = 200,  # number of frames in the end
    # width = 1920,
    # height = 1080,
    # shape_scale = 2.5, # scale factor for the logo
    # tsp_quality_factor = 50,
    # filename = "julia_hd.mp4",
    # )

    gif_options = (
        npoints = 1001, # rough number of points for the shape => number of circles
        nplay_frames = 600, # number of frames for the animation of fourier
        nruns = 1, # how often it's drawn
        nend_frames = 200,  # number of frames in the end
        width = 520,
        height = 653,
        tsp_quality_factor = 10,
        filename = "jacob_outline.gif",
    )

    # gif_options = (
    # npoints = 651, # rough number of points for the shape => number of circles
    # nplay_frames = 600, # number of frames for the animation of fourier
    # nruns = 2, # how often it's drawn
    # nend_frames = 0,  # number of frames in the end
    # width = 350,
    # height = 219,
    # shape_scale = 0.8, # scale factor for the logo
    # tsp_quality_factor = 80,
    # filename = "julia_logo_dft.gif",
    # )
    return animate_fourier(gif_options)
end

main()

And here is a reference image of my head and shoulders:

Which results in something like this:

jacob_outline

Important to note is that shelling functions and the above steps will only go so far. You may need to instead open up your selected image that you want to outline in a separate image editor like Gimp to manually do something image manipulation. I think I may have had to do that… Although I cannot recall!

@Wikunia , anything else you could add here?

Does that help @BuddhiLW ?

P.S. Thanks for using Javis! As always, please let us know if you have any feedback or how we can help! :smile: We love to see posts about Javis and what you create!

2 Likes