"""Classes to help animating actors"""
import fnmatch
import math
import random
import serge.actor
import serge.engine
import serge.serialize
import serge.registry
import serge.sound
import serge.blocks.effects
[docs]class AnimationExists(Exception):
"""An animation already exists with the same name"""
[docs]class AnimationNotFound(Exception):
"""The animation was not found"""
[docs]class NotPaused(Exception):
"""Tried to start when an animation was not paused"""
[docs]class AlreadyPaused(Exception):
"""Tried to pause when an animation was already paused"""
[docs]class AnimatedActor(serge.actor.Actor):
"""Implements an actor that can have animations applying to it"""
def __init__(self, tag, name=None):
"""Initialise the actor"""
super(AnimatedActor, self).__init__(tag, name)
#
self.animations = {}
self._paused = False
[docs] def addAnimation(self, animation, name):
"""Add an animation to this actor"""
if name in self.animations:
raise AnimationExists('An animation named "%s" already exists for this actor (%s)' % (
name, self.getNiceName()
))
self.animations[name] = animation
animation.setActor(self)
animation.setName(name)
return animation
[docs] def addRegisteredAnimation(self, name):
"""Add an animation from the animation registry"""
return self.addAnimation(Animations.getItem(name), name)
[docs] def addRegisteredAnimations(self, names):
"""Add a number of animations from the animation registry"""
for name in names:
self.addRegisteredAnimation(name)
[docs] def removeAnimation(self, name):
"""Remove a named animation"""
try:
del(self.animations[name])
except KeyError:
raise AnimationNotFound('No animation called "%s" found for actor %s' % (
name, self.getNiceName()
))
[docs] def removeAnimations(self):
"""Remove all the animations"""
self.animations.clear()
[docs] def removeAnimationsMatching(self, pattern):
"""Remove all animations matching a matching pattern"""
for name in self.animations.keys():
if fnmatch.fnmatch(name, pattern):
self.removeAnimation(name)
[docs] def getAnimation(self, name):
"""Return the named animation"""
try:
return self.animations[name]
except KeyError:
raise AnimationNotFound('No animation called "%s" found for actor %s' % (
name, self.getNiceName()
))
[docs] def getAnimations(self):
"""Return all the animations"""
return self.animations.values()
[docs] def pauseAnimations(self, safe=False):
"""Pause all animations"""
if not safe and self._paused:
raise AlreadyPaused('Animations for %s are already paused' % self.getNiceName())
self._paused = True
[docs] def unpauseAnimations(self, safe=False):
"""Start all animations"""
if not safe and not self._paused:
raise NotPaused('Animations for %s are not paused' % self.getNiceName())
self._paused = False
[docs] def animationsPaused(self):
"""Return True if our animations are paused"""
return self._paused
[docs] def restartAnimations(self):
"""Restart all animations"""
self._paused = False
for animation in self.getAnimations():
animation.restart()
[docs] def completeAnimations(self):
"""Complete all animations"""
self._paused = True
for animation in self.getAnimations():
animation.finish()
animation.update()
[docs] def updateActor(self, interval, world):
"""Update the actor"""
super(AnimatedActor, self).updateActor(interval, world)
#
if not self._paused:
for animation in self.animations.values():
animation.updateActor(interval, world)
[docs]class Animation(serge.blocks.effects.Effect):
"""The basic animation class"""
my_properties = (
serge.serialize.F('duration', 0, 'the length of time the animation runs for in one direction'),
serge.serialize.F('_original_start', 0, 'the original start condition'),
serge.serialize.F('_original_end', 0, 'the original end condition'),
)
def __init__(self, duration=1000, done=None, loop=False, paused=False):
"""Initialise the animation"""
self.actor = None
self.duration = duration
self._original_start = 0
self._original_end = self.duration
#
super(Animation, self).__init__(done=done, persistent=True)
#
#
self.init()
self.paused = paused
self.loop = loop
[docs] def setActor(self, actor):
"""Set our actor"""
self.actor = actor
[docs] def setName(self, name):
"""Set our name"""
self.name = name
[docs] def init(self):
"""Initialise the properties"""
super(Animation, self).init()
self.name = None
self.start = self._original_start
self.end = self._original_end
self.current = 0
self.iteration = 0
self.fraction = 0.0
self.complete = False
self.direction = 1
self.interval = 0
self._pause_next_cycle = False
[docs] def unpause(self):
"""Un-pause the animation"""
super(Animation, self).unpause()
self._pause_next_cycle = False
[docs] def restart(self):
"""Restart the animation"""
self.init()
self.update()
[docs] def updateActor(self, interval, world):
"""Update the animation effect"""
super(Animation, self).updateActor(interval, world)
#
# Do not advance time if we are paused
if self.paused:
return
#
self.current += self.direction * interval
self.iteration += 1
self.interval = interval
#
# Watch for bouncing
if self.loop:
if self.direction == 1 and self.current >= self.end:
self.current = self.duration - (self.current - self.duration)
self.direction *= -1
elif self.direction == -1 and self.current <= 0:
if self._pause_next_cycle:
self.current = 0
self.pause()
else:
self.current = -self.current
self.direction *= -1
#
# Map back into proper space
# TODO: a proper implementation should also account for going through more than one iteration
self.current = min(self.duration, max(0, self.current))
self.fraction = min(1.0, float(self.current) / self.duration)
#
self.update()
#
if self.fraction == 1.0:
self.finish()
[docs] def finish(self):
"""Finish the effect"""
self.current = self.end
self.fraction = 1.0
self._effectComplete(None)
self.complete = True
[docs] def pauseAtNextCycle(self):
"""Set the animation to pause at the next time it renews a cycle"""
self._pause_next_cycle = True
[docs] def update(self):
"""The main method implementing the animation effect
This is the method you should implement
"""
[docs]class AnimationRegistry(serge.registry.GeneralStore):
"""A place to register animations so they can be easily re-used"""
def _registerItem(self, name, item):
"""Register the animation"""
#
# Remember the settings used to create the sound
self.raw_items.append([name, item])
self.items[name] = item
return None
[docs] def getItem(self, name):
"""Return an animation
We need to deepcopy the animation to avoid returning the same one
each time.
"""
item = super(AnimationRegistry, self).getItem(name)
return item.copy()
# Singleton animation registry
Animations = AnimationRegistry()
#
# Now we have some specific examples of useful animations
[docs]class PulsedVisibility(Animation):
"""An animation that turns an actor on and off with visibility"""
my_properties = (
serge.serialize.F('on_fraction', 0.5, 'the fraction of the time to stay visibible for'),
)
def __init__(self, duration, on_fraction=0.5):
"""Initialise the animation"""
super(PulsedVisibility, self).__init__(duration=duration, loop=True)
self.on_fraction = on_fraction
[docs] def update(self):
"""Update the animation"""
self.actor.visible = self.fraction <= self.on_fraction
[docs]class ColourCycle(Animation):
"""Animate the colour property of an object between a beginning and end"""
def __init__(self, obj, start_colour, end_colour, duration,
attribute='colour', loop=False, done=None):
"""Initialise the animation"""
super(ColourCycle, self).__init__(duration, loop=loop, done=done)
#
self.obj = obj
self.start_colour = start_colour
self.end_colour = end_colour
self.attribute = attribute
[docs] def update(self):
"""Update the colour of the object"""
#
# Work out the values for each element of the colour
colours = []
for x, y in zip(self.start_colour, self.end_colour):
colours.append(float(x + (y - x) * self.fraction))
#
self.setColour(tuple(colours))
[docs] def setColour(self, colour):
"""Set the colour"""
setattr(self.obj, self.attribute, colour)
[docs]class ColourText(ColourCycle):
"""Animate the colour of a text object by calling its setColour method"""
[docs] def setColour(self, colour):
"""Set the colour"""
self.obj.setColour(colour)
if len(colour) == 4:
self.obj.setAlpha(float(colour[-1]) / 255)
[docs]class MoveWithVelocity(Animation):
"""Animate the motion of an actor with a constant velocity"""
my_properties = (
serge.serialize.F('vx', 0.0, 'the x velocity to move with'),
serge.serialize.F('vy', 0.0, 'the y velocity to move with'),
)
def __init__(self, (vx, vy), loop=False, done=None):
"""Initialise the animation"""
super(MoveWithVelocity, self).__init__(duration=1000, loop=loop, done=done)
#
self.vx = vx
self.vy = vy
[docs] def update(self):
"""Update the actor"""
self.actor.move(self.interval / 1000.0 * self.vx, self.interval / 1000.0 * self.vy)
[docs]class MouseOverAnimation(Animation):
"""A base class to use for animations that can be activated by mouse over
Animations extending this class can set mouse_over_only to True and then
the animation will run only when the mouse is over the actor. When the
mouse moves out then the animation will run to the beginning of the
cycle and then stop.
"""
my_properties = (
serge.serialize.B('mouse_over_only', False, 'whether we only animate on mouse over'),
)
def __init__(self, duration, mouse_over_only, loop=False, done=None, paused=False):
"""Initialise the animation"""
super(MouseOverAnimation, self).__init__(duration, loop=loop, done=done, paused=paused)
#
self.mouse_over_only = mouse_over_only
[docs] def init(self):
"""Initialise the animation"""
super(MouseOverAnimation, self).init()
self.mouse = serge.engine.CurrentEngine().getMouse()
[docs] def updateActor(self, interval, world):
"""Update the animation"""
#
# Check if we should re-activate
if self.mouse_over_only:
inside = self.mouse.getScreenPoint().isInside(self.actor)
if inside:
if self.paused:
self.unpause()
else:
self.pauseAtNextCycle()
#
super(MouseOverAnimation, self).updateActor(interval, world)
[docs]class PulseZoom(MouseOverAnimation):
"""Cycle the zoom of an actor between a high and low value"""
my_properties = (
serge.serialize.F('start_zoom', False, 'the initial zoom value'),
serge.serialize.F('end_zoom', False, 'the final zoom value'),
)
def __init__(self, start_zoom, end_zoom, duration,
loop=False, done=None, mouse_over_only=False, paused=False):
"""Initialise the animation"""
super(PulseZoom, self).__init__(
duration, mouse_over_only=mouse_over_only, loop=loop, done=done, paused=paused)
#
self.start_zoom = start_zoom
self.end_zoom = end_zoom
[docs] def update(self):
"""Update the zoom of the object"""
super(PulseZoom, self).update()
if not self.paused:
zoom = float(self.fraction * (self.end_zoom - self.start_zoom) + self.start_zoom)
self.actor.setZoom(zoom)
[docs]class PulseRotate(MouseOverAnimation):
"""Cycle the rotation of an actor between a high and low value
The rotation limits are set as the min and max angle. The actual
start point of the animation is half way between the two. This
means that if you stop it on a cycle it will return to the mid point.
"""
my_properties = (
serge.serialize.F('min_angle', False, 'the low value of the angle'),
serge.serialize.F('max_angle', False, 'the high value of the angle'),
)
def __init__(self, min_angle, max_angle, duration,
loop=False, done=None, mouse_over_only=False, paused=False):
"""Initialise the animation"""
super(PulseRotate, self).__init__(
duration, mouse_over_only=mouse_over_only, loop=loop, done=done, paused=paused)
#
self.min_angle = min_angle
self.max_angle = max_angle
[docs] def update(self):
"""Update the angle of the object"""
super(PulseRotate, self).update()
if not self.paused:
time_fraction = self.fraction if self.direction == 1 else 2.0 - self.fraction
effective_fraction = math.sin(time_fraction * math.pi) / 2 + 0.5
angle = float(effective_fraction * (self.max_angle - self.min_angle) + self.min_angle)
self.actor.setAngle(angle)
[docs]class MouseOverSound(Animation):
"""Plays a sound when mousing over something"""
my_properties = (
serge.serialize.S('sound', '', 'the sound to play'),
serge.serialize.B('over', False, 'whether the mouse is over us'),
)
def __init__(self, sound):
"""Initialise the animation"""
super(MouseOverSound, self).__init__(duration=1)
#
self.sound = sound
[docs] def init(self):
"""Initialise the animation"""
super(MouseOverSound, self).init()
self.mouse = serge.engine.CurrentEngine().getMouse()
self.over = False
[docs] def update(self):
"""Update the animation"""
if self.mouse.getScreenPoint().isInside(self.actor):
if not self.over:
serge.sound.Sounds.play(self.sound)
self.over = True
else:
self.over = False
[docs]class TweenAnimation(Animation):
"""Tween a variable"""
def __init__(self, obj, attribute, start, end, duration, function=None,
delay=0, after=None, repeat=False, ping_pong=True, integer=False,
set_immediately=True, is_method=False):
"""Initialise the tween"""
super(TweenAnimation, self).__init__(duration, loop=repeat)
self.obj = obj
self.attribute = attribute
self.start = self._original_start = start
self.end = self._original_end = end
self.function = function if function else TweenAnimation.linearTween
self.duration = self.to_go = float(duration)
self.complete = False
self.delay = delay
self.after = after
self.repeat = repeat
self.ping_pong = ping_pong
self.integer = integer
self.is_method = is_method
if set_immediately:
self.setValue(self.start)
[docs] def update(self):
"""Perform the tween"""
dt = self.interval
#
# Handle the delay
if self.delay:
self.delay -= dt
if self.delay >= 0:
return
dt = abs(dt)
#
# Calculate the new value
self.to_go -= dt
if self.to_go <= 0:
value = self.end
self.complete = True
else:
value = self.function(1.0 - self.to_go / self.duration, self.start, self.end)
#
self.setValue(value)
if self.complete and self.after:
self.after()
if self.complete and self.repeat:
self.to_go = float(self.duration)
self.complete = False
if self.ping_pong:
self.start, self.end = self.end, self.start
#
# Remove when complete
if self.complete:
self.actor.removeAnimation(self.name)
[docs] def setValue(self, value):
"""Set the value"""
if not self.is_method:
setattr(self.obj, self.attribute, value if not self.integer else int(value))
else:
method = getattr(self.obj, self.attribute)
method(value if not self.integer else int(value))
@staticmethod
[docs] def linearTween(fraction, start, end):
"""Linear tween from start to end"""
return fraction * (end - start) + start
@staticmethod
[docs] def sinOut(fraction, start, end):
"""Sine out tween"""
return TweenAnimation.linearTween(math.sin(fraction * math.pi / 2.0), start, end)
@staticmethod
[docs] def sinInOut(fraction, start, end):
"""Sine out tween"""
return TweenAnimation.linearTween((math.sin((fraction - 0.5) * math.pi) + 1.0) / 2, start, end)
@staticmethod
[docs] def colourTween(fraction, start, end):
"""Tween a colour"""
return [TweenAnimation.linearTween(fraction, start[i], end[i]) for i in range(3)]
@staticmethod
[docs] def randomColour(fraction, start, end):
"""Tween a random colour"""
colour = []
for s, e in zip(start, end):
if s == e:
colour.append(s)
else:
if s > e:
s, e = e, s
colour.append(random.randrange(s, e))
return colour
[docs]class MovementTweenAnimation(TweenAnimation):
"""Tweens the position of an actor between two locations"""
def __init__(self, obj, *args, **kw):
"""Initialise the tween"""
super(MovementTweenAnimation, self).__init__(obj, None, *args, **kw)
[docs] def setValue(self, value):
"""Set the position"""
self.obj.moveTo(*value)