The drawing model

The underlying drawing model is that you build paths, and these are filled and/or stroked, using the current graphics state, which specifies colors, line thicknesses, scale, orientation, opacity, and so on.

You can modify the current graphics state by transforming/rotating/scaling it, setting color and style parameters, and so on. Subsequent graphics use the new state, but the graphics you've already drawn are unchanged.

Many of the drawing functions have an action argument, supplied as a keyword argument (eg action=:fill). This action determines what happens to the current path. It can be :fill, :stroke, :fillstroke, :fillpreserve, :strokepreserve, :clip, or :path. The default is usually :path, ie. add the graphics to the current path.

The main Julia data types you'll encounter in Luxor are:

Name of typePurpose
Drawingholds the current drawing
Pointspecifies 2D points
BoundingBoxdefines a bounding box
Tabledefines a table with different column widths and row heights
Partitiondefines a table defined by cell width and height
Tilerdefines a rectangular grid of tiles
BezierPathSegmenta Bezier path segment defined by 4 points
BezierPathcontains a series of BezierPathSegments
GridRectdefines a rectangular grid
GridHexdefines a hexagonal grid
Sceneused to define a scene for an animation
Turtlerepresents a turtle for drawing turtle graphics
Pathcontains a complete path

Points and coordinates

You specify points on the drawing surface using Point(x, y).

The default origin (ie the x = 0, y = 0 point) is at the top left corner: the x axis runs left to right across the page, and the y axis runs top to bottom down the page, so Y coordinates increase downwards.

By default, Point(0, 100) is below Point(0, 0).

Note

Although this is the preferred coordinate system for most computer graphics software, including Luxor and Cairo, but mathematicians and scientists may well be used to the other convention, where the origin is in the center of the drawing and the y-axis increases up the page. See the macros such as @png, @svg, and @pdf which will put the origin at the center for you.

You can reposition the origin at any time, using origin. The 'user space' can be modified by functions such as scale, translate, and rotate, or more directly using matrix transforms.

The Point type holds two coordinates, x and y. For example:

julia> P = Point(12.0, 13.0)
Luxor.Point(12.0, 13.0)

julia> P.x
12.0

julia> P.y
13.0

Points are immutable, so you can't change P's x or y values directly. But it's easy to make new points based on existing ones.

Points can be added together:

julia> Q = Point(4, 5)
Luxor.Point(4.0, 5.0)

julia> P + Q
Luxor.Point(16.0, 18.0)

You can add and multiply Points and scalars:

julia> 10P
Luxor.Point(120.0, 130.0)

julia> P + 100
Luxor.Point(112.0, 113.0)

You can also make new points by mixing Points and tuples:

julia> P + (10, 0)
Luxor.Point(22.0, 13.0)

julia> Q * (0.5, 0.5)
Luxor.Point(2.0, 2.5)

You can also create points from tuples:

julia> Point((1.0, 14))
Point(1.0, 14.0)

julia> plist = (1.0, 2.0), (-10, 10), (14.2, 15.4);

julia> Point.(plist)
3-element Array{Point,1}:
 Point(1.0, 2.0)
 Point(-10.0, 10.0)
 Point(14.2, 15.4)

You can use the letter O as a shortcut to refer to the current Origin, Point(0, 0). Most coding fonts clearly show the difference between the letter O and the digit 0.

rulers()
box.([O + (i, 0) for i in range(0, stop=200, length=5)], 20, 20, action = :stroke)

point example

Angles are usually supplied in radians, measured starting at the positive x-axis turning towards the positive y-axis (which usually points 'down' the page or canvas). So rotations look ‘clockwise’. (The main exception is for turtle graphics, which conventionally let you supply angles in degrees.)

Coordinates are usually interpreted as PostScript points, where a point is 1/72 of an inch, or as pixels, at a DPI of 72ppi.

Because Julia allows you to combine numbers and variables directly, you can supply units with dimensions and have them converted to points (assuming the current scale is 1:1):

  • inch (in is unavailable, being used by for syntax)
  • cm (centimeters)
  • mm (millimeters)

For example:

rect(Point(20mm, 2cm), 5inch, (22/7)inch, action = :fill)

The drawing surface

The origin function moves the 0/0 point to the center of the drawing. It's often convenient to do this at the beginning of a program.

You can use functions like scale, rotate, and translate to change the coordinate system.

background usually fills the drawing with a color, covering any previous contents. By default, PDF drawings have a white background, whereas PNG drawings have no background so that the background appears transparent in other applications.

If there is a current clipping region, background fills just that region. In the next example, the first background fills the entire drawing with magenta, but the calls in the loop fill only the active clipping region, a table cell defined by the Table iterator:

background("magenta")
origin()
table = Table(5, 5, 100, 50)
for (pos, n) in table
    box(pos,
        table.colwidths[table.currentcol],
        table.rowheights[table.currentrow],
        action = :clip)
    background(randomhue()...)
    clipreset()
end

background

The rulers function draws a couple of rulers to indicate the position and orientation of the current axes.

background("gray80")
origin()
rulers()

axes

Save and restore: layers and state

gsave saves a copy of the current graphics settings (current axis rotation, position, scale, line and text settings, color, and so on). When the next grestore is called, all changes you've made to the graphics settings will be discarded, and the previous settings are restored, so things return to how they were when you last used gsave. gsave and grestore should always be balanced in pairs, enclosing the functions.

@svg begin
    circle(Point(0, 0), 100, action = :stroke)
    gsave()
        sethue("red")
        rule(Point(0, 0))
        rule(Point(0, 0), pi/2)
    grestore()
    circle(Point(0, 0), 200, action = :stroke)
end

The @layer macro is a shorter synonym for a gsave...grestore pair.

@svg begin
    circle(Point(0, 0), 100, action = :stroke)
    @layer (sethue("red"); rule(Point(0, 0)); rule(O, π/2))
    circle(Point(0, 0), 200, action = :stroke)
end

or

@svg begin
    circle(Point(0, 0), 100, action = :stroke)
    @layer begin
        sethue("red")
        rule(Point(0, 0))
        rule(Point(0, 0), pi/2)
    end
    circle(Point(0, 0), 200, action = :stroke)
end

Return the current drawing

In some situations you'll want to explicitly return the current drawing to the calling function. Use currentdrawing or assign the result of Drawing() to a variable.

Working in IDEs and notebooks

You can use an environment such as a Jupyter or Pluto notebook or the Juno or VS Code IDEs, and load Luxor at the start of a session. The first drawing will take a few seconds, because the Cairo graphics engine needs to warm up. Subsequent drawings are then much quicker. (This is true of much graphics and plotting work. Julia compiles each function when it first encounters it, and then calls the compiled versions for the rest of the session.)

Working in Jupyter

Jupyter

Working in VS Code

VS Code

Working in Pluto

Pluto

SVG images

Luxor can create new SVG images, either in a file or in memory, and can also place existing SVG images on a drawing. See Placing images for more. It's also possible to obtain the source of the current SVG drawing as a string. For example, this code draws the Julia logo using SVG code and stores the SVG in s:

Drawing(500, 500, :svg)
origin()
julialogo()
finish()
s = svgstring()

You can now examine the SVG elements in s programmatically:

eachmatch(r"rgb\(.*?\)", s) |> collect
5-element Vector{RegexMatch}:
 RegexMatch("rgb(0%,0%,0%)")
 RegexMatch("rgb(79.6%,23.5%,20%)")
 RegexMatch("rgb(25.1%,38.8%,84.7%)")
 RegexMatch("rgb(58.4%,34.5%,69.8%)")
 RegexMatch("rgb(22%,59.6%,14.9%)")