Connecting coordinate transforms in Luxor

I’m working on my first project with Luxor.jl. As I understand it, Luxor maintains both a “global” coordinate system and a “local” system, and the typical workflow is to transform the local coordinates in some useful way and then draw stuff axis-aligned near the origin. (I think, disclaimers apply, Luxor is really cool but I’ve been consistently confused trying to use it.)

My question is how to draw an element which connects different coordinate systems. As a toy example, the below snippet draws 10 circles, each at the origin of a different local coordinate system:

@drawsvg begin
	for _ in 1:10
		circle(O, 3, :fill)
		# new coordinates
		translate(70 * rand() - 35, 70 * rand() - 35)
		rotate(2 * pi * rand())
	end
end 200 200

image
But what if I want to draw lines connecting the ith circle draw to the (i+1)th circle? Then each line needs to connect a known point (specifically the origin) in different coordinate systems. Point objects are just wrappers around (x,y), not references to specific points on the drawing. So if you have something like p = Point(1,2) and then apply some coordinate transformations, you lose the information of the global point p used to refer to.

The least-bad solution I’ve found is to manually record the world position of each point and then draw the connecting lines in a @layer with reset coordinates. That’s not so bad in this case where there’s just one point to remember, but in my actual use case each drawing done in a specific coordinate system can have an a priori unknown number of points that need to be connected to future coordinate systems, so this approach ends up feeling pretty fiddly. Is there a cleaner way?

@drawsvg begin
	prev = nothing
	for _ in 1:10
		circle(O, 3, :fill)
		cur_global = getworldposition()
		if prev != nothing
			@layer begin
				origin()
				line(prev, cur_global, :stroke)
			end
		end
		prev = cur_global
		translate(70 * rand() - 35, 70 * rand() - 35)
		rotate(2 * pi * rand())
	end
end 200 200

Hi! It may be that your application is too advanced for the simple drawing paradigm used by Luxor (“Cairo for tourists!”).

Although, the paradigm is quite common, I think. For example, take these characters you’re currently reading. The shapes of each one are defined with coordinates relative to (0, 0) - usually at the bottom left corner. When each character is drawn, the origin is temporarily shifted to the correct location, scale and rotation are applied, then the coordinates are drawn. Finally, the current location/scale etc is discarded, and the process begins again for the next character. (This is my understanding, at least.) Once something’s been drawn on the canvas, it’s also discarded, and you have no further access to it.

So, I think the only way you can maintain multiple points in multiple coordinate systems is to track them yourself, ie keep all the geometry accessible in Julia before you start making marks on the canvas. Perhaps make a structure that stores the current matrix for each point?

mutable struct PointMatrix
    point::Point
    matrix::Array
end

Or maybe the more sophisticated feature set of Makie.jl can handle this kind of multiple coordinate systems easier.

(I’m sorry Luxor is confusing - is there too little documentation, or too much?)

It may be that your application is too advanced for the simple drawing paradigm used by Luxor

I’ve seen the amazing examples you have throughout the docs and can therefore say with confidence that my use case is not beyond the capabilities of Luxor :slight_smile: FWIW, the use case is a personal project / toy application that will convert arbitrary unicode strings into text that looks like Nomai writing from the (excellent) game Outer Wilds, e.g.:

structure that stores the current matrix for each point

Ah, that makes sense. How do I then translate an instance of PointMatrix to a Point in the current coordinate system? Do I need to multiply inv(cairotojuliamatrix(pm.matrix)) * [p.x, p.y, 1.0] to get the “global” coordinates, then multiply by cairotojuliamatrix(getmatrix())?

(I’m sorry Luxor is confusing - is there too little documentation, or too much?)

I don’t mean to be ungrateful for useful software freely provided! Definitely happy to discuss any of this more by DM/email/this thread/etc if it’s helpful to you. My new user experience was:

  • Easy installation, easy to get first graphics shown, all that great. It was not a priori obvious to me how much drastically easier it’d be to learn Luxor in Pluto with @drawsvg rather than Drawing(...) ... finish; preview() in the REPL. Faster feedback loops!
  • Some curse of knowledge in the docs, where the neophyte reader has to infer basic things the docs author already knows. Examples include
    • That a core workflow is the transform - draw from origin - untransform loop
    • Relatedly trying to keep track of absolute positions is probably doomed.
    • All drawing is stateful and happens at global state. (I think…though now I wonder how different Pluto cells each with @drawsvg macros work.) The combination of global state + stateful drawing “feels” very different than most Julia code which seems to emphasize a highly functional side-effect-free style. Big brain shift.
    • That if you want to draw a series of connecting lines you need a polygon, not line
    • Some of the examples are, IMHO, “too clever.” E.g.:
      • Even the first quick and short tutorial is doing clever things with colors and overlapping circles and rescaling, which takes precious brain cycles to parses for a new reader still literally trying to figure out which way is up.
    c = colors[mod1(n, end)]
    for i in 5:-0.1:1
        setcolor(rescale(i, 5, 1, 0.5, 3) .* c)
        circle(pos + (i/2, i/2), rescale(i, 5, 1, radius, radius/6), action = :fill)
    end
  • There are a lot of docs. Mostly this is good!
    • Most of my confusions were eventually resolved by searching through the docs.
    • Unsurprisingly, lots of the docs had to do with stuff that wasn’t relevant to me. That’s always going to happen since not every project touches every bit of functionality, but does slightly give a sense of “woah there’s a lot here and I am confused” for a new user. E.g. I don’t need image/pixel functionality, animations, typesetting.
    • I think I personally made a mistake by not starting with the explanations section of the docs. (Thinking here of 4 kinds of documentation.) I started at the basic tutorial and that ended up maybe confusing me more.
    • Now that I’ve built a mental map of the territory, the docs feel nice and well organized, but n=1 the first few nights of playing with Luxor I didn’t know what I was looking for.

I will say that a lot of the docstrings on individual functions feel pretty sparse. The written explanations/tutorials are good, the reference is comprehensive, but the docstrings don’t connect to concepts. E.g. if I’ve just learned about moving the coordinate system, do I want move or translate? That took me a while. translate says “Translate the workspace to x and y or to pt .” which feels similar to move’s “move to a point”.

A partial list of things that confused me in my first days with Luxor:

  • Why are there two coordinate systems, upper-left origin and center origin? Which do I want?
  • Oh wait there are three, current coordinates are different. Can I just not use stateful transformations and work with a single global system?
  • What’s move vs translate?
  • Okay the matrix represents the current transformation, but why is it 2x3? Why do I keep hearing about 3x3 matrices? Why do the examples keep manually setting the matrix values? That doesn’t feel like “for tourists!” How do I apply a matrix to a point?
  • The docs mention that paths can have subpaths, but how do I create a subpath?
  • What if I want to make joined lines in a T shape, ie there’s one vertex with 3 lines coming out of it. Is that possible or do I have to make a L poly and then just add a line?
  • How can I create a polygon once and then draw copies of it?

Anyway I think Luxor is probably the right tool it’s just taken me a while to learn the basics of Luxorthought. Thanks for the package!

Cool. I was thinking that you could track a sequence of random coordinate transformations by storing them in an array:

mutable struct PointMatrix
    point::Point
    matrix::Array
end

a = PointMatrix[]
@draw begin
    background("black")
    sethue("cyan")
    for i in 1:100
        randompoint1 = rand(BoundingBox())
        @layer begin
            translate(randompoint1)
            scale(rand(1:5))
            rotate(rand() * 2π)
            randompoint2 = rand(BoundingBox())
            box(randompoint2, 5, 5, :fill)
            push!(a, PointMatrix(randompoint2, getmatrix()))
        end
    end

    sethue("magenta")
    for ptm in a
        setmatrix(ptm.matrix)
        box(ptm.point, 10, 10, :stroke)
    end
end

Here the two loops draw the same points. Although translate/scale/rotate are relative transformations of the current coordinate system, whereas setmatrix redefines the current coordinate system.

Feel free to ask more questions here or on github!

1 Like

Thanks for the help! Apologies for being dense, but I’m still missing how to “connect” different iterations with different matrices. For the sake of simplicity let’s say “connect” is literally “draw a line between”. So if I have

ptm_A::PointMatrix # generated several loops ago, current matrix is different
pt::Point # recently generated, relative to the current matrix

How can I draw a line from the location represented by ptm to that represented by pt?

I don’t think I really understand your problem, but I wonder whether - if you’re using lots of different coordinate systems - you might be better off just keeping a record of the points’ global coordinates, and use those.

Otherwise I’d suggest you make a minimal example of the situation and open an issue on GitHub.