ch02.md 5.6 KB

Chapter 2. Drawing a line

A line is a series of adjacent points. We will limit ourselves to two types of lines: Horizontal and vertical lines

When drawing these lines point by point, just one coordinate changes,
while the other stays constant.

A point is an object consisting of x and y coordinates, and we define it and use it like this:

  import pixels
  type
    Point = object # 1
       x: int      # 2
       y: int
  var p = Point(x: 5, y: 7) # 3
  putPixel(p.x, p.y) # 4
  • 1 Defining a Point as an object.
  • 2 Coordinates x and y are integers.
  • 3 Variable p is an instance of a Point with coordinates (5, 7).
  • 4 We can access fields of an object with a dot. In this case, we access the x and y coordinates of the point p with p.x and p.y.

2.1. Drawing horizontal and vertical lines

A line can be defined by a starting and an ending point, or by a starting point and its length and a direction. We’ll show both approaches.

2.1.1. Drawing a line using one point, length, direction

We can define a line by its starting point, its length, and its direction. Having these two information, we can then draw a line pixel by pixel.

A line length is a positive integer (a zero would be just a point, not a line; a negative value for the length is physically impossible). A direction can be either horizontal or vertical.

This means that we will limit ourselves to always create lines from left to right (we specify the leftmost point of a line and then increase the x coordinate) or top to bottom (we increase the y coordinate).

We can start by creating two similar procs: one for drawing horizontal lines, and one for drawing vertical lines.

  import pixels
  type
    Point = object
       x: int
       y: int
  proc drawHorizontalLine(start: Point; length: Positive) = # 1
    for delta in 0 .. length:  # 2
       putPixel(start.x + delta, start.y) # 3
  proc drawVerticalLine(start: Point; length: Positive) =
    for delta in 0 .. length:
       putPixel(start.x, start.y + delta) # 4
  let a = Point(x: 60, y: 40)
  drawHorizontalLine(a, 50) # 5
  drawVerticalLine(a, 30)   # 6
  • 1 The length is defined as Nim’s built-in Positive type, which is a subtype of int, containing just positive numbers.
  • 2 The p..q is an (inline) iterator, which iterates from p to q (both ends are inclusive) in the ascending order, i.e. p must be smaller than q for the iteration to happen. In this case we’re iterating from zero to length.
  • 3 For a horizontal line, the y coordinate remains constant, and in each iteration step we draw a pixel one pixel to the right from the previous step.
  • 4 Similarly, for a vertical line, the x coordinate stays constant, and we increase the y coordinate to draw a new pixel.
  • 5 This will draw a horizontal line from (60, 40) to (110, 40).
  • 6 This will draw a vertical line from (60, 40) to (60, 70).

We can have one proc, drawLine, which will draw both horizontal and vertical lines, depending on the provided user’s parameter.

To model a set of a limited amount of values, we use the enum type. The benefits of this choice will be visible later, when we will see one of the features of Nim: exhaustiveness checking.

We can now extend the previous example with these lines:

  type
    Direction = enum   1
       Horizontal      2
       Vertical
  proc drawLine(start: Point; length: Positive; direction: Direction) = 3
    case direction   4
    of Horizontal:
       drawHorizontalLine(start, length) 5
    of Vertical:
       drawVerticalLine(start, length)
  drawLine(a, 50, Horizontal) 6
  drawLine(a, 30, Vertical)
  • 1 A Direction is an enumeration (enum) with two possible values.
  • 2 The values of a Direction are: Horizontal and Vertical.
  • 3 We now have an extra parameter of the Direction type.
  • 4 This is a case statement. It is similar to if statement, but more powerful. Not only we get separate branches for each case, we also have exhaustiveness checking: we cannot by accident leave out some cases. Concretely, we must account for all the enum values.
  • 5 We delegate the task of drawing lines to the previously defined procs.
  • 6 This will draw the same lines as in the example before.

2.1.2. Drawing a line by specifying start and end

If we use two points to define a line, we don’t have to know (or calculate) the line length, and the direction can be inferred from the relation of these two points: if they have the same y coordinate, we are drawing a horizontal line, and a vertical line between two points with the same x coordinate.

We will always iterate from a point with a smaller value of the changing coordinate, but we will allow our users to specify the points in any order.

We can achieve this with recursion.

  proc drawHorizontalLine(a, b: Point) = # 1
    if b.x < a.x:
      drawHorizontalLine(b, a) # 2
    else:
      for x in a.x .. b.x:
        putPixel(x, a.y) # 3
  proc drawVerticalLine(a, b: Point) =
    if b.y < a.y:
      drawVerticalLine(b, a)
    else:
      for y in a.y .. b.y:
        putPixel(a.x, y)
  let
    p = Point(x: 20, y: 20)
    q = Point(x: 50, y: 20)
    r = Point(x: 20, y: -10)
  drawHorizontalLine(p, q) # 4
  drawVerticalLine(p, r)   # 5
  • 1 Even though we already have a proc of the same name, there is no overriding: they are different procs because they have different parameters.
  • 2 Notice the reversed order of the arguments.
  • 3 The x is a changing variable, while the y remains constant.
  • 4 Draws a horizontal line between (20, 20) and (50, 20).
  • 5 Since r.y is smaller than p.y, this will in turn call drawVerticalLine(r, p) and draw a vertical line between (20, -10) and (20, 20).