From 561572885c58e550893154a5e8bc57a5d5a451f6 Mon Sep 17 00:00:00 2001 From: yansnow78 Date: Tue, 1 Feb 2022 13:05:49 +0100 Subject: [PATCH 1/5] add subrect property --- pgzero/actor.py | 77 ++++++++++++++++++++++++++++++++++++++-------- pgzero/loaders.py | 8 +++-- test/test_actor.py | 6 ++++ 3 files changed, 77 insertions(+), 14 deletions(-) diff --git a/pgzero/actor.py b/pgzero/actor.py index e8697e9f..068e71f8 100644 --- a/pgzero/actor.py +++ b/pgzero/actor.py @@ -1,3 +1,4 @@ +from typing import Union import pygame from math import radians, sin, cos, atan2, degrees, sqrt @@ -5,6 +6,7 @@ from . import loaders from . import rect from . import spellcheck +from .rect import ZRect ANCHORS = { @@ -108,28 +110,41 @@ class Actor: _anchor = _anchor_value = (0, 0) _angle = 0.0 _opacity = 1.0 + _image_name = None + _subrect = None + + 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.image = image + self.subrect = kwargs.pop('subrect', None) self._init_position(pos, anchor, **kwargs) def __getattr__(self, attr): @@ -214,7 +229,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." @@ -324,14 +340,51 @@ 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: Union[pygame.Rect, ZRect]): + 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) + # if self._subrect is None: + # self._orig_surf = loaders.images.load(self.image) + # else: + # self._orig_surf = loaders.images.load(self.image)\ + # .subsurface(self._subrect) + # self._surface_cache.clear() # Clear out old image's cache. + 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 diff --git a/pgzero/loaders.py b/pgzero/loaders.py index ff3107c7..1d69da8d 100644 --- a/pgzero/loaders.py +++ b/pgzero/loaders.py @@ -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 "".format(self.__dir__()) diff --git a/test/test_actor.py b/test/test_actor.py index b74410c4..d2413b88 100644 --- a/test/test_actor.py +++ b/test/test_actor.py @@ -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)) From 61e3ab4f97c74424b37c06593846ba360332dfc3 Mon Sep 17 00:00:00 2001 From: yansnow78 Date: Thu, 10 Feb 2022 11:27:11 +0100 Subject: [PATCH 2/5] remove some commented code in subrect setter --- pgzero/actor.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pgzero/actor.py b/pgzero/actor.py index 068e71f8..d0d3f9a9 100644 --- a/pgzero/actor.py +++ b/pgzero/actor.py @@ -366,12 +366,6 @@ def subrect(self, subrect: Union[pygame.Rect, ZRect]): 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) - # if self._subrect is None: - # self._orig_surf = loaders.images.load(self.image) - # else: - # self._orig_surf = loaders.images.load(self.image)\ - # .subsurface(self._subrect) - # self._surface_cache.clear() # Clear out old image's cache. key = self._surface_cachekey() if key not in self._surface_cache.keys(): self._surface_cache[key] = [None] * 2 From ea040f995361e6216f85ea46f12a68cb30a465f7 Mon Sep 17 00:00:00 2001 From: yansnow78 Date: Thu, 10 Feb 2022 11:44:53 +0100 Subject: [PATCH 3/5] add 3 new classes FramesList, FrameBasicAnimation, FrameAnimation --- pgzero/_common.py | 13 +++ pgzero/image_animation.py | 171 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 pgzero/_common.py create mode 100644 pgzero/image_animation.py diff --git a/pgzero/_common.py b/pgzero/_common.py new file mode 100644 index 00000000..5e2f7e39 --- /dev/null +++ b/pgzero/_common.py @@ -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], +] diff --git a/pgzero/image_animation.py b/pgzero/image_animation.py new file mode 100644 index 00000000..5f7e4bb1 --- /dev/null +++ b/pgzero/image_animation.py @@ -0,0 +1,171 @@ +import pygame +from pgzero.actor import Actor +from pgzero import loaders +import time +from typing import List, Sequence, Callable +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) -> (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) From 5e679a7b959bbb9391ec16b0eaf919ec51c6869b Mon Sep 17 00:00:00 2001 From: yansnow78 Date: Thu, 10 Feb 2022 17:06:17 +0100 Subject: [PATCH 4/5] correct small errors around subrect property --- pgzero/actor.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pgzero/actor.py b/pgzero/actor.py index d0d3f9a9..ef7b2db5 100644 --- a/pgzero/actor.py +++ b/pgzero/actor.py @@ -1,4 +1,3 @@ -from typing import Union import pygame from math import radians, sin, cos, atan2, degrees, sqrt @@ -7,7 +6,7 @@ from . import rect from . import spellcheck from .rect import ZRect - +from ._common import _CanBeRect ANCHORS = { 'x': { @@ -101,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("_") ] @@ -110,8 +109,6 @@ class Actor: _anchor = _anchor_value = (0, 0) _angle = 0.0 _opacity = 1.0 - _image_name = None - _subrect = None def _surface_cachekey(self): if self.subrect is None: @@ -142,9 +139,10 @@ def __init__(self, image, pos=POS_TOPLEFT, anchor=ANCHOR_CENTER, **kwargs): 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.pop('subrect', None) + self.subrect = kwargs.get('subrect', None) self._init_position(pos, anchor, **kwargs) def __getattr__(self, attr): @@ -355,7 +353,7 @@ def subrect(self): return self._subrect @subrect.setter - def subrect(self, subrect: Union[pygame.Rect, ZRect]): + def subrect(self, subrect: _CanBeRect): subr = subrect if subrect is not None: if not isinstance(self.subrect, ZRect): From 5db172a591960e366e8d46f90bff5301db9f0810 Mon Sep 17 00:00:00 2001 From: yansnow78 Date: Sat, 12 Feb 2022 12:39:41 +0100 Subject: [PATCH 5/5] Fix flake8 issues --- pgzero/image_animation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgzero/image_animation.py b/pgzero/image_animation.py index 5f7e4bb1..56030757 100644 --- a/pgzero/image_animation.py +++ b/pgzero/image_animation.py @@ -2,7 +2,7 @@ from pgzero.actor import Actor from pgzero import loaders import time -from typing import List, Sequence, Callable +from typing import List, Sequence, Callable, Tuple from ._common import _CanBeRect from .rect import ZRect from .clock import each_tick, unschedule @@ -94,7 +94,7 @@ def __init__(self, actor: Actor, frames: FramesList, fps: int, self._finished = False super().__init__(actor, frames) - def animate(self, dt=-1) -> (int, int): + def animate(self, dt=-1) -> Tuple[int, int]: # calculate elapsed time if dt == -1: now = time.time() @@ -142,7 +142,7 @@ def play_several(self, nbr_of_loops: int, on_finished: Callable = None): def play_during(self, duration: int, on_finished: Callable = None): self.play(duration=duration, on_finished=on_finished) - + def play_infinite(self): self.play()