Device and user space in generativepy.drawing

By Martin McBride, 2020-08-30
Tags: generativepy tutorial device space user space affine transform transform
Categories: generativepy generativepy tutorial

In the Simple Image rticle, we used generativepy to create a simple image of a rectangle.

We used pixel coordinates to draw the rectangle. That is to say, we created a rectangle that had a size of 250 by 200 units, and the final image contained a rectangle that was exactly 250 by 200 pixels in size.

That isn't the only option. As an alternative, we can scale our drawing space. For example, we can scale the space by a factor of 100. We can then draw a rectangle that is 2.5 units by 2 units, and it will appear on the image at 250 by 200 pixels.

This type of scaling has a couple of advantages:

  • We can use the most convenient units when we make our drawing. For example, if we wanted to draw a right-angled triangle with sides of 3cm, 4cm and 5cm, we could draw it using lengths of 3, 4 and 5 units. We could then scale it to be whatever pixel size we wanted on the final image.
  • If we need to create the same image at different pixel sizes, we can do it very easily. For example, we might want a 400-pixel wide image of the triangle for a web page, but a 4000-pixel wide image for a book illustration. We can do this using the same drawing code, just changing the scaling.

This article mainly covers device and user space for vector drawing (ie the drawing and movie modules). There is a section at the end that covers user space for the bitmap module.

Device and user space.

generativepy uses the concept of device space and user space to do this scaling (actually it makes direct use of the Pycairo implementation).

Device space is fixed, and always represents the pixel coordinates of the final output image.

User space maps on to device space using a transformation that we can choose in our code.

Whenever we draw anything, it is always drawn in user space coordinates, and the coordinates get mapped onto device space using the current transform. However, the initial transform is 1:1, so if we never change the transform it appears as if we are drawing in device space.

The transformation between user and device space can include scaling, translation, rotation, mirroring, and shearing in any combination (in fact it can be any affine transformation). You can change the current transform at any time.

Initialising the user space transform

To make things a bit easier, generativepy allows you to specify the two most commonly used transforms - scaling and translation - in the setup function of the drawing module.

Whether you use this feature or not, you can still make further changes to the transform by Transform objects.

Scaling user space

In this example, we will see how to scale user space. We will use the previous example of an orange rectangle.

Here is the code:

from generativepy.drawing import make_image, setup
from generativepy.color import Color
from generativepy.geometry import Rectangle, Circle

def draw_rect(ctx, width, height, frame_no, frame_count):
    setup(ctx, width, height, width=5, background=Color(0.4))
    color = Color(1, 0.5, 0)
    Rectangle(ctx).of_corner_size((1, 1.5), 2.5, 2).fill(color)

make_image("user-scale.png", draw_rect, 500, 400)

The main difference here is that we have added a width=5 parameter to the setup function.

This tells generativepy to set the user space width to 5 units. Since the device width is 500 pixels, this establishes a user space scaling of 100 - that is, 1 unit in user space maps onto 100 pixels in device space.

The relationship between device and user space is shown here:

Here is the image it produces (which is identical to the image created in the previous tutorial, even though the rectangle is expressed in a different user space).

The setup function accepts either a width, or height, or both.

  • Since the pixel size is 500 by 400, setting width to 5 creates a scale factor of 100, so the user space of 5 by 4 maps onto device space 500 by 400.
  • If you prefer you could set the height to 4 instead. This will also create a scale factor of 100 and has exactly the same effect as setting width to 5.
  • You can set both width and height. This creates the possibility of having different scale factors in the x and y directions. For example, width=5 and height=8 creates a scale factor of 100 in the x-direction but 50 in the y-direction. User space of 5 by 8 maps onto device space 500 by 400. This means that objects are "squashed" in the y-direction, so for example, if you draw a square it will appear as an elongated rectangle.

Of course, you don't need to stick to integer scale factors. For example, you could take a 500 by 400 device space and select a width of 857 if you wanted, which would create a scale factor of about 0.583.

Changing the pixel size of the output image

Suppose we wanted to create a different-sized image. Say 2635 by 2108 pixel (these dimensions happen to be in the ratio 5:4, like the original image). All we need to change is the make_image call:

make_image("rectangle-user.png", draw_rect, 2635, 2108)

This will create an image that has been perfectly scaled up to the new size, with no other code changes required.

Scaling and translating user space

In this example, we will scale and translate user space.

By default, the origin (0, 0) is always in the top left of the image. That isn't always what you want, and it can be changed like this:

from generativepy.drawing import make_image, setup
from generativepy.color import Color
from generativepy.geometry import Rectangle, Circle

def draw_circle(ctx, width, height, frame_no, frame_count):
    setup(ctx, width, height, width=4,
          startx=-2, starty=-2, background=Color(0.4))    
    color = Color("magenta")
    Circle(ctx).of_center_radius((0, 0), 1.5).fill(color)

make_image("circle-user.png", draw_circle, 400, 400)

This time the pixel size is 400 square, and we have set width=4 so our user space is 4 units square.

But notice that we have also set startx=-2 and starty=-2, which means that the whole user-space is shifted by -2 in the x and y directions. The top left of the image is now (-2, -2), which means that the origin (0, 0) is now at the centre of the image, like this:

This means that when we draw a circle centred on the origin, it is right in the centre of the image:

User space with the bitmap and nparray modules

The bitmap module uses the Python imaging library (PIL) to manipulate bitmap images, rather than using Pycairo to manipulate vector graphics.

PIL works exclusively in pixel coordinates and does not have any concept of a user space. However, generativepy provides a Scaler class that can perform similar calculations. Here is how it is used:

from generativepy.bitmap import Scaler

scaler = Scaler(300, 200, width=3, height=2, startx=-1.5, starty=-1)

print(scaler.user_to_device(1, .5))   # (250, 150)
print(scaler.device_to_user(20, 50))  # (-1.3, -.5)

The scaler is initialised with a pixel size of 300 by 200, and a user size of 3 by 2 (a scale factor of 100), with a user space offset of (-1.5, -1), like this:

The point (1, .5) in user space is converted to device space as ( (1+1.5)100, (0.5+1)100 ), or (250, 150).

The point (20, 50) in device space is converted to user space as ( 20/100 - 1.5, 50/100 -1 ) or (-1.3, -.5).

The scaler class can also be used with the nparray module.

See also

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

Join the PythonInformer Newsletter

Sign up using this form to receive an email when new content is added:

Popular tags

2d arrays abstract data type alignment and angle animation arc array arrays bar chart bar style behavioural pattern bezier curve built-in function callable object chain circle classes clipping close closure cmyk colour combinations comparison operator comprehension context context manager conversion count creational pattern data science data types decorator design pattern device space dictionary drawing duck typing efficiency ellipse else encryption enumerate fill filter font font style for loop formula function function composition function plot functools game development generativepy tutorial generator geometry gif global variable gradient greyscale higher order function hsl html image image processing imagesurface immutable object in operator index inner function input installing iter iterable iterator itertools join l system lambda function latex len lerp line line plot line style linear gradient linspace list list comprehension logical operator lru_cache magic method mandelbrot mandelbrot set map marker style matplotlib monad mutability named parameter numeric python numpy object open operator optimisation optional parameter or pandas partial application path pattern permutations pie chart pil pillow polygon pong positional parameter print product programming paradigms programming techniques pure function python standard library radial gradient range recipes rectangle recursion reduce regular polygon repeat rgb rotation roundrect scaling scatter plot scipy sector segment sequence setup shape singleton slice slicing sound spirograph sprite square str stream string stroke structural pattern subpath symmetric encryption template tex text text metrics tinkerbell fractal transform translation transparency triangle truthy value tuple turtle unpacking user space vectorisation webserver website while loop zip zip_longest