A screen is a grid of pixels
Computer graphics is the craft of making a picture from numbers. Everything on a screen is a grid of pixels, and a program draws by choosing which pixels to colour.
In this lesson you place a pixel with two coordinates, then draw a straight line the way graphics software does, by stepping along the grid.
By the end of this lesson
Note
I can place pixels on a grid with x and y coordinates and draw a straight line by stepping.
Key words
| Word | What it means | Example |
|---|---|---|
| Pixel | one dot of the picture | |
| Coordinates | two numbers, x and y, that name a pixel | (3, 2) |
| Origin | the corner where x and y are both 0 | |
| Slope | how far a line rises for each step across | |
| Rasterise | to turn a shape into pixels |
The cells students fill in are left blank in this preview.
A pixel has an x and a y
A picture is a grid of pixels. The origin, where x and y are both 0, is the top left corner. The number x counts pixels across to the right, and y counts pixels down.
Down is the part that surprises people, because a maths graph counts y upwards. A screen starts at the top and works down, so row 0 is the top row.
Worked example: pixels in a picture
| Pixel | x (across) | y (down) | Position in a flat list |
|---|---|---|---|
| Top left | 0 | 0 | 0 |
| Top right | 5 | 0 | 5 |
| Second row, third pixel | 2 | 1 | 8 |
| Bottom right | 5 | 3 | 23 |
The cells students fill in are left blank in this preview.
A grid can be one flat list
Memory is a single row of boxes, so a picture is often stored row after row in one list. The pixel at x, y sits at position y times the width, plus x.
In the table, the second row starts at position 6, because the first row used 6 places. Its third pixel, x of 2, is at 6 + 2 = 8.
Make a tiny picture with Pillow
docker exec jupyter python3 -c "from PIL import Image; p = Image.new('RGB', (4, 3), (10, 20, 30)); p.putpixel((3, 2), (255, 0, 0)); print(p.size, p.getpixel((0, 0)), p.getpixel((3, 2)))"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Pillow makes a picture and sets a pixel
The Run above makes a picture 4 pixels wide and 3 high, filled with the colour (10, 20, 30). It colours the pixel at x of 3 and y of 2 red, then reads two pixels back.
It prints (4, 3) (10, 20, 30) (255, 0, 0). Pillow takes the pixel as (x, y), across first, the same order as the table.
A line is found by stepping along x
A straight line has a slope: the rise in y divided by the run in x. To rasterise it, step x one pixel at a time and work out where the line is in y.
The line rarely lands exactly on a pixel, so round y to the nearest whole number. Exactly halfway, we round up.
Worked example: a line in pixels
| x | Exact y | Rounded y |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0.67 | 1 |
| 2 | 1.33 | 1 |
| 3 | 2 | 2 |
| 4 | 2.67 | 3 |
| 5 | 3.33 | 3 |
| 6 | 4 | 4 |
The cells students fill in are left blank in this preview.
List the pixels of that line
docker exec jupyter python3 -c "print([(x, (8 * x + 6) // 12) for x in range(7)])"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Whole numbers avoid rounding surprises
The Run prints [(0, 0), (1, 1), (2, 1), (3, 2), (4, 3), (5, 3), (6, 4)], the same pixels as the table.
It uses only whole numbers: y is (2 times rise times step, plus run) divided by (2 times run), with //. Floating point numbers can land a hair under 0.5, and then a pixel moves.
A common mistake
Warning
Many people think stepping along x works for every line. It works only when the line is more across than up. A steep line rises faster than it runs, so one step in x jumps several rows and leaves gaps. For a steep line, step along y instead.
Draw a line in text
width = 8
height = 5
picture = []
for y in range(height):
picture.append(["."] * width)
# A line from (0, 0) to (7, 4). Step along x and work out y each time.
x0 = 0
y0 = 0
x1 = 7
y1 = 4
dx = x1 - x0
dy = y1 - y0
for x in range(x0, x1 + 1):
y = y0 + (2 * dy * (x - x0) + dx) // (2 * dx)
picture[y][x] = "#"
for row in picture:
print("".join(row))
# Change y1 to 2, then to 0, and press Run. The line gets flatter.
Save a line as a real picture
docker exec jupyter python3 -c "from PIL import Image; p = Image.new('L', (8, 5), 0); [p.putpixel((x, (8 * x + 7) // 14), 255) for x in range(8)]; p.save('line.png'); print(p.size)"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Open line.png in JupyterLab
The Run prints (8, 5) and saves line.png in the lab's work folder. Open it from the JupyterLab file list on the left.
You see a black picture, 8 by 5 pixels, with a thin white diagonal from the top left corner down to the bottom right. It is tiny, so zoom in to see the separate pixels.