Layered.jl

First, we create a square canvas. The canvas function returns the canvas and the top layer. There is only one top layer and we add all other layers and shapes to it.

using Layered

defaultfont("Arial")

c, l = canvas(250, 250, color = "tomato")
c

Returning the canvas c displays the image in some environments. Alternatively, you can save a canvas with the svg, pdf or png functions.

We can add a circle shape to the top layer using circle!. O is a shorthand for origin, i.e., the point (0, 0). Adding Fill("teal") to the returned Shape object sets an attribute.

c1 = circle!(l, O, 50) + Fill("teal")

c

You can see that the circle also has a black stroke, which it inherited from the default attributes of the top layer.

We add another circle, this time with a non-mutating syntax. The returned object is not yet placed in any layer, but we can add it via push! or, as in this example pushfirst!, which means it is placed below the first circle and therefore partially occluded by it.

There's another attribute Linestyle which we can just add after Fill.

X(50) is a shorthand for P(50, 0).

c2 = circle(-X(50), 50) + Fill("orange") + Linestyle(:dashed)
pushfirst!(l, c2)

c

Let's add another circle. Y(80) is a shorthand for P(0, 80).

c3 = circle!(l, Y(80), 20) + Fill("red")

c

We can also pass a function as a first argument to shape methods like circle or lines. The lines function expects an array of Line objects as input. This is returned by the function outertangents, which is called at drawing time with the Circle objects inside c1 and c3.

All geometric objects used as arguments for such closures are first transformed into the reference space of the layer in which the new shape is placed. In this case, that is just l, so really no transformation is necessary. But through this technique, the two circles could theoretically be in two different layers, and the lines drawn into a third layer, and it would still work.

lines!(outertangents, l, c1, c3) + Linestyle(:dashed)

c

We can also add text. This time we pass the first argument function using the do syntax. In this case, we refer to the c1 circle and we use its center as the text's anchor point.

txt!(l, c1) do c1
    Txt(c1.center, "Layered.jl", 14, :c, :c, deg(0))
end + Textfill("white")

c

You can also pass a function without arguments if that helps you organize your code a little more neatly. In this case, we create a 2D grid of points and place crosses at these points. The array of cross point vectors is drawn by the polygons function. We use pushfirst! again to put the stars below the other content.

polys = polygons() do
    ps = P.(grid(-100:20:100, -100:20:100)...)
    ncross.(ps, 8, 5, 0.3)
end + Fill("white", 0.3) + Stroke(nothing)
pushfirst!(l, polys)

c

A piece of art, really.

Let's use a clipping mask. A clipping mask is a shape outside of which nothing is drawn. First, we create a diamond shape with a rotated square:

diamond = rect!(l, O, 140, 140, deg(45))

c

We don't want to see the square, though, but only use it as a clipping mask. First, we can make it invisible by adding the Invisible attribute (a shorthand for Visible(false)):

diamond + Invisible

c

Now we assign a clip to the top layer using the diamond as the clipping shape.

l + Clip(diamond)

c