Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add subrect property to Actor and 3 new class to do frame animation #278

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions pgzero/_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import Sequence, Tuple, Union, List
from pygame import Vector2, Rect
from .rect import ZRect

_Coordinate = Union[Tuple[float, float], Sequence[float], Vector2]
_CanBeRect = Union[
ZRect,
Rect,
Tuple[int, int, int, int],
List[int],
Tuple[_Coordinate, _Coordinate],
List[_Coordinate],
]
75 changes: 60 additions & 15 deletions pgzero/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from . import loaders
from . import rect
from . import spellcheck

from .rect import ZRect
from ._common import _CanBeRect

ANCHORS = {
'x': {
Expand Down Expand Up @@ -99,7 +100,7 @@ def _set_opacity(actor, current_surface):


class Actor:
EXPECTED_INIT_KWARGS = SYMBOLIC_POSITIONS
EXPECTED_INIT_KWARGS = SYMBOLIC_POSITIONS | set(("subrect",))
DELEGATED_ATTRIBUTES = [
a for a in dir(rect.ZRect) if not a.startswith("_")
]
Expand All @@ -109,27 +110,39 @@ class Actor:
_angle = 0.0
_opacity = 1.0

def _surface_cachekey(self):
if self.subrect is None:
hashv = 0
else:
hashv = hash((self.subrect.x, self.subrect.y,
self.subrect.width, self.subrect.height))
return self._image_name + '.' + str(hashv)

def _build_transformed_surf(self):
cache_len = len(self._surface_cache)
key = self._surface_cachekey()
surface_cache = self._surface_cache[key][1]
cache_len = len(surface_cache)
if cache_len == 0:
last = self._orig_surf
last = self._surface_cache[key][0]
else:
last = self._surface_cache[-1]
last = surface_cache[-1]
for f in self.function_order[cache_len:]:
new_surf = f(self, last)
self._surface_cache.append(new_surf)
surface_cache.append(new_surf)
last = new_surf
return self._surface_cache[-1]
return surface_cache[-1]

def __init__(self, image, pos=POS_TOPLEFT, anchor=ANCHOR_CENTER, **kwargs):
self._handle_unexpected_kwargs(kwargs)

self._surface_cache = []
self._surface_cache = {}
self.__dict__["_rect"] = rect.ZRect((0, 0), (0, 0))
# Initialise it at (0, 0) for size (0, 0).
# We'll move it to the right place and resize it later

self._subrect = None
self._image_name: str = None
self.image = image
self.subrect = kwargs.get('subrect', None)
self._init_position(pos, anchor, **kwargs)

def __getattr__(self, attr):
Expand Down Expand Up @@ -214,7 +227,8 @@ def _set_symbolic_pos(self, symbolic_pos_dict):
def _update_transform(self, function):
if function in self.function_order:
i = self.function_order.index(function)
del self._surface_cache[i:]
for k in self._surface_cache.keys():
del self._surface_cache[k][1][i:]
else:
raise IndexError(
"function {!r} does not have a registered order."
Expand Down Expand Up @@ -324,14 +338,45 @@ def image(self):

@image.setter
def image(self, image):
self._image_name = image
self._orig_surf = loaders.images.load(image)
self._surface_cache.clear() # Clear out old image's cache.
self._update_pos()
if self._image_name != image:
self._image_name = image
self._orig_surf = loaders.images.load(image)
key = self._surface_cachekey()
if key not in self._surface_cache.keys():
self._surface_cache[key] = [None] * 2
self._surface_cache[key][0] = self._orig_surf
self._surface_cache[key][1] = [] # Clear out old image's cache.
self._update_pos()

@property
def subrect(self):
return self._subrect

@subrect.setter
def subrect(self, subrect: _CanBeRect):
subr = subrect
if subrect is not None:
if not isinstance(self.subrect, ZRect):
subr = pygame.Rect(subrect)
if subr != self._subrect:
self._subrect = subr
subrect_tuple = None
if self._subrect is not None:
subrect_tuple = (subr.x, subr.y, subr.w, subr.h)
self._orig_surf = loaders.images.load(self.image, subrect=subrect_tuple)
key = self._surface_cachekey()
if key not in self._surface_cache.keys():
self._surface_cache[key] = [None] * 2
self._surface_cache[key][0] = self._orig_surf
self._surface_cache[key][1] = [] # Clear out old image's cache.
self._update_pos()

def _update_pos(self):
p = self.pos
self.width, self.height = self._orig_surf.get_size()
if self.subrect is None:
self.width, self.height = self._orig_surf.get_size()
else:
self.width, self.height = self.subrect.width, self.subrect.height
self._calc_anchor()
self.pos = p

Expand Down
171 changes: 171 additions & 0 deletions pgzero/image_animation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import pygame
from pgzero.actor import Actor
from pgzero import loaders
import time
from typing import List, Sequence, Callable, Tuple
from ._common import _CanBeRect
from .rect import ZRect
from .clock import each_tick, unschedule


class FramesList:

def __init__(self, **kwargs):
self._imgs: List[pygame.Surface] = []
self._rects: List[ZRect] = []
pass

def addFromSheet(self, sheet_name: str, cols: int, rows: int, cnt: int = 0,
subrect: _CanBeRect = None):
sheet: pygame.Surface = loaders.images.load(sheet_name)
if subrect is not None:
sheet = sheet.subsurface(subrect)
i = 0
for row in range(0, rows):
for col in range(0, cols):
if cnt != 0 and i == cnt:
return
width = sheet.get_width()/cols
height = sheet.get_height()/rows
self._rects.append((int(col*width), int(row*height),
int(width), int(height)))
self._imgs.append(sheet_name)
i += 1

def addFromList(self, frame_images: Sequence[str]):
for imgName in frame_images:
self._rects.append(None)
self._imgs.append(imgName)

def add(self, image: str, subrect: _CanBeRect = None):
self.frames_rects.append(subrect)
self.frames_imgs.append(image)

def __len__(self) -> int:
return len(self._imgs)


class FrameBasicAnimation:
def __init__(self, actor: Actor, frames: FramesList):
self._idx: int = 0
self._actor = actor
self._actor_subrect = None
self._actor_image = None
self._actor = actor
self._frames = frames

def store_actor_image(self):
self._actor_subrect = self._actor.subrect
self._actor_image = self._actor.image

def restore_actor_image(self):
self._actor.image = self._actor_image
self._actor.subrect = self._actor_subrect

def sel_frame(self, idx):
idx = idx % len(self._frames)
if (self._idx != idx):
self._idx = idx
self._actor.image = self._frames._imgs[self._idx]
self._actor.subrect = self._frames._rects[self._idx]

def next_frame(self) -> int:
self._idx = (self._idx+1) % len(self._frames)
self.sel_frame(self._idx)
return self._idx

def prev_frame(self) -> int:
self._idx = (self._idx-1) % len(self._frames)
self.sel_frame(self._idx)
return self._idx


class FrameAnimation(FrameBasicAnimation):
def __init__(self, actor: Actor, frames: FramesList, fps: int,
restore_image_at_stop: bool = True):
self._time_start = 0
self._time = 0
self._restore_image_at_stop = restore_image_at_stop
self.fps = fps
self._stop_at_loop = -1
self._stop_at_index = -1
self._duration = 0
self._running = False
self._finished = False
super().__init__(actor, frames)

def animate(self, dt=-1) -> Tuple[int, int]:
# calculate elapsed time
if dt == -1:
now = time.time()
if self._time_start == 0:
self._time_start = now
self._time = now - self._time_start
else:
self._time = self._time + dt

# go to next frame base on elapsed time
frame_idx = int(self._time * self.fps) % len(self._frames)
loop = int(self._time * self.fps) // len(self._frames)
self.sel_frame(frame_idx)

# check stop condition
if self._duration != 0:
if self._time > self._duration:
self.stop(True)
else:
if (self._stop_at_loop != -1
and loop >= self._stop_at_loop and frame_idx >= self._stop_at_idx):
self.stop(True)

return (loop, frame_idx)

def play(self, stop_at_loop: int = -1, stop_at_idx: int = -1, duration: float = 0,
on_finished: Callable = None) -> bool:
if self._running:
return False
self._duration = duration
self._running = True
self._finished = False
self.on_finished = on_finished
self._stop_at_loop = stop_at_loop
self._stop_at_idx = stop_at_idx
self.store_actor_image()
each_tick(self.animate)
return True

def play_once(self, on_finished: Callable = None):
self.play(0, len(self._frames)-1, on_finished=on_finished)

def play_several(self, nbr_of_loops: int, on_finished: Callable = None):
self.play(nbr_of_loops-1, len(self._frames)-1, on_finished=on_finished)

def play_during(self, duration: int, on_finished: Callable = None):
self.play(duration=duration, on_finished=on_finished)

def play_infinite(self):
self.play()

def pause(self):
if not self._paused:
unschedule(self.animate)
self._paused = True

def unpause(self):
if self._paused:
each_tick(self.animate)
self._paused = False

def stop(self, call_on_finished: bool = False):
unschedule(self.animate)
self._running = False
self._finished = True
self._paused = False
if self._restore_image_at_stop:
self.restore_actor_image()
if call_on_finished and self.on_finished:
argcount = self.on_finished.__code__.co_argcount
if argcount == 0:
self.on_finished()
elif argcount == 1:
self.on_finished(self._actor)
8 changes: 6 additions & 2 deletions pgzero/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,12 @@ class ImageLoader(ResourceLoader):
EXTNS = ['png', 'gif', 'jpg', 'jpeg', 'bmp']
TYPE = 'image'

def _load(self, path):
return pygame.image.load(path).convert_alpha()
def _load(self, path, *args, **kwargs):
subrect = kwargs.pop('subrect', None)
if subrect is None:
return pygame.image.load(path).convert_alpha()
else:
return pygame.image.load(path).convert_alpha().subsurface(subrect)

def __repr__(self):
return "<Images images={}>".format(self.__dir__())
Expand Down
6 changes: 6 additions & 0 deletions test/test_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,9 @@ def test_dir_correct(self):
a = Actor("alien")
for attribute in dir(a):
a.__getattr__(attribute)

def test_subrect(self):
"""Ensure opacity is initially set to its default value."""
a = Actor('alien')
a.subrect = (10, 12, 30, 43)
self.assertEqual((a.width, a.height), (30, 43))