From 3e27159a48ff25b35191fc33f4aa605a6295d9a3 Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Sat, 5 Dec 2020 16:02:33 -0800 Subject: [PATCH 1/9] Support 'end_frame' animation property on images --- mpfmc/assets/image.py | 8 ++++++++ mpfmc/widgets/image.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/mpfmc/assets/image.py b/mpfmc/assets/image.py index 35bc9406..baf43634 100644 --- a/mpfmc/assets/image.py +++ b/mpfmc/assets/image.py @@ -237,6 +237,14 @@ def do_load(self): # load first texture to speed up first display self._callbacks.add(lambda x: self._image.texture) + self.machine.log.info("Loaded image {}, is anim? {}".format(self._image.filename, self._image.anim_available)) + # self.machine.log.info(dir(self._image)) + # if self._image.anim_available: + self._image.on_texture(self._on_texture) + + def _on_texture(self, **kwargs): + self.machine.log.info("Image texture: {}".format(kwargs)) + def _do_unload(self): # This is the method that's called to unload the asset. It's called by # the main thread so you don't have to worry about thread diff --git a/mpfmc/widgets/image.py b/mpfmc/widgets/image.py index f845edf1..67cbb80c 100644 --- a/mpfmc/widgets/image.py +++ b/mpfmc/widgets/image.py @@ -18,7 +18,7 @@ class ImageWidget(Widget): widget_type_name = 'Image' merge_settings = ('height', 'width') - animation_properties = ('x', 'y', 'color', 'rotation', 'scale', 'fps', 'current_frame', 'opacity') + animation_properties = ('x', 'y', 'color', 'rotation', 'scale', 'fps', 'current_frame', 'end_frame', 'opacity') def __init__(self, mc: "MpfMc", config: dict, key: Optional[str] = None, **kwargs) -> None: super().__init__(mc=mc, config=config, key=key) @@ -26,6 +26,7 @@ def __init__(self, mc: "MpfMc", config: dict, key: Optional[str] = None, **kwarg self._image = None # type: ImageAsset self._current_loop = 0 + self._end_frame = -1 # Retrieve the specified image asset to display. This widget simply # draws a rectangle using the texture from the loaded image asset to @@ -118,8 +119,13 @@ def _on_texture_change(self, *args) -> None: self.size = self.texture.size self._draw_widget() - # Handle animation looping (when applicable) ci = self._image.image + if self._end_frame > -1 and ci.anim_index == self._end_frame: + self._end_frame = -1 + ci.anim_reset(False) + return + + # Handle animation looping (when applicable) if ci.anim_available and self.loops > -1 and ci.anim_index == len(ci.image.textures) - 1: self._current_loop += 1 if self._current_loop > self.loops: @@ -225,6 +231,24 @@ def _set_current_frame(self, value: Union[int, float]): '''The current frame of the animation. ''' + def _get_end_frame(self) -> int: + return self._end_frame + + def _set_end_frame(self, value: int): + if not self._image.image.anim_available or not hasattr(self._image.image.image, 'textures'): + return + frame = (int(value) - 1) % len(self._image.image.image.textures) + self.mc.log.info("Setting end frame to {}, current frame is {}".format(frame, self._image.image.anim_index)) + if frame == self._image.image.anim_index - 1: + return + + self._end_frame = frame + self._image.image.anim_reset(True) + + end_frame = AliasProperty(_get_end_frame, _set_end_frame) + '''The target frame at which the animation will stop. + ''' + rotation = NumericProperty(0) '''Rotation angle value of the widget. From f9bca6c618951c137f5c5d3d550911dca691271b Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Sat, 5 Dec 2020 19:42:38 -0800 Subject: [PATCH 2/9] Refactor images to track by index, rather than frame --- mpfmc/widgets/image.py | 48 ++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/mpfmc/widgets/image.py b/mpfmc/widgets/image.py index 67cbb80c..e869b6eb 100644 --- a/mpfmc/widgets/image.py +++ b/mpfmc/widgets/image.py @@ -26,7 +26,7 @@ def __init__(self, mc: "MpfMc", config: dict, key: Optional[str] = None, **kwarg self._image = None # type: ImageAsset self._current_loop = 0 - self._end_frame = -1 + self._end_index = -1 # Retrieve the specified image asset to display. This widget simply # draws a rectangle using the texture from the loaded image asset to @@ -104,12 +104,15 @@ def _image_loaded(self, *args) -> None: self.fps = self.config['fps'] self.loops = self.config['loops'] self.start_frame = self.config['start_frame'] - + self._end_index = self.start_frame - 1 # Always play so we can get the starting frame rendered - self.play(start_frame=self.start_frame) + self.mc.log.info("Loaded calling play for image {} with start frame {}".format(self._image.image.filename, self.start_frame)) # If auto_play is not enabled, immediately stop - if not self.config['auto_play']: - self.stop() + # if self.config['auto_play']: + self.play(start_frame=self.start_frame, auto_play=self.config['auto_play']) + # if not self.config['auto_play']: + # self.stop() + self.mc.log.info(" - loaded play complete for image {}, anim index is now {} and current frame is {}".format(self._image.image.filename, self._image.image.anim_index, self.current_frame)) def _on_texture_change(self, *args) -> None: """Update texture from image asset (callback when image texture changes).""" @@ -120,9 +123,15 @@ def _on_texture_change(self, *args) -> None: self._draw_widget() ci = self._image.image - if self._end_frame > -1 and ci.anim_index == self._end_frame: - self._end_frame = -1 + + # Check if this is the end frame to stop the image. For some reason, after the image + # stops the anim_index will increment one last time, so check for end_index - 1 to prevent + # a full rollover on subsequent calls to the same end frame. + if self._end_index > -1 and ci.anim_index == self._end_index - 1: + self.mc.log.info("Image {} end index {} hit, stopping with anim index {}".format(self._image.image.filename, self._end_index, ci.anim_index)) + self._end_index = -1 ci.anim_reset(False) + self.mc.log.info(" - anim_reset false is now complete, what's the anim index? {}".format(ci.anim_index)) return # Handle animation looping (when applicable) @@ -140,6 +149,7 @@ def prepare_for_removal(self) -> None: self._image.references -= 1 if self._image.references == 0: try: + self.mc.log.info("Preparing to REMOVE image {}".format(self._image.image.filename)) self._image.image.anim_reset(False) # If the image was already unloaded from memory except AttributeError: @@ -158,14 +168,16 @@ def _draw_widget(self, *args): Scale(self.scale).origin = anchor Rectangle(pos=self.pos, size=self.size, texture=self.texture) - def play(self, start_frame: Optional[int] = 0): + def play(self, start_frame: Optional[int] = 0, auto_play: Optional[bool] = True): """Play the image animation (if images supports it).""" + self.mc.log.info("Playing image with start frame {}, current index returns {}".format(start_frame, self.current_frame)) if start_frame: self.current_frame = start_frame # pylint: disable-msg=protected-access - self._image.image._anim_index = start_frame - self._image.image.anim_reset(True) + self._image.image._anim_index = start_frame - 1 + self.mc.log.info(" - play is calling anim_reset, start frame {} current_frame {} anim_index {}".format(start_frame, self.current_frame, self._image.image.anim_index)) + self._image.image.anim_reset(auto_play) def stop(self) -> None: """Stop the image animation.""" @@ -219,12 +231,15 @@ def _get_current_frame(self) -> int: def _set_current_frame(self, value: Union[int, float]): if not self._image.image.anim_available or not hasattr(self._image.image.image, 'textures'): return - + self.mc.log.info("Setting current frame, value is {} and texture length is {}".format(value, len(self._image.image.image.textures))) frame = (int(value) - 1) % len(self._image.image.image.textures) + self.mc.log.info(" - resulting calculated frame index: {}".format(frame)) if frame == self._image.image.anim_index: + self.mc.log.info(" - anim index is also {}, no action".format(frame)) return else: - self._image.image._anim_index = frame # pylint: disable-msg=protected-access + self.mc.log.info(" - anim index is {}, setting to {} and calling play".format(self._image.image.anim_index, frame)) + self._image.image._anim_index = frame # pylint: disable-msg=protected-access self._image.image.anim_reset(True) current_frame = AliasProperty(_get_current_frame, _set_current_frame) @@ -232,17 +247,18 @@ def _set_current_frame(self, value: Union[int, float]): ''' def _get_end_frame(self) -> int: - return self._end_frame + return self._end_index + 1 def _set_end_frame(self, value: int): if not self._image.image.anim_available or not hasattr(self._image.image.image, 'textures'): return frame = (int(value) - 1) % len(self._image.image.image.textures) - self.mc.log.info("Setting end frame to {}, current frame is {}".format(frame, self._image.image.anim_index)) - if frame == self._image.image.anim_index - 1: + self.mc.log.info("Setting end index to {}, current index is {}".format(frame, self._image.image.anim_index)) + if frame == self._image.image.anim_index: + self.mc.log.info(" - end index matchen current index, not doing anything") return - self._end_frame = frame + self._end_index = frame self._image.image.anim_reset(True) end_frame = AliasProperty(_get_end_frame, _set_end_frame) From 52a165ff3f43e89eda0e79adac9ec8140cab116e Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Sat, 5 Dec 2020 20:47:14 -0800 Subject: [PATCH 3/9] Frame skips working, not great on the wraparound --- mpfmc/assets/image.py | 8 +++++++- mpfmc/widgets/image.py | 23 +++++++++++++++++------ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/mpfmc/assets/image.py b/mpfmc/assets/image.py index baf43634..aaed13c0 100644 --- a/mpfmc/assets/image.py +++ b/mpfmc/assets/image.py @@ -191,7 +191,7 @@ class ImageAsset(McAsset): pool_config_section = 'image_pools' # Will setup groups if present asset_group_class = ImagePool # Class or None to not use pools - __slots__ = ["references", "_image"] + __slots__ = ["frame_skips", "references", "_image"] def __init__(self, mc, name, file, config): super().__init__(mc, name, file, config) # be sure to call super @@ -200,6 +200,7 @@ def __init__(self, mc, name, file, config): # you don't need to do anything. self._image = None # holds the actual image in memory + self.frame_skips = None self.references = 0 @property @@ -234,6 +235,11 @@ def do_load(self): self._image.anim_reset(False) + if self.config.get('frame_skips'): + self.frame_skips = { s['from'] - 1: s['to'] -1 for s in self.config['frame_skips']} + self.machine.log.info("IMage frame skips are: {}".format(self.frame_skips)) + + # load first texture to speed up first display self._callbacks.add(lambda x: self._image.texture) diff --git a/mpfmc/widgets/image.py b/mpfmc/widgets/image.py index e869b6eb..67d88bb5 100644 --- a/mpfmc/widgets/image.py +++ b/mpfmc/widgets/image.py @@ -27,6 +27,7 @@ def __init__(self, mc: "MpfMc", config: dict, key: Optional[str] = None, **kwarg self._image = None # type: ImageAsset self._current_loop = 0 self._end_index = -1 + self._frame_skips = None # Retrieve the specified image asset to display. This widget simply # draws a rectangle using the texture from the loaded image asset to @@ -72,6 +73,11 @@ def __init__(self, mc: "MpfMc", config: dict, key: Optional[str] = None, **kwarg self.size = (0, 0) self._image.load(callback=self._image_loaded) + if self.config.get('frame_skips'): + self._frame_skips = { s['start'] - 1: s['end'] -1 for s in self.config['frame_skips']} + self.mc.log.info("IMage frame skips are: {}".format(self._frame_skips)) + else: + self.mc.log.info("no skips, maybe the image? {}".format(self._image)) # Bind to all properties that when changed need to force # the widget to be redrawn self.bind(pos=self._draw_widget, @@ -127,12 +133,17 @@ def _on_texture_change(self, *args) -> None: # Check if this is the end frame to stop the image. For some reason, after the image # stops the anim_index will increment one last time, so check for end_index - 1 to prevent # a full rollover on subsequent calls to the same end frame. - if self._end_index > -1 and ci.anim_index == self._end_index - 1: - self.mc.log.info("Image {} end index {} hit, stopping with anim index {}".format(self._image.image.filename, self._end_index, ci.anim_index)) - self._end_index = -1 - ci.anim_reset(False) - self.mc.log.info(" - anim_reset false is now complete, what's the anim index? {}".format(ci.anim_index)) - return + if self._end_index > -1: + if ci.anim_index == self._end_index - 1: + self.mc.log.info("Image {} end index {} hit, stopping with anim index {}".format(self._image.image.filename, self._end_index, ci.anim_index)) + self._end_index = -1 + ci.anim_reset(False) + self.mc.log.info(" - anim_reset false is now complete, what's the anim index? {}".format(ci.anim_index)) + return + + elif self._image.frame_skips and self._image.frame_skips.get(ci.anim_index) is not None and self._end_index > self._image.frame_skips[ci.anim_index]: + self.mc.log.info(" - SKIPPING from frame {} to {}".format(ci.anim_index, self._image.frame_skips[ci.anim_index])) + self.current_frame = self._image.frame_skips[ci.anim_index] # Handle animation looping (when applicable) if ci.anim_available and self.loops > -1 and ci.anim_index == len(ci.image.textures) - 1: From 1bd91e55d3ebf09f3dddd2cdd416e0537d417ab8 Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Sun, 6 Dec 2020 15:50:26 -0800 Subject: [PATCH 4/9] Fix rollover bug --- mpfmc/assets/image.py | 9 +++++++++ mpfmc/widgets/image.py | 7 +++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/mpfmc/assets/image.py b/mpfmc/assets/image.py index aaed13c0..d1a29d38 100644 --- a/mpfmc/assets/image.py +++ b/mpfmc/assets/image.py @@ -7,6 +7,7 @@ from kivy.core.image import Image, ImageLoaderBase, ImageLoader, Texture from mpf.core.assets import AssetPool +from mpf.core.utility_functions import Util from mpfmc.assets.mc_asset import McAsset @@ -222,6 +223,14 @@ def do_load(self): # and anything that was waiting for it to load will be called. So # all you have to do here is load and return. + if self.config.get('image_template'): + try: + template = self.machine.machine_config['image_templates'][self.config['image_template']] + self.config = Util.dict_merge(template, self.config) + self.machine.log.info(" - Merged configs with template! {}".format(self.config)) + except KeyError: + raise KeyError("Image template '{}' was not found, referenced in image config {}".format(self.config['image_template'], self.config)) + if self.machine.machine_config['mpf-mc']['zip_lazy_loading']: # lazy loading for zip file image sequences ImageLoader.zip_loader = KivyImageLoaderPatch.lazy_zip_loader diff --git a/mpfmc/widgets/image.py b/mpfmc/widgets/image.py index 67d88bb5..c2cb71e0 100644 --- a/mpfmc/widgets/image.py +++ b/mpfmc/widgets/image.py @@ -141,8 +141,11 @@ def _on_texture_change(self, *args) -> None: self.mc.log.info(" - anim_reset false is now complete, what's the anim index? {}".format(ci.anim_index)) return - elif self._image.frame_skips and self._image.frame_skips.get(ci.anim_index) is not None and self._end_index > self._image.frame_skips[ci.anim_index]: - self.mc.log.info(" - SKIPPING from frame {} to {}".format(ci.anim_index, self._image.frame_skips[ci.anim_index])) + # If the end_index is after the skip to, OR if the skip to is before the skip from (i.e. looping around) and the end_index is after the skip from + elif self._image.frame_skips and self._image.frame_skips.get(ci.anim_index) is not None and \ + (self._end_index > self._image.frame_skips[ci.anim_index] or self._end_index < ci.anim_index) and not \ + (self._end_index > ci.anim_index and self._image.frame_skips[ci.anim_index] < ci.anim_index): + self.mc.log.info(" - SKIPPING from frame {} to {} because end index is {}".format(ci.anim_index, self._image.frame_skips[ci.anim_index], self._end_index)) self.current_frame = self._image.frame_skips[ci.anim_index] # Handle animation looping (when applicable) From 96727d5443f499743ee9dfd756e3275e987b598f Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Sun, 6 Dec 2020 16:02:09 -0800 Subject: [PATCH 5/9] Cleanup logs --- mpfmc/assets/image.py | 10 +--------- mpfmc/widgets/image.py | 23 +---------------------- 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/mpfmc/assets/image.py b/mpfmc/assets/image.py index d1a29d38..63b7c4d0 100644 --- a/mpfmc/assets/image.py +++ b/mpfmc/assets/image.py @@ -227,7 +227,6 @@ def do_load(self): try: template = self.machine.machine_config['image_templates'][self.config['image_template']] self.config = Util.dict_merge(template, self.config) - self.machine.log.info(" - Merged configs with template! {}".format(self.config)) except KeyError: raise KeyError("Image template '{}' was not found, referenced in image config {}".format(self.config['image_template'], self.config)) @@ -245,21 +244,14 @@ def do_load(self): self._image.anim_reset(False) if self.config.get('frame_skips'): + # Frames are provided in 1-index values, but the image animates in zero-index values self.frame_skips = { s['from'] - 1: s['to'] -1 for s in self.config['frame_skips']} - self.machine.log.info("IMage frame skips are: {}".format(self.frame_skips)) # load first texture to speed up first display self._callbacks.add(lambda x: self._image.texture) - - self.machine.log.info("Loaded image {}, is anim? {}".format(self._image.filename, self._image.anim_available)) - # self.machine.log.info(dir(self._image)) - # if self._image.anim_available: self._image.on_texture(self._on_texture) - def _on_texture(self, **kwargs): - self.machine.log.info("Image texture: {}".format(kwargs)) - def _do_unload(self): # This is the method that's called to unload the asset. It's called by # the main thread so you don't have to worry about thread diff --git a/mpfmc/widgets/image.py b/mpfmc/widgets/image.py index c2cb71e0..c540e8e8 100644 --- a/mpfmc/widgets/image.py +++ b/mpfmc/widgets/image.py @@ -75,9 +75,7 @@ def __init__(self, mc: "MpfMc", config: dict, key: Optional[str] = None, **kwarg if self.config.get('frame_skips'): self._frame_skips = { s['start'] - 1: s['end'] -1 for s in self.config['frame_skips']} - self.mc.log.info("IMage frame skips are: {}".format(self._frame_skips)) - else: - self.mc.log.info("no skips, maybe the image? {}".format(self._image)) + # Bind to all properties that when changed need to force # the widget to be redrawn self.bind(pos=self._draw_widget, @@ -111,14 +109,7 @@ def _image_loaded(self, *args) -> None: self.loops = self.config['loops'] self.start_frame = self.config['start_frame'] self._end_index = self.start_frame - 1 - # Always play so we can get the starting frame rendered - self.mc.log.info("Loaded calling play for image {} with start frame {}".format(self._image.image.filename, self.start_frame)) - # If auto_play is not enabled, immediately stop - # if self.config['auto_play']: self.play(start_frame=self.start_frame, auto_play=self.config['auto_play']) - # if not self.config['auto_play']: - # self.stop() - self.mc.log.info(" - loaded play complete for image {}, anim index is now {} and current frame is {}".format(self._image.image.filename, self._image.image.anim_index, self.current_frame)) def _on_texture_change(self, *args) -> None: """Update texture from image asset (callback when image texture changes).""" @@ -135,17 +126,14 @@ def _on_texture_change(self, *args) -> None: # a full rollover on subsequent calls to the same end frame. if self._end_index > -1: if ci.anim_index == self._end_index - 1: - self.mc.log.info("Image {} end index {} hit, stopping with anim index {}".format(self._image.image.filename, self._end_index, ci.anim_index)) self._end_index = -1 ci.anim_reset(False) - self.mc.log.info(" - anim_reset false is now complete, what's the anim index? {}".format(ci.anim_index)) return # If the end_index is after the skip to, OR if the skip to is before the skip from (i.e. looping around) and the end_index is after the skip from elif self._image.frame_skips and self._image.frame_skips.get(ci.anim_index) is not None and \ (self._end_index > self._image.frame_skips[ci.anim_index] or self._end_index < ci.anim_index) and not \ (self._end_index > ci.anim_index and self._image.frame_skips[ci.anim_index] < ci.anim_index): - self.mc.log.info(" - SKIPPING from frame {} to {} because end index is {}".format(ci.anim_index, self._image.frame_skips[ci.anim_index], self._end_index)) self.current_frame = self._image.frame_skips[ci.anim_index] # Handle animation looping (when applicable) @@ -163,7 +151,6 @@ def prepare_for_removal(self) -> None: self._image.references -= 1 if self._image.references == 0: try: - self.mc.log.info("Preparing to REMOVE image {}".format(self._image.image.filename)) self._image.image.anim_reset(False) # If the image was already unloaded from memory except AttributeError: @@ -184,13 +171,11 @@ def _draw_widget(self, *args): def play(self, start_frame: Optional[int] = 0, auto_play: Optional[bool] = True): """Play the image animation (if images supports it).""" - self.mc.log.info("Playing image with start frame {}, current index returns {}".format(start_frame, self.current_frame)) if start_frame: self.current_frame = start_frame # pylint: disable-msg=protected-access self._image.image._anim_index = start_frame - 1 - self.mc.log.info(" - play is calling anim_reset, start frame {} current_frame {} anim_index {}".format(start_frame, self.current_frame, self._image.image.anim_index)) self._image.image.anim_reset(auto_play) def stop(self) -> None: @@ -245,14 +230,10 @@ def _get_current_frame(self) -> int: def _set_current_frame(self, value: Union[int, float]): if not self._image.image.anim_available or not hasattr(self._image.image.image, 'textures'): return - self.mc.log.info("Setting current frame, value is {} and texture length is {}".format(value, len(self._image.image.image.textures))) frame = (int(value) - 1) % len(self._image.image.image.textures) - self.mc.log.info(" - resulting calculated frame index: {}".format(frame)) if frame == self._image.image.anim_index: - self.mc.log.info(" - anim index is also {}, no action".format(frame)) return else: - self.mc.log.info(" - anim index is {}, setting to {} and calling play".format(self._image.image.anim_index, frame)) self._image.image._anim_index = frame # pylint: disable-msg=protected-access self._image.image.anim_reset(True) @@ -267,9 +248,7 @@ def _set_end_frame(self, value: int): if not self._image.image.anim_available or not hasattr(self._image.image.image, 'textures'): return frame = (int(value) - 1) % len(self._image.image.image.textures) - self.mc.log.info("Setting end index to {}, current index is {}".format(frame, self._image.image.anim_index)) if frame == self._image.image.anim_index: - self.mc.log.info(" - end index matchen current index, not doing anything") return self._end_index = frame From e46a7c5867ef49b3c4ccfc9d1333b4036d3bd7b9 Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Sun, 6 Dec 2020 16:32:14 -0800 Subject: [PATCH 6/9] Remove extra line --- mpfmc/assets/image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mpfmc/assets/image.py b/mpfmc/assets/image.py index 63b7c4d0..7357ee67 100644 --- a/mpfmc/assets/image.py +++ b/mpfmc/assets/image.py @@ -250,7 +250,6 @@ def do_load(self): # load first texture to speed up first display self._callbacks.add(lambda x: self._image.texture) - self._image.on_texture(self._on_texture) def _do_unload(self): # This is the method that's called to unload the asset. It's called by From 9691a29fbebcbb5234be15180f03c14eb86e1993 Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Sun, 6 Dec 2020 17:09:10 -0800 Subject: [PATCH 7/9] A happy linter is a happy PR --- mpfmc/assets/image.py | 6 +++--- mpfmc/widgets/image.py | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/mpfmc/assets/image.py b/mpfmc/assets/image.py index 7357ee67..b81b8731 100644 --- a/mpfmc/assets/image.py +++ b/mpfmc/assets/image.py @@ -228,7 +228,8 @@ def do_load(self): template = self.machine.machine_config['image_templates'][self.config['image_template']] self.config = Util.dict_merge(template, self.config) except KeyError: - raise KeyError("Image template '{}' was not found, referenced in image config {}".format(self.config['image_template'], self.config)) + raise KeyError("Image template '{}' was not found, referenced in image config {}".format( + self.config['image_template'], self.config)) if self.machine.machine_config['mpf-mc']['zip_lazy_loading']: # lazy loading for zip file image sequences @@ -245,8 +246,7 @@ def do_load(self): if self.config.get('frame_skips'): # Frames are provided in 1-index values, but the image animates in zero-index values - self.frame_skips = { s['from'] - 1: s['to'] -1 for s in self.config['frame_skips']} - + self.frame_skips = {s['from'] - 1: s['to'] - 1 for s in self.config['frame_skips']} # load first texture to speed up first display self._callbacks.add(lambda x: self._image.texture) diff --git a/mpfmc/widgets/image.py b/mpfmc/widgets/image.py index c540e8e8..a58aa485 100644 --- a/mpfmc/widgets/image.py +++ b/mpfmc/widgets/image.py @@ -74,7 +74,7 @@ def __init__(self, mc: "MpfMc", config: dict, key: Optional[str] = None, **kwarg self._image.load(callback=self._image_loaded) if self.config.get('frame_skips'): - self._frame_skips = { s['start'] - 1: s['end'] -1 for s in self.config['frame_skips']} + self._frame_skips = {s['start'] - 1: s['end'] - 1 for s in self.config['frame_skips']} # Bind to all properties that when changed need to force # the widget to be redrawn @@ -130,11 +130,12 @@ def _on_texture_change(self, *args) -> None: ci.anim_reset(False) return - # If the end_index is after the skip to, OR if the skip to is before the skip from (i.e. looping around) and the end_index is after the skip from - elif self._image.frame_skips and self._image.frame_skips.get(ci.anim_index) is not None and \ - (self._end_index > self._image.frame_skips[ci.anim_index] or self._end_index < ci.anim_index) and not \ - (self._end_index > ci.anim_index and self._image.frame_skips[ci.anim_index] < ci.anim_index): - self.current_frame = self._image.frame_skips[ci.anim_index] + skip_to = self._image.frame_skips and self._image.frame_skips.get(ci.anim_index) + # If the end_index is after the skip to or before the current position (i.e. we need to loop) + # But not if the skip will cause a loop around and bypass the end_index ahead + if skip_to is not None and (self._end_index > skip_to or self._end_index < ci.anim_index) and not \ + (self._end_index > ci.anim_index and skip_to < ci.anim_index): + self.current_frame = skip_to # Handle animation looping (when applicable) if ci.anim_available and self.loops > -1 and ci.anim_index == len(ci.image.textures) - 1: @@ -234,7 +235,7 @@ def _set_current_frame(self, value: Union[int, float]): if frame == self._image.image.anim_index: return else: - self._image.image._anim_index = frame # pylint: disable-msg=protected-access + self._image.image._anim_index = frame # pylint: disable-msg=protected-access self._image.image.anim_reset(True) current_frame = AliasProperty(_get_current_frame, _set_current_frame) From ffa602b91cb008d36a180c83c4a914b0944c5f82 Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Mon, 14 Dec 2020 13:56:02 -0800 Subject: [PATCH 8/9] Remove duplicate skip_frames logic, cleanup comments --- mpfmc/widgets/image.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/mpfmc/widgets/image.py b/mpfmc/widgets/image.py index a58aa485..2dbfec23 100644 --- a/mpfmc/widgets/image.py +++ b/mpfmc/widgets/image.py @@ -27,7 +27,6 @@ def __init__(self, mc: "MpfMc", config: dict, key: Optional[str] = None, **kwarg self._image = None # type: ImageAsset self._current_loop = 0 self._end_index = -1 - self._frame_skips = None # Retrieve the specified image asset to display. This widget simply # draws a rectangle using the texture from the loaded image asset to @@ -73,9 +72,6 @@ def __init__(self, mc: "MpfMc", config: dict, key: Optional[str] = None, **kwarg self.size = (0, 0) self._image.load(callback=self._image_loaded) - if self.config.get('frame_skips'): - self._frame_skips = {s['start'] - 1: s['end'] - 1 for s in self.config['frame_skips']} - # Bind to all properties that when changed need to force # the widget to be redrawn self.bind(pos=self._draw_widget, @@ -108,7 +104,10 @@ def _image_loaded(self, *args) -> None: self.fps = self.config['fps'] self.loops = self.config['loops'] self.start_frame = self.config['start_frame'] - self._end_index = self.start_frame - 1 + # If not auto playing, set the end index to be the start frame + if not self.config['auto_play']: + # Frame numbers start at 1 and indexes at 0, so subtract 1 + self._end_index = self.start_frame - 1 self.play(start_frame=self.start_frame, auto_play=self.config['auto_play']) def _on_texture_change(self, *args) -> None: @@ -123,7 +122,7 @@ def _on_texture_change(self, *args) -> None: # Check if this is the end frame to stop the image. For some reason, after the image # stops the anim_index will increment one last time, so check for end_index - 1 to prevent - # a full rollover on subsequent calls to the same end frame. + # a full animation loop on subsequent calls to the same end frame. if self._end_index > -1: if ci.anim_index == self._end_index - 1: self._end_index = -1 @@ -131,8 +130,8 @@ def _on_texture_change(self, *args) -> None: return skip_to = self._image.frame_skips and self._image.frame_skips.get(ci.anim_index) - # If the end_index is after the skip to or before the current position (i.e. we need to loop) - # But not if the skip will cause a loop around and bypass the end_index ahead + # Skip if the end_index is after the skip_to or before the current position (i.e. we need to loop), + # but not if the skip will cause a loop around and bypass the end_index ahead if skip_to is not None and (self._end_index > skip_to or self._end_index < ci.anim_index) and not \ (self._end_index > ci.anim_index and skip_to < ci.anim_index): self.current_frame = skip_to From 69407c2623f83b2e4f9f9d04408079dc5a279565 Mon Sep 17 00:00:00 2001 From: Anthony van Winkle Date: Mon, 14 Dec 2020 17:47:38 -0800 Subject: [PATCH 9/9] Unit tests for image frame hold and skip --- .../config/test_animated_images.yaml | 24 ++++++++++++++- .../animated_images/images/reel.gif | Bin 0 -> 4144 bytes mpfmc/tests/test_AnimatedImages.py | 28 ++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 mpfmc/tests/machine_files/animated_images/images/reel.gif diff --git a/mpfmc/tests/machine_files/animated_images/config/test_animated_images.yaml b/mpfmc/tests/machine_files/animated_images/config/test_animated_images.yaml index 057074fa..13afd2c7 100644 --- a/mpfmc/tests/machine_files/animated_images/config/test_animated_images.yaml +++ b/mpfmc/tests/machine_files/animated_images/config/test_animated_images.yaml @@ -5,6 +5,13 @@ displays: width: 400 height: 300 +images: + stick-figures-skipframes: + file: reel.gif + frame_skips: + - from: 3 + to: 8 + slides: slide1: - type: image @@ -14,7 +21,6 @@ slides: - image: busy-stick-figures-animated type: image y: 100 -# auto_play: no x: 250 - type: text text: ZIP FILE OF PNGs @@ -41,6 +47,20 @@ slides: type: image y: 100 x: 250 + slide3: + - image: busy-stick-figures-animated + type: image + auto_play: false + start_frame: 4 + slide4: + - image: stick-figures-skipframes + type: image + auto_play: false + animations: + advance_frames: + - property: end_frame + value: 10 + duration: 0 slide_player: slide1: slide1 slide1_remove: @@ -48,3 +68,5 @@ slide_player: slide2: slide2: priority: 200 + slide3: slide3 + slide4: slide4 diff --git a/mpfmc/tests/machine_files/animated_images/images/reel.gif b/mpfmc/tests/machine_files/animated_images/images/reel.gif new file mode 100644 index 0000000000000000000000000000000000000000..d3cb94543c6038549db7e073c8503770f14607a5 GIT binary patch literal 4144 zcmZ{nS5y<+(uR>D9Yk8FO7BST7LeYX5CWngp`&yNO_5@NfOJR*QF=m^4gpk3XrTzw zo66E~$;REG_P< zn`+5ON|9Xoeamx3O8Vzj{7?AvnTYrj_8NoVp)Wwz%_P(=w4RejFHwZ8QKs_uBQ|Hc zL4ejem~9#2G!n){DZ{Ln;MEk4M*hdbhT1~84eJJr}(fyU39&_{y|Y;3YXV? zDA7++KbZL4>$W)e^t`PxI|_9%P&st6kR)*TO{u6!)kPxsj2lS5&=$5bwTdCX=Fsj> zPKlMBGGWnF_$4?BUH?Si{oL{-S~#rN+d*0SGqL@OfV9)yG+vzi&&kwB175czWc-46 zdGVr?{t@Zg0)$~%g7#3kl?AvakN9*%jY75(6x=7#SILz~&b9ij75DQ`# zX*{?yfF5A8{}nl5XJU_vyZda@~&FyAeGpUUGPZ+)^g)!4R z;$X&&g2tmwrk`<69H%jIaHA#N3`U*}K9OPv-Y8Cs`KC~u+Gx2$gYU&}p#PW4e-O+3i#YD%&f9$C#xym?s!8$C zi=8o;l>lXOyMr#01aBes4W2{7h9F`;C_~foKIw?EJKPoUg{;u%+( zu6&k2^}sdEGaNP$5;#VQY@;xuy$~?6*Rtn^-^t>>AS?_$m5gxQ{3Qvy^?X6|!_w-T z@`v%8(dV@>Zxy2zRMhszXp`PsQ?}1NaW&;F@_G!OP@i#lR=|_Y{@lU8iE^RmrKTFO zWoMODw}Eugo)5;DrxGDz&_5t_CD+JU_H2-9+{eM>jh!VW)r`~fPhhWd!EH}!X?>NPdIFd*14B1Tjxwj+A@AA`Z;g=jkuJOSdA(xLhMQU_wJV8sW~$GGoO#D8ta}+cg7bpOM9>{gx#D?iOXBmJl%28m8-= zq#<{XG{SR4a zfBX7yQo7VVa8-`o2P6cb7?%POT#0O$i4V@B9uPGwk?>rX`$j$Coo|0lcz`FZa<#+N zvNF4L-m)9-ZZK6bX>%5@hpUE<-+qnX48ZP$(=L9YhT2yR?Ax})+$d=z*Bz6(kH$?f z$mSP+KLr5m+X3>^Q;Mf%mN9#*P;H!&61Xq$2^^;xdG!HsnNda`e)hy2sKOMQFdNw~ zhV4BNku$*SpIEZZ^yQSF9k?%rGD>i{6(I)8vtD%^7t@|A?6hG9xSYA;gv+89P*1s@ zZF8%e>?hu;dT#=O4G*=2o7pkUv6$Xaf1FJhn{e8N(0Dckdx+%|%z-@$NHz8X^i*f& z+22r4)tBoqDUICXr*enZaCRmysr7GXJ39{OsCcAqL5zyD(xj>go;8O&9WIJz8$_K? zSKfighT2J9RnCkX@Yy&@IQyiVh}`JXz}>@@jodJZDsEFb+372MHrspQ6vS+=qIVqx zH_#*C9W)^j>OG z6hYN^qe}W@*m$&`cam2{nr#*f&rMhy#`GCfe{p^dq_jo_4i-nG5 z*889Ng;_=&gPWcdqRA)--+@AX@;WTau1ArSaRRYDfPQ5Qv^>@i$3|exOe>mcI*7Jn znAKVDNDn@r7LC!0BX1e=h{02nUvWfYrI5IuIoMX22Q8_dMDz}}wHqrz+L%jy=iExr zHXwYiwrpZoeD{27LHD>Pzx4wE2_OD)H*6g_*YZ+-s_+@^;)OIIWGN9;SbXNVjOvrj67sJr*NUpMFAz?#X- z;1LG~8(Y=`=l#H%;JI=`&giYu_3!1Zb5x;|A_;b&HgTmRqbF)%#)C9rxv0bo^6UM2 z!C@^Cy2NinA>S_PfcI37qJ21eO6*$QhAgL-3KJcw!+sPFa_HiDG*p+rY^>_HRGZT+;4 zcd;XEjpww7rAdq6oL!r|-e?UCE}>wVHqQcLB7GoJ(I-%4m-PH85=F9ouxr$TCcE|V z0sA;Qh|Faj&Q3kew0ypwCa2$Oo8~kcQZ80x=GQ+^GJP7>GR8S{x zFIo{+gXIiyR0`_us)RYx7=P(I#@w3cZp$wEa?>n*Z9Fx|J5${g95RT{{De~VTv<}K z&?(993BoV2Q*XGx9Kg$JrQN+#m7=l>i&f)Zf zO#yfI_m2w2w}Ggo*qUQ?!5eq^x$;nWvXQd+1dE+JwrYFQ?;n5pwOeK=CGCt$arir< z|5%UIzrFb%p%#A+MD|TzZbO#H1%dJr_xg(X`qSyQhe}vJ)W@_Oy{EeJ3Ql3xBa2Gn z74Sp9Pzi^wMb*ecTyqc&BqU(khKlST9w>{7ESf>O%H^k!$*szFqhI;sYh8+Z7*`a7 zYV$P?w5=P0o8-HvbQL&t}Ddx&TJJ6?4Bz`PF`TIQgunow)cF%}w2e+nMvS7q^)uGepn)GoBV3y&O72 zx?;F5-#2q$JR&ZcGGJNH=x$(rW({L}PQa-4a1B_sm`p{Enmxn)fz2!$8(#6?$&(lO zWrnGOx3IXWG4Mh^uWd5u*!SfW|P`kU8r&^pYhZu_~}x zjk@fOP}B5qH_%Xl&IhYtZ*#WJ zCI|A6?0zZsEs?AGwc|>B2}@XLsZ8-~-;8^7sF?D=`2tZYQZK2y2fu4pB z)7!8=y~68PrpmGU)EGY&oH0V7)gDE)tjHwd!SNjadNYOE-~HrT-(;dmz3~j7fjavW ziYWj=2TIj>^+xs8C3JIRmR!0(QHexdfoW=YsW+ZSZ^9Atsi-fDajTBB(Fn|wqKw)* z|0;{t%9UHZLen4x?1KXwWCZ+bfEFbtL0DwG$=mh}scONWu{F!%c`{}3?D*p{n-JFo z>=RpAFU^`3U;BNRda?ONoU>kxzU6lJ`>muqD;p2!=6Hinx--x$+mA$b3&RcT#`VYW zYO?p;(RE4JTe!I%+0H1_F4Vr}d6UGB3y>cIlUR7>0@fXoWkJI%i80=b3ytfk;|g@A zce@UfwG~R06m@tNpOe&H)K=KWUI?zsOHQhIek*VHkbHP?eDaPw&pG~ z+t1q&``D+8cU&JQxI{+U{jn3(l;d{QvDBD+~F1g literal 0 HcmV?d00001 diff --git a/mpfmc/tests/test_AnimatedImages.py b/mpfmc/tests/test_AnimatedImages.py index 7dbde840..de63911f 100644 --- a/mpfmc/tests/test_AnimatedImages.py +++ b/mpfmc/tests/test_AnimatedImages.py @@ -62,3 +62,31 @@ def test_animated_images(self): self.advance_time(1) self.assertTrue(self.mc.images["busy-stick-figures-animated"].image._anim_ev) #self.assertEqual(1, self.mc.images["busy-stick-figures-animated"].references) + + def test_start_frame_hold(self): + self.mc.events.post('slide3') + self.advance_time() + stick_figures = self.mc.targets['default'].current_slide.widgets[0].widget + for _ in range(4): + self.advance_time() + self.assertEqual(stick_figures.current_frame, 4) + + def test_skip_frames(self): + self.mc.events.post('slide4') + self.advance_time() + stick_figures = self.mc.targets['default'].current_slide.widgets[0].widget + self.advance_time() + self.assertEqual(stick_figures.current_frame, 0) + self.mc.events.post('advance_frames') + self.advance_time() + self.assertEqual(stick_figures.current_frame, 1) + self.advance_time() + self.assertEqual(stick_figures.current_frame, 2) + self.advance_time() + self.assertEqual(stick_figures.current_frame, 3) + self.advance_time() + self.assertEqual(stick_figures.current_frame, 9) + self.advance_time() + self.assertEqual(stick_figures.current_frame, 10) + self.advance_time() + self.assertEqual(stick_figures.current_frame, 10)