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

Animated Images: end_frame, frame_skips, image_templates #411

Merged
merged 10 commits into from
Dec 15, 2020
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