Simple shapes

# Simple graphics

In Luxor, there are different ways of working with graphical items. You can either draw them immediately (ie place them on the drawing, and they're then fixed). Or you can construct geometric objects as lists of points for further processing. Watch out for a `vertices=true` option, which returns coordinate data rather than draws a shape.

## Rectangles and boxes

The simple rectangle and box shapes can be made in different ways.

``````rulers()
sethue("red")
rect(O, 100, 100, :stroke)
sethue("blue")
box(O, 100, 100, :stroke)`````` `rect()` rectangles are positioned by a corner, but a box made with `box()` can either be defined by its center and dimensions, or by two opposite corners. If you want the coordinates of the corners of a box, rather than draw one immediately, use:

``box(centerpoint, width, height, vertices=true)``

or

``box(corner1,  corner2, vertices=true)``

`box` is also able to draw some of the other Luxor objects, such as BoundingBoxes and Table cells.

``rect(xmin, ymin, w, h, action)``

Create a rectangle with one corner at (`xmin`/`ymin`) with width `w` and height `h` and then do an action.

See `box()` for more ways to do similar things, such as supplying two opposite corners, placing by centerpoint and dimensions.

``rect(cornerpoint, w, h, action)``

Create a rectangle with one corner at `cornerpoint` with width `w` and height `h` and do an action.

``box(cornerpoint1, cornerpoint2, action=:nothing; vertices=false)``

Create a rectangle between two points and do an action. Use `vertices=true` to return an array of the four corner points rather than draw the box.

``box(points::AbstractArray, action=:nothing)``

Create a box/rectangle using the first two points of an array of Points to defined opposite corners.

``box(pt::Point, width, height, action=:nothing; vertices=false)``

Create a box/rectangle centered at point `pt` with width and height. Use `vertices=true` to return an array of the four corner points rather than draw the box.

``box(x, y, width, height, action=:nothing)``

Create a box/rectangle centered at point `x/y` with width and height.

``box(x, y, width, height, cornerradius, action=:nothing)``

Create a box/rectangle centered at point `x/y` with `width` and `height`. Round each corner by `cornerradius`.

``box(t::Table, r::Int, c::Int, action::Symbol=:nothing)``

Draw a box in table `t` at row `r` and column `c`.

``box(t::Table, cellnumber::Int, action::Symbol=:nothing; vertices=false)``

Draw box `cellnumber` in table `t`.

``box(bbox::BoundingBox, :action)``

Make a box using the bounds in `bbox`.

``box(tile::BoxmapTile, action::Symbol=:nothing; vertices=false)``

Use a Boxmaptile to make or draw a rectangular box. Use `vertices=true` to obtain the coordinates.

Create boxmaps using `boxmap()`.

For regular polygons, triangles, pentagons, and so on, see the next section on Polygons.

## Circles and ellipses

There are various ways to make circles, including by center and radius, or passing through two points:

``````p1 = O
p2 = Point(100, 0)
sethue("red")
circle(p1, 40, :fill)
sethue("green")
circle(p1, p2, :stroke)
sethue("black")
arrow(O, Point(0, -40))
map(p -> circle(p, 4, :fill), [p1, p2])`````` Or passing through three points. The `center3pts()` function returns the center position and radius of a circle passing through three points:

``````sethue("black")
p1 = Point(0, -50)
p2 = Point(100, 0)
p3 = Point(0, 65)
map(p -> circle(p, 4, :fill), [p1, p2, p3])
sethue("orange")
circle(center3pts(p1, p2, p3)..., :stroke)`````` ``circle(x, y, r, action=:nothing)``

Make a circle of radius `r` centered at `x`/`y`.

`action` is one of the actions applied by `do_action`, defaulting to `:nothing`. You can also use `ellipse()` to draw circles and place them by their centerpoint.

``circle(pt, r, action=:nothing)``

Make a circle centered at `pt`.

``circle(pt1::Point, pt2::Point, action=:nothing)``

Make a circle that passes through two points that define the diameter:

``center3pts(a::Point, b::Point, c::Point)``

Find the radius and center point for three points lying on a circle.

returns `(centerpoint, radius)` of a circle. Then you can use `circle()` to place a circle, or `arc()` to draw an arc passing through those points.

If there's no such circle, then you'll see an error message in the console and the function returns `(Point(0,0), 0)`.

With `ellipse()` you can place ellipses and circles by defining the center point and the width and height.

``````tiles = Tiler(500, 300, 5, 5)
width = 20
height = 25
for (pos, n) in tiles
global width, height
randomhue()
ellipse(pos, width, height, :fill)
sethue("black")
label = string(round(width/height, digits=2))
textcentered(label, pos.x, pos.y + 25)
width += 2
end`````` `ellipse()` can also construct polygons that are approximations to ellipses. You supply two focal points and a length which is the sum of the distances of a point on the perimeter to the two focii.

``````fontface("Menlo")

f1 = Point(-100, 0)
f2 = Point(100, 0)

circle.([f1, f2], 3, :fill)

epoly = ellipse(f1, f2, 250, vertices=true)
poly(epoly, :stroke,  close=true)

pt = epoly[rand(1:end)]

poly([f1, pt, f2], :stroke)

label("f1", :W, f1, offset=10)
label("f2", :E, f2, offset=10)

label(string(round(distance(f1, pt), digits=1)), :SE, midpoint(f1, pt))
label(string(round(distance(pt, f2), digits=1)), :SW, midpoint(pt, f2))

label("ellipse(f1, f2, 250)", :S, Point(0, 75))`````` The advantage of this method is that there's a `vertices=true` option, allowing further scope for polygon manipulation.

``````f1 = Point(-100, 0)
f2 = Point(100, 0)
ellipsepoly = ellipse(f1, f2, 170, :none, vertices=true)
[ begin
setgray(rescale(c, 150, 1, 0, 1))
poly(offsetpoly(ellipsepoly, c), close=true, :fill);
rotate(pi/20)
end
for c in 150:-10:1 ]`````` ``ellipse(xc, yc, w, h, action=:none)``

Make an ellipse, centered at `xc/yc`, fitting in a box of width `w` and height `h`.

``ellipse(cpt, w, h, action=:none)``

Make an ellipse, centered at point `c`, with width `w`, and height `h`.

``````ellipse(focus1::Point, focus2::Point, k, action=:none;
stepvalue=pi/100,
vertices=false,
reversepath=false)``````

Build a polygon approximation to an ellipse, given two points and a distance, `k`, which is the sum of the distances to the focii of any points on the ellipse (or the shortest length of string required to go from one focus to the perimeter and on to the other focus).

`circlepath()` constructs a circular path from Bézier curves, which allows you to use circles as paths.

``````setline(4)
tiles = Tiler(600, 250, 1, 5)
for (pos, n) in tiles
randomhue()
circlepath(pos, tiles.tilewidth/2, :path)
newsubpath()
circlepath(pos, rand(5:tiles.tilewidth/2 - 1), :fill, reversepath=true)
end`````` ``````circlepath(center::Point, radius, action=:none;
reversepath=false,
kappa = 0.5522847498307936)``````

Draw a circle using Bézier curves.

The magic value, `kappa`, is `4.0 * (sqrt(2.0) - 1.0) / 3.0`.

### Circles and tangents

Functions to make circles that are tangential to other circles include:

• `circletangent2circles()` makes circles of a particular radius tangential to two circles
• `circlepointtangent()` makes circles of a particular radius passing through a point and tangential to another circle

These functions can return 0, 1, or 2 points (since there are often two solutions to a specific geometric layout).

`circletangent2circles()` takes the required radius and two existing circles:

``````circle1 = (Point(-100, 0), 90)
circle(circle1..., :stroke)
circle2 = (Point(100, 0), 90)
circle(circle2..., :stroke)

requiredradius = 25
ncandidates, p1, p2 = circletangent2circles(requiredradius, circle1..., circle2...)

if ncandidates==2
sethue("orange")
circle(p1, requiredradius, :fill)
sethue("green")
circle(p2, requiredradius, :fill)
sethue("purple")
circle(p1, requiredradius, :stroke)
circle(p2, requiredradius, :stroke)
end

# the circles are 10 apart, so there should be just one circle
# that fits there

requiredradius = 10
ncandidates, p1, p2 = circletangent2circles(requiredradius, circle1..., circle2...)

if ncandidates==1
sethue("blue")
circle(p1, requiredradius, :fill)
sethue("cyan")
circle(p1, requiredradius, :stroke)
end`````` `circlepointtangent()` looks for circles of a specified radius that pass through a point and are tangential to a circle. There are usually two candidates.

``````circle1 = (Point(-100, 0), 90)
circle(circle1..., :stroke)

requiredradius = 50
requiredpassthrough = O + (80, 0)
ncandidates, p1, p2 = circlepointtangent(requiredpassthrough, requiredradius, circle1...)

if ncandidates==2
sethue("orange")
circle(p1, requiredradius, :stroke)
sethue("green")
circle(p2, requiredradius, :stroke)
end

sethue("black")
circle(requiredpassthrough, 4, :fill)`````` ``circletangent2circles(radius, circle1center::Point, circle1radius, circle2center::Point, circle2radius)``

Find the centers of up to two circles of radius `radius` that are tangent to the two circles defined by `circle1...` and `circle2...`. These two circles can overlap, but one can't be inside the other.

• (0, O, O) - no such circles exist

• (1, pt1, O) - 1 circle exists, centered at pt1

• (2, pt1, pt2) - 2 circles exist, with centers at pt1 and pt2

(The O are just dummy points so that three values are always returned.)

``circlepointtangent(through::Point, radius, targetcenter::Point, targetradius)``

Find the centers of up to two circles of radius `radius` that pass through point `through` and are tangential to a circle that has radius `targetradius` and center `targetcenter`.

This function returns a tuple:

• (0, O, O) - no circles exist

• (1, pt1, O) - 1 circle exists, centered at pt1

• (2, pt1, pt2) - 2 circles exist, with centers at pt1 and pt2

(The O are just dummy points so that three values are always returned.)

## More curved shapes: sectors, spirals, and squircles

A sector (technically an "annular sector") has an inner and outer radius, as well as start and end angles.

``````sethue("tomato")
sector(50, 90, pi/2, 0, :fill)
sethue("olive")
sector(Point(O.x + 200, O.y), 50, 90, 0, pi/2, :fill)`````` You can also supply a value for a corner radius. The same sector is drawn but with rounded corners.

``````sethue("tomato")
sector(50, 90, pi/2, 0, 15, :fill)
sethue("olive")
sector(Point(O.x + 200, O.y), 50, 90, 0, pi/2, 15, :fill)`````` ``sector(centerpoint::Point, innerradius, outerradius, startangle, endangle, action:none)``

Draw an annular sector centered at `centerpoint`.

``````sector(innerradius::Real, outerradius::Real, startangle::Real, endangle::Real,
action::Symbol=:none)``````

Draw an annular sector centered at the origin.

``````sector(centerpoint::Point, innerradius, outerradius, startangle, endangle,
cornerradius, action:none)``````

Draw an annular sector with rounded corners, basically a bent sausage shape, centered at `centerpoint`.

TODO: The results aren't 100% accurate at the moment. There are small discontinuities where the curves join.

The cornerradius is reduced from the supplied value if neceesary to prevent overshoots.

``````sector(innerradius::Real, outerradius::Real, startangle::Real, endangle::Real,
cornerradius::Real, action::Symbol=:none)``````

Draw an annular sector with rounded corners, centered at the current origin.

A pie (or wedge) has start and end angles.

``pie(0, 0, 100, pi/2, pi, :fill)`` ``pie(x, y, radius, startangle, endangle, action=:none)``

Draw a pie shape centered at `x`/`y`. Angles start at the positive x-axis and are measured clockwise.

``pie(centerpoint, radius, startangle, endangle, action=:none)``

Draw a pie shape centered at `centerpoint`.

Angles start at the positive x-axis and are measured clockwise.

``pie(radius, startangle, endangle, action=:none)``

Draw a pie shape centered at the origin

To construct spirals, use the `spiral()` function. These can be drawn directly, or used as polygons. The default is to draw Archimedean (non-logarithmic) spirals.

``````spiraldata = [
(-2, "Lituus",      50),
(-1, "Hyperbolic", 100),
( 1, "Archimedes",   1),
( 2, "Fermat",       5)]

grid = GridRect(O - (200, 0), 130, 50)

for aspiral in spiraldata
@layer begin
translate(nextgridpoint(grid))
spiral(last(aspiral), first(aspiral), period=20pi, :stroke)
label(aspiral, :S, offset=100)
end
end`````` Use the `log=true` option to draw logarithmic (Bernoulli or Fibonacci) spirals.

``````spiraldata = [
(10,  0.05),
(4,   0.10),
(0.5, 0.17)]

grid = GridRect(O - (200, 0), 175, 50)
for aspiral in spiraldata
@layer begin
translate(nextgridpoint(grid))
spiral(first(aspiral), last(aspiral), log=true, period=10pi, :stroke)
label(string(aspiral), :S, offset=100)
end
end``````

Modify the `stepby` and `period` parameters to taste, or collect the vertices for further processing. ``````spiral(a, b, action::Symbol=:none;
stepby = 0.01,
period = 4pi,
vertices = false,
log=false)``````

Make a spiral. The two primary parameters `a` and `b` determine the start radius, and the tightness.

For linear spirals (`log=false`), `b` values are:

``````lituus: -2

hyperbolic spiral: -1

Archimedes' spiral: 1

Fermat's spiral: 2``````

For logarithmic spirals (`log=true`):

``golden spiral: b = ln(phi)/ (pi/2) (about 0.30)``

Values of `b` around 0.1 produce tighter, staircase-like spirals.

A squircle is a cross between a square and a circle. You can adjust the squariness and circularity of it to taste by supplying a value for the root (keyword `rt`):

``````setline(2)
tiles = Tiler(600, 250, 1, 3)
for (pos, n) in tiles
sethue("lavender")
squircle(pos, 80, 80, rt=[0.3, 0.5, 0.7][n], :fillpreserve)
sethue("grey20")
strokepath()
textcentered("rt = \$([0.3, 0.5, 0.7][n])", pos)
end`````` ``````squircle(center::Point, hradius, vradius, action=:none;
rt = 0.5, stepby = pi/40, vertices=false)``````

Make a squircle or superellipse (basically a rectangle with rounded corners). Specify the center position, horizontal radius (distance from center to a side), and vertical radius (distance from center to top or bottom):

The root (`rt`) option defaults to 0.5, and gives an intermediate shape. Values less than 0.5 make the shape more rectangular. Values above make the shape more round. The horizontal and vertical radii can be different.

To draw a simple rounded rectangle, supply a corner radius:

``````setline(4)
box(O, 200, 150, 10, :stroke)`````` Or you could smooth the corners of a box, like so:

``````setline(4)
polysmooth(box(O, 200, 150, vertices=true), 10, :stroke)`````` ## Paths and positions

A path is a sequence of lines and curves. You can add lines and curves to the current path, then use `closepath()` to join the last point to the first.

A path can have subpaths, created with`newsubpath()`, which can form holes.

There is a 'current position' which you can set with `move()`, and can use implicitly in functions like `line()`, `rline()`, `text()`, `arc()` and `curve()`.

``move(pt)``

Move to a point.

``rmove(pt)``

Move relative to current position by the `pt`'s x and y:

``newpath()``

Create a new path. This is Cairo's `new_path()` function.

``newsubpath()``

Add a new subpath to the current path. This is Cairo's `new_sub_path()` function. It can be used for example to make holes in shapes.

``closepath()``

Close the current path. This is Cairo's `close_path()` function.

## Lines

Use `line()` and `rline()` to draw straight lines. `line(pt1, pt2, action)` draws a line between two points. `line(pt)` adds a line to the current path going from the current position to the point. `rline(pt)` adds a line relative to the current position.

``line(pt)``

Draw a line from the current position to the `pt`.

``line(pt1::Point, pt2::Point, action=:nothing)``

Make a line between two points, `pt1` and `pt2` and do an action.

``rline(pt)``

Draw a line relative to the current position to the `pt`.

You can use `rule()` to draw a line through a point, optionally at an angle to the current x-axis.

``````y = 10
for x in 10 .^ range(0, length=100, stop=3)
global y
circle(Point(x, y), 2, :fill)
rule(Point(x, y), -pi/2, boundingbox=BoundingBox(centered=false))
y += 2
end`````` Use the `boundingbox` keyword argument to crop the ruled lines with a BoundingBox.

``````origin()
box(BoundingBox() * 0.9, :stroke)
for x in 10 .^ range(0, length=100, stop=3)
rule(Point(x, 0), pi/2,  boundingbox=BoundingBox() * 0.9)
rule(Point(-x, 0), pi/2, boundingbox=BoundingBox() * 0.9)
end``````
``true`` ``````rule(pos, theta;
boundingbox=BoundingBox())``````

Draw a straight line through `pos` at an angle `theta` from the x axis.

By default, the line spans the entire drawing, but you can supply a BoundingBox to change the extent of the line.

``````rule(O)       # draws an x axis
rule(O, pi/2) # draws a  y axis``````

The function:

``rule(O, pi/2, boundingbox=BoundingBox()/2)``

draws a line that spans a bounding box half the width and height of the drawing.

## Arcs and curves

There are a few standard arc-drawing commands, such as `curve()`, `arc()`, `carc()`, and `arc2r()`. Because these are often used when building complex paths, they usually add arc sections to the current path. To construct a sequence of lines and arcs, use the `:path` action, followed by a final `:stroke` or similar.

`curve()` constructs Bézier curves from control points:

``````setline(.5)
pt1 = Point(0, -125)
pt2 = Point(200, 125)
pt3 = Point(200, -125)

label.(string.(["O", "control point 1", "control point 2", "control point 3"]),
:e,
[O, pt1, pt2, pt3])

sethue("red")
map(p -> circle(p, 4, :fill), [O, pt1, pt2, pt3])

line(O, pt1, :stroke)
line(pt2, pt3, :stroke)

sethue("black")
setline(3)

# start a path
move(O)
curve(pt1, pt2, pt3) #  add to current path
strokepath()`````` `arc2r()` draws a circular arc centered at a point that passes through two other points:

``````tiles = Tiler(700, 200, 1, 6)
for (pos, n) in tiles
c1, pt2, pt3 = ngon(pos, rand(10:50), 3, rand(0:pi/12:2pi), vertices=true)
sethue("black")
map(pt -> circle(pt, 4, :fill), [c1, pt3])
sethue("red")
circle(pt2, 4, :fill)
randomhue()
arc2r(c1, pt2, pt3, :stroke)
end`````` ``arc(xc, yc, radius, angle1, angle2, action=:nothing)``

Add an arc to the current path from `angle1` to `angle2` going clockwise, centered at xc, yc.

Angles are defined relative to the x-axis, positive clockwise.

``arc(centerpoint::Point, radius, angle1, angle2, action=:nothing)``

Add an arc to the current path from `angle1` to `angle2` going clockwise, centered at `centerpoint`.

``  arc2r(c1::Point, p2::Point, p3::Point, action=:nothing)``

Add a circular arc centered at `c1` that starts at `p2` and ends at `p3`, going clockwise, to the current path.

`c1`-`p2` really determines the radius. If `p3` doesn't lie on the circular path, it will be used only as an indication of the arc's length, rather than its position.

``carc(xc, yc, radius, angle1, angle2, action=:nothing)``

Add an arc to the current path from `angle1` to `angle2` going counterclockwise, centered at `xc`/`yc`.

Angles are defined relative to the x-axis, positive clockwise.

``carc(centerpoint::Point, radius, angle1, angle2, action=:nothing)``

Add an arc centered at `centerpoint` to the current path from `angle1` to `angle2`, going counterclockwise.

``carc2r(c1::Point, p2::Point, p3::Point, action=:nothing)``

Add a circular arc centered at `c1` that starts at `p2` and ends at `p3`, going counterclockwise, to the current path.

`c1`-`p2` really determines the radius. If `p3` doesn't lie on the circular path, it will be used only as an indication of the arc's length, rather than its position.

``````curve(x1, y1, x2, y2, x3, y3)
curve(p1, p2, p3)``````

Add a Bézier curve.

The spline starts at the current position, finishing at `x3/y3` (`p3`), following two control points `x1/y1` (`p1`) and `x2/y2` (`p2`).