"""Basic graphic representing an image.
---NODOC---
TODO:
- a solution to the rotation problem (jaggy edges if they were the surface borders)
- why can't we move rotate earlier?
- use subsurface for crop transform (but requires rect to be within surface)
- GraphicView probably doesn't work if in different manager - need to have own _dirty?
- something that wraps a Graphic to be a copy of it, like Animation does, and has .graphic setter - for use in other classes
- use in Animation, etc.
---NODOC---
"""
from math import sin, cos, pi
import pygame as pg
from pygame import Rect
from ..conf import conf
from ..util import (ir, pos_in_rect, align_rect, normalise_colour, has_alpha,
blank_sfc, combine_drawn, Owned)
[docs]class Graphic (Owned):
"""Something that can be drawn to the screen.
Graphic(img, pos=(0, 0), layer=0, pool=conf.DEFAULT_RESOURCE_POOL,
res_mgr=conf.GAME.resources)
:arg img: surface or filename (under :data:`conf.IMG_DIR`) to load. If a
surface, it should be already converted for blitting.
:arg pos: initial ``(x, y)`` position. The existence of a default is because
you might use :meth:`align` immediately on adding to a
:class:`GraphicsManager <engine.gfx.container.GraphicsManager>`.
:arg layer: the layer to draw in, lower being closer to the 'front'. This can
actually be any hashable object except ``None``, as long as all
layers used in the same
:class:`GraphicsManager <engine.gfx.container.GraphicsManager>` can
be ordered with respect to each other.
:arg pool: :class:`ResourceManager <engine.res.ResourceManager>` resource pool
name to cache any loaded images in.
:arg res_mgr: :class:`ResourceManager <engine.res.ResourceManager>` instance to
use to load any images.
Many properties of a graphic, such as :attr:`pos` and :attr:`size`, can be
changed in two main ways: by setting the attribute directly, or by calling the
corresponding method. The former is more natural, and is useful for
:meth:`sched.Scheduler.interp() <engine.sched.Scheduler.interp>`, while the
latter all return the graphic, and so can be chained together.
Position and size can also be retrieved and altered using list indexing, like
with Pygame rects. Altering size in any way applies the :meth:`resize`
transformation.
:meth:`resize`, :meth:`crop`, :meth:`flip`, :meth:`opacify` and :meth:`rotate`
correspond to builtin transforms (see :meth:`transform`).
"""
is_view = False
_builtin_transforms = ('crop', 'flip', 'tint', 'resize', 'rotate')
def __init__ (self, img, pos=(0, 0), layer=0,
pool=conf.DEFAULT_RESOURCE_POOL, res_mgr=None):
Owned.__init__(self, 1)
self._resource_pool = pool
self._resource_manager = res_mgr
if isinstance(img, basestring):
#: Filename of the loaded image, or ``None`` if a surface was
#: given.
self.fn = img
img = self._load_img(img)
else:
self.fn = None
self._orig_sfc = self._surface = img
# postrot is the rect drawn in
self._postrot_rect = self._rect = Rect(pos, img.get_size())
self._last_postrot_rect = Rect(self._postrot_rect)
#: :attr:`rect` at the time of the last draw.
self.last_rect = Rect(self._rect)
self._anchor = (0, 0)
self._rot_anchor = 'center'
self._rot_offset = (0, 0) # postrot_pos = pos + rot_offset
self._must_apply_rot = False
#: A list of transformations applied to the graphic. Always contains
#: the builtin transforms as strings (though they do nothing
#: by default); other transforms are added through :meth:`transform`,
#: and are functions.
self.transforms = list(self._builtin_transforms)
self._last_transforms = list(self.transforms)
# {function: (args, previous_surface, resulting_surface, apply_fn,
# undo_fn)}
# last 2 None for non-builtins
self._transforms = {}
# {function: (args, previous_size, resulting_size, apply_fn, undo_fn)}
# last 4 None for non-builtins
self._queued_transforms = {}
#: Whether the graphic is completely opaque; do not change.
self.opaque = not has_alpha(img)
self._layer = layer
#: When blitting the surface, this is passed as the ``special_flags``
#: argument.
self._last_blit_flags = self.blit_flags = 0
#: Whether currently (supposed to be) visible on-screen.
self.visible = True
#: Whether this graphic was visible at the time of the last draw; do
#: not change.
self.was_visible = False
self._scale = (1, 1)
self._cropped_rect = None
self._flipped = (False, False)
self._tint_colour = (255, 255, 255, 255)
self._angle = 0
self._scale_fn = pg.transform.smoothscale
self._rotate_fn = lambda sfc, angle: \
pg.transform.rotozoom(sfc, angle * 180 / pi, 1)
self._rotate_threshold = 2 * pi / 500
self._orig_dirty = False # where original surface is changed
# where final surface is changed; gets used (and reset) by manager
self._dirty = []
# {cb: evts}
self._cbs = {}
# {evt: cbs}
self._evts = {}
def __getitem__ (self, i):
if isinstance(i, slice):
# Rect is weird and only accepts slices through slice syntax
# this is the easiest way around it (and slicing doesn't work with
# Python 3 anyway)
r = self._rect
return [r[i] for i in range(4)[i]]
else:
return self._rect[i]
def __setitem__ (self, i, v):
r = Rect(self._rect)
if isinstance(i, slice):
for v_i, r_i in enumerate(range(4)[i]):
r[r_i] = v[v_i]
else:
r[i] = v
self.rect = r
@property
def orig_sfc (self):
"""The surface before any transforms.
When setting this, the surface should be already converted for blitting.
"""
return self._orig_sfc
@orig_sfc.setter
def orig_sfc (self, sfc):
size = sfc.get_size()
old_sfc = self._orig_sfc
self._orig_sfc = sfc
if size != old_sfc.get_size():
self.size_changed(size)
self._orig_dirty = True
self._call_cbs('change orig', old_sfc, sfc)
@property
def surface (self):
"""The (possibly transformed) surface that will be used for drawing.
Accessing this will cause all queued transformations to be applied.
"""
self.render()
return self._surface
# appearance properties
@property
def rect (self):
"""``pygame.Rect`` giving the on-screen area covered.
May be set directly, but not altered in-place.
This is actually the rect before rotation, which is probably what you want,
really. To get the real rect, use :attr:`postrot_rect`.
"""
return self._rect
@rect.setter
def rect (self, rect):
# need to set dirty in old and new rects (if changed)
rect = Rect(rect)
old_rect = self._rect
self._rect = Rect(rect.topleft, self._rect.size)
if rect.size != old_rect.size:
self.resize(*rect.size)
@property
def x (self):
"""``x`` co-ordinate of the top-left corner of :attr:`rect`."""
return self._rect[0]
@x.setter
def x (self, x):
r = Rect(self._rect)
r[0] = x
self.rect = r
@property
def y (self):
"""``y`` co-ordinate of the top-left corner of :attr:`rect`."""
return self._rect[1]
@y.setter
def y (self, y):
r = Rect(self._rect)
r[1] = y
self.rect = r
@property
def pos (self):
"""``(``:attr:`x` ``,`` :attr:`y` ``)``."""
return self._rect.topleft
@pos.setter
def pos (self, pos):
self.rect = (pos, self._rect.size)
@property
def w (self):
"""Width of :attr:`rect`; uses :meth:`resize`."""
return self._rect[2]
@w.setter
def w (self, w):
r = Rect(self._rect)
r[2] = w
self.rect = r
@property
def h (self):
"""Height of :attr:`rect`; uses :meth:`resize`."""
return self._rect[3]
@h.setter
def h (self, h):
r = Rect(self._rect)
r[3] = h
self.rect = r
@property
def size (self):
"""``(``:attr:`w` ``,`` :attr:`h` ``)``."""
return self._rect.size
@size.setter
def size (self, size):
self.rect = (self._rect.topleft, size)
@property
def scale_x (self):
"""Scaling ratio of the graphic on the x-axis; uses :meth:`rescale`."""
return self._scale[0]
@scale_x.setter
def scale_x (self, scale_x):
self.rescale(scale_x, self._scale[1])
@property
def scale_y (self):
"""Scaling ratio of the graphic on the y-axis; uses :meth:`rescale`."""
return self._scale[1]
@scale_y.setter
def scale_y (self, scale_y):
self.rescale(self._scale[0], scale_y)
@property
def scale (self):
"""``(``:attr:`scale_x` ``,`` :attr:`scale_y` ``)``.
Can be set to a single number to scale by in both dimensions.
"""
return self._scale
@scale.setter
def scale (self, scale):
if isinstance(scale, (int, float)):
self.rescale(scale, scale)
else:
self.rescale(*scale)
@property
def cropped_rect (self):
"""The rect currently cropped to."""
if self._cropped_rect is None:
return Rect((0, 0), self.sz_before_transform('crop'))
else:
return self._cropped_rect
@cropped_rect.setter
def cropped_rect (self, rect):
self.crop(rect)
@property
def flipped_x (self):
"""Whether flipped on the x-axis."""
return self._flipped[0]
@flipped_x.setter
def flipped_x (self, flipped_x):
self.flip(flipped_x, self._flipped[1])
@property
def flipped_y (self):
"""Whether flipped on the y-axis."""
return self._flipped[0]
@flipped_x.setter
def flipped_y (self, flipped_y):
self.flip(self._flipped[0], flipped_y)
@property
def flipped (self):
"""``(``:attr:`flipped_x` ``,`` :attr:`flipped_y` ``)``.
Can be set to a single value to apply to both dimensions.
"""
return self._flipped
@flipped.setter
def flipped (self, flipped):
if isinstance(flipped, (bool, int)):
self.flip(flipped, flipped)
else:
self.flip(*flipped)
@property
def tint_colour (self):
"""Tinted colour of the graphic, as taken by
:func:`engine.util.normalise_colour`."""
return self._tint_colour
@tint_colour.setter
def tint_colour (self, colour):
self.tint(colour)
@property
def opacity (self):
"""Opacity of the graphic, from ``0`` (transparent) to ``255``."""
return self._tint_colour[3]
@opacity.setter
def opacity (self, opacity):
self.opacify(opacity)
@property
def angle (self):
"""Current rotation angle, anti-clockwise in radians.
Also see :attr:`rot_anchor`.
"""
return self._angle
@angle.setter
def angle (self, angle):
self.rotate(angle)
@property
def postrot_rect (self):
"""``pygame.Rect`` giving the on-screen area covered after rotation."""
self.render()
return self._postrot_rect
@property
def anchor (self):
"""The point within :attr:`rect` to fix in place when size changes.
This is a position as taken by :func:`engine.util.pos_in_rect` (where the
``rect`` argument will be :attr:`rect`). Defaults to ``(0, 0)``.
"""
return self._anchor
@anchor.setter
def anchor (self, anchor):
self._anchor = anchor
self.retransform('resize')
@property
def rot_anchor (self):
"""Like :attr:`anchor`, used for rotation.
Defaults to ``'center'``.
"""
return self._rot_anchor
@rot_anchor.setter
def rot_anchor (self, anchor):
self._rot_anchor = anchor
self.retransform('rotate')
@property
def scale_fn (self):
"""Function to use for scaling.
Defaults to ``pygame.transform.smoothscale`` (and should have the same
signature as this default).
"""
return self._scale_fn
@scale_fn.setter
def scale_fn (self, scale_fn):
self._scale_fn = scale_fn
self.retransform('resize')
@property
def rotate_fn (self):
"""Function to use for rotating.
Uses ``pygame.transform.rotozoom`` by default. Takes the surface and angle (as
passed to :meth:`rotate`) and returns the new rotated surface.
"""
return self._rotate_fn
@rotate_fn.setter
def rotate_fn (self, rotate_fn):
self._rotate_fn = rotate_fn
self.retransform('rotate')
@property
def rotate_threshold (self):
"""Only rotate when the angle changes by this much.
Defaults to ``2 * pi / 500``."""
return self._rotate_threshold
@rotate_threshold.setter
def rotate_threshold (self, rotate_threshold):
self._rotate_threshold = rotate_threshold
self.retransform('rotate')
# other properties
@property
def owner (self):
"""The thing that 'owns' this graphic.
This is usually a
:class:`GraphicsManager <engine.gfx.container.GraphicsManager>` instance, or
``None``, but may be any other object. If this object has ``'add'``, ``'rm'``
or ``'orig_size'`` attributes, these must be implemented like in
:class:`GraphicsManager <engine.gfx.container.GraphicsManager>`.
This property may be changed directly.
"""
return Owned.owner.fget(self)
@property
def layer (self):
"""As taken by the constructor."""
return self._layer
@layer.setter
def layer (self, layer):
if layer != self._layer:
# change layer in gm by removing, setting attribute, then adding
m = self.owner
if hasattr(m, 'rm'):
m.rm(self)
self._layer = layer
if hasattr(m, 'add'):
m.add(self)
# movement
[docs] def move_to (self, x = None, y = None):
"""Move to the given position.
move_to([x][, y]) -> self
Omitted arguments are unchanged.
"""
r = Rect(self._rect)
if x is not None:
r[0] = x
if y is not None:
r[1] = y
self.rect = r
return self
[docs] def move_by (self, dx = 0, dy = 0):
"""Move by the given number of pixels.
move_by(dx = 0, dy = 0) -> self
"""
self.rect = self._rect.move(dx, dy)
return self
[docs] def align (self, alignment = 0, pad = 0, offset = 0, within = None):
"""Position this graphic within a rect.
align(alignment = 0, pad = 0, offset = 0,
within = self.owner.orig_sfc.get_rect()) -> self
All arguments are as taken by :func:`engine.util.align_rect`.
"""
if within is None:
if not hasattr(self.owner, 'orig_size'):
raise TypeError('received no \'within\' argument and owner '
'has no \'orig_size\' attribute')
within = Rect((0, 0), self.owner.orig_size)
self.pos = align_rect(self._rect, within, alignment, pad, offset)
return self
# transform
"""Doc for _gen_mods_* methods.
Each builtin transform requires a _gen_mods_<transform> method, as follows:
_gen_mods_<transform>(src_sz, first_time, last_args, *args)
-> ((apply_fn, undo_fn), dest_sz)
src_sz: size before the transform.
first_time: whether this is the first time these modifiers have been generated.
last_args: transform arguments at the time of the last modifier generation, or
None. Guaranteed to be non-None if first_time is False
If first_time is False and the modifiers would not be different from
previously, the return value may be None.
apply_fn, undo_fn: functions that take the Graphic instance and apply or undo
modifiers that the transform requires (such as setting
transform attributes like angle).
dest_sz: the size after the transform.
"""
def _sfc_before_transform (self, transform_fn):
"""Get queued/applied previous surface (size) for a transform function.
Loops backwards until the transform in question is not an unapplied builtin.
Transform may be an index in transforms.
Returns (sfc, is_size), or (None, None) if the transform doesn't exist.
"""
t_ks = self.transforms
if isinstance(transform_fn, int):
i = transform_fn
if i < 0 or i >= len(self.transforms):
return (None, None)
else:
try:
i = self.transforms.index(transform_fn)
except ValueError:
return (None, None)
q = self._queued_transforms
ts = self._transforms
while True:
if i == 0:
# first transform
return (self._orig_sfc, False)
else:
# use previous transform's final surface
i -= 1
fn = t_ks[i]
if fn in q:
if isinstance(fn, basestring):
return (q[fn][1], True)
# else doesn't store size: continue
elif fn in ts:
return (ts[fn][1], False)
# else continue
def _undo_transforms (self, transform_fn, include=True):
"""Undo modifiers up to the given transform.
transform_fn may be an index in transforms.
include: whether to undo for the given transform.
"""
t_ks = self.transforms
q = self._queued_transforms
ts = self._transforms
if isinstance(transform_fn, int):
i = transform_fn
else:
i = t_ks.index(transform_fn)
if not include:
i += 1
for fn in reversed(t_ks[i:]):
if isinstance(fn, basestring):
if fn in q:
q[fn][4](self)
elif fn in ts:
ts[fn][4](self)
# else non-applied builtin
# else non-builtin: nothing to undo
def _apply_transforms (self, transform_fn, regen, include=True):
"""Apply modifiers from the given transforms.
transform_fn may be an index in transforms.
regen: whether to force regeneration of transform modifiers.
include: whether to apply for the given transform.
"""
t_ks = self.transforms
q = self._queued_transforms
ts = self._transforms
if isinstance(transform_fn, int):
i = transform_fn
else:
i = t_ks.index(transform_fn)
if not include:
i += 1
src_sz = self.sz_before_transform(i)
for fn in t_ks[i:]:
if isinstance(fn, basestring):
if fn in q:
pool = q
elif fn in ts:
pool = ts
else:
# non-applied builtin
continue
args, src, dest, apply_fn, undo_fn = pool[fn]
if regen:
gen_mods = getattr(self, '_gen_mods_' + fn)
mods, dest_sz = gen_mods(src_sz, False, args, *args)
if mods is not None:
apply_fn, undo_fn = mods
elif pool == q:
dest_sz = dest
else:
dest_sz = dest.get_size()
apply_fn(self)
# update in transform store
if pool == q:
src = src_sz
dest = dest_sz
pool[fn] = (args, src, dest, apply_fn, undo_fn)
src_sz = dest_sz
# else non-builtin: nothing to apply
[docs] def size_changed (self, size):
"""Tell the graphic that the original size has changed.
:arg size: the new original size to use.
'Original' means before any transforms. This method is for use by subclasses,
to call when :attr:`orig_sfc` will change, but will not be set until
:meth:`render` is called to avoid unnecessary computations. The new position
is determined by :attr:`anchor`.
"""
old_final_size = self._rect.size
got_transforms = bool(self.transforms)
if got_transforms:
self._undo_transforms(0)
# compute offset due to anchor
old_size = self.size
old_ox, old_oy = pos_in_rect(self.anchor, self._rect)
new_ox, new_oy = pos_in_rect(self.anchor, size)
x, y = self._rect.topleft
self._rect = Rect((x + old_ox - new_ox, y + old_oy - new_oy), size)
if got_transforms:
self._apply_transforms(0, True)
self._call_cbs('resize orig', old_size, size)
final_size = self._rect.size
if final_size != old_final_size:
self._call_cbs('resize', old_final_size, final_size)
def _load_img (self, fn, force_load = False):
# load image from disk/cache
resources = self._resource_manager
if resources is None:
resources = conf.GAME.resources
return resources.img(fn, pool = self._resource_pool,
force_load = force_load)
[docs] def reload (self):
"""Reload from disk if possible.
If successful, all transformations are reapplied afterwards, if any.
"""
if self.fn is not None:
# this calls a setter
self.orig_sfc = self._load_img(self.fn, True)
def _gen_mods_resize (self, src_sz, first_time, last_args, w, h,
scale=False):
# mods are size-dependent, so they always change
ax, ay = pos_in_rect(self.anchor, ((0, 0), src_sz), True)
ow, oh = src_sz
if scale:
w = ir(scale[0] * ow)
h = ir(scale[1] * oh)
else:
if w is None:
w = ow
elif w is False:
w = ir(ow * float(h) / oh)
if h is None:
h = oh
elif h is False:
h = ir(oh * float(w) / ow)
scale = (float(w) / ow, float(h) / oh)
ox = ir((1 - scale[0]) * ax)
oy = ir((1 - scale[1]) * ay)
def apply_fn (g):
g._scale = scale
x, y = g._rect.topleft
g._rect = Rect(x + ox, y + oy, w, h)
def undo_fn (g):
g._scale = (1, 1)
x, y = g._rect.topleft
g._rect = Rect(x - ox, y - oy, ow, oh)
return ((apply_fn, undo_fn), (w, h))
def _resize (self, src, dest, dirty, last_args, w, h, scale=False):
start_w, start_h = src.get_size()
def parse_args (w, h, scale):
if scale:
w = ir(scale[0] * start_w)
h = ir(scale[1] * start_h)
else:
if w is None:
w = start_w
elif w is False:
w = ir(start_w * float(h) / start_h)
if h is None:
h = start_h
elif h is False:
h = ir(start_h * float(w) / start_w)
return (w, h)
w, h = parse_args(w, h, scale)
new_dirty = True
if dirty is not True and last_args is not None:
if (w, h) == parse_args(*last_args):
# same as last time
if dirty:
# transform dirty rects
scale = (float(w) / start_w, float(h) / start_h)
new_dirty = []
for r in dirty:
new_dirty.append(Rect(*(
ir(x * scale[i % 2]) for i, x in enumerate(r)
)).inflate(2, 2))
# but do full transform
else:
return (dest, False)
if w == start_w and h == start_h:
# transform does nothing
return (src, new_dirty if last_args is None else True)
# full transform
return (self.scale_fn(src, (w, h)), new_dirty)
[docs] def resize (self, w=None, h=None, scale=False):
"""Resize the graphic.
resize([w][, h]) -> self
:arg w: the new width.
:arg h: the new height.
No scaling occurs in omitted dimensions. Also see :attr:`anchor`.
"""
return self.transform('resize', w, h, scale)
[docs] def rescale (self, w=1, h=1):
"""A convenience wrapper around :meth:`resize` to scale by a ratio.
rescale(w=1, h=1) -> self
:arg w: the new width; ratio of the width before scaling.
:arg h: the new height; ratio of the height before scaling.
"""
return self.resize(None, None, (w, h))
[docs] def resize_both (self, w=False, h=False):
"""Resize with constant aspect ratio.
resize_both(w=False, h=False) -> self
:arg w: the new width; pass only one of ``w`` and ``h``.
:arg h: the new height.
"""
if (w is False) + (h is False) != 1:
raise TypeError('expected exactly one of w or h')
return self.resize(w, h)
[docs] def rescale_both (self, scale=1):
"""A convenience wrapper around :meth:`rescale` to scale the same on
both axes.
rescale_both(scale=1) -> self
:arg scale: ratio to scale both width and height by.
"""
return self.rescale(scale, scale)
def _gen_mods_crop (self, src_sz, first_time, last_args, rect):
rect = Rect(rect)
if first_time or Rect(last_args[0]) != rect:
def apply_fn (g):
g._rect = g._rect.move(rect.x, rect.y)
g._cropped_rect = rect
def undo_fn (g):
g._rect = g._rect.move(-rect.x, -rect.y)
g._cropped_rect = None
mods = (apply_fn, undo_fn)
else:
mods = None
return (mods, rect.size)
def _crop (self, src, dest, dirty, last_args, rect):
start = src.get_rect()
rect = Rect(rect)
if dirty is not True and last_args is not None:
if Rect(last_args[0]) == rect:
# same size as last time
if dirty:
# clip dirty rects inside cropped rect; if there's a
# border, it remains empty as before, so isn't dirtied
new_dirty = []
offset = (-rect.x, -rect.y)
for r in dirty:
r = r.clip(rect)
if r:
s = r.move(offset)
new_dirty.append(s)
dest.blit(src, s, r)
return (dest, new_dirty)
else:
return (dest, False)
if start == rect:
# no cropping occurs
return (src, dirty if last_args is None else True)
# do a full transform
if start.contains(rect) and not has_alpha(src):
new_sfc = pg.Surface(rect.size)
else:
# not (no longer) opaque
new_sfc = blank_sfc(rect.size)
new_sfc.blit(src, ((0, 0), rect.size), rect)
return (new_sfc, True)
[docs] def crop (self, rect):
"""Crop the surface to the given rect.
crop(rect) -> self
``rect`` need not be contained in the current surface rect.
"""
return self.transform('crop', Rect(rect))
def _gen_mods_flip (self, src_sz, first_time, last_args, x=False, y=False):
if first_time or last_args != (x, y):
def apply_fn (g):
g._flipped = (x, y)
def undo_fn (g):
g._flipped = (False, False)
mods = (apply_fn, undo_fn)
else:
mods = None
return (mods, src_sz)
def _flip (self, src, dest, dirty, last_args, x=False, y=False):
if dirty is not True and last_args is not None and last_args == (x, y):
if dirty:
# check if a partial transform would be quicker
w, h = src.get_rect().size
alpha = has_alpha(src)
k = 5 if alpha else 3.5
if k * sum(r[2] * r[3] for r in dirty) ** .75 < w * h ** .75:
# it would (this is all empirical and quite rough)
new_dirty = []
flip = pg.transform.flip
for r in dirty:
# copy this rect to a new surface
sfc = pg.Surface(r.size)
if alpha:
sfc = sfc.convert_alpha()
sfc.blit(src, (0, 0), r)
# transform the rect
r = Rect((w - r.x - r.w if x else r.x,
h - r.y - r.h if y else r.y), r.size)
new_dirty.append(r)
# flip and blit to destination
dest.blit(flip(sfc, x, y), r)
return (dest, new_dirty)
else:
return (dest, False)
if not x and not y:
# transform does nothing
return (src, dirty if last_args is None else True)
# do a full transform
new_sfc = pg.transform.flip(src, x, y)
return (new_sfc, True)
[docs] def flip (self, x = False, y = False):
"""Flip the graphic over either axis.
flip(x = False, y = False) -> self
:arg x: whether to flip over the x-axis.
:arg y: whether to flip over the y-axis.
"""
return self.transform('flip', bool(x), bool(y))
def _gen_mods_tint (self, src_sz, first_time, last_args, colour):
colour = normalise_colour(colour)
if first_time or normalise_colour(last_args[0]) != colour:
def apply_fn (g):
g._tint_colour = colour
def undo_fn (g):
g._tint_colour = (255, 255, 255, 255)
mods = (apply_fn, undo_fn)
else:
mods = None
return (mods, src_sz)
def _tint (self, src, dest, dirty, last_args, colour):
colour = normalise_colour(colour)
if (dirty is False and last_args is not None and
normalise_colour(last_args[0]) == colour):
return (dest, False)
if colour == (255, 255, 255, 255):
# transform does nothing
return (src, dirty if last_args is None else True)
# full transform
if not has_alpha(src):
src = src.convert_alpha()
new_sfc = pg.Surface(src.get_size()).convert_alpha()
new_sfc.fill(colour)
if colour[3] > 0:
new_sfc.blit(src, (0, 0), special_flags=pg.BLEND_RGBA_MULT)
return (new_sfc, True)
[docs] def tint (self, colour):
"""Set tint colour, as taken by :func:`engine.util.normalise_colour`.
tint(colour) -> self
This doesn't actually add any colour; it just alters the amount of colour in
each channel.
"""
return self.transform('tint', colour)
[docs] def opacify (self, opacity):
"""Set opacity, from ``0`` (transparent) to ``255``.
opacify(opacity) -> self
(Sorry about the name---``fade`` would be nice, but conflicts with
:meth:`GraphicsManager.fade() <engine.gfx.container.GraphicsManager.fade>`.)
"""
return self.transform('tint', self._tint_colour[:3] + (opacity,))
def _gen_mods_rotate (self, src_sz, first_time, last_args, angle):
# - dest_sz will never get used: all following transforms are
# guaranteed to be non-builtins, if the user does nothing silly
# - mods are size-dependent, so they always change
# - computation of rot_offset happens at draw time, since it's only
# needed then, and only internally
def apply_fn (g):
g._angle = angle
g._must_apply_rot = True
def undo_fn (g):
g._angle = 0
g._rot_offset = (0, 0)
g._must_apply_rot = False
return ((apply_fn, undo_fn), src_sz)
def _rotate (self, src, dest, dirty, last_args, angle):
if not dirty and last_args is not None:
# if last_angle == angle, then surface size didn't change, so
# neither did the centre point
if abs(angle - last_args[0]) < self.rotate_threshold:
# no change to result
return (dest, False)
if abs(angle) < self.rotate_threshold:
# transform does nothing
return (src, dirty if last_args is None else True)
# do a full transform
# if not already alpha and we might end up with borders, convert to
# alpha
if angle % (pi / 2) != 0 and not has_alpha(src):
src = src.convert_alpha()
new_sfc = self.rotate_fn(src, angle)
return (new_sfc, True)
[docs] def rotate (self, angle):
"""Rotate the graphic.
rotate(angle) -> self
:arg angle: the angle in radians to rotate to, anti-clockwise from the original
graphic.
Also see :attr:`rot_anchor`.
"""
return self.transform('rotate', angle)
# drawing
def _opaque_in (self, rect):
"""Whether this draws opaque pixels in the whole of the given rect."""
return self.opaque and self._postrot_rect.contains(rect)
[docs] def snapshot (self, copy = True):
"""Return a copy of this graphic.
The copy is shallow, which means the new graphic will not transform as this one
does, but will be an exact copy of the *current state*.
:arg copy: whether to copy the final surface of this graphic/initial surface of
the returned graphic. Since under some circumstances, this graphic
can modify its final surface, this is often necessary. However, if
you do not plan to modify this graphic further and will not alter
the inital surface (:attr:`orig_sfc`) of the returned graphic, you
maybe safely pass ``False`` for reduced CPU and memory usage.
"""
self.render()
sfc = self._surface.copy() if copy else self._surface
g = Graphic(sfc, self._postrot_rect.topleft, self._layer,
self.blit_flags)
for attr in ('visible', 'scale_fn', 'rotate_fn', 'rotate_threshold',
'anchor', 'rot_anchor'):
setattr(g, attr, getattr(self, attr))
return g
[docs] def view (self):
"""Return a 'view' to this graphic.
This is a wrapper around the graphic that allows assigning a different position
and visibility (:attr:`visible`, :attr:`layer`, etc.) without affecting the
original graphic (or any other wrappers). It is a subclass of this graphic's
class.
Changes to the image represented by either the wrapper or the original graphic
affect both instances. This includes both transformations and changes to the
original surface.
This may not be used on subclasses that define a ``child`` property.
"""
parent_cls = type(self)
class GraphicView (parent_cls):
is_view = True
_faked_attrs = (
'_rect', 'last_rect', '_postrot_rect', '_last_postrot_rect',
'visible', 'was_visible', '_layer',
# Owned
'max_owners', '_on_full', '_owners'
)
def __init__ (self, graphic):
#: The ``graphic`` argument taken by the constructor.
while graphic.is_view:
graphic = graphic.child
self.child = graphic
for attr in self._faked_attrs:
setattr(self, attr, getattr(graphic, attr))
Owned.__init__(self, 1)
def __getattr__ (self, attr):
# existing attributes are returned without a call here
return getattr(self.child, attr)
def __setattr__ (self, attr, val):
# set on this instance if this is an outer attribute or a
# property, else set on the contained graphic
if (attr == 'child' or attr in self._faked_attrs or
hasattr(type(self.child), attr)):
parent_cls.__setattr__(self, attr, val)
else:
setattr(self.child, attr, val)
return GraphicView(self)
[docs] def dirty (self, *rects):
"""Mark some or all of the graphic as changed.
This is to be used when you alter the original surface (:attr:`orig_sfc`)---do
not alter any other (transformed) surfaces. Takes any number of rects to flag
as dirty. If none are given, the whole of the graphic is flagged.
"""
dirty = [Rect(r) for r in rects] if rects else True
self._orig_dirty = combine_drawn(self._orig_dirty, dirty)
self._call_cbs('draw orig')
[docs] def render (self):
"""Update the final surface.
This propagates changes from queued transformations and changes to the original
surface.
"""
t_ks = self.transforms
last_t_ks = self._last_transforms
q = self._queued_transforms
ts = self._transforms
self._queued_transforms = {}
# work out where to start (re)applying transforms from
dirty = self._orig_dirty
self._orig_dirty = False
if dirty:
i = 0
elif q:
i = min(t_ks.index(fn) for fn in q)
i = min(i, *(last_t_ks.index(fn) for fn in q if fn in last_t_ks))
else:
i = len(t_ks)
# apply transforms
orig_final_sfc = self._surface
before_rot = sfc = self._orig_sfc
passed_rot = False
for j, fn in enumerate(t_ks):
if fn != last_t_ks[j]:
# differ from last transform order at this point
dirty = True
i = j
if not dirty and fn not in q and fn in ts:
# nothing is different at this point
# grab surface to start next transform at
sfc = ts[fn][2]
if not passed_rot:
if fn == 'rotate':
passed_rot = True
else:
before_rot = sfc
if j < i:
continue
if fn in ts:
# done this transform before
last_args, src, dest, apply_fn, undo_fn = ts[fn]
else:
last_args = dest = None
if fn in q:
# got new args
args, src_sz, dest_sz, apply_fn, undo_fn = q[fn]
elif last_args is not None:
# transform with same args
args = last_args
else:
# does nothing
continue
f = getattr(self, '_' + fn) if isinstance(fn, basestring) else fn
new_sfc, dirty = f(sfc, dest, dirty, last_args, *args)
if dirty or dest is None:
# transformed for the first time or something changed in
# retransforming
# have modifier functions following code above
ts[fn] = (args, sfc, new_sfc, apply_fn, undo_fn)
sfc = new_sfc
if not passed_rot:
if fn == 'rotate':
passed_rot = True
else:
before_rot = sfc
if len(last_t_ks) > len(t_ks):
# might have just removed transforms from the end
dirty = True
self._last_transforms = list(t_ks)
if self._must_apply_rot:
self._must_apply_rot = False
# compute draw offset due to rotation
angle = ts['rotate'][0][0]
w_orig, h_orig = before_rot.get_size()
w, h = sfc.get_size()
ax, ay = pos_in_rect(self.rot_anchor, (w_orig, h_orig))
# v = c - about
vx = w_orig / 2. - ax
vy = h_orig / 2. - ay
# c_new - about_new = v.rotate(angle)
s = sin(angle)
c = cos(angle)
ax_new = w / 2. - (c * vx + s * vy)
ay_new = h / 2. - (-s * vx + c * vy)
# about = offset + about_new
self._rot_offset = (ir(ax - ax_new), ir(ay - ay_new))
if dirty:
self._dirty = combine_drawn(self._dirty, dirty)
# change current surface and rect
self._surface = sfc
self.opaque = not has_alpha(sfc)
self._rect = r = Rect(self._rect.topleft, before_rot.get_size())
self._postrot_rect = pr = r.move(self._rot_offset)
pr.size = sfc.get_size()
if sfc != orig_final_sfc:
self._call_cbs('change', orig_final_sfc, sfc)
else:
self._call_cbs('draw')
def _pre_draw (self):
"""Called by
:class:`GraphicsManager <engine.gfx.container.GraphicsManager>` before
drawing."""
self.render()
dirty = self._dirty
if self._rect != self.last_rect:
dirty = True
self._postrot_rect = Rect(
self._rect.move(self._rot_offset).topleft,
self._postrot_rect.size
)
if self.blit_flags != self._last_blit_flags:
dirty = True
self._last_blit_flags = self.blit_flags
# fastdraw needs dirty to be a list
if dirty:
pr = self._postrot_rect
if dirty is True:
dirty = [self._last_postrot_rect, pr]
else:
# translate dirty rects
pr = pr.topleft
dirty = [d_r.move(pr) for d_r in dirty]
else:
dirty = []
self._dirty = dirty
def _draw (self, dest, rects):
"""Draw the graphic.
_draw(dest, rects)
dest: pygame.Surface to draw to.
rects: list of rects to draw in.
Should never alter any state that is not internal to the graphic.
"""
sfc = self._surface
blit = dest.blit
pr = self._postrot_rect
offset = (-pr[0], -pr[1])
for r in rects:
blit(sfc, r, r.move(offset), self.blit_flags)
self._last_postrot_rect = pr
self.last_rect = self._rect
[docs] def cb (self, cb, *evts):
"""Register a callback for a number of events.
cb(cb, *evts) -> self
:arg cb: callback function; it is called with the event name followed by
event-specific arguments. If this callback was already registered,
the previous set of events specified is overridden.
:arg evts: event names to register the callback for; if none are given, it is
called for all event types.
Event types:
draw
The content of :attr:`surface` changed without the surface itself changing.
Arguments: surface.
draw orig
The content of :attr:`orig_sfc` changed without the surface itself
changing.
Arguments: surface.
change
:attr:`surface` changed to a different surface.
Arguments: old surface, new surface.
change orig
:attr:`orig_sfc` changed to a different surface.
Arguments: old surface, new surface.
resize
:attr:`surface` changed to a different surface with a different size.
Arguments: old size, new size (both ``(width, height)``).
resize orig
:attr:`orig_sfc` changed to a different surface with a different size.
Arguments: old size, new size (both ``(width, height)``).
"""
self.rm_cbs(cb)
self._cbs[cb] = evts if evts else None
if not evts:
evts = (None,)
all_evts = self._evts
for evt in evts:
all_evts.setdefault(evt, set()).add(cb)
return self
[docs] def rm_cbs (self, *cbs):
"""Remove any number of callbacks registered with events.
rm_cbs(*cbs) -> self
Missing items are ignored.
"""
all_cbs = self._cbs
all_evts = self._evts
for cb in cbs:
if cb in all_cbs:
evts = all_cbs[cb]
del all_cbs[cb]
if evts is None:
evts = (evts,)
for evt in evts:
all_evts[evt].remove(cb)
if not all_evts[evt]:
del all_evts[evt]
return self
def _call_cbs (self, evt, *args):
"""Call callbacks registered for the given event."""
for cb in self._evts.get(evt, set()).union(
self._evts.get(None, set())
):
cb(evt, *args)