Animations
Experimental API
The GUI animation stack lives in arcade.gui.experimental and
may change within minor version updates without a deprecation period.
Import the names from there, e.g.
from arcade.gui.experimental import UIAnimatedGroup, rel.
Animations bring a GUI to life: buttons that pop when hovered, menus that
fade in, titles that gently pulse. Arcade’s GUI provides a small, high level
animation API for this, built around a single class:
UIAnimatedGroup and its
animate() method.
# scale the group to 108% within 0.15 seconds, with a "back out" overshoot
group.animate(scale=1.08, duration=0.15, ease=Easing.BACK_OUT)
This page walks you through everything you need to know, from the first tween to staggered menu entrances.
Quick start
UIAnimatedGroup wraps a single child
widget (or a whole subtree) and renders it into a cached surface. Its
transform properties can then be animated with
animate(). Pass the
target values as keyword arguments, and optionally a duration
(seconds), an easing function and a delay:
from arcade.anim import Easing
from arcade.gui.experimental import UIAnimatedGroup
group = UIAnimatedGroup(child=UIFlatButton(text="Play"))
layout.add(group)
# move the group to x=400 within half a second
group.animate(offset_x=400, duration=0.5)
# animate several properties at once with an easing curve
group.animate(scale=1.0, alpha=255, duration=0.5, ease=Easing.BACK_OUT)
# start after a delay (useful for staggered entrances)
group.animate(alpha=255, duration=0.3, delay=0.2)
The animation always starts from the current value of the property, so animations blend smoothly with whatever state the group is in. To animate from a fixed value, set the property first:
group.alpha = 0
group.animate(alpha=255, duration=0.3) # fade in from fully transparent
Animations are updated automatically as long as the
UIManager receives on_update events — no manual
update calls are needed.
What can be animated?
UIAnimatedGroup provides transform
properties which do not affect layouting. This is the recommended way to
animate widgets — the wrapped subtree is cached, so animating the group is
basically free:
Property |
Effect |
|---|---|
|
Scale factor around |
|
Rotation in degrees around |
|
Opacity of the whole subtree (0-255) |
|
Visual translation, without affecting layout |
|
Color multiplier applied to the whole subtree |
from arcade.gui.experimental import UIAnimatedGroup
group = UIAnimatedGroup(child=UIFlatButton(text="Play"))
layout.add(group)
group.animate(angle=360, duration=0.6, ease=Easing.CUBIC)
Because the wrapped subtree is cached, animating the group itself is basically free — the widgets inside are not re-rendered.
Note
If you only need the cached rendering without the interactive states
(hovered, pressed, on_click), use the non-interactive
UIRenderGroup instead.
Warning
Any writable attribute holding a numeric value can technically be
animated, but animating layout-controlled values like rect or
center_x of a widget inside a UILayout fights
the layout — the next layout pass will undo your animation. Animate the
group’s transform properties instead.
Sequences
animate() returns an
Animation. Use
then() to append steps which run
after the previous one finished:
# wiggle: right, left, back to center
group.animate(angle=3, duration=0.06) \
.then(angle=-3, duration=0.12) \
.then(angle=0, duration=0.06)
A then() without properties acts as a pause:
group.animate(alpha=0, duration=0.5).then(duration=0.2).on_finish(arcade.exit)
Each step reads its start values when it begins, so sequences stay smooth even if something else moved the property in between.
Repeating animations
Use repeat and yoyo for idle animations like pulsing or swaying.
yoyo=True plays the animation backwards to its start values,
repeat=True repeats forever (or pass a number for a fixed count):
# gentle pulse: 100% -> 105% -> 100%, forever
group.animate(scale=1.05, duration=0.6, ease=Easing.SINE, repeat=True, yoyo=True)
repeat loops the whole sequence, including all then() steps:
# sway: lean right, swing left, back to center, forever
group.animate(angle=2, duration=0.6, ease=Easing.SINE, repeat=True) \
.then(angle=-2, duration=1.2, ease=Easing.SINE) \
.then(angle=0, duration=0.6, ease=Easing.SINE)
For endless movement in one direction, use
rel() to animate relative to the
current value. Every iteration continues from where the last one ended:
from arcade.gui.experimental import rel
# spin clockwise, one full rotation every 8 seconds, forever
group.animate(angle=rel(-360), duration=8, repeat=True)
Reacting to hover and other state changes
GUI animations are usually triggered by state changes — a group being
hovered, pressed or shown. UIAnimatedGroup
is interactive itself: its hovered, pressed and on_click states
are hit-tested against the group’s untransformed rect, so the trigger
zone stays stable while the visuals scale or rotate. Bind to the group’s
properties and start animations in the callback:
from arcade.gui import bind
from arcade.gui.experimental import UIAnimatedGroup
group = UIAnimatedGroup(child=UIFlatButton(text="Play"))
def on_hover_change():
if group.hovered:
group.animate(scale=1.08, duration=0.15, ease=Easing.BACK_OUT)
else:
group.animate(scale=1.0, duration=0.15)
bind(group, "hovered", on_hover_change)
Notice there is no cleanup code: starting a new animation takes over its
properties from other running animations. If the “grow” animation is still
running when the mouse leaves, the “shrink” animation simply takes over
scale from the current value — other animations (and other properties of
the same animation) keep running untouched.
This means the natural way to change direction is to just start a new animation.
Finishing and stopping
Use on_finish() to run code when
an animation completes, and
stop() to cancel one:
# fade out the menu, then close the window
group.animate(alpha=0, scale=0.8, duration=0.5, ease=Easing.SINE_IN) \
.then(duration=0.1) \
.on_finish(arcade.exit)
# keep a handle to cancel later
idle = group.animate(scale=1.05, duration=0.6, repeat=True, yoyo=True)
...
idle.stop() # leaves properties at their current values
on_finish is not invoked for stopped or taken-over animations, and
endlessly repeating animations never finish.
Easing
Easing functions shape the speed of an animation over time. Arcade ships the
common set in arcade.anim.Easing — each exists as NAME
(in-out), NAME_IN and NAME_OUT variant:
LINEAR, SINE, QUAD, CUBIC, QUART, QUINT, EXPO,
CIRC, BACK, ELASTIC, BOUNCE
As a rule of thumb: use *_OUT variants for entrances (fast start, soft
landing), *_IN variants for exits, and BACK_OUT or ELASTIC_OUT
for playful pops. Any callable (t: float) -> float works as well.
A complete example
The example arcade.examples.gui.exp_animations combines everything from
this page into an animated main menu — staggered pop-in entrances, pulsing
and swaying idle animations, hover pops and a fade-out on quit:
python -m arcade.examples.gui.exp_animations
Low level: transitions
Under the hood, animations implement the
TransitionBase protocol and are ticked
via add_transition(). The
low level building blocks in
arcade.gui.experimental.transition remain available for custom
behavior:
TransitionAttr- tween a single attributeTransitionAttrIncr- tween an attribute by an incrementTransitionAttrSet- set a value after a timeTransitionDelay- waitTransitionChain/TransitionParallel- combine transitions sequentially (+) or in parallel (|)
They can be freely combined with
Animation objects using the + and
| operators. Most code should prefer
animate().