Transforms in Pycairo - translate


Martin McBride, 2019-09-27
Tags transform translation user space device space
Categories pycairo

As we have seen in earlier examples, Pycairo uses a coordinate system to create images. In this section we will look at how we can use transforms to make this system more flexible.

We will see how to apply:

  • Translation, to move items around the page (this article).
  • Scaling, to change the size of items.
  • Rotation, to change the orientation of items.

User space and device space

Pycairo actually has two separate coordinate spaces user space and device space. You do not usually need to think about them separately, because by default they are identical.

Here is what user space and device space look like when we are creating a 600 by 400 pixel image:

Device space is a fixed space that represents the output image. The space is 600 by 400, and represents the actual pixels in the output image.

User space is a the space where you actually define the shapes you want to draw. User space isn't measured in pixels, it is measured in units, where a unit can be changed to be whatever you like. When Pycairo draws a shape, it maps the coordinates from user space to device space, to calculate where to place the shape.

With the default mapping, this is a very simple calculation. For example, this code defines a rectangle in user space at (100, 100). The rectangle is 100 units wide and 200 units high:

ctx.rectangle(100, 100, 100, 200)
ctx.set_source_rgb(0, 0, 1)
ctx.fill()

This defines the rectangle in user space. Pycairo then draws the rectangle in device space. Because of the default mapping, the rectangle is drawn in exactly the same size and place in device space:

Translation

The translate function moves the position of user space relative to device space. For example, we can call translate like this:

ctx.translate(200, 100)
ctx.rectangle(100, 100, 100, 200)
ctx.set_source_rgb(0, 0, 1)
ctx.fill()

User space is shifted 200 units to the right and 100 units down. It will look like this:

As before, we have drawn our rectangle at location (100, 100). But remember that the coordinates are specified in user space, then transformed into device space to actually draw on the page. Location (100, 100) in user space maps onto (300, 200) in device space due to the translation.

Notice that device space is unchanged - device space never changes, it represents the pixels of the final image file.

Benefits of transforms

Of course, instead of using translate we could simply have modified our code to draw the rectangle in a different place:

ctx.rectangle(300, 200, 100, 200)
ctx.set_source_rgb(0, 0, 1)
ctx.fill()

This would have the same effect.

However, it is often easier to use translation because it simplifies your code. For example, if we had code to draw a complex shape that we wanted to duplicate in different places on the page, we would need to write our drawing code to offset all parts of the shape when we wanted to draw it in a different place. But if we use transforms, we just need a single call to translate and our original drawing code will automatically draw the shape in a different place.

Transforms can also be used to scale or rotate objects, or even perform more complex transformations, which would be very difficult to implement without transforms.

Undoing a transform

What if we have drawn a particular shape using a translation, but then we want to draw some more shapes using the default translation? There are several ways to do this. The most obvious is to apply the reverse translation:

ctx.translate(200, 100)
ctx.rectangle(100, 100, 100, 200)
ctx.set_source_rgb(0, 0, 1)
ctx.fill()
ctx.translate(-200, -100)

This code draws the rectangle with the translation (200, 100) applied. It then translates user space through (-200, -100), which puts it back in its original position, so any further drawing will take place in the original coordinates. This works, but it can get a bit complicated if you have complex transformations.

A better way is to use save and restore. Here is how it works:

ctx.save()
ctx.translate(200, 100)
ctx.rectangle(100, 100, 100, 200)
ctx.set_source_rgb(0, 0, 1)
ctx.fill()
ctx.restore()

We call save before the initial translate. This saves the state of the context, which includes any transformations currently applied.

When we have finished drawing, we call restore, which sets the context back to the state it was in when save was first called. Any transformations will be undone.

If you found this article useful, you might be interested in the book Computer Graphics in Python or other books by the same author.

Prev

Popular tags

2d arrays abstract data type alignment and animation arc array arrays bezier curve built-in function callable object circle classes close closure cmyk colour comparison operator comprehension context context manager conversion creational pattern data types design pattern device space dictionary drawing duck typing efficiency else encryption enumerate fill filter font font style for loop function function composition function plot functools game development generativepy tutorial generator geometry gif gradient greyscale higher order function hsl html image image processing imagesurface immutable object index inner function input installing iter iterable iterator itertools l system lambda function len line linspace list list comprehension logical operator lru_cache magic method mandelbrot mandelbrot set map monad mutability named parameter numeric python numpy object open operator optional parameter or partial application path polygon positional parameter print pure function pycairo radial gradient range recipes rectangle recursion reduce rgb rotation scaling sector segment sequence singleton slice slicing sound spirograph sprite square str stream string stroke subpath symmetric encryption template text text metrics tinkerbell fractal transform translation transparency tuple turtle unpacking user space vectorisation webserver website while loop zip