Intro to (images in) Go – drawing

This post is part of a series. For a listing of all the posts, as well as instructions on running the code, see here.

In the previous post we looked at how to create a Canvas type onto which we could draw a gradient.

While gradients are all fun and games, to really start drawing we need to be able to draw lines onto our canvas. To do this we’ll need some way of representing points on the canvas. The image type in Go comes with a Point type, however this takes it’s coordinates as integers and to represent lines exactly we’ll want to work with floating point types.

Vector type

Defining a Vector type is pretty straightforward:

type Vector struct {
  X, Y float64

We’ll also want some utility methods to allow us to add or subtract vectors, get their length, and rotate and scale them. Here is how we’d add the Rotate function (the rest can be seen in the code example on github):

func (v *Vector) Rotate(angle float64) {
  cos, sin := math.Cos(angle), math.Sin(angle)
  v.X, v.Y = v.X*cos+v.Y*sin, v.Y*cos-v.X*sin

Given that you code is growing a bit, to keep things tidy we’ll define the Vector type in its own file, vector.go, rather than including it directly in our programs. We’ll also do the same for the Canvas. As the file is part of the same package if we use go build to run it we’ll just pick up the method definitions. However when using go run it is necessary to specify both files e.g. go run lines.go canvas.go vector.go.

Full vector.go source on github

Drawing lines

Ok great, so we have ourselves a vector type, let’s use it to draw a line. To do this we’ll create a function that takes a start vector and an end vector and draws the line pixel by pixel between them. For now we won’t worrying about antialiasing and just round to the nearest pixel each time we draw.

Instead we’ll draw the line using the following process:

  1. Get the vector delta = end - start, that represents the line
  2. Normalize this vector to unit length
  3. Draw a pixel at the start point and then move along the line, by the displacement given by our normalized delta vector.
  4. Draw another pixel and move along again, repeating this until we get to end

Whenever we draw a pixel we will be snapping our precise floating point position to the nearest pixel, and this will lead to antialiasing. In code the above becomes:

func (c Canvas) DrawLine(color color.RGBA, from Vector, to Vector) {
  delta := to.Sub(from)
  length := delta.Length()
  x_step, y_step := delta.X/length, delta.Y/length
  limit := int(length + 0.5)
  for i := 0; i < limit; i++ {
    x := from.X + float64(i)*x_step
    y := from.Y + float64(i)*y_step
    c.Set(int(x), int(y), color)

Now we can draw things like this:


Full lines.go source on github

Drawing a spiral

Drawing a spiral is pretty straightforward now, especially as our Vector type support scaling and rotating. The process is:

  1. Start with a vector v, e.g. {0, 1} and a position p, e.g. {0, 0}
  2. Draw v onto the canvas, at position p
  3. Displace p by v
  4. Rotate v and scale it down
  5. Repeat from step 2. until v becomes sufficiently small

So, in Go-speak:

func (c Canvas) DrawSpiral(color color.RGBA, from Vector) {
  dir := Vector{0, 5}
  last := from
  for i := 0; i < 10000; i++ {
    next := last.Add(dir)
    c.DrawLine(color, last, next)
    last = next

Great, now all that remains is to create an 12 foot poster from our “spiral-art” attach it to the ceiling, and lie on the floor forever more.


Full spirals.go source on github

  • Pingback: | Intro to (images in) Go – part 1

  • Gabriel Pozoz

    These post are great and the last picture is really cute: D

  • Joseph Kimmel

    thanks for these posts – i’m curious though , when i run your code i get a spiral with an empty center, whereas yours the centers are filled in. i thought it must be as simple as tweaking some parameter, but i’ve yet to find the parameters that result in a filled center.

    • pheelicks

      Hmm, that is strange. If you just copied the code it should come out the same. The spirals are drawn from the outside in, ie they spiral inwards. The number of iterations is controlled by the length of the for loop, in my examples that number is 10000. If this number is lower it would result in a spiral with an empty center. Perhaps try increasing that parameter?

    • AreaMan

      I have the same effects. I bumped it up to 100,000 and no change.
      I’m on Ubuntu Linux 13.10, 64-bit. Hollow centres.

      • pheelicks

        Try playing around with the other parameters, especially the size of the dir Vector. If this is bigger, the spiral lasts longer, you’ll also need to increase the rotation on each step to keep the spiral as tightly wound. To make the picture above I used a larger Canvas and then rescaled it down to make it appear less blocky, which will also reduce the size of the holes. That said, I can’t figure out what particular combination I used to make the image in the blog – sorry!

  • Craig Woodward

    The DrawLine function needs to be altered to at least draw a line with a limit of 1, otherwise the scaling makes it go below 1 and stop drawing prematurely. When you make that change it fills in the circle given enough iterations.