Text in generativepy


Martin McBride, 2021-05-01
Categories generativepy generativepy tutorial

The generativepy drawing module allows you to add text to your images, using the Text class.

You can create text using any font on your system, and of course, choose whatever size and colour you wish.

Text behaves like any other shape, so as well as filling the text you can also:

  • Stroke the text, using solid or dashed lines.
  • Create a Path based on the text shape, that can be reused.
  • Use the text shape as a clip path.

There are also various ways to align text and some useful offsetting functions.

Text font, size and colour

This sample code creates text in various styles:

from generativepy.drawing import make_image, setup
from generativepy.color import Color
from generativepy.geometry import Text


def draw_alpha(ctx, pixel_width, pixel_height, frame_no, frame_count):
    setup(ctx, pixel_width, pixel_height, background=Color(1))

    Text(ctx).of("Filled Times", (100, 100)).font("Times").size(40).fill(Color('blue'))
    Text(ctx).of("Filled Arial", (100, 150)).font("Arial").size(40).fill(Color('red'))
    Text(ctx).of("Small", (100, 180)).font("Arial").size(20).fill(Color('darkgreen'))
    Text(ctx).of("Large", (100, 240)).font("Arial").size(60).fill(Color('magenta'))
    Text(ctx).of("Stroke", (100, 310)).font("Arial").size(60).stroke(Color('black'), 4)
    Text(ctx).of("Fill Stroke", (100, 380)).font("Arial").size(60)\
                                           .fill(Color('blue')).stroke(Color('red'), 2)
    Text(ctx).of("Dashed", (100, 450)).font("Arial").size(60).stroke(Color('black'), 3, dash=[4])


make_image("text-drawing.png", draw_alpha, 500, 500)

Here is the result:

The top two lines of text are drawn with this code:

    Text(ctx).of("Filled Times", (100, 100)).font("Times").size(40).fill(Color('blue'))
    Text(ctx).of("Filled Arial", (100, 150)).font("Arial").size(40).fill(Color('red'))

We set up a basic text object like this:

  • Text(ctx) creates the object
  • of(text, pos) sets the text and position. For example, in the first line, we set the text to "Filled Times" and the position to (100, 100). This means that the baseline of the start of the text is at that position.
  • font(fontname) selects the font, for example Times.
  • size(40) sets the size of the text. The value 40 makes the text approximately 40 units high. In this case, since we are using default scaling, the size is measured in pixels. The exact size of the text depends on the font you use.
  • fill fills the text in the selected colour.

So the code above displays "Filled Times" in Times font in blue, and "Filled Arial" in Arial font in red.

The next code changes the size of the text:

    Text(ctx).of("Small", (100, 180)).font("Arial").size(20).fill(Color('darkgreen'))
    Text(ctx).of("Large", (100, 240)).font("Arial").size(60).fill(Color('magenta'))

All we have done here is change the size. The text "Small" is displayed in 20 pixel high text, the text "Large" is displayed in 20 pixel high text.

Finally, we can stroke the text:

    Text(ctx).of("Stroke", (100, 310)).font("Arial").size(60).stroke(Color('black'), 4)
    Text(ctx).of("Fill Stroke", (100, 380)).font("Arial").size(60)\
                                           .fill(Color('blue')).stroke(Color('red'), 2)
    Text(ctx).of("Dashed", (100, 450)).font("Arial").size(60).stroke(Color('black'), 3, dash=[4])

As far as generativepy is concerned, text is just a shape. The first case strokes the text "Stroke" using a black, 4-pixel outline.

The second case fills and strokes the text "Fill Stroke".

The third case strokes the text "Dashed", but it sets a dash array

Alignment

For any piece of text, we can mark several key positions:

Horizontally:

  • Left is the position of the leftmost point that is marked by the text.
  • Right is the position of the rightmost point that is marked by the text.
  • Centre is halfway between the left and right positions.

Vertically:

  • Top is the position of the highest point that is marked by the text.
  • Bottom is the position of the lowest point that is marked by the text.
  • Middle is halfway between the top and bottom positions.
  • Baseline is the line position of the text. This is the line the text sits on. The tails of characters such as g or q go below the baseline.

We can align the text to any position, horizontally and vertically. The default alignment is left and baseline. Here is an example of different types of alignment:

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

'''
Create text using the geometry module.
'''

def draw(ctx, width, height, frame_no, frame_count):
    setup(ctx, width, height, width=5, background=Color(0.8))

    Text(ctx).of("Left", (0.5, 0.5)).font("Times").size(0.2).align_left().align_baseline().fill(Color('blue'))
    Text(ctx).of("Aligned", (0.5, 0.7)).font("Times").size(0.2).align_left().align_baseline().fill(Color('red'))
    Text(ctx).of("Text", (0.5, 0.9)).font("Times").size(0.2).align_left().align_baseline().fill(Color('blue'))

    Text(ctx).of("Centre", (2.5, 0.5)).font("Times").size(0.2).align_center().align_baseline().fill(Color('blue'))
    Text(ctx).of("Aligned", (2.5, 0.7)).font("Times").size(0.2).align_center().align_baseline().fill(Color('red'))
    Text(ctx).of("Text", (2.5, 0.9)).font("Times").size(0.2).align_center().align_baseline().fill(Color('blue'))

    Text(ctx).of("Right", (4.5, 0.5)).font("Times").size(0.2).align_right().align_baseline().fill(Color('blue'))
    Text(ctx).of("Aligned", (4.5, 0.7)).font("Times").size(0.2).align_right().align_baseline().fill(Color('red'))
    Text(ctx).of("Text", (4.5, 0.9)).font("Times").size(0.2).align_right().align_baseline().fill(Color('blue'))

    Circle(ctx).of_center_radius((1.9, 2), 0.02).fill(Color(0, 0, 1))
    Text(ctx).of("gTop", (2, 2)).font("Times").size(0.2).align_left().align_top().fill(Color('black'))

    Circle(ctx).of_center_radius((1.9, 2.5), 0.02).fill(Color(0, 0, 1))
    Text(ctx).of("gMid", (2, 2.5)).font("Times").size(0.2).align_left().align_middle().fill(Color('black'))

    Circle(ctx).of_center_radius((1.9, 3), 0.02).fill(Color(0, 0, 1))
    Text(ctx).of("gBase", (2, 3)).font("Times").size(0.2).align_left().align_baseline().fill(Color('black'))

    Circle(ctx).of_center_radius((1.9, 3.5), 0.02).fill(Color(0, 0, 1))
    Text(ctx).of("gBottom", (2, 3.5)).font("Times").size(0.2).align_left().align_bottom().fill(Color('black'))

make_image("/tmp/geometry-text.png", draw, 500, 500)

Here is the result:

You can use the align function as an alternative:

    Text(ctx).of("Centre", (2.5, 0.5)).font("Times").size(0.2).align(CENTER, BASELINE).fill(Color('blue'))

Flip

The setup function in the drawing module has an option to flip the y-axis so that y-coordinates increase from bottom to top (rather than the default top to bottom). This means the coordinate system matches the system normally used in mathematics.

The only problem with flipping the y-axis is that it causes text to be drawn upside down.

If you call the flip method on a Text objects, it will cause the text to be flipped too, so the text will appear correctly.

Offset

Text is often used to label particular points or lines on a diagram, and you will usually want the text to be close to a particular point but not right on top of it.

You can achieve this by changing the position of the point. However, the offset method is a convenient alternative. Here is an example:

from generativepy.drawing import make_image, setup
from generativepy.color import Color
from generativepy.geometry import Text, Circle, Line
import math


def draw_alpha(ctx, pixel_width, pixel_height, frame_no, frame_count):
    setup(ctx, pixel_width, pixel_height, background=Color(1))

    a = (100, 100)
    Circle(ctx).of_center_radius(a, 5).fill(Color('red'))
    Text(ctx).of("A", a).font("Arial").size(40).offset(20, 30).fill(Color(0))

    b = (300, 100)
    angle = math.pi*3/4
    x = (b[0]+150*math.cos(angle), b[1]+150*math.sin(angle))
    Line(ctx).of_start_end(b, x).stroke(Color('orange'), 4, dash=[5])
    Circle(ctx).of_center_radius(b, 5).fill(Color('red'))
    Text(ctx).of("B", b).font("Arial").size(40).offset_angle(angle, 100).fill(Color(0))

    c = (500, 100)
    d = (600, 50)
    Line(ctx).of_start_end(c, d).stroke(Color('orange'), 4, dash=[5])
    Circle(ctx).of_center_radius(c, 5).fill(Color('red'))
    Circle(ctx).of_center_radius(d, 5).fill(Color('blue'))
    Text(ctx).of("C", c).font("Arial").size(40).offset_towards(d, 30).fill(Color(0))


make_image("text-offset.png", draw_alpha, 700, 200)

Here is the result:

The first block draws the letter A:

    a = (100, 100)
    Circle(ctx).of_center_radius(a, 5).fill(Color('red'))
    Text(ctx).of("A", a).font("Arial").size(40).offset(20, 30).fill(Color(0))

Here we draw a red circle at point a. We add the text letter A with a position a but an offset of (20, 30), which puts the letter slightly to the right and below the point.

We then draw the letter B:

    b = (300, 100)
    angle = math.pi*3/4
    x = (b[0]+150*math.cos(angle), b[1]+150*math.sin(angle))
    Line(ctx).of_start_end(b, x).stroke(Color('orange'), 4, dash=[5])
    Circle(ctx).of_center_radius(b, 5).fill(Color('red'))
    Text(ctx).of("B", b).font("Arial").size(40).offset_angle(angle, 100).fill(Color(0))

This time we draw a red circle at point b. We add the text letter B at position b but use offset_angle to offset it. We place it at a distance of 100 from b, at an angle of 2 radians (which is about 120 degrees). The orange dashed line shows the angle, the text is shifted in that direction.

Finally we then draw the letter C:

    c = (500, 100)
    d = (600, 50)
    Line(ctx).of_start_end(c, d).stroke(Color('orange'), 4, dash=[5])
    Circle(ctx).of_center_radius(c, 5).fill(Color('red'))
    Circle(ctx).of_center_radius(d, 5).fill(Color('blue'))
    Text(ctx).of("C", c).font("Arial").size(40).offset_towards(d, 30).fill(Color(0))

This time we draw a red circle at point c. We add the text letter C at position c but use offset_towards to offset it. We place it at a distance 50 from c, in the direction of point d = (600, 50). The line from c to d is shown in orange.

Paths and clipping

You can create a path from some text. That path can be re-drawn multiple times, using different sizes, colours and styles if you wish. For an example, see the Path tutorial.

You can also use text as a clipping path. In that case, anything you draw will be clipped to the text. This is a good way to create text with a fancy fill, see the clipping tutorial.

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 behavioural pattern bezier curve built-in function callable object chain circle classes close closure cmyk colour combinations comparison operator comprehension context context manager conversion count creational pattern data types design pattern device space dictionary drawing duck typing efficiency ellipse 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 linear gradient 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 pattern permutations polygon positional parameter print pure function python standard library radial gradient range recipes rectangle recursion reduce repeat rgb rotation scaling sector segment sequence setup shape singleton slice slicing sound spirograph sprite square str stream string stroke structural pattern subpath symmetric encryption template text text metrics tinkerbell fractal transform translation transparency triangle tuple turtle unpacking user space vectorisation webserver website while loop zip