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

Add support for simple image animations #219

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions examples/basic/animateimages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
WIDTH = 200
HEIGHT = 300

INSTRUCTIONS = """\
===================================
Move player left and right with A and D.
Pause and resume the top left explosion with P.
Trigger the bottom left explosion with B.
Move back and forth through the bottom right explosion images
with the left and right arrows.
"""

pauseable = Actor('explosion3', (50, 50))
constant = Actor('explosion3', (150, 50))
temporary = Actor('explosion3', (50, 150))
manual = Actor('explosion3', (150, 150))
player = Actor('girl_walk1_right', midbottom=(WIDTH // 2, HEIGHT))
actors = [pauseable, constant, temporary, manual, player]

images = ['explosion3', 'explosion2', 'explosion1']
walk_right = ['girl_walk1_right', 'girl_walk2_right']
walk_left = ['girl_walk1_left', 'girl_walk2_left']

def remove_boom3():
actors.remove(temporary)

# An animation that can be paused with the P key (see on_key_down)
pausable_anim = animate.images(pauseable, images, every=0.4, source='clock')
# An animation that will run constantly
animate.images(constant, images, every=0.8, source='clock')
# An animation that is triggered by the B key and runs 3 times
temp_anim = animate.images(temporary, images, every=0.4, source='clock',
autostart=False, limit=3, on_finished=remove_boom3)
# An animation that only runs based on direct input
manual_anim = animate.images(manual, images)
# An animation with two sets of images (for increasing and decreasing value)
# Used to animate a character walking left and right
animate.images(player, walk_left, walk_right, every=20, source='actor_x')

def draw():
screen.clear()
for actor in actors:
actor.draw()

def update():
if keyboard.a:
player.x -= 2
elif keyboard.d:
player.x += 2

def on_key_down(key):
if key == keys.P:
if pausable_anim.running:
pausable_anim.stop()
else:
pausable_anim.start()
elif key == keys.B:
temp_anim.start()
elif key == keys.LEFT:
manual_anim.prev()
elif key == keys.RIGHT:
manual_anim.next()

print(INSTRUCTIONS)

Binary file added examples/basic/images/explosion1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/basic/images/explosion2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/basic/images/explosion3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/basic/images/girl_walk1_left.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/basic/images/girl_walk1_right.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/basic/images/girl_walk2_left.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/basic/images/girl_walk2_right.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions pgzero/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,90 @@ def _remove_target(self, target, stop=True):
if not self.targets and stop:
self.stop()

class ImageAnimation:
_ACTOR_PROPERTIES = {'actor_x', 'actor_y'}
def __init__(self, actor, images, alt_images=None, every=1.0, source=None,
autostart=True, limit=None, on_finished=None):
self.actor = actor
self.main_images = images
self.alt_images = alt_images or []
self._images = images
self.freq = every
self.value = 0
self.count = len(images)
self._update_value = self._update_direct
self.running = False
self.limit = limit
self.on_finished = on_finished
self.finished = False
self.source = source
if not source:
# No external source - update() must be called manually
return
elif source in self._ACTOR_PROPERTIES:
self._update_value = self._update_property(source)
elif source != 'clock':
raise TypeError("invalid source: {}".format(source))
self.direct_updates_only = False
if autostart:
self.start()

def update(self, dv=1):
last_value = self.value
self._update_value(dv)
if self.alt_images:
if self.value > last_value:
self._images = self.alt_images
elif self.value < last_value:
self._images = self.main_images
index = int(self.value // self.freq) % self.count
self.actor.image = self._images[index]
if self.limit \
and self.value / (self.count * self.freq - dv) >= self.limit:
self._stop(finished=True)

def start(self):
if not self.source:
raise ValueError("start() not supported without a source")
if not self.finished:
each_tick(self.update)
self.running = True

def stop(self):
if not self.source:
raise ValueError("stop() not supported without a source")
self._stop(finished=False)

def next(self):
self.update(self.freq)

def prev(self):
self.update(-self.freq)

def _update_direct(self, dv):
self.value += dv

def _update_property(self, actor_property):
_, prop_name = actor_property.split('_', 1)
def update_func(dv):
self.value = getattr(self.actor, prop_name)
return update_func

def _stop(self, finished):
if not self.finished:
unschedule(self.update)
self.running = False
if finished and self.on_finished:
self.on_finished()
self.finished = True


def animate(object, tween='linear', duration=1, on_finished=None, **targets):
return Animation(object, tween, duration, on_finished=on_finished,
**targets)

def _images(actor, images, alt_images=None, every=1.0, source=None,
autostart=True, limit=None, on_finished=None):
return ImageAnimation(actor, images, alt_images, every, source,
autostart, limit, on_finished)
animate.images = _images
54 changes: 54 additions & 0 deletions test/test_animation_images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from unittest import TestCase
from types import SimpleNamespace

from pgzero.animation import animate
from pgzero import clock

class AnimationImageTest(TestCase):
def test_clock_animation(self):
"""Test that animation scheduled via the clock works"""
obj = SimpleNamespace()
obj.image = 'img1'

animate.images(obj, ['img1', 'img2'], every=2, source='clock')

self.assertEqual(obj.image, 'img1')
clock.tick(1)
self.assertEqual(obj.image, 'img1')
clock.tick(1)
self.assertEqual(obj.image, 'img2')
clock.tick(1)
self.assertEqual(obj.image, 'img2')
clock.tick(1)
# The value cycles back around
self.assertEqual(obj.image, 'img1')

def test_actor_x_y_animation(self):
"""Test that animation of actor_x and actor_y works"""
obj1 = SimpleNamespace()
obj1.image = 'img1'
obj1.x = 40
obj2 = SimpleNamespace()
obj2.image = 'img1'
obj2.y = 40

animate.images(obj1, ['img1', 'img2'], every=2, source='actor_x')
animate.images(obj2, ['img1', 'img2'], every=2, source='actor_y')
self.assertEqual(obj1.image, 'img1')
self.assertEqual(obj2.image, 'img1')

obj1.x = 41
obj2.y = 42
clock.tick(1)
self.assertEqual(obj1.image, 'img1')
self.assertEqual(obj2.image, 'img2')
obj1.x = 42
clock.tick(1)
self.assertEqual(obj1.image, 'img2')
self.assertEqual(obj2.image, 'img2')
obj1.x = 44
obj2.y = 44
clock.tick(1)
# The value cycles back around
self.assertEqual(obj1.image, 'img1')
self.assertEqual(obj2.image, 'img1')