# This notebook is a semi top-down explanation. This cell needs to be
# executed first so that the operators and helper functions are defined
# All of this is explained in the later half of the notebook
using Compose, Interact
Compose.set_default_graphic_size(2inch, 2inch)
points_f = [
(.1, .1),
(.9, .1),
(.9, .2),
(.2, .2),
(.2, .4),
(.6, .4),
(.6, .5),
(.2, .5),
(.2, .9),
(.1, .9),
(.1, .1)
]
f = compose(context(), stroke("black"), line(points_f))
rot(pic) = compose(context(rotation=Rotation(-deg2rad(90))), pic)
flip(pic) = compose(context(mirror=Mirror(deg2rad(90), 0.5w, 0.5h)), pic)
above(m, n, p, q) =
compose(context(),
(context(0, 0, 1, m/(m+n)), p),
(context(0, m/(m+n), 1, n/(m+n)), q))
above(p, q) = above(1, 1, p, q)
beside(m, n, p, q) =
compose(context(),
(context(0, 0, m/(m+n), 1), p),
(context(m/(m+n), 0, n/(m+n), 1), q))
beside(p, q) = beside(1, 1, p, q)
over(p, q) = compose(context(),
(context(), p), (context(), q))
rot45(pic) =
compose(context(0, 0, 1/sqrt(2), 1/sqrt(2),
rotation=Rotation(-deg2rad(45), 0w, 0h)), pic)
# Utility function to zoom out and look at the context
zoomout(pic) = compose(context(),
(context(0.2, 0.2, 0.6, 0.6), pic),
(context(0.2, 0.2, 0.6, 0.6), fill(nothing), stroke("black"), strokedash([0.5mm, 0.5mm]),
polygon([(0, 0), (1, 0), (1, 1), (0, 1)])))
function read_path(p_str)
tokens = [try parsefloat(x) catch symbol(x) end for x in split(p_str, r"[\s,]+")]
path(tokens)
end
fish = compose(context(units=UnitBox(260, 260)), stroke("black"),
read_path(strip(readall("fish.path"))))
rotatable(pic) = @manipulate for θ=0:0.001:2π
compose(context(rotation=Rotation(θ)), pic)
end
blank = compose(context())
fliprot45(pic) = rot45(compose(context(mirror=Mirror(deg2rad(-45))),pic))
# Hide this cell.
display(MIME("text/html"), """<script>
var cell = \$(".container .cell").eq(0), ia = cell.find(".input_area")
if (cell.find(".toggle-button").length == 0) {
ia.after(
\$('<button class="toggle-button">Toggle hidden code</button>').click(
function (){ ia.toggle() }
)
)
ia.hide()
}
</script>""")
Functional Geometry is a paper by Peter Henderson (original (1982), revisited (2002)) which deconstructs the MC Escher woodcut Square Limit

A picture is an example of a complex object that can be described in terms of its parts. Yet a picture needs to be rendered on a printer or a screen by a device that expects to be given a sequence of commands. Programming that sequence of commands directly is much harder than having an application generate the commands automatically from the simpler, denotational description.
A picture is a denotation of something to draw.
e.g. The value of f here denotes the picture of the letter F
f
We begin specifying the algebra of pictures we will use to describe Square Limit with a few operations that operate on pictures to give other pictures, namely:
rot : picture → pictureflip : picture → picturerot45 : picture → pictureabove : picture × picture → pictureabove : int × int × picture × picture → picturebeside : picture × picture → picturebeside : int × int × picture × picture → pictureover : picture → picturerot : picture → pictureRotate a picture anti-clockwise by 90°
rot(f)
flip : picture → pictureFlip a picture along its virtical center axis
flip(f)
rot(flip(f))
rotate the picture anti-clockwise by 45°, then flip it across the new virtical axis. In the paper this is implemented as $flip(rot45(fish))$. This function is rather specific to the problem at hand.
fliprot45(fish) |> zoomout # zoomout shows the bounding box
above : picture × picture → pictureplace a picture above another.
above(f, f)
above : int × int × picture × picture → picturegiven m, n, picture1 and picture2, return a picture where picture1 is placed above picture2 such that their heights occupy the total height in m:n ratio
above(1, 2, f, f)
beside : picture × picture → pictureSimilar to above but in the left-to-right direction.
beside(f, f)
beside : int × int × picture × picture → picturebeside(1, 2, f, f)
above(beside(f, f), f)
over : picture → pictureplace a picture upon another
over(f, flip(f))
We will now study some of the properties of the fish.
fish |> zoomout
rotatable(fish |> zoomout)
over(fish, rot(rot(fish))) |> zoomout
There is a certain kind of arrangement that is used to tile parts of the image. We call it t
fish2 = fliprot45(fish)
fish3 = rot(rot(rot(fish2)))
t = over(fish, over(fish2, fish3))
t |> zoomout
There is another kind of arrangement of fish which lies at the very center and regions on the diagonals. We will call it u.
u = over(over(fish2, rot(fish2)),
over(rot(rot(fish2)), rot(rot(rot(fish2)))))
u |> zoomout
quartet tiles 4 images in a 2x2 grid
quartet(p, q, r, s) =
above(beside(p, q), beside(r, s))
quartet(f,flip(f),rot(f),f)
# 2inch x 2inch canvas is no more sufficient, so let's blow it up a bit
Compose.set_default_graphic_size(5inch, 5inch)
quartet(u, u, u, u) |> zoomout
Notice how the fish interlock without leaving out any space in between them. Escher FTW.
cycle is a quartet of the same picture with each successive tile rotated by 90° anti-clockwise
cycle(p) =
quartet(p, rot(p), rot(rot(p)), rot(rot(rot(p))))
cycle(f)
A nonet is a grid of 9 pictures.
nonet(p, q, r,
s, t, u,
v, w, x) =
above(1,2,beside(1,2,p,beside(1,1,q,r)),
above(1,1,beside(1,2,s,beside(1,1,t,u)),
beside(1,2,v,beside(1,1,w,x))))
nonet(f, f, f, f, f, f, f, f, f)
Note: blank denotes a blank picture
There is a certain pattern which makes up the mid region of each of the four edges of the image. We will call this arrangement side
the 1 in side1 represents 1 level of recursion. This is the simplest side.
side1 = quartet(blank, blank, rot(t), t)
side1 |> zoomout
A side that is 2 levels deep.
side2 = quartet(side1,side1,rot(t),t)
side2 |> zoomout
A side that is n level deep.
side(n) =
if n == 1 side1 # basis
else quartet(side(n-1),side(n-1),rot(t),t) # induction
end
side(3) |> zoomout
# @manipulate lets us watch what happens as the levels increase
@manipulate for level=slider(1:4, value=1)
side(level) |> zoomout
end
Similarly, there is a certain kind of arrangement which makes up the corners of the artwork.
A corner level 1 deep is simply
corner1 = quartet(blank,blank,blank,u)
corner1 |> zoomout
A corner 2 levels deep, it is built using corner1, side1 and u.
corner2 = quartet(corner1,side1,rot(side1),u)
corner2 |> zoomout
An n level deep corner.
corner(n) =
n == 1 ? corner1 :
quartet(corner(n-1), side(n-1), rot(side(n-1)), u)
corner(3) |> zoomout
# Touring the corners with a slider:
@manipulate for level=slider(1:4, value=1)
corner(level) |> zoomout
end
We are almost there.
Square limit is a nonet of various rotations of corner at the corners, side at the sides and u in the center.
The following equation puts this precisely:
squarelimit(n) =
nonet(corner(n), side(n), rot(rot(rot(corner(n)))),
rot(side(n)), u, rot(rot(rot(side(n)))),
rot(corner(n)), rot(rot(side(n))), rot(rot(corner(n))))
# We render a level-3 square limit on a 10inch x 10inch SVG for maximum awesome.
draw(SVG(10inch, 10inch), squarelimit(3))
We will now implement the basic operators rot, flip, fliprot45, above, below and over with Compose.jl. (this explanation is taken mostly from the Compose website)
Compose graphics are created as a tree data structure. There are 3 types of objects that make up the nodes of the tree:
Context: An internal node, defines the transformation matrix to be applied for its childrenForm: A leaf node that defines some geometry, like a line or a polygonProperty: A leaf node that modifies how its parent's subtree is drawn, like fill color, font family, or line width.The all-important function in Compose, is called, not surprisingly, compose. Calling compose(a, b) will return a new tree rooted at a and with b attached as a child.
That's enough to start drawing some simple shapes.
Compose.set_default_graphic_size(2inch, 2inch) # switch back to smaller output
compose(context(), rectangle(), fill("tomato"))
Furthermore, more complex trees can be formed by grouping subtrees with parenthesis.
tomato_bisque =
compose(context(),
(context(), circle(), fill("bisque")),
(context(), rectangle(), fill("tomato")))
We can introspect this object with the introspect function. It returns a tree depiction of the picture
introspect(tomato_bisque)