Animation¶
node.animate(property, to, duration_ms, easing=None, on_complete=None)
eases one property from wherever it is now to to. It returns at once; the
window advances every running animation each frame.
card.animate("opacity", 0.0, 300)
card.animate("translate_y", -24, 300, easing=(0.3, 0.0, 0.8, 0.15),
on_complete=lambda: card.remove())
What animates¶
| Property | Value |
|---|---|
fill, stroke_color |
a color — interpolated per component in sRGB |
stroke_width, opacity |
a number |
corner_radius |
a number, or a 4-tuple of corners |
shadows |
a list of shadows; lists of different lengths fade the extra shadows in or out |
translate_x, translate_y, scale, rotation_deg |
a number — each part animates on its own |
scroll_offset |
a scroll view's offset, in pixels |
data, trim_start, trim_end |
a path's shape and stroke trim |
Layout properties animate too (0.5.6), in pixels:
| Property | Value |
|---|---|
width, height, min_width, min_height, max_width, max_height, flex_basis |
a number of pixels |
x, y |
a number — the inset from the left and top |
padding, padding_top ... padding_left, margin, margin_top ... margin_left |
a number; padding and margin animate all four sides together |
gap, row_gap, column_gap |
a number |
panel.animate("height", 240, 200) # an accordion opening
rail.animate("width", 80, 250, easing=(0.2, 0.0, 0, 1.0))
Layout runs again each frame of the animation, from the animating node up to the
root, so a large tree costs more per frame than a transform does. Prefer
translate_* and scale where a layout change isn't needed.
tois a number of pixels."auto"and percentages can't be animated to: anautoend has no size until layout, so measure first (get("layout_height")) and animate to that.- It starts from a number.
widthandheightstart from the size layout gave the node, even when they wereautoor a percentage. Any other property that isautoor a percentage raisesValueError: set it to a number first. getreads the current value,get_targetthe end, and an interrupted animation retargets from where it is, as every other does.node.set(width=...)ends the animation (itsanimation_endhasfinished=False);stop_animation("width")stops it where it is.- Shorthands (
padding,margin,gap) animate each side as its own animation, with its ownanimation_end(padding_top, ...).on_completeis called once, from the first side. - Don't animate
x/yof the child of a scroll view or virtual list: they place that child themselves every frame. font_sizedoes not animate: it would reshape the text every frame.
Any other name raises ValueError.
Easing¶
easing is "linear" (the default), a cubic bezier (x1, y1, x2, y2),
exactly as CSS cubic-bezier() takes it, or a spring. Design systems publish their
motion curves as bezier values, so they drop straight in:
STANDARD = (0.2, 0.0, 0.0, 1.0)
EMPHASIZED_DECELERATE = (0.05, 0.7, 0.1, 1.0)
drawer.animate("translate_x", 0, 400, easing=EMPHASIZED_DECELERATE)
Springs¶
easing="spring" or easing=("spring", bounce) (0.5.4) animates with a damped
spring instead of a fixed curve. duration_ms is the spring's period, about one
cycle of its main motion, and bounce is how far it overshoots, from -1 to 1
(exclusive): 0.0 is the quickest settle with no overshoot, towards 1.0 rings more
and for longer, and a negative value is slower and softer. The plain string is a
spring with bounce=0.2.
card.animate("translate_y", -24, 350, easing=("spring", 0.35)) # lifts and settles
panel.animate("scale", 1.0, 300, easing="spring")
A spring lasts until it has settled (within a tenth of a percent of its target), so
it runs longer than duration_ms, and on_complete fires then. A duration_ms of
0 still snaps. Springs work on every animatable property, colours included.
Springs also fix what a retargeted animation looked like. Animating a number again while it is moving starts the new spring at the speed the old motion had, so a card you drag away and let go, or a hover that reverses mid-way, carries on smoothly instead of stalling and restarting. That carry applies to numbers (opacity, the transform parts, a scroll offset, a stroke width and so on); a colour or a shadow list springs without it.
Retargeting and reading back¶
Calling animate again on a property that's still moving starts from where
it is now, so a hover that ends halfway through its fade reverses smoothly
rather than jumping. Three calls inspect a property in flight:
get(name)— the value on screen right now.get_target(name)— where it's heading; the same asgetwhen idle.stop_animation(name)— stop where it is.
duration_ms=0 jumps to the target on the next frame.
Completion¶
on_complete runs once, with no arguments, in the frame the animation
arrives. An animation replaced by another, or stopped, never calls it —
so a completion never fires for a value you've since changed your mind
about. Chain steps by starting the next from the last one's completion:
def settle():
chip.animate("scale", 1.0, 120)
chip.animate("scale", 1.2, 120, on_complete=settle)
Or listen once for every animation on a node with the animation_end event
(0.5.6), which a framework can use to chain steps without a callback per call:
def ended(event):
if event.finished: # False: it was replaced or stopped
print(event.property, "done")
card.on("animation_end", ended)
It fires for every animation on the node, on_complete or not, one event per
animation, after that animation's on_complete. It doesn't bubble.
Morphing paths¶
Animating a path's data to another path morphs between them. Two closed
shapes, or two open lines, morph smoothly — both are resampled by length and
aligned at the best starting point. Paths with different numbers of subpaths,
or an open path against a closed one, switch halfway instead. The last frame
is exactly the target data, so a morphing play button ends as a crisp pause
icon. See examples/path_morph.py.
Testing animations¶
Without a live window no frames run, so tests drive time themselves with
window.advance(ms). It moves the window's clock forward by exactly ms,
then runs animations, their completions, and layout at the new time (and, since 0.5.6,
any window.after/window.every timer due in the span, each at its own moment):
window.advance(0) # pin the clock at "now"
card.animate("opacity", 0.0, 200)
window.advance(100)
assert card.get("opacity") == 0.5
window.advance(100)
assert card.get("opacity") == 0.0
The first call pins the window's clock; after that only advance moves it,
until App.run() returns the window to real time.