# 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 → picture
flip : picture → picture
rot45 : picture → picture
above : picture × picture → picture
above : int × int × picture × picture → picture
beside : picture × picture → picture
beside : int × int × picture × picture → picture
over : picture → picture
rot : picture → picture
Rotate a picture anti-clockwise by 90°
rot(f)
flip : picture → picture
Flip 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 → picture
place a picture above another.
above(f, f)
above : int × int × picture × picture → picture
given 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 → picture
Similar to above
but in the left-to-right direction.
beside(f, f)
beside : int × int × picture × picture → picture
beside(1, 2, f, f)
above(beside(f, f), f)
over : picture → picture
place 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)