Skip to content

Commit

Permalink
Merge pull request #411 from avanwinkle/image-frame-animation-events
Browse files Browse the repository at this point in the history
Animated Images: end_frame, frame_skips, image_templates
  • Loading branch information
avanwinkle authored Dec 15, 2020
2 parents 907e812 + 10a9343 commit 603f45a
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 14 deletions.
16 changes: 15 additions & 1 deletion mpfmc/assets/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -41,10 +47,26 @@ 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:
slide1: remove
slide2:
slide2:
priority: 200
slide3: slide3
slide4: slide4
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions mpfmc/tests/test_AnimatedImages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
57 changes: 45 additions & 12 deletions mpfmc/widgets/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ 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)
self.size = (0, 0)

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
Expand Down Expand Up @@ -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)."""
Expand All @@ -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:
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 603f45a

Please sign in to comment.