diff --git a/mpfmc/assets/image.py b/mpfmc/assets/image.py index cfda9b58..652c4f73 100644 --- a/mpfmc/assets/image.py +++ b/mpfmc/assets/image.py @@ -8,6 +8,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 @@ -195,7 +196,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 @@ -204,6 +205,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 @@ -225,6 +227,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) + 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 @@ -238,6 +248,10 @@ 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']} + # load first texture to speed up first display self._callbacks.add(lambda x: self._image.texture) 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 00000000..d3cb9454 Binary files /dev/null and b/mpfmc/tests/machine_files/animated_images/images/reel.gif differ 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) diff --git a/mpfmc/widgets/image.py b/mpfmc/widgets/image.py index cc380f23..3995e158 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_index = -1 # Retrieve the specified image asset to display. This widget simply # draws a rectangle using the texture from the loaded image asset to @@ -103,12 +104,11 @@ def _image_loaded(self, *args) -> None: self.fps = self.config['fps'] self.loops = self.config['loops'] self.start_frame = self.config['start_frame'] - - # Always play so we can get the starting frame rendered - self.play(start_frame=self.start_frame) - # If auto_play is not enabled, immediately stop + # If not auto playing, set the end index to be the start frame if not self.config['auto_play']: - self.stop() + # 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: """Update texture from image asset (callback when image texture changes).""" @@ -118,8 +118,25 @@ def _on_texture_change(self, *args) -> None: self.size = self.texture.size self._draw_widget() - # Handle animation looping (when applicable) ci = self._image.image + + # 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 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 + ci.anim_reset(False) + return + + skip_to = self._image.frame_skips and self._image.frame_skips.get(ci.anim_index) + # 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 + + # 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: @@ -152,14 +169,14 @@ 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).""" 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._image.image.anim_reset(auto_play) def stop(self) -> None: """Stop the image animation.""" @@ -213,18 +230,34 @@ 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 - frame = (int(value) - 1) % len(self._image.image.image.textures) 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) '''The current frame of the animation. ''' + def _get_end_frame(self) -> int: + 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) + if frame == self._image.image.anim_index: + return + + self._end_index = 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.