Source code for ppb.features.animation

"""
A system for producing animated sprites.

Only supports frame-by-frame, not gif, apng, or full motion video.
"""
import time
import re
import ppb

FILE_PATTERN = re.compile(r'\{(\d+)\.\.(\d+)\}')


[docs]class Animation: """ An "image" that actually rotates through numbered files at the specified rate. """ # Override this to change the clock used for frames. clock = ppb.get_time
[docs] def __init__(self, filename, frames_per_second): """ :param str filename: A path containing a ``{2..4}`` indicating the frame number :param number frames_per_second: The number of frames to show each second """ self._filename = filename self.frames_per_second = frames_per_second self._paused_frame = None self._pause_level = 0 self._frames = [] self._offset = -self._clock() self._compile_filename()
def __repr__(self): return f"{type(self).__name__}({self._filename!r}, {self.frames_per_second!r})" # Do we need pickle/copy dunders?
[docs] def copy(self): """ Create a new Animation with the same filename and framerate. Pause status and starting time are reset. """ return type(self)(self._filename, self.frames_per_second)
def _clock(self): return type(self).clock() @property def filename(self): return self._filename @filename.setter def filename(self, value): self._filename = value self._compile_filename() def _compile_filename(self): match = FILE_PATTERN.search(self._filename) start, end = match.groups() numdigits = min(len(start), len(end)) start = int(start) end = int(end) template = FILE_PATTERN.sub( '{:0%dd}' % numdigits, self._filename, ) self._frames = [ ppb.Image(template.format(n)) for n in range(start, end + 1) ]
[docs] def pause(self): """ Pause the animation. """ if not self._pause_level: self._paused_time = self._clock() + self._offset self._paused_frame = self.current_frame self._pause_level += 1
[docs] def unpause(self): """ Unpause the animation. """ self._pause_level -= 1 if not self._pause_level: self._offset = self._paused_time - self._clock()
def _current_frame(self, time): if not self._pause_level: return ( int((time + self._offset) * self.frames_per_second) % len(self._frames) ) else: return self._paused_frame @property def current_frame(self): """ Compute the number of the current frame (0-indexed) """ if not self._pause_level: return ( int((self._clock() + self._offset) * self.frames_per_second) % len(self._frames) ) else: return self._paused_frame
[docs] def load(self): """ Get the current frame path. """ return self._frames[self.current_frame].load()
# This is so that if you assign an Animation to a class, instances will get # their own copy, so their animations run independently. _prop_name = None def __get__(self, obj, type=None): if obj is None: return self v = vars(obj) if self._prop_name not in v: v[self._prop_name] = self.copy() return v[self._prop_name] # Don't need __set__() or __delete__(), additional accesses will be via # __dict__ directly. def __set_name__(self, owner, name): self._prop_name = name