In the last episode, we implemented the core of a Logo interpreter. Today we'll add on a renderer, because without a renderer it's basically a few unit tests that pass which isn't that impressive to show off. Let's get started.
I've tagged the code before this episode as before_episode_167
if you want to
follow along.
git checkout before_episode_167
So before we started the last episode I added a Logo.Renderer that just drew a box, to make sure we'd have an easy way to start drawing when the time came. We can see it in iex:
iex -S mix
Logo.Window.start([])
I also wrote up an example script that we haven't explored yet. Let's look at it and tweak it a bit to pass our turtle instance into the window we render:
# We'll import Logo.Instance so we don't have to write that bit all the time
import Logo.Instance
# We're starting an instance here
{:ok, logo} = Logo.Instance.start
logo
|> color({255, 0, 0})
|> pen_down
|> forward(400)
|> right(90)
|> forward(200)
|> right(180)
|> forward(400)
# Finally, we'll pass our instance into the Logo.Window to be drawn
Logo.Window.start(logo)
OK, so next we need to open up the window and just fix a few references to make it clear what we're passing through all these functions:
# Just :%s/config/instance/g in lib/logo/window.ex
Finally, we'll open up the renderer and fix this reference there as well:
# same deal, :%s/config/instance/g
OK, so that's got us ready to actually implement the rendering. Let's run the tests, which will at least confirm I didn't fat-finger any syntax changes.
((( do it )))
Back in the renderer we'll look at the render function. Before I change anything, I've got comma-big-t mapped to running the example, which at present should run the Turtle but then follow it up with running the renderer, which just draws a square somewhere on the canvas.
((( do it )))
Now in the renderer and write some hopeful code to help us think about what we want to do.
def render(canvas, instance) do
# So we've passed in the instance. Let's get the turtle out of this
# instance
turtle = Instance.get_turtle(instance)
# Now we have a turtle...but we really need to draw some shapes. The turtle
# doesn't store any shapes though. It's just an actor that's doing its
# work. We want to take the shapes off of the turtle, and then draw a list
# of them. What we want to do is this:
# draw_shapes(canvas, turtle.shapes)
# But we can't grab those yet, so we'll leave that in but commented out.
# Let's think through what we want to be in that turtle.shapes key.
# Really we just need a list of shapes to draw, and a shape is named tuple.
# We'll start out defining a line, because that's all we need for the
# fractals. We'll leave a pattern for introducing new shapes later though.
draw_shapes(canvas, [{:line, {0, 0}, {100, 100}}])
end
# Alright, so now we want a recursive function that goes through the shapes,
# drawing until the list is empty.
def draw_shapes(canvas, [head|rest]) do
draw_shape(canvas, head)
draw_shapes(canvas, rest)
end
def draw_shapes(_canvas, []), do: :ok
# Now all we have to do is define how to draw a line. We'll just make a pen
# for now - it's immediately obvious that we need to store the colors with the
# shapes though.
def draw_shape(canvas, {:line, {x1, y1}, {x2, y2}}) do
# So here's a red pen for our line
pen = :wxPen.new({255, 0, 0, 255})
# We'll set the pen in our graphics context
:wxGraphicsContext.setPen(canvas, pen)
# And now we just use the drawLines function, which takes a list of points
:wxGraphicsContext.drawLines(canvas, [{x1, y1}, {x2, y2}])
:ok
end
Let's run the example, and we should end up with a line from {0, 0} to {100, 100}.
((( do it )))
Now all we need to do is build up the list of shapes from the turtle as it walks around. Let's open up the Logo.Instance and add that property to it to store the shapes:
defmodule Turtle do
defstruct [
color: nil,
pen_down: false,
x: 0,
y: 0,
angle: 0,
shapes: []
]
end
The right way to move on from here would be to write a test, but I'm kind of eager to see the turtle drawing so let's skip it this once...may future-me forgive us. Go down to where we handle the cast for forward and we'll add a shape if the pen is down:
def handle_cast({:forward, amount}, turtle) do
delta_x = amount * :math.cos(radians(turtle.angle))
delta_y = amount * :math.sin(radians(turtle.angle))
shapes = turtle.shapes
if(turtle.pen_down) do
shapes = [{:line, {turtle.x, turtle.y}, {turtle.x + delta_x, turtle.y + delta_y}}|shapes]
end
{:noreply, %Turtle{turtle|x: turtle.x + delta_x, y: turtle.y + delta_y, shapes: shapes}}
end
There's almost certainly a nicer way to handle that, but this works and it's not the worst code I've ever written. This should be sufficient to make our renderer work, so let's go back to the renderer and fetch the shapes instead of passing in our hard coded shapes:
((( do it )))
With that in place all we really need to do is run the example and see what we end up with.
((( do it, comma-big-t )))
Here we can see that we don't quite have a T. That's because we started off in the top left, pointing right, rather than in the middle pointing up. Rather than solving this in a perfect manner, I think we'd just like to move our turtle to a specific location at the beginning of our example.
logo
|> move_to({300, 300})
|> color({255, 0, 0})
|> pen_down
|> forward(400)
|> right(90)
|> forward(200)
|> right(180)
|> forward(400)
Logo.Window.start(logo)
So of course this doesn't exist yet. We'll open up the tests and implement it quickly:
test "can move to a particular position", meta do
turtle = meta[:pid]
|> Instance.move_to({300, 300})
|> Instance.get_turtle
assert 300 = turtle.x
assert 300 = turtle.y
end
We can run the tests, and obviously this function doesn't exist. Open up Logo.Instance:
def move_to(pid, {x, y}) do
:ok = GenServer.cast(pid, {:move_to, {x, y}})
pid
end
#...
def handle_cast({:move_to, {x, y}}, turtle) do
{:noreply, %Turtle{turtle|x: x, y: y}}
end
Run the tests, and they'll pass. Now let's run the example again and our T should not be stuck in the top right any longer:
((( comma-big-T )))
OK, so our basic example works. Now I'm going to add a more interesting example that I got from an online logo gallery and ported.
((( open examples/interesting.exs )))
# Ported from https://turight(leacademy.com/view/programs/555aa690f45859910a3c986a/en
import Logo.Instance
{:ok, logo} = Logo.Instance.start
logo
|> move_to({300, 300})
|> color({255, 0, 0})
|> pen_down
|> right(45)
|> forward(200)
|> right(90)
|> forward(200)
|> right(90)
|> forward(200)
|> right(90)
|> forward(200)
|> right(90)
|> forward(200)
|> right(90)
|> forward(50)
|> right(90)
|> forward(50)
|> left(90)
|> forward(50)
|> right(90)
|> forward(50)
|> left(90)
|> forward(50)
|> right(90)
|> forward(50)
|> left(90)
|> forward(50)
|> right(90)
|> forward(50)
|> right(90)
|> forward(200)
|> right(90)
|> forward(150)
|> right(90)
|> forward(50)
|> right(90)
|> forward(50)
|> left(90)
|> forward(50)
|> right(90)
|> forward(50)
|> left(90)
|> forward(50)
|> right(90)
|> forward(50)
Logo.Window.start(logo)
Now let's run it:
mix run examples/interesting.exs
Sweet. Our logo interpreter works essentially out of the box (granted, we manually parsed it into function calls) with a logo program from elsewhere, and we get some interesting output. We'll use this later to explore L System fractals. See you soon!