Combining Animations¶
So far every animation has taken exactly one second and happened strictly after the one before it. Real videos need more control than that: things happening at the same time, at different speeds, with different easing.
Algan handles all of this with AnimationContext s: with blocks that
change when the animations inside them happen and how long they take.
Here’s a basic example, playing two animations at the same time:
Example: ControllingSync ¶
from algan import *
square = Square().spawn()
with Sync():
square.move(RIGHT * 2)
square.rotate(90, OUT)
Scene.wait()
Scene.save_video()
Everything inside a Sync block
plays simultaneously, so the square above slides and turns at once.
The Four Contexts¶
The four basic contexts are:
Context |
Behaviour |
|---|---|
with animations sequenced – each animation starts when the previous one finishes. This is the default behaviour when not in any context. |
|
with animations synchronized – everything starts together at the same time. |
|
with animations lagged – |
|
with animations off – Instant, changes apply in a single frame, taking no time at all. |
Example: ControllingContexts ¶
from algan import *
with Off():
square = Square().spawn()
circle = Circle().spawn()
square.move(LEFT)
circle.move(RIGHT)
with Sync():
square.rotate(360, OUT)
circle.move(RIGHT)
with Seq():
square.move(RIGHT*2)
circle.move(UP)
with Lag(0.5):
square.move_to(ORIGIN)
circle.move_to(ORIGIN)
Scene.wait()
Scene.save_video()
Off is useful for doing scene setup, use it to put
everything in position for where it should first appear.
Important
Before a Mob is spawned, its animations are turned Off regardless of the surrounding animation context.
Note
Sync is equivalent to Lag(0) and Seq is equivalent to Lag(1).
Timing¶
Two arguments control how long a context takes:
runtime– the total runtime of the whole block, in seconds. The animations inside are rescaled to fit.runtime_per_part– the runtime of each individual animation inside.
Example: ControllingTiming ¶
from algan import *
circle = Circle().spawn()
with Seq(runtime=1):
circle.move(LEFT)
circle.move(UP)
circle.move(RIGHT * 2)
circle.move(DOWN)
with Seq(runtime_per_part=5):
circle.rotate(360, UP)
circle.move_to(ORIGIN)
Scene.wait()
Scene.save_video()
The first block squeezes four moves into one second total; the second gives each
of its two animations five seconds. If you set both, runtime overrides runtime_per_part.
Nesting Contexts¶
Contexts nest, and this is where they get really useful. A nested context is treated
by its parent as a single animation, so you can build up a complex piece of
choreography out of small, readable (and reusable!) blocks. A nested context also inherits every
parameter you did not set (runtime_per_part, lag_ratio, easing)
so you can set a house style on the outside and only override the exceptions.
Example: ControllingNesting ¶
from algan import *
circle = Circle().spawn()
square = Square().spawn()
with Sync():
with Seq():
with Sync():
circle.move(LEFT * 3)
circle.rotate(180, UP)
with Sync():
circle.move(UP)
circle.color = YELLOW_A
with Sync():
circle.move(RIGHT * 3)
circle.glow = 0.5
with Seq():
with Sync():
square.move(RIGHT * 3)
square.rotate(180, OUT)
with Sync():
square.move(DOWN)
square.color = GREEN_E
with Sync():
square.move(LEFT * 3)
square.glow = 0.5
Scene.wait()
Scene.save_video()
The outer Sync sees two things, the circle’s three-step routine and
the square’s, and so plays them together, even though each is internally a
sequence of pairs. Wrapping either routine in a function would let you reuse the
whole choreography as one animation.
Easing With Rate Functions¶
A easing maps progress through an animation (0 to 1) to how far
along the change should be at that moment. It is what makes motion feel like it
accelerates and settles rather than snapping between states.
Algan’s default is easings.smooth: a gentle ease in and out.
You can pass a different one to any context:
Example: ControllingRateFuncs ¶
from algan import *
with Off():
squares = [Square(color=c).scale(0.4).move((i-1)*DOWN*1.5 + LEFT*3).spawn()
for i, c in enumerate((BLUE, GREEN, YELLOW))]
funcs = (easings.identity, easings.smooth, easings.ease_out_quintic)
with Sync(runtime=2):
for square, func in zip(squares, funcs):
with Seq(easing=func):
square.move(RIGHT * 6)
Scene.wait()
Scene.save_video()
The three squares cover the same distance in the same time but arrive differently. Here are some useful rate functions:
Function |
Feel |
|---|---|
|
Ease in and out. The default, and the right choice most of the time. |
|
Constant speed. Use it for anything that should look mechanical (a rotating turntable, a camera orbit, a clock hand). |
|
Fast start, long settle. Good for things arriving on screen. |
|
Sharp acceleration / deceleration. |
The easings reference lists the whole catalogue.
Writing your own rate function¶
A rate function is just a function from a tensor in [0, 1] to a tensor in
[0, 1], so you can write your own:
Example: ControllingCustomRateFunc ¶
from algan import *
def bounce_out(t):
return 1 - (1 - t) ** 2
square = Square(color=BLUE).spawn()
with Seq(easing=bounce_out, runtime=2):
square.move(DOWN * 2)
Scene.wait()
Scene.save_video()
easings.inversed(f) gives you the time-reversed version of any rate
function, and passing composed_easing instead of easing composes
with the parent context’s easing rather than replacing it.
Note
A context that uses easing applies it across the whole block. If
you want a long orbit to run at constant speed, put easing on the
context that owns the orbit, not on an enclosing one that also holds other
animations.
See also
Audio and Speech –
AudioandSpeechare contexts too, and they take their runtime from a sound file rather than fromruntime.Animating Out of Order – writing animations to a point on the timeline of your own choosing, for when each Mob’s start time is a function of something about that Mob.