Skip to content

Instantly share code, notes, and snippets.

@un1tz3r0
Created March 18, 2019 06:05
Show Gist options
  • Select an option

  • Save un1tz3r0/6173d316c06223914d718387ff23ee99 to your computer and use it in GitHub Desktop.

Select an option

Save un1tz3r0/6173d316c06223914d718387ff23ee99 to your computer and use it in GitHub Desktop.
Some helpers for drawing fancy graphics in PyGame. Code mostly borrowed from elsewhere, cobbled together and missing functionality added where it was needed.
"""
Rounded rectangles for PyGame in both non-antialiased and antialiased varieties. 2019 edition... now with anti-aliased alpha support!
"""
import pygame
import pygame as pg
from pygame import gfxdraw
from collections import abc
def round_rect(surface, rect, color, rad=20, border=0, inside=(0,0,0,0)):
"""
Draw a rect with rounded corners to surface. Argument rad can be specified
to adjust curvature of edges (given in pixels). An optional border
width can also be supplied; if not provided the rect will be filled.
Both the color and optional interior color (the inside argument) support
alpha.
"""
rect = pg.Rect(rect)
zeroed_rect = rect.copy()
zeroed_rect.topleft = 0,0
image = pg.Surface(rect.size).convert_alpha()
image.fill((0,0,0,0))
_render_region(image, zeroed_rect, color, rad)
if border:
zeroed_rect.inflate_ip(-2*border, -2*border)
_render_region(image, zeroed_rect, inside, max(0, rad-border))
surface.blit(image, rect)
def _render_region(image, rect, color, rad):
"""Helper function for round_rect."""
corners = rect.inflate(-2*rad, -2*rad)
for attribute in ("topleft", "topright", "bottomleft", "bottomright"):
pg.draw.circle(image, color, getattr(corners,attribute), rad)
image.fill(color, rect.inflate(-2*rad,0))
image.fill(color, rect.inflate(0,-2*rad))
def _aa_render_region(image, rect, color, rad):
"""Helper function for aa_round_rect."""
corners = rect.inflate(-2*rad-1, -2*rad-1)
for attribute in ("topleft", "topright", "bottomleft", "bottomright"):
x, y = getattr(corners, attribute)
gfxdraw.aacircle(image, x, y, rad, color)
gfxdraw.filled_circle(image, x, y, rad, color)
image.fill(color, rect.inflate(-2*rad,0))
image.fill(color, rect.inflate(0,-2*rad))
def _aa_round_rect_noalpha(surface, rect, color, rad=20, border=0, inside=(0,0,0)):
"""
Draw an antialiased rounded rect on the target surface. Alpha is not
supported in this implementation but other than that usage is identical to
round_rect.
"""
rect = pg.Rect(rect)
_aa_render_region(surface, rect, color, rad)
if border:
rect.inflate_ip(-2*border, -2*border)
_aa_render_region(surface, rect, inside, max(0, rad-border))
def _resolve_alpha_color(color, alpha=255):
if color is None:
color = defaults
try:
r, g, b, a = color
except:
try:
r, g, b = color
a = alpha
except:
raise
return ((r, g, b), a)
def aa_round_rect(dest, rect, color, radius=20, width=0, inside=None):
if inside is None:
inside=border
borderwidth = width
bordercolor, borderalpha = _resolve_alpha_color(color)
insidecolor, insidealpha = _resolve_alpha_color(inside)
# render the grayscale alpha channel into a surface
maskbuf = pygame.surface.Surface(rect.size, 0, 24)
maskbuf.fill((0, 0, 0))
# draw anti-aliased grayscale alpha channel in 24-bit rgb
_aa_round_rect_noalpha(maskbuf, maskbuf.get_rect(), (borderalpha, borderalpha, borderalpha), radius, borderwidth, (insidealpha, insidealpha, insidealpha))
# ready for the magic part? first convert-alpha adds a fourth alpha channel but it's all 255 = opaque
alphabuf = maskbuf.convert_alpha()
# we actually want the opposite... the grayscale image we drew as alpha on a completely white image
# so we can remap the rgba components in each pixel by setting the component masks and shifts,
alphabuf.set_masks((0xff, 0xff, 0xff, 0xff00))
alphabuf.set_shifts((0, 0, 0, 8))
# now r, g and b all use the last (formerly all-255s alpha) component and alpha uses the next
# most significant byte, what formerly was the blue component. a final convert_alpha() copies the
# remapped pixels back into normal rgba format.
bothbuf = alphabuf.convert_alpha() # just use alphabuf inplace maybe?
# now that we have an alpha mask, fill a surface with the border and fill color/shape
fillbuf = maskbuf # pygame.surface.Surface(rect.size, 0, 24) # reuse maskbuf to render fill in
fillbuf.fill(bordercolor if borderwidth > 0 else insidecolor)
_aa_round_rect_noalpha(fillbuf, fillbuf.get_rect(), bordercolor, radius, borderwidth, insidecolor)
bothbuf.blit(fillbuf, (0,0), None, pygame.BLEND_RGBA_MULT)
dest.blit(bothbuf, rect.topleft)
#del fillbuf
del maskbuf
del bothbuf
del alphabuf
# ptext module: place this in your import directory.
# ptext.draw(text, pos=None, **options)
# Please see README.md for explanation of options.
# https://github.com/cosmologicon/pygame-text
from __future__ import division, print_function
from math import ceil, sin, cos, radians, exp
from collections import namedtuple
import pygame
DEFAULT_FONT_SIZE = 24
REFERENCE_FONT_SIZE = 100
DEFAULT_LINE_HEIGHT = 1.0
DEFAULT_PARAGRAPH_SPACE = 0.0
DEFAULT_FONT_NAME = None
FONT_NAME_TEMPLATE = "%s"
DEFAULT_COLOR = "white"
DEFAULT_BACKGROUND = None
DEFAULT_SHADE = 0
DEFAULT_OUTLINE_COLOR = "black"
DEFAULT_SHADOW_COLOR = "black"
OUTLINE_UNIT = 1 / 24
SHADOW_UNIT = 1 / 18
DEFAULT_ALIGN = "left" # left, center, or right
DEFAULT_ANCHOR = 0, 0 # 0, 0 = top left ; 1, 1 = bottom right
DEFAULT_STRIP = True
ALPHA_RESOLUTION = 16
ANGLE_RESOLUTION_DEGREES = 3
AUTO_CLEAN = True
MEMORY_LIMIT_MB = 64
MEMORY_REDUCTION_FACTOR = 0.5
pygame.font.init()
# Options objects encapsulate the keyword arguments to functions that take a lot of keyword
# arguments.
# Options object base class. Subclass for Options objects specific to different functions.
# Specify valid fields in the _fields list. Unspecified fields default to None, unless otherwise
# specified in the _defaults list.
class _Options(object):
_fields = ()
_defaults = {}
def __init__(self, **kwargs):
fields = self._allfields()
badfields = set(kwargs) - fields
if badfields:
raise ValueError("Unrecognized args: " + ", ".join(badfields))
for field in fields:
value = kwargs[field] if field in kwargs else self._defaults.get(field)
setattr(self, field, value)
@classmethod
def _allfields(cls):
return set(cls._fields) | set(cls._defaults)
def update(self, **newkwargs):
kwargs = { field: getattr(self, field) for field in self._allfields() }
kwargs.update(**newkwargs)
return kwargs
def key(self):
return tuple(getattr(self, field) for field in sorted(self._allfields()))
def getsuboptions(self, optclass):
return { field: getattr(self, field) for field in optclass._allfields() if hasattr(self, field) }
_default_surf_sentinel = ()
# Options argument for the draw function. Specifies both text styling and positioning.
class _DrawOptions(_Options):
_fields = ("pos",
"fontname", "fontsize", "sysfontname", "antialias", "bold", "italic", "underline",
"color", "background",
"top", "left", "bottom", "right", "topleft", "bottomleft", "topright", "bottomright",
"midtop", "midleft", "midbottom", "midright", "center", "centerx", "centery",
"width", "widthem", "lineheight", "pspace", "strip", "align",
"owidth", "ocolor", "shadow", "scolor", "gcolor", "shade",
"alpha", "anchor", "angle", "surf", "cache")
_defaults = {
"antialias": True, "alpha": 1.0, "angle": 0,
"surf": _default_surf_sentinel, "cache": True }
def __init__(self, **kwargs):
_Options.__init__(self, **kwargs)
self.expandposition()
self.expandanchor()
self.resolvesurf()
# Expand each 2-element position specifier and overwrite the corresponding 1-element
# position specifiers.
def expandposition(self):
if self.topleft: self.left, self.top = self.topleft
if self.bottomleft: self.left, self.bottom = self.bottomleft
if self.topright: self.right, self.top = self.topright
if self.bottomright: self.right, self.bottom = self.bottomright
if self.midtop: self.centerx, self.top = self.midtop
if self.midleft: self.left, self.centery = self.midleft
if self.midbottom: self.centerx, self.bottom = self.midbottom
if self.midright: self.right, self.centery = self.midright
if self.center: self.centerx, self.centery = self.center
# Update the pos and anchor fields, if unspecified, to be specified by the positional
# keyword arguments.
def expandanchor(self):
x, y = self.pos or (None, None)
hanchor, vanchor = self.anchor or (None, None)
if self.left is not None: x, hanchor = self.left, 0
if self.centerx is not None: x, hanchor = self.centerx, 0.5
if self.right is not None: x, hanchor = self.right, 1
if self.top is not None: y, vanchor = self.top, 0
if self.centery is not None: y, vanchor = self.centery, 0.5
if self.bottom is not None: y, vanchor = self.bottom, 1
if x is None:
raise ValueError("Unable to determine horizontal position")
if y is None:
raise ValueError("Unable to determine vertical position")
self.pos = x, y
if self.align is None: self.align = hanchor
if hanchor is None: hanchor = DEFAULT_ANCHOR[0]
if vanchor is None: vanchor = DEFAULT_ANCHOR[1]
self.anchor = hanchor, vanchor
# Unspecified surf values default to the display surface.
def resolvesurf(self):
if self.surf is _default_surf_sentinel:
self.surf = pygame.display.get_surface()
def togetsurfoptions(self):
return self.getsuboptions(_GetsurfOptions)
class _DrawboxOptions(_Options):
_fields = (
"fontname", "sysfontname", "antialias", "bold", "italic", "underline",
"color", "background",
"lineheight", "pspace", "strip", "align",
"owidth", "ocolor", "shadow", "scolor", "gcolor", "shade",
"alpha", "anchor", "angle", "surf", "cache")
_defaults = {
"antialias": True, "alpha": 1.0, "angle": 0, "anchor": (0.5, 0.5),
"surf": _default_surf_sentinel, "cache": True }
def __init__(self, **kwargs):
_Options.__init__(self, **kwargs)
if self.fontname is None: self.fontname = DEFAULT_FONT_NAME
if self.lineheight is None: self.lineheight = DEFAULT_LINE_HEIGHT
if self.pspace is None: self.pspace = DEFAULT_PARAGRAPH_SPACE
def todrawoptions(self):
return self.getsuboptions(_DrawOptions)
def tofitsizeoptions(self):
return self.getsuboptions(_FitsizeOptions)
class _GetsurfOptions(_Options):
_fields = ("fontname", "fontsize", "sysfontname", "bold", "italic", "underline", "width",
"widthem", "strip", "color", "background", "antialias", "ocolor", "owidth", "scolor",
"shadow", "gcolor", "shade", "alpha", "align", "lineheight", "pspace", "angle", "cache")
_defaults = { "antialias": True, "alpha": 1.0, "angle": 0, "cache": True }
def __init__(self, **kwargs):
_Options.__init__(self, **kwargs)
if self.fontname is None: self.fontname = DEFAULT_FONT_NAME
if self.fontsize is None: self.fontsize = DEFAULT_FONT_SIZE
self.fontsize = int(round(self.fontsize))
if self.align is None: self.align = DEFAULT_ALIGN
if self.align in ["left", "center", "right"]:
self.align = [0, 0.5, 1][["left", "center", "right"].index(self.align)]
if self.lineheight is None: self.lineheight = DEFAULT_LINE_HEIGHT
if self.pspace is None: self.pspace = DEFAULT_PARAGRAPH_SPACE
self.color = _resolvecolor(self.color, DEFAULT_COLOR)
self.background = _resolvecolor(self.background, DEFAULT_BACKGROUND)
self.gcolor = _resolvecolor(self.gcolor, None)
if self.shade is None: self.shade = DEFAULT_SHADE
if self.shade:
self.gcolor = _applyshade(self.gcolor or self.color, self.shade)
self.shade = 0
self.ocolor = None if self.owidth is None else _resolvecolor(self.ocolor, DEFAULT_OUTLINE_COLOR)
self.scolor = None if self.shadow is None else _resolvecolor(self.scolor, DEFAULT_SHADOW_COLOR)
self._opx = None if self.owidth is None else ceil(self.owidth * self.fontsize * OUTLINE_UNIT)
self._spx = None if self.shadow is None else tuple(ceil(s * self.fontsize * SHADOW_UNIT) for s in self.shadow)
self.alpha = _resolvealpha(self.alpha)
self.angle = _resolveangle(self.angle)
self.strip = DEFAULT_STRIP if self.strip is None else self.strip
def towrapoptions(self):
return self.getsuboptions(_WrapOptions)
def togetfontoptions(self):
return self.getsuboptions(_GetfontOptions)
class _WrapOptions(_Options):
_fields = ("fontname", "fontsize", "sysfontname",
"bold", "italic", "underline", "width", "widthem", "strip")
def __init__(self, **kwargs):
_Options.__init__(self, **kwargs)
if self.widthem is not None and self.width is not None:
raise ValueError("Can't set both width and widthem")
if self.widthem is not None:
self.width = self.widthem * REFERENCE_FONT_SIZE
self.fontsize = REFERENCE_FONT_SIZE
if self.strip is None:
self.strip = DEFAULT_STRIP
def togetfontoptions(self):
return self.getsuboptions(_GetfontOptions)
class _GetfontOptions(_Options):
_fields = ("fontname", "fontsize", "sysfontname", "bold", "italic", "underline")
def __init__(self, **kwargs):
_Options.__init__(self, **kwargs)
if self.fontname is not None and self.sysfontname is not None:
raise ValueError("Can't set both fontname and sysfontname")
if self.fontname is None and self.sysfontname is None:
fontname = DEFAULT_FONT_NAME
if self.fontsize is None:
self.fontsize = DEFAULT_FONT_SIZE
def getfontpath(self):
return self.fontname if self.fontname is None else FONT_NAME_TEMPLATE % self.fontname
class _FitsizeOptions(_Options):
_fields = ("fontname", "sysfontname", "bold", "italic", "underline",
"lineheight", "pspace", "strip")
def togetfontoptions(self):
return self.getsuboptions(_GetfontOptions)
def towrapoptions(self):
return self.getsuboptions(_WrapOptions)
_font_cache = {}
def getfont(**kwargs):
options = _GetfontOptions(**kwargs)
key = options.key()
if key in _font_cache: return _font_cache[key]
if options.sysfontname is not None:
font = pygame.font.SysFont(options.sysfontname, options.fontsize, options.bold or False, options.italic or False)
else:
try:
font = pygame.font.Font(options.getfontpath(), options.fontsize)
except IOError:
raise IOError("unable to read font filename: %s" % options.getfontpath())
if options.bold is not None:
font.set_bold(options.bold)
if options.italic is not None:
font.set_italic(options.italic)
if options.underline is not None:
font.set_underline(options.underline)
_font_cache[key] = font
return font
def wrap(text, **kwargs):
options = _WrapOptions(**kwargs)
font = getfont(**options.togetfontoptions())
getwidth = lambda line: font.size(line)[0]
# Apparently Font.render accepts None for the text argument, in which case it's treated as the
# empty string. We match that behavior here.
if text is None: text = ""
paras = text.replace("\t", " ").split("\n")
lines = []
for jpara, para in enumerate(paras):
if options.strip:
para = para.rstrip(" ")
if options.width is None:
lines.append((para, jpara))
continue
if not para:
lines.append(("", jpara))
continue
# A break point is defined as any space character that immediately follows a non-space
# character, or the end of the paragraph. These are the points that will be considered for
# breaking a line off the front of the paragraph, although exactly how much whitespace goes
# into the line depends on options.strip.
# A valid break point is any break point such that breaking here will keep the width of the
# line within options.width, with the exception that the first break point in the
# paragraph is always valid. The goal of this algorithm is to find the last valid break
# point.
# Preserve paragraph leading spaces in all cases.
lspaces = len(para) - len(para.lstrip(" "))
# At any given time, a is the index of a known valid break point, and line = para[:a].
a = para.index(" ", lspaces) if " " in para[lspaces:] else len(para)
line = para[:a]
while a + 1 < len(para):
# b is the next break point, with bline the corresponding line to add.
if " " not in para[a+1:]:
b = len(para)
bline = para
else:
# Find a space character that immediately follows a non-space character.
b = para.index(" ", a + 1)
while para[b-1] == " ":
if " " in para[b+1:]:
b = para.index(" ", b + 1)
else:
b = len(para)
break
bline = para[:b]
bline = para[:b]
if getwidth(bline) <= options.width:
a, line = b, bline
else:
# Last vaild break point located.
if not options.strip:
# If options.strip is False, maintain as many spaces from after the break point
# as will keep us under options.width.
nspaces = len(para[a:]) - len(para[a:].lstrip(" "))
for jspace in range(nspaces):
nline = line + " "
if getwidth(nline) > options.width:
break
line = nline
lines.append((line, jpara))
# Start the search over with the rest of the paragraph.
para = para[a:].lstrip(" ")
a = para.index(" ", 1) if " " in para[1:] else len(para)
line = para[:a]
# Handle the case of the first valid break point of the last line being the end of the line.
# In this case there are no trailing spaces.
if para:
lines.append((line, jpara))
return lines
# Return the largest integer in the range [xmin, xmax] such that f(x) is True.
def _binarysearch(f, xmin = 1, xmax = 256):
if not f(xmin): return xmin
if f(xmax): return xmax
# xmin is the largest known value for which f(x) is True
# xmax is the smallest known value for which f(x) is False
while xmax - xmin > 1:
x = (xmax + xmin) // 2
if f(x):
xmin = x
else:
xmax = x
return xmin
_fit_cache = {}
def _fitsize(text, size, **kwargs):
options = _FitsizeOptions(**kwargs)
key = text, size, options.key()
if key in _fit_cache: return _fit_cache[key]
width, height = size
def fits(fontsize):
texts = wrap(text, fontsize=fontsize, width=width, **options.towrapoptions())
font = getfont(fontsize=fontsize, **options.togetfontoptions())
w = max(font.size(line)[0] for line, jpara in texts)
linesize = font.get_linesize() * options.lineheight
paraspace = font.get_linesize() * options.pspace
h = int(round((len(texts) - 1) * linesize + texts[-1][1] * paraspace)) + font.get_height()
return w <= width and h <= height
fontsize = _binarysearch(fits)
_fit_cache[key] = fontsize
return fontsize
# Returns the color as a color RGB or RGBA tuple (i.e. 3 or 4 integers in the range 0-255)
# If color is None, fall back to the default. If default is also None, return None.
# Both color and default can be a list, tuple, a color name, an HTML color format string, a hex
# number string, or an integer pixel value. See pygame.Color constructor for specification.
def _resolvecolor(color, default):
if color is None: color = default
if color is None: return None
try:
return tuple(pygame.Color(color))
except ValueError:
return tuple(color)
def _applyshade(color, shade):
f = exp(-0.4 * shade)
r, g, b = [
min(max(int(round((c + 50) * f - 50)), 0), 255)
for c in color[:3]
]
return (r, g, b) + tuple(color[3:])
def _resolvealpha(alpha):
if alpha >= 1:
return 1
return max(int(round(alpha * ALPHA_RESOLUTION)) / ALPHA_RESOLUTION, 0)
def _resolveangle(angle):
if not angle:
return 0
angle %= 360
return int(round(angle / ANGLE_RESOLUTION_DEGREES)) * ANGLE_RESOLUTION_DEGREES
# Return the set of points in the circle radius r, using Bresenham's circle algorithm
_circle_cache = {}
def _circlepoints(r):
r = int(round(r))
if r in _circle_cache:
return _circle_cache[r]
x, y, e = r, 0, 1 - r
_circle_cache[r] = points = []
while x >= y:
points.append((x, y))
y += 1
if e < 0:
e += 2 * y - 1
else:
x -= 1
e += 2 * (y - x) - 1
points += [(y, x) for x, y in points if x > y]
points += [(-x, y) for x, y in points if x]
points += [(x, -y) for x, y in points if y]
points.sort()
return points
# Rotate the given surface by the given angle, in degrees.
# If angle is an exact multiple of 90, use pygame.transform.rotate, otherwise fall back to
# pygame.transform.rotozoom.
def _rotatesurf(surf, angle):
if angle in (90, 180, 270):
return pygame.transform.rotate(surf, angle)
else:
return pygame.transform.rotozoom(surf, angle, 1.0)
# Apply the given alpha value to a copy of the Surface.
def _fadesurf(surf, alpha):
surf = surf.copy()
asurf = surf.copy()
asurf.fill((255, 255, 255, int(round(255 * alpha))))
surf.blit(asurf, (0, 0), None, pygame.BLEND_RGBA_MULT)
return surf
def _istransparent(color):
return len(color) > 3 and color[3] == 0
# Produce a 1xh Surface with the given color gradient.
_grad_cache = {}
def _gradsurf(h, y0, y1, color0, color1):
key = h, y0, y1, color0, color1
if key in _grad_cache:
return _grad_cache[key]
surf = pygame.Surface((1, h)).convert_alpha()
r0, g0, b0 = color0[:3]
r1, g1, b1 = color1[:3]
for y in range(h):
f = min(max((y - y0) / (y1 - y0), 0), 1)
g = 1 - f
surf.set_at((0, y), (
int(round(g * r0 + f * r1)),
int(round(g * g0 + f * g1)),
int(round(g * b0 + f * b1)),
0
))
_grad_cache[key] = surf
return surf
_surf_cache = {}
_surf_tick_usage = {}
_surf_size_total = 0
_unrotated_size = {}
_tick = 0
def getsurf(text, **kwargs):
global _tick, _surf_size_total
options = _GetsurfOptions(**kwargs)
key = text, options.key()
if key in _surf_cache:
_surf_tick_usage[key] = _tick
_tick += 1
return _surf_cache[key]
texts = wrap(text, **options.towrapoptions())
if options.angle:
surf0 = getsurf(text, **options.update(angle = 0))
surf = _rotatesurf(surf0, options.angle)
_unrotated_size[(surf.get_size(), options.angle, text)] = surf0.get_size()
elif options.alpha < 1.0:
surf = _fadesurf(getsurf(text, **options.update(alpha = 1.0)), options.alpha)
elif options._spx is not None:
color = (0, 0, 0) if _istransparent(options.color) else options.color
surf0 = getsurf(text, **options.update(background = (0, 0, 0, 0), color = color, shadow = None, scolor = None))
ssurf = getsurf(text, **options.update(background = (0, 0, 0, 0), color = options.scolor, shadow = None, scolor = None, gcolor = None))
w0, h0 = surf0.get_size()
sx, sy = options._spx
surf = pygame.Surface((w0 + abs(sx), h0 + abs(sy))).convert_alpha()
surf.fill(options.background or (0, 0, 0, 0))
dx, dy = max(sx, 0), max(sy, 0)
surf.blit(ssurf, (dx, dy))
x0, y0 = abs(sx) - dx, abs(sy) - dy
if _istransparent(options.color):
surf.blit(surf0, (x0, y0), None, pygame.BLEND_RGBA_SUB)
else:
surf.blit(surf0, (x0, y0))
elif options._opx is not None:
color = (0, 0, 0) if _istransparent(options.color) else options.color
surf0 = getsurf(text, **options.update(color = color, ocolor = None, owidth = None))
osurf = getsurf(text, **options.update(color = options.ocolor, ocolor = None, owidth = None, background = (0,0,0,0), gcolor = None))
w0, h0 = surf0.get_size()
opx = options._opx
surf = pygame.Surface((w0 + 2 * opx, h0 + 2 * opx)).convert_alpha()
surf.fill(options.background or (0, 0, 0, 0))
for dx, dy in _circlepoints(opx):
surf.blit(osurf, (dx + opx, dy + opx))
if _istransparent(options.color):
surf.blit(surf0, (opx, opx), None, pygame.BLEND_RGBA_SUB)
else:
surf.blit(surf0, (opx, opx))
else:
font = getfont(**options.togetfontoptions())
color = options.color
if options.gcolor is not None:
color = 0, 0, 0
# pygame.Font.render does not allow passing None as an argument value for background.
if options.background is None or (len(options.background) > 3 and options.background[3] == 0) or options.gcolor is not None:
lsurfs = [font.render(text, options.antialias, color).convert_alpha() for text, jpara in texts]
else:
lsurfs = [font.render(text, options.antialias, color, options.background).convert_alpha() for text, jpara in texts]
if options.gcolor is not None:
gsurf0 = _gradsurf(lsurfs[0].get_height(), 0.5 * font.get_ascent(), font.get_ascent(), options.color, options.gcolor)
for lsurf in lsurfs:
gsurf = pygame.transform.scale(gsurf0, lsurf.get_size())
lsurf.blit(gsurf, (0, 0), None, pygame.BLEND_RGBA_ADD)
if len(lsurfs) == 1 and options.gcolor is None:
surf = lsurfs[0]
else:
w = max(lsurf.get_width() for lsurf in lsurfs)
linesize = font.get_linesize() * options.lineheight
parasize = font.get_linesize() * options.pspace
ys = [int(round(k * linesize + jpara * parasize)) for k, (text, jpara) in enumerate(texts)]
h = ys[-1] + font.get_height()
surf = pygame.Surface((w, h)).convert_alpha()
surf.fill(options.background or (0, 0, 0, 0))
for y, lsurf in zip(ys, lsurfs):
x = int(round(options.align * (w - lsurf.get_width())))
surf.blit(lsurf, (x, y))
if options.cache:
w, h = surf.get_size()
_surf_size_total += 4 * w * h
_surf_cache[key] = surf
_surf_tick_usage[key] = _tick
_tick += 1
return surf
# The actual position on the screen where the surf is to be blitted, rather than the specified
# anchor position.
def _blitpos(angle, pos, anchor, tsurf, text):
angle = _resolveangle(angle)
x, y = pos
hanchor, vanchor = anchor
if angle:
w0, h0 = _unrotated_size[(tsurf.get_size(), angle, text)]
S, C = sin(radians(angle)), cos(radians(angle))
dx, dy = (0.5 - hanchor) * w0, (0.5 - vanchor) * h0
x += dx * C + dy * S - 0.5 * tsurf.get_width()
y += -dx * S + dy * C - 0.5 * tsurf.get_height()
else:
x -= hanchor * tsurf.get_width()
y -= vanchor * tsurf.get_height()
x = int(round(x))
y = int(round(y))
return x, y
def draw(text, pos=None, **kwargs):
options = _DrawOptions(pos = pos, **kwargs)
tsurf = getsurf(text, **options.togetsurfoptions())
pos = _blitpos(options.angle, options.pos, options.anchor, tsurf, text)
if options.surf is not None:
options.surf.blit(tsurf, pos)
if AUTO_CLEAN:
clean()
return tsurf, pos
def drawbox(text, rect, **kwargs):
options = _DrawboxOptions(**kwargs)
rect = pygame.Rect(rect)
hanchor, vanchor = options.anchor
x = rect.x + hanchor * rect.width
y = rect.y + vanchor * rect.height
fontsize = _fitsize(text, rect.size, **options.tofitsizeoptions())
return draw(text, pos=(x,y), width=rect.width, fontsize=fontsize, **options.todrawoptions())
def clean():
global _surf_size_total
memory_limit = MEMORY_LIMIT_MB * (1 << 20)
if _surf_size_total < memory_limit:
return
memory_limit *= MEMORY_REDUCTION_FACTOR
keys = sorted(_surf_cache, key=_surf_tick_usage.get)
for key in keys:
w, h = _surf_cache[key].get_size()
del _surf_cache[key]
del _surf_tick_usage[key]
_surf_size_total -= 4 * w * h
if _surf_size_total < memory_limit:
break
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment