Source code for engine.gfx.container

"""Graphics containers: :class:`GraphicsGroup` and :class:`GraphicsManager`.

---NODOC---

TODO:
 - make it possible for GM to have transparent BG (only if orig_sfc has alpha)
 - GraphicsGroup:
    - allow for transforms
    - internal layers (has allowed range in manager, and distributes graphics within it)
 - ignore off-screen (OoB) things (clip all dirty rects and discard zero-size ones)
    - do it before _pre_draw
 - GraphicsManager.offset to offset the viewing window (Surface.scroll is fast?)
    - supports parallax: set to {layer: ratio} or (function(layer) -> ratio) or set a Graphic property (make GraphicView have its own copy)
 - do something with/like dispman

---NODOC---

"""

import sys

import pygame as pg

from .. import sched
from ..util import ir, normalise_colour, blank_sfc, combine_drawn
try:
    from _gm import fastdraw
except ImportError:
    print >> sys.stderr, 'error: couldn\'t import _gm; did you remember to `make\'?'
    sys.exit(1)
from .graphic import Graphic
from .graphics import Colour


[docs]class GraphicsGroup (object): """Convenience wrapper for grouping a number of graphics in a simple way. GraphicsGroup(x=0, y=0) Arguments determine the group's position (:attr:`pos`); unlike for graphics, this may be floating-point. This is a ``{graphic: rel}`` mapping, where ``graphic`` is a :class:`Graphic <engine.gfx.graphic.Graphic>` instance and ``rel`` is the graphic's ``(x, y)`` position relative to this group. Adding graphics is possible with something like ``group[graphic] = rel`` (instead of using :meth:`add`). :attr:`graphic_attrs` contains some properties of this :class:`GraphicsGroup` which correspond to those of :class:`Graphic <engine.gfx.graphic.Graphic>`. These can be set to apply to all contained graphics. """ #: Attributes which are mapped to #: :class:`Graphic <engine.gfx.graphic.Graphic>` attributes. graphic_attrs = ('layer', 'visible', 'blit_flags', 'anchor', 'rot_anchor', 'scale_fn', 'rotate_fn', 'rotate_threshold') def __init__ (self, x=0, y=0): self._pos = [x, y] #: {graphic: rel} self._graphics = {} self._manager = None def __nonzero__ (self): return bool(self._graphics) def __contains__ (self, graphic): return graphic in self._graphics def __iter__ (self): return iter(self._graphics) def __len__ (self): return len(self._graphics) def __getitem__ (self, graphic): return self._graphics[graphic] def __setitem__ (self, graphic, rel): self.add(graphic, *rel) def __delitem__ (self, graphic): self.rm(graphic) def __setattr__ (self, attr, val): if attr in self.graphic_attrs: for g in self: setattr(g, attr, val) else: object.__setattr__(self, attr, val) @property def rect (self): """The ``pygame.Rect`` covered by graphics in this group. The top-left of this is not necessarily the same as :attr:`pos`. """ graphics = self._graphics.keys() if graphics: if len(graphics) == 1: return graphics[0]._rect else: return graphics[0]._rect.unionall( [g._rect for g in graphics[1:]] ) else: return pygame.Rect(0, 0, 0, 0) @property def x (self): """``x`` co-ordinate of the group's top-left corner.""" return self._pos[0] @x.setter def x (self, x): self.pos = (x, self._pos[1]) @property def y (self): """``y`` co-ordinate of the group's top-left corner.""" return self._pos[1] @y.setter def y (self, y): self.pos = (self._pos[0], y) @property def pos (self): """``[``:attr:`x` ``,`` :attr:`y` ``]``.""" return self._pos @pos.setter def pos (self, pos): x, y = pos self._pos = [x, y] # move graphics x = ir(x) y = ir(y) for g, (rel_x, rel_y) in self._graphics.iteritems(): # rel_{x,y} are ints g.pos = (x + rel_x, y + rel_y) @property def w (self): """Width of :attr:`rect`.""" return self.rect.width @property def h (self): """Height of :attr:`rect`.""" return self.rect.height @property def size (self): """``(``:attr:`w` ``,`` :attr:`h` ``)``.""" return self.rect.size
[docs] def move_by (self, dx=0, dy=0): """Move by the given number of pixels.""" self.pos = (self._pos[0] + dx, self._pos[1] + dy)
[docs] def add (self, *graphics): """Add graphics. Call either as ``add(graphic, dx=0, dy=0)`` for a single graphic, or pass any number of arguments which are ``(graphic, dx=0, dy=0)`` tuples or just ``graphic``. In each case: :arg graphic: :class:`Graphic <engine.gfx.graphic.Graphic>` instance or the ``img`` argument to :class:`Graphic <engine.gfx.graphic.Graphic>` to create one. :arg dx,dy: position relative to the group. :return: a list of added :class:`Graphic <engine.gfx.graphic.Graphic>` instances (possibly created in this call), in the order given. If any ``graphic`` is already in the group, this call changes its relative position (and unspecified ``dx`` and ``dy`` are unchanged, rather than set to ``0``). Note that graphics need not be added to a :class:`GraphicsManager` individually ---set this using :attr:`manager`. """ if len(graphics) >= 2 and isinstance(graphics[1], (int, float)): graphics = (graphics,) rtn = [] for graphic in graphics: # parse argument if isinstance(graphic, (Graphic, pg.Surface, basestring)): graphic = [graphic] else: graphic = list(graphic) if len(graphic) < 2: graphic.append(None) if len(graphic) < 3: graphic.append(None) graphic, dx, dy = graphic if not isinstance(graphic, Graphic): graphic = Graphic(graphic, pos) if self._manager is not None: self._manager.add(graphic) # determine new position for the graphic if graphic in self._graphics: if dx is None: dx = self._graphics[graphic][0] if dy is None: dy = self._graphics[graphic][1] else: if dx is None: dx = 0 if dy is None: dy = 0 rel = (ir(dx), ir(dy)) pos = (ir(self._pos[0]) + rel[0], ir(self._pos[1]) + rel[1]) self._graphics[graphic] = rel graphic.pos = pos rtn.append(graphic) return rtn
[docs] def rm (self, *graphics): """Remove graphics previously added using :meth:`add`. Raises ``KeyError`` for missing graphics. """ gm = self._manager for g in graphics: del self._graphics[g] if gm is not None: gm.rm(g)
@property def manager (self): """The :class:`GraphicsManager <engine.gfx.container.GraphicsManager>` to put graphics in.""" return self._manager @manager.setter def manager (self, manager): if manager is self._manager: return if self._manager is not None: self._manager.rm(*self._graphics) if manager is not None: manager.add(*self._graphics) self._manager = manager
[docs]class GraphicsManager (Graphic): """Draws things to a surface intelligently. GraphicsManager(scheduler[, sfc], pos=(0, 0), layer=0) :arg scheduler: a :class:`sched.Scheduler <engine.sched.Scheduler>` instance this manager should use for timing. :arg sfc: the surface to draw to; can be a ``(width, height)`` tuple to create a new transparent surface of this size. If not given or ``None``, nothing is drawn. This becomes :attr:`orig_sfc` and can be changed using this attribute. Other arguments are as taken by :class:`Graphic <engine.gfx.graphic.Graphic>`. Since this is a :class:`Graphic <engine.gfx.graphic.Graphic>` subclass, it can be added to other :class:`GraphicsManager` instances and supports transformations. None of this can be done until the manager has a surface, however, and transformations are only applied in :attr:`Graphic.surface <engine.gfx.graphic.Graphic.surface>`, not in :attr:`orig_sfc`. """ def __init__ (self, scheduler, sfc=None, pos=(0, 0), layer=0): #: The ``scheduler`` argument passed to the constructor. self.scheduler = scheduler self._init_as_graphic = False self._init_as_graphic_args = (pos, layer) self._orig_sfc = None self.orig_sfc = sfc # calls setter self._gm_dirty = False self._overlay = None self._fade_id = None self.fading = False #: ``{layer: graphics}`` dict, where ``graphics`` is a set of the #: graphics in layer ``layer``, each as taken by :meth:`add`. self.graphics = {} #: A list of layers that contain graphics, lowest first. self.layers = [] @property def orig_sfc (self): """Like :attr:`Graphic.orig_sfc <engine.gfx.graphic.Graphic.orig_sfc>`. This is the ``sfc`` argument passed to the constructor. Retrieving this causes all graphics to be drawn/updated first. """ self.draw() return Graphic.orig_sfc.fget(self) @orig_sfc.setter def orig_sfc (self, sfc): if sfc is not None and not isinstance(sfc, pg.Surface): sfc = blank_sfc(sfc) if sfc is not self._orig_sfc: self._orig_sfc = sfc if sfc is not None: if self._init_as_graphic: Graphic.orig_sfc.fset(self, sfc) else: Graphic.__init__(self, sfc, *self._init_as_graphic_args) self._init_as_graphic = True del self._init_as_graphic_args @property def orig_size (self): """The size of the surface before any transforms.""" return self._orig_sfc.get_size() @property def overlay (self): """A :class:`Graphic <engine.gfx.graphic.Graphic>` which is always drawn on top, or ``None``. There may only ever be one overlay; changing this attribute removes any previous overlay from the :class:`GraphicsManager`. """ return self._overlay @overlay.setter def overlay (self, overlay): # remove any previous overlay if self._overlay is not None: self.rm(self._overlay) # set now since used in add() self._overlay = overlay if overlay is not None: # remove any current manager overlay.release(overlay.owner) # put in the reserved layer None (sorts less than any other object) overlay._layer = None # add to this manager self.add(overlay) def _set_layers_from_set (self, ls): if None in ls: ls.remove(None) self.layers = [None] + sorted(ls) else: self.layers = sorted(ls)
[docs] def add (self, *graphics): """Add graphics. Takes any number of :class:`Graphic <engine.gfx.graphic.Graphic>` instances, and returns a list of added graphics. """ all_gs = self.graphics ls = set(self.layers) for g in graphics: l = g.layer if l is None and g is not self._overlay: raise ValueError('a graphic\'s layer must not be None') if l in ls: all_gs[l].add(g) else: all_gs[l] = set((g,)) ls.add(l) g.own(self, lambda g, gm: self.rm(g)) # don't draw over any possible previous location g.was_visible = False self._set_layers_from_set(ls) return graphics
[docs] def rm (self, *graphics): """Remove graphics. Takes any number of :class:`Graphic <engine.gfx.graphic.Graphic>` instances. Missing graphics are ignored. """ all_graphics = self.graphics ls = set(self.layers) for g in graphics: l = g.layer if l in ls: all_gs = all_graphics[l] if g in all_gs: # remove from graphics all_gs.remove(g) g.release(self) # draw over previous location if g.was_visible: self.dirty(g._last_postrot_rect) # remove layer if not all_gs: del all_graphics[l] ls.remove(l) # else not added: fail silently self._set_layers_from_set(ls)
[docs] def fade_to (self, t, colour=(0, 0, 0), resolution = None): """Fade to a colour. fade_to(t, colour=(0, 0, 0)[, resolution]) :arg t: how many seconds to take to reach ``colour``. :arg colour: the ``(R, G, B[, A = 255])`` colour to fade to. :arg resolution: as taken by :meth:`Scheduler.interp() <engine.sched.Scheduler.interp>`. If already fading, the current colour is used as the initial colour; otherwise, the initial colour is taken to be ``(R, G, B, 0)`` for the given value of ``colour``. After fading, the overlay persists; set :attr:`overlay` to ``None`` to remove it. """ colour = normalise_colour(colour) if self._fade_id is None: # doesn't already exist initial_colour = colour[:3] + (0,) else: initial_colour = self._overlay.colour self.fade(sched.interp_linear(initial_colour, (colour, t)), round_val = True, resolution = resolution)
[docs] def fade_from (self, t, colour=None, resolution = None): """Fade from a colour to no overlay. fade_from(t[, colour][, resolution]) :arg t: how many seconds to take to reach transparency. :arg colour: the ``(R, G, B[, A = 255])`` colour to fade from; if not given, the current colour is used, else ``(0, 0, 0)``. :arg resolution: as taken by :meth:`Scheduler.interp() <engine.sched.Scheduler.interp>`. Any running fade is canceled, and the final colour is taken to be ``(R, G, B, 0)`` for the given value of ``colour``. After fading, the overlay is removed. """ if colour is None: if self._fade_id is None: # doesn't already exist colour = (0, 0, 0) else: colour = self._overlay.colour colour = normalise_colour(colour) final_colour = colour[:3] + (0,) def end (): self.cancel_fade() self.fade(sched.interp_linear(colour, (final_colour, t)), end=end, round_val=True, resolution=resolution)
[docs] def fade (self, get_val, *args, **kw): """Fade between colours. Takes arguments like :meth:`Scheduler.interp() <engine.sched.Scheduler.interp>`, with ``set_val`` omitted. Any currently running fade will be canceled. After fading, the overlay persists; set :attr:`overlay` to ``None`` to remove it. """ if self._fade_id is not None: # already fading self.cancel_fade() # set colour to initial colour val = get_val(0) if val is None: # interpolation already ended return self.overlay = Colour(val, self.orig_size) self._fade_id = self.scheduler.interp( get_val, (self._overlay, 'colour'), *args, **kw ) self.fading = True
[docs] def cancel_fade (self): """Cancel any currently running fade and remove the overlay.""" if self._fade_id is not None: self.scheduler.rm_timeout(self._fade_id) self._fade_id = None self.fading = False self.overlay = None
def dirty (self, *rects): """:inherit:""" if self._surface is None: # nothing to mark as dirty return if not rects: rects = True self._gm_dirty = combine_drawn(self._gm_dirty, rects)
[docs] def draw (self, handle_dirty = True): """Update the display (:attr:`orig_sfc`). :arg handle_dirty: whether to propagate changed areas to the transformation pipeline implemented by :class:`Graphic <engine.gfx.graphic.Graphic>`. Pass ``False`` if you don't intend to use this manager as a graphic. Returns ``True`` if the entire surface changed, or a list of rects that cover changed parts of the surface, or ``False`` if nothing changed. """ layers = self.layers sfc = self._orig_sfc if not layers or sfc is None: return False graphics = self.graphics dirty = self._gm_dirty self._gm_dirty = [] if dirty is True: dirty = [sfc.get_rect()] elif dirty is False: dirty = [] dirty = fastdraw(layers, sfc, graphics, dirty) if dirty and handle_dirty: Graphic.dirty(self, *dirty) if self._orig_dirty: dirty = combine_drawn(dirty, self._orig_dirty) if not handle_dirty: self._orig_dirty = False return dirty
def render (self): """:inherit:""" self.draw() Graphic.render(self)