[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

Re: [pygame] Module I made for drawing text with pygame



gummo.py

Do with it as ya like. But ya probably would like it best in the ptext demo directory. :)

Gumm

On 3/16/2015 11:01 PM, bw wrote:
Yes...I have acquired clunx capacitor 518400. No one can stop me now. And with this new font module I will win the Pyweek!

Oh, uh. Sorry, got a little excited. :)

I really like the convenience of this module, Christopher. I look forward to playing with it some. Some of those effects and alignments are a bit of a pain to code from scratch. The interface is as easy to use as an auto-clunker!

Very nice.

Those are some handsome fonts.

Gumm

On 3/16/2015 9:13 PM, Christopher Night wrote:
Please let me know if you have any feedback or criticism on the module I have written here:
https://github.com/cosmologicon/pygame-text

This module provides what is, for me, a more convenient interface to drawing text with pygame.font. It also provides a few effects, such as outlines and drop shadows. I plan to use this module in future PyWeeks.

Thanks,
Christopher


#!/usr/bin/env python

"""gummo.py - An interactive demo by Gummbum for the ptext module

Usage:
    python gummo.py -h
"""

# Note: My SysFont choices are wimpy in Windows 7, and ptext does not support SysFonts. If SysFonts ever become a choice
# the behavior can be enablded at Settings.enable_sysfonts.

import argparse
import re

import pygame
from pygame.locals import *

import ptext


class Settings(object):
    screen_width, screen_height = 1204, 768
    gui_font = 'Roboto_Condensed'
    gui_font_size = 16
    use_sysfonts = False
    font_dir = 'fonts'
    bg_image = None
    menu_labels = "Name Size FGColor GColor BGColor Outline Shadow Alpha Antialias TextAlign BGImage".split()
    font_names = ['Boogaloo', 'Bubblegum_Sans', 'CherryCreamSoda', 'Roboto_Condensed']
    text = """
To clunk, or not to clunk--that is the clunk:
Whether 'tis clookier in the clunk to clunk
The clunx and clunks of clooky clunk
Or to clunk clunks against a clunk of clunx
And by clunking clunk them.
"""
    enable_sysfonts = False  # enable sysfont behavior (needs sysfont support in ptext module)


def size(font, things):
    max_w = max_h = 0
    for text in [str(t) for t in things]:
        w, h = font.size(text)
        max_w = w if w > max_w else max_w
        max_h = h if h > max_h else max_h
    return max_w, max_h


def size_ints(font, start, end, step):
    return size(font, [str(i) for i in range(start, end, step) + [end]])


def size_floats(font, start, end, step, precision):
    def frange(start, end=None, step=1.0, precision=2):
        assert step >= 10 ** -precision
        if end is None:
            end = start + 0.0
            start = 0.0
        if step is None:
            step = 1.0
        precision = int(abs(precision))
        n = round(start, precision)
        while True:
            yield n
            n += step
            if step > 0 and n >= end:
                break
            elif step < 0 and n <= end:
                break
    return size(font, frange(start, end, step, precision))


def make_text_labels(font, texts):
    max_w, max_h = size(font, texts)
    return [Label(Rect(15, 15 + max_h * i, max_w, max_h), t) for i, t in enumerate(texts)]


class Label(object):

    def __init__(self, rect, text):
        self.rect = rect
        self.text = text

    def get_text(self):
        return self.text

    def draw(self, **ptext_args):
        ptext.drawbox(self.text, self.rect, anchor=(1, 0), **ptext_args)




def make_text_menu(font, texts, topleft):
    dim = size(font, texts)
    return TextThing(Rect(topleft, dim), texts)


class TextThing(object):

    def __init__(self, rect, text_list):
        self.rect = rect
        self.text_list = text_list
        self.text = text_list[0]

    def scroll(self, way, pos):
        if way == 'down':
            self.get_prev(pos)
        elif way == 'up':
            self.get_next(pos)

    def get_text(self):
        return self.text

    def get_next(self, pos=None):
        t = self.text_list.pop(0)
        self.text_list.append(t)
        self.text = self.text_list[0]
        return self.text

    def get_prev(self, pos=None):
        t = self.text_list.pop()
        self.text_list.insert(0, t)
        self.text = t
        return t

    def draw(self, **ptext_args):
        ptext.draw(self.text, self.rect.topleft, **ptext_args)

    def __str__(self):
        return self.get_text()


class IntThing(object):

    def __init__(self, rect, value, min_value, max_value, step=1):
        self.rect = rect
        self.value = value
        self.min_value = min_value
        self.max_value = max_value
        self.step = step
        self.text = str(value)

    def scroll(self, way, pos):
        if way == 'down':
            self.get_prev(pos)
        elif way == 'up':
            self.get_next(pos)

    def get_text(self):
        return self.text

    def get_next(self, pos=None):
        return self._increment(self.step)

    def get_prev(self, pos=None):
        return self._increment(-self.step)

    def _increment(self, step):
        n = self.value + step
        if n < self.min_value:
            n = self.min_value
        elif n > self.max_value:
            n = self.max_value
        self.value = n
        self.text = str(n)
        return self.text

    def draw(self, **ptext_args):
        ptext.draw(self.text, self.rect.topleft, **ptext_args)

    def __str__(self):
        return self.get_text()


class FloatThing(object):

    def __init__(self, rect, value, min_value, max_value, step=0.1, precision=1):
        self.rect = rect
        self.value = round(value, precision)
        self.min_value = min_value
        self.max_value = max_value
        self.step = step
        self.precision = precision
        self.fmt = '{:0.' + str(precision) + 'f}'
        self.text = self.fmt.format(self.value)

    def scroll(self, way, pos):
        if way == 'down':
            self.get_prev(pos)
        elif way == 'up':
            self.get_next(pos)

    def get_text(self):
        return self.text

    def get_next(self, pos=None):
        return self._increment(self.step)

    def get_prev(self, pos=None):
        return self._increment(-self.step)

    def _increment(self, step):
        n = self.value + step
        if n < self.min_value:
            n = self.min_value
        elif n > self.max_value:
            n = self.max_value
        self.value = round(n, self.precision)
        self.text = self.fmt.format(self.value)
        return self.text

    def draw(self, **ptext_args):
        ptext.draw(self.text, self.rect.topleft, **ptext_args)

    def __str__(self):
        return self.get_text()


def make_boolean_menu(font, value, topleft):
    dim = size(font, ['True', 'False'])
    return BooleanThing(Rect(topleft, dim), value)


class BooleanThing(object):

    def __init__(self, rect, value):
        self.rect = rect
        self.value = value
        self.text = str(value)

    def scroll(self, way, pos):
        if way == 'down':
            self.get_prev(pos)
        elif way == 'up':
            self.get_next(pos)

    def get_text(self):
        return self.text

    def get_next(self, pos=None):
        return self._increment()

    def get_prev(self, pos=None):
        return self._increment()

    def _increment(self):
        self.value = not self.value
        self.text = str(self.value)
        return self.text

    def draw(self, **ptext_args):
        ptext.draw(self.text, self.rect.topleft, **ptext_args)

    def __str__(self):
        return self.get_text()


def make_color_menu(font, value, topleft):
    x, y = size(font, [s * 3 for s in '0123456789'])
    x *= 3      # three for r, g, b
    x += 3 + 3  # three pixels between columns
    return ColorThing(Rect(topleft, (x, y)), value)


class ColorThing(object):

    def __init__(self, rect, color):
        self.rect = rect
        if isinstance(color, Color):
            self.color = color
        elif isinstance(color, str):
            self.color = Color(color)
        else:
            self.color = Color(*color)
        self.text = 'c.r c.g c.b'.format(c=self.color)
        x, y, w, h = self.rect
        self.rects = dict(zip('rgb', [Rect(x + 2 * n + w * n / 3.0, y, w / 3, h) for n in (0, 1, 2)]))

    def scroll(self, way, pos):
        if way == 'down':
            self.get_prev(pos)
        elif way == 'up':
            self.get_next(pos)

    def get_text(self):
        return self.text

    def get_next(self, mouse_pos):
        return self._increment(mouse_pos, 5)

    def get_prev(self, mouse_pos):
        return self._increment(mouse_pos, -5)

    def draw(self, **ptext_args):
        for c in 'rgb':
            rect = self.rects[c]
            ptext.draw('{:03d}'.format(getattr(self.color, c)), rect.topleft, **ptext_args)

    def _increment(self, mouse_pos, step):
        color = dict(zip('rgb', self.color[:3]))
        for c in 'rgb':
            if self.rects[c].collidepoint(mouse_pos):
                n = color[c] + step
                if n > 255:
                    n = 255
                elif n < 0:
                    n = 0
                setattr(self.color, c, n)
        self.text = 'c.r c.g c.b'.format(c=self.color)
        return self.get_text()

    def __str__(self):
        return self.get_text()


class Tweaker(object):

    def __init__(self):
        ptext.FONT_NAME_TEMPLATE = '{}/%s.ttf'.format(Settings.font_dir)

        self.screen = pygame.display.set_mode((Settings.screen_width, Settings.screen_height))
        self.screen_rect = self.screen.get_rect()
        self.text = Settings.text

        self.clock = pygame.time.Clock()
        self.running = False
        self.ticks_per_second = 60
        self.time_step = 1.0 / self.ticks_per_second

        self.clear_color = Color(35, 0, 30)
        self.label_ptext_args = dict(fontname=Settings.gui_font, color=(138, 64, 255))
        self.widget_color = Color('white')

        # Make the widgets.
        self.widget_ptext_args = dict(fontname=Settings.gui_font, fontsize=Settings.gui_font_size, color='white')
        self.widget_color = Color('white')
        self.widget_font = 'Roboto_Condensed'
        f = ptext.getfont(Settings.gui_font, Settings.gui_font_size)
        # Labels
        self.labels = make_text_labels(f, Settings.menu_labels)
        # Font name, size, fg, bg
        x, y = self.labels[0].rect.topright
        self.font_name = make_text_menu(f, Settings.font_names, (x + 10, y))
        self.font_size = IntThing(Rect((x + 10, self.labels[1].rect.y), size_ints(f, 1, 36, 1)), 36, 1, 64)
        self.fg_color = make_color_menu(f, Color('orange'), (x + 10, self.labels[2].rect.y))
        self.g_color = make_color_menu(f, Color('orange'), (x + 10, self.labels[3].rect.y))
        self.bg_color = make_color_menu(f, Color('black'), (x + 10, self.labels[4].rect.y))
        # Outline color, width
        y = self.labels[5].rect.y
        self.outline = make_boolean_menu(f, False, (x + 10, y))
        self.outline_width = FloatThing(
            Rect((self.outline.rect.right + 10, y), size_floats(f, 0.1, 5.0, 0.1, 1)),
            1.0, 0.1, 5.0)
        self.outline_color = make_color_menu(f, Color('black'), (self.outline_width.rect.right + 10, y))
        # Shadow color, width
        y = self.labels[6].rect.y
        self.shadow = make_boolean_menu(f, False, (x + 10, y))
        self.shadow_x = IntThing(Rect((self.shadow.rect.right + 10, y), size_ints(f, -9, 9, 1)), 1, -9, 9)
        self.shadow_y = IntThing(Rect((self.shadow_x.rect.right, y), size_ints(f, -9, 9, 1)), 1, -9, 9)
        self.shadow_color = make_color_menu(f, Color('black'), (self.shadow_y.rect.right + 5, y))
        # Alpha, Antialias, TextAlign
        self.alpha = FloatThing(Rect((x + 10, self.labels[7].rect.y), size_floats(f, 0, 1, 0.1, 1)), 1, 0, 1, 0.1)
        self.antialias = make_boolean_menu(f, True, (x + 10, self.labels[8].rect.y))
        self.textalign = make_text_menu(f, ['center', 'left', 'right'], (x + 10, self.labels[9].rect.y))
        # BG Image, scale and fill color
        y = self.labels[10].rect.y
        self.show_bg = make_text_menu(f, ['show', 'hide'], (x + 10, y))
        self.scale_bg = FloatThing(
            Rect((self.show_bg.rect.right + 5, y), size_floats(f, 0.2, 10, 0.2, 1)), 1, 0.2, 10, 0.2)
        self.fill_color = make_color_menu(f, Color(35, 0, 30), (self.scale_bg.rect.right + 5, y))
        # All the widgets
        self.widgets = [
            self.font_name, self.font_size, self.fg_color, self.g_color, self.bg_color,
            self.outline, self.outline_width,
            self.shadow, self.shadow_x, self.shadow_y, self.shadow_color,
            self.alpha, self.antialias, self.textalign, self.show_bg, self.scale_bg, self.fill_color]
        # Subsurface for rendering the text
        max_w = reduce(max, [w.rect.right for w in self.widgets]) + 5
        x, y, w, h = self.screen_rect
        self.text_surf = self.screen.subsurface((max_w, 0, w - max_w, h))
        self.text_rect = self.text_surf.get_rect()
        # Make a background image
        if Settings.bg_image:
            self.bg_surface = pygame.image.load(Settings.bg_image)
        else:
            surf = ptext.getsurf(
                'CLOOOKY!', 'CherryCreamSoda', 140, width=self.text_rect.w, color='skyblue', owidth=0.2, cache=False)
            self.bg_surface = pygame.transform.scale(surf, self.text_rect.size)
        self.bg_surface_rect = self.bg_surface.get_rect(center=self.text_rect.center)

        r = self.labels[-1].rect
        self.text_pos = r.x, r.bottom + 10

        self.mouse_rect = Rect(0, 0, 16, 16)
        self.mouse_image = pygame.Surface(self.mouse_rect.size)
        self.mouse_image.set_alpha(160)
        self.mouse_image.set_colorkey(Color('black'))
        pygame.draw.line(self.mouse_image, Color('yellow'), (0, 0), (0, 5), 1)
        pygame.draw.line(self.mouse_image, Color('yellow'), (0, 0), (5, 0), 1)
        pygame.draw.line(self.mouse_image, Color('yellow'), (0, 0), (15, 15), 1)
        pygame.mouse.set_visible(False)

        self.up_pressed = False
        self.down_pressed = False
        self.repeat_throttle = 0.0

        pygame.display.set_caption('Hover mouse, use wheel or cursor keys to change settings')

    def run(self):
        self.running = True
        while self.running:
            self.clock.tick(self.ticks_per_second)
            self.update()
            self.draw()

    def update(self):
        self.do_events()
        self.update_key_repeat()

    def update_key_repeat(self):
        self.repeat_throttle -= self.time_step
        if self.repeat_throttle > 0.0:
            return
        else:
            self.repeat_throttle = 1.0 / 7.0
        if self.up_pressed:
            self.do_scroll('up', pygame.mouse.get_pos())
        if self.down_pressed:
            self.do_scroll('down', pygame.mouse.get_pos())

    def draw(self):
        self.screen.fill(self.clear_color)
        if self.fill_color.color != self.clear_color:
            self.text_surf.fill(self.fill_color.color)
        self.draw_bg()
        self.draw_labels()
        self.draw_widgets()
        self.draw_text()
        self.draw_frame()
        self.screen.blit(self.mouse_image, self.mouse_rect)
        pygame.display.flip()

    def draw_bg(self):
        if self.show_bg.text == 'show':
            surf = self.bg_surface
            if self.scale_bg.value != 1.0:
                surf = pygame.transform.rotozoom(surf, 0, self.scale_bg.value)
            rect = surf.get_rect(center=self.text_rect.center)
            self.text_surf.blit(surf, rect)

    def draw_labels(self):
        for label in self.labels:
            label.draw(**self.label_ptext_args)

    def draw_widgets(self):
        for w in self.widgets:
            w.draw(**self.widget_ptext_args)

    def draw_text(self):
        rect = self.text_rect
        g_color = self.g_color.color if self.g_color.color != self.fg_color.color else None
        bg_color = self.bg_color.color if self.bg_color.color != (0, 0, 0) else None
        outline_width = self.outline_width.value if self.outline.value else None
        outline_color = self.outline_color.color if self.outline.value else None
        shadow = (self.shadow_x.value, self.shadow_y.value) if self.shadow.value else None
        shadow_color = self.shadow_color.color if self.shadow.value else None
        ptext.draw(
            self.text, self.text_pos, surf=self.text_surf,
            fontname=self.font_name.text, color=self.fg_color.color, gcolor=g_color, background=bg_color,
            fontsize=self.font_size.value, centerx=rect.centerx, centery=rect.centery,
            owidth=outline_width, ocolor=outline_color, shadow=shadow, scolor=shadow_color,
            alpha=self.alpha.value, antialias=self.antialias.value, textalign=self.textalign.text)

    def draw_frame(self):
        pygame.draw.rect(self.screen, Color('grey'), self.screen_rect, 3)
        pygame.draw.rect(self.text_surf, Color('grey'), self.text_rect, 3)

    def do_events(self):
        for e in pygame.event.get():
            if e.type == KEYDOWN:
                self.key_down(e)
            elif e.type == KEYUP:
                self.key_up(e)
            elif e.type == MOUSEBUTTONDOWN:
                self.mouse_button_down(e)
            elif e.type == MOUSEBUTTONUP:
                self.mouse_button_up(e)
            elif e.type == MOUSEMOTION:
                self.mouse_motion(e)
            elif e.type == QUIT:
                self.quit()

    def key_down(self, e):
        if e.key == K_UP:
            self.up_pressed = True
            self.repeat_throttle = 0.0
        elif e.key == K_DOWN:
            self.down_pressed = True
            self.repeat_throttle = 0.0
        elif e.key == K_ESCAPE:
            self.quit()

    def key_up(self, e):
        if e.key == K_UP:
            self.up_pressed = False
        elif e.key == K_DOWN:
            self.down_pressed = False

    def mouse_button_down(self, e):
        pass

    def mouse_button_up(self, e):
        if e.button == 4:
            self.do_scroll('up', e.pos)
        elif e.button == 5:
            self.do_scroll('down', e.pos)

    def do_scroll(self, way, pos):
        for menu in self.widgets:
            rect = menu.rect
            if rect.collidepoint(pos):
                menu.scroll(way, pos)
                break

    def mouse_motion(self, e):
        self.mouse_rect.topleft = e.pos

    def quit(self):
        self.running = False


def parse_args():
    """parse command line args and update global Settings"""
    screen_size = [Settings.screen_width, Settings.screen_height]
    parser = argparse.ArgumentParser(description='Interactive demo for the ptext module')
    parser.add_argument(
        '-b', '--bgimage', dest='bgimage', action='store', default=None, help='load a custom background image')
    parser.add_argument(
        '-d', '--fontdir', dest='fontdir', action='store', default='',
        help='custom font dir (default={})'.format(Settings.font_dir))
    parser.add_argument(
        '-g', '--geometry', dest='geometry', action='store', default=screen_size, type=int, nargs=2, metavar='N',
        help='size of window, as -g W H (default={} {})'.format(*screen_size))
    if Settings.enable_sysfonts:
        parser.add_argument(
            '-s', '--sysfonts', dest='sysfonts', action='store_true',
            help='use system fonts (default=use files in font dir)')
    parser.add_argument(
        '-t', '--text', dest='text', default=None, action='store', help='custom text to display (default=internal)')
    parser.add_argument(
        'fontfiles', nargs=argparse.REMAINDER, help='list of files in FONTDIR to use')
    args = parser.parse_args()
    if args.bgimage:
        Settings.bg_image = args.bgimage
    if args.geometry:
        Settings.screen_width, Settings.screen_height = args.geometry
    if args.fontdir:
        Settings.font_dir = args.fontdir
    if args.text:
        Settings.text = args.text
    if Settings.enable_sysfonts and args.sysfonts:  # unsupported; leave disabled for now
        Settings.use_sysfonts = args.sysfonts
        Settings.font_names = pygame.font.get_fonts()
    if args.fontfiles:
        Settings.font_names = [re.sub(r'\.(ttf|TTF)$', '', s) for s in args.fontfiles]


if __name__ == '__main__':
    parse_args()
    pygame.init()
    Tweaker().run()