Let's Draw a Line!
(or) Finally!
Now that we’ve got the tools we need to create and save an image, and a base framework to build additional functionality on top of, let’s turn to the next important part of a graphics library: being able to draw something on to the image.
This post is part of a series:
- Part 1 - Generating Art in Elixir
- Part 2 - Actually Generating Art in Elixir
- Part 3 - Elixirizing our Rust
- Part 4 - Let's Draw a Line! (this post)
Let’s start very simple, with the humble straight line.
cairo
provides two functions for drawing a straight line:
cairo_line_to
cairo_rel_line_to
Each takes two arguments, floats representing the x and y coordinates of
the destination point. The difference lies in how they translate those
coordinates, and that difference is quite possibly revealed by the names of the
functions. cairo_line_to
draws a line to the given coordinates interpreted in
absolute space, while cairo_rel_line_to
interprets the given coordinates as
deltas to be applied relative to the origin point.
Speaking of origin points, you’ll have noticed that each of these functions takes only one set of coordinates, the end point of the line. So how do we know where to start drawing?
As part of its state, the in-memory Cairo context keeps a record of its “current
point”. This point starts out at (0, 0), and is updated every time a line (or
other shape) is drawn on the image surface, or by using two related functions:
cairo_move_to
and cairo_rel_move_to
. As you might imagine, these act much
the same way as the _line_to
functions above, but simply update the “current
point” without rendering a line onto the surface.
Two other functions it will be useful to know about before diving into
implementing this functionality in Xairo
, are cairo_stroke
and cairo_fill
.
These are used to complete a path built of _line_to
, _move_to
and other
related functions, and then render it to the image surface either by rendering
the path as a single line, or attempting to fill in the path, respectively.
These functions are related to paint
which we already saw implemented in a
previous post. For now, though, since we’re only drawing straight lines, we’ll
only worry about _stroke
.
So let’s look at the functions we need to implement. For now, let’s leave aside
the _rel_*_to
functions, since their implementation will be very close to the
absolute coordinate functions, and also _fill
, since it will be a very small
and predictable variation on _stroke
. So that leaves us with:
line_to
move_to
stroke
On the Elixir side
In C and Rust, the line_to
and move_to
functions take 2 arguments, the x
and y coordinates as floats. This is fine, and we could implement
Xairo.line_to
and Xairo.move_to
to take 2 arguments (in addition to the
image resource) without an problem. But in order to make Xairo
as
user-friendly as possible, wouldn’t it be nice to provide, through
function signatures, some context for those two floats? What if we wrap them in
a very simple Point
struct?
defmodule Xairo.Point do
defstruct [:x, :y]
def new(x, y) do
%__MODULE__{
x: x * 1.0,
y: y * 1.0
}
end
end
then we can write Xairo.line_to
and Xairo.move_to
, which take a single,
contextualized, Point
rather than two unmarked float values.
Now we have a choice. We can either have our NIF for line_to
accept x and
y, and unpack the Point
in Xairo.line_to
, or we could keep that context
through the NIF into Rust, and unpack the coordinates only when it’s time to
pass them into cairo
. I prefer the second option, maintaining struct contexts
as much as possible through the API code, both in Elixir and Rust, and
unwrapping them only when it’s necessary to communicate with the original C API.
This means that for Xairo.line_to
the code we write in Elixir, along with the
Point
struct definition from above, looks like
defmodule Xairo.Native do
...
def line_to(_image, _point), do: :erlang.nif_error(:nif_not_loaded)
end
defmodule Xairo do
...
def line_to(%Image{} = image, Point{} = point) do
Xairo.Native.line_to(image.reference, point)
image
end
end
How cool is that? Barely any code at all. As we’ll see next, things are pretty much the same over in Rust
On the Rust side
In the Ruby code above, we saw that instead of passing two floats into Rust
as the coordinates, we’re passing an Elixir struct that we’ve created. As I
alluded to in an earlier post,1 rustler
actually makes passing structured
data between Elixir and Rust very simple by providing a NifStruct
trait. So we
define our Rust struct
#[derive(Copy,Clone,Debug,NifStruct)]
#[module = "Xairo.Point"]
pub struct Point {
pub x: f64,
pub y: f64
}
We derive the NifStruct
trait and add an annotation matching it to the
struct’s name in Elixir, and that is all we have to do to be able to use
the struct directly in our NIF
#[rustler::nif]
fn line_to(context: WrapperRA, point: Point) -> WrapperRA {
context.context.line_to(point.x, point.y);
context
}
We implement move_to
and stroke
in the same way, and then we should be able
to draw some lines on our image, right?
Well, just about. Except for paint
, none of our functions deal with color. So
how to do we tell cairo
what color to use when it draws a line?
Our NIF for paint
cheated just a little bit by wrapping two cairo
functions
togther. It calls set_source_rgb
and paint
. This was fine a little while
ago because we only had one function that could render color on to the canvas.
But now we’re adding more, so let’s surface set_source_rgb
out into its own
NIF.2 While we’re at it, let’s provide an RGB
struct context for those
values like we did with Point
. The implementation of this struct in Elixir
and Rust will be left as an exercise for the reader.
At the end of this, our Xairo
Elixir module looks like
defmodule Xairo do
...
def line_to(%Image{} = image, Point{} = point) do
Xairo.Native.line_to(image.reference, point)
image
end
def move_to(%Image{} = image, Point{} = point) do
Xairo.Native.move_to(image.reference, point)
image
end
def set_source_rgb(%Image{} = image, %RGB{} = rgb) do
Xairo.Native.set_source_rgb(image.reference, rgb)
image
end
def paint(%Image{} = image) do
Xairo.Native.paint(image.reference)
image
end
def stroke(%Image{} = image) do
Xairo.Native.stroke(image.reference)
image
end
and Rust like
#[rustler::nif]
fn line_to(context: WrapperRA, point: Point) -> WrapperRA {
context.context.line_to(point.x, point.y);
context
}
#[rustler::nif]
fn move_to(context: WrapperRA, point: Point) -> WrapperRA {
context.context.move_to(point.x, point.y);
context
}
#[rustler::nif]
fn set_source_rgb(context: WrapperRA, rgb: RGB) -> WrapperRA {
context.context.set_source_rgb(rgb.red, rgb.green, rgb.blue);
context
}
#[rustler::nif]
fn paint(context: WrapperRa) -> WrapperRa {
context.context.paint().unwrap();
context
}
#[rustler::nif]
fn stroke(context: WrapperRa) -> WrapperRa {
context.context.stroke().unwrap();
context
}
A nice 1-to-1 mapping of the API, with our structured data kept contextualized until it needs to be destructed to pass to the native API.
Let’s end by putting it all together and drawing an empty white image with two horizontal black lines:
Xairo.Image.new(100, 100)
|> Xairo.set_source_rgb(RGB.new(255, 255, 255))
|> Xairo.paint()
|> Xairo.set_source_rgb(RGB.new(0, 0, 0))
|> Xairo.move_to(Point.new(20, 20))
|> Xairo.line_to(Point.new(80, 20))
|> Xairo.move_to(Point.new(20, 80))
|> Xairo.line_to(Point.new(80, 80))
|> Xairo.stroke()
|> Xairo.save_image("lines.png")
and that gives us
just as we hoped
Next time we’ll look at some of that duplication creeping into Xairo
, and talk
about next steps for the library. Thanks for reading!
Footnotes
-
In that previous post, I decided against using this option because of the complexity of the data we wanted to pass in the struct. Here, where we’re only passing two floating point numbers, it is much simpler. (for the Rust-ier among you, this approach is possible as long as the field types all implement the
Copy
trait.) ↩ -
We could implement
stroke
(and later,fill
) similarly, where they take the color and set it before rendering. Because of howcairo
works under the hood, you can set the color at any point during a path’s creation/extension as long as it’s before you callstroke
. But, from a workflow perspective, it is more natural when drawing to select the color first, so being able to separate these functions lends itself to a more familiar workflow when usingXairo
. ↩