Graphics are placed on the current drawing according to the current transformation matrix. You can either modify this indirectly, using functions, or set the matrix directly.
Transformation functions
For basic transformations, use translate(tx, ty)
, scale(sx, sy)
, and rotate(a)
.
Translation
translate(pos)
(and translate(x, y)
) shift the current origin to pos
or by the specified amounts in x and y. It's relative and cumulative, rather than absolute:
for i in range(0, step=30, length=6)
sethue(HSV(i, 1, 1)) # from Colors
setopacity(0.5)
circle(Point(0, 0), 40, :fillpreserve)
setcolor("black")
strokepath()
translate(50, 0)
end
Scaling
scale(x, y)
and scale(n)
scale the current workspace by the specified amounts. It's relative to the current scale, not to the drawing's original.
origin()
for i in range(0, step=30, length=6)
sethue(HSV(i, 1, 1)) # from Colors
circle(Point(0, 0), 130, :fillpreserve)
setcolor("black")
strokepath()
scale(0.8)
end
Rotation
rotate
rotates the current workspace by the specified amount about the current 0/0 point. It's relative to the previous rotation - "rotate by".
origin()
for i in 1:8
randomhue()
squircle(Point(40, 0), 20, 30, :fillpreserve)
sethue("black")
strokepath()
rotate(π/4)
end
origin
resets the matrix then moves the origin to the center of the page.
Use the getscale
, gettranslation
, and getrotation
functions to find the current values.
To quickly return home after many changes, you can use setmatrix([1, 0, 0, 1, 0, 0])
to reset the matrix to the default.
Linear interpolation ("lerp")
rescale
is a convenient utility function for linear interpolation. An easy way to visualize it is by imagining two number lines. A value relative to a pair of low and high values is rescaled to have the equivalent value relative to another pair of low and high values.
This function is sometimes called “lerp” in other systems. For example, in Processing, the lerp()
function takes the form lerp(low, high, value)
, where the returned value lies between low
and high
corresponding to how value
lies between 0 and 1.
The equivalent to lerp(10, 20, 0.5)
in Luxor is rescale(0.5, 0, 1, 10, 20)
. Luxor requires a ‘from’ scale (here ... 0, 1, ...
) although the ‘to’ scale is optional and defaults to 0, 1
.
Scaling of line thickness
Line thicknesses are not scaled by default. For example, with a current line thickness set by setline(1)
, lines drawn before and after scale(2)
will be the same thickness. If you want line thicknesses to respond to the current scale, so that a line thickness of 1 is scaled by n
after calls to scale(n)
, you can call setstrokescale
with true
to enable stroke scaling, and setstrokescale(false)
to disable it. You can also enable stroke scaling when creating a new Drawing
by passing the named argument strokescale
during Drawing
construction (i.e., Drawing(400, 400, strokescale=true)
).
Matrices
In Luxor, there's always a current matrix that determines how coordinates are interpreted in the current workspace. In Cairo, it's a six element array:
\[\begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ \end{bmatrix}\]
and Luxor/Cairo matrix functions accept and return simple 6-element vectors:
julia> getmatrix()
6-element Array{Float64,1}:
1.0
0.0
0.0
1.0
0.0
0.0
You can convert between the 6-element and 3x3 versions of a transformation matrix using the functions cairotojuliamatrix
and juliatocairomatrix
.
transform(a)
transforms the current workspace by ‘multiplying’ the current matrix with matrix a
. For example, transform([1, 0, xskew, 1, 50, 0])
skews the current matrix by xskew
radians and moves it 50 in x and 0 in y.
function boxtext(p, t)
sethue("grey30")
box(p, 30, 50, :fill)
sethue("white")
textcentered(t, p)
end
for i in 0:5
xskew = tand(i * 5.0)
transform([1, 0, xskew, 1, 50, 0])
boxtext(O, string(round(rad2deg(xskew), digits=1), "°"))
end
getmatrix
gets the current matrix, setmatrix(a)
sets the matrix to array a
.
Other functions include getmatrix
, setmatrix
, transform
, crossproduct
, blendmatrix
, rotationmatrix
, scalingmatrix
, and translationmatrix
.
Use the getscale
, gettranslation
, and getrotation
functions to find the current values of the current matrix. These can also find the values of arbitrary 3x3 matrices.
World position
If you use translate
to move the origin to different places on a drawing, you can use getworldposition
to find the "true" world coordinates of points. In the following example, we temporarily translate to a random point, and "drop a pin" that remembers the new origin in terms of the drawing's world coordinates. After the temporary translation is over, we have a record of where it was.
origin()
@layer begin
translate(0.7rand(BoundingBox()))
pin = getworldposition()
end
label("you went ... ", :n, O, offset = 10)
label("... here", :n, pin, offset = 20)
arrow(O, pin)
Coordinate conventions
In Luxor, by convention, the y axis points downwards, and the x axis points to the right.
There are basically two conventions for computer graphics:
most computer graphics systems (HTML, SVG, Processing, Cairo, Luxor, image processing, most GUIs, etc) use the “y downwards” convention
mathematical illustrations, such as graphs, figures, Plots.jl, plots, etc. use the “y upwards” convention
You could use a transformation matrix to reflect the Luxor drawing space in the x axis.
pts = (Point(0, 0), Point(50, 100))
@layer begin
translate(table[1])
arrow(pts...)
rulers()
end
@layer begin
translate(table[2])
transform([1 0 0 -1 0 0]) # <--
arrow(pts...)
rulers()
end
If you do this and try to place text, all your text will be incorrectly drawn upside down, so you'd need to enclose any text placement with another matrix transformation.
Advanced transformations
For more powerful transformations of graphic elements, consider using Julia packages which are designed specifically for the purpose.
The following example uses the Rotations and CoordinateTransformations packages. It sets up some transformations which can then be composed in the correct order to transform points.
using CoordinateTransformations, Rotations, StaticArrays, LinearAlgebra
rawpts = [
[0.1, 0.1],
[0.1, -0.1],
[-0.1, -0.1],
[-0.1, 0.1]
]
function transform_point(pt, transformation)
x, y, _ = transformation(SVector(pt[1], pt[2], 1.0))
return Point(x, y)
end
𝕊1 = LinearMap(UniformScaling(60))
𝕋1 = Translation(20, 30, 0)
ℝ1 = LinearMap(RotZ(π/3))
pts = map(pt -> transform_point(pt, 𝕋1 ∘ ℝ1 ∘ 𝕊1), rawpts)
...