Pixels2GenAI
Path ii Continuum
M 08 · 8.3.2 · hands-on

8.3.2 Thank You

A four-track staggered cinematic: characters slide in, a speech bubble fades in, text rises. Independent timelines composited per frame.

Duration22–28 min
Levelintermediate
Load
Prereqs8.1.2 (easing), 8.2.1 (multi-object animation)

Overview

A motion-graphics cinematic is rarely a single animation — it’s multiple staggered animations that compose into a sequence. This lesson builds a “thank you” closing card from four independent timelines: a left character slides in, a right character slides in, a speech bubble fades in, and a “Thank You” text rises from below. Each animation runs on its own clock; the composer picks the right element from each timeline every frame.

The architectural pattern — multiple timelines, one render pass — is the foundation of every motion-graphics package since the 1990s. Adobe After Effects calls them “tracks”; Apple Motion calls them “behaviors”; Lottie (Airbnb’s open-source mobile animation format) calls them “shapeLayer.” The Python equivalent here is a list of value sequences, one per animated parameter, indexed by frame number. No frameworks, no animation engine — just plain Python lists.

Learning objectives

  1. Generate per-parameter value sequences as Python lists, one per animated property.
  2. Use sine_move(start, end, frames, wait_frames) to produce eased-out motion with explicit pre-roll holds.
  3. Stagger animations so they overlap in time — characters slide in, then the bubble fades, then the text rises.
  4. Composite the frame by pasting each pre-positioned element in z-order — back to front.

Quick start — four-track composition

python · quick_start.py
import math
from PIL import Image

def sine_move(start, end, frames, wait_frames=0):
    seq = [start] * wait_frames
    for i in range(frames):
        eased = math.sin(i / frames * math.pi / 2)
        seq.append(int(start + (end - start) * eased))
    return seq

# Stagger plan: characters in by frame 80; bubble fades 80-110; text rises 100-140
panda_xs = sine_move(-280, 200, 80) + [200] * 60
pingu_xs = sine_move(1800, 1240, 80) + [1240] * 60
bubble_alpha = [0] * 80 + sine_move(0, 255, 30) + [255] * 30
text_ys = [200] * 100 + sine_move(200, 30, 40)
An animated GIF showing a panda character sliding in from the left edge, a penguin character sliding in from the right edge, a speech bubble fading in between them, and the words 'Thank You' rising upward into the bubble
Fig. 1 140-frame staged cinematic. Four parameters animated independently; one render pass per frame.

Core concepts

Concept 1 — Per-parameter value sequences

Every animated property gets a list of values, one per frame. panda_xs[42] is the panda’s x-position at frame 42. The list is pre-computed; the render loop just indexes:

python · render_loop.py
for f in range(N_FRAMES):
    canvas = blank.copy()
    canvas.paste(panda, (panda_xs[f], YCHARS), panda)
    canvas.paste(pingu, (pingu_xs[f], YCHARS), pingu)
    canvas.paste(bubble.with_alpha(bubble_alpha[f]), (XBUBBLE, YBUBBLE), bubble)
    canvas.paste(text_tile, (XCENTER, YCHARS - 80 + text_ys[f]))
    frames.append(canvas)

For our four-track cinematic, four lists, all of length N_FRAMES. For a larger production — 50 tracks, 1000 frames — the pattern scales the same way. Total memory: 50,000 small numbers, negligible.

Concept 2 — sine_move with wait_frames

The same ease-out-cubic from 8.1.2, packaged as a sequence generator that supports a leading “hold at start” phase:

python · sine_move.py
def sine_move(start, end, frames, wait_frames=0):
    seq = [start] * wait_frames               # hold at start
    for i in range(frames):
        eased = math.sin(i / frames * math.pi / 2)  # quarter-sine ease
        seq.append(int(start + (end - start) * eased))
    return seq

The wait_frames parameter is the canonical way to express staggered start. Sliding in the panda might be “wait 0 frames, then move from -280 to 200 over 80 frames” — sine_move(-280, 200, 80). Sliding in the speech bubble starts after the characters arrive: [255] * 80 + sine_move(0, 255, 30) — wait 80, then animate alpha 0 to 255 over 30.

Concept 3 — Z-order compositing

Each frame is composed back-to-front:

  1. Blank canvas (background).
  2. Panda + pingu (mid layer).
  3. Speech bubble (in front of characters, behind text).
  4. Text (front-most).

The order matters because each paste overwrites pixels from the layers behind. If you paste the text before the bubble, the bubble covers the text. The Pillow paste(...) method uses the alpha channel of the pasted image to blend; without alpha, it’s a hard rectangular paste.

For a more sophisticated composition, replace paste with explicit alpha compositing: result = src_alpha * src + (1 - src_alpha) * dst. Used when you need partial transparency or feathered edges.

Exercises

Three exercises in Execute → Modify → Create order: render the scene, restagger the timing, then add a fifth animation track.

EXECUTE I.

Render the Thank You scene

Run thank_you.py. The script renders all 140 frames at 60 ms per frame.

thank_you.py — full reference

Reflection questions

  • The panda enters from the left, the pingu from the right. What happens if both enter from the same side?
  • The bubble alpha goes from 0 to 255. What does a partial maximum (say 180) look like?
  • The text starts at y = 200 (well below the bubble) and rises to y = 30. Why is it rendered with paste only when text_ys[f] < 200?
MODIFY II.

Restagger the timing

Modify the stagger plan to test alternative pacings.

Goals

  1. Simultaneous entry. Drop all wait_frames so all four tracks start at frame 0. The animation will feel chaotic.
  2. Slower bubble. Increase the bubble’s frames from 30 to 60. The fade-in feels more deliberate.
  3. Sequential entry. Characters slide in one at a time — left first, then right — by adding a 30-frame wait on pingu_xs.
CREATE III.

Add a fifth track — confetti rain

Add a confetti-rain effect that starts as the text rises. Use random positions and per-piece falling speeds.

python · exercise3_starter.py
import math, numpy as np
from PIL import Image, ImageDraw

def confetti_positions(rng, n_pieces, frame, n_frames):
    """Return a list of (x, y, color) tuples for one frame of confetti."""
    # TODO 1: stable seeded positions (independent of frame)
    # xs = rng.integers(0, MAXX, n_pieces)
    # colors = ... (per-piece random colour)
    # vy = ... (per-piece falling speed)

    # TODO 2: y positions depend on frame
    # ys = starting_y + vy * (frame - confetti_start_frame)
    pass

Make it your own

  • Sound effect. Pair with a brief audio sting on the moment the text arrives. The audio reinforces the visual climax.
  • Bouncing arrival. Replace sine_move for the text with an ease-out-bounce from 8.1.2’s bounce easing. The text “lands” with a small bounce.
  • Multi-language. Render the text tile with three different “Thank You” translations and crossfade between them. Same architecture, more tracks.

Downloads

thank_you.py — staggered cinematic

Summary

Common pitfalls to avoid

  • Hardcoded frame counts. Make N_FRAMES a constant and reference it throughout — easier to change the duration without re-typing.
  • Mismatched track lengths. All sequences must be the same length as the render loop. Pad with the final value or wrap the indexer with min(f, len(seq) - 1).
  • Forgetting z-order. Pasting the text first puts it behind the bubble. Order matters; back-to-front always.
  • Stagger that hides motion. If two tracks start and stop at the same time, you lose the cascading effect. Always offset by at least 5-10 frames.

References

  1. [1] Thomas, F., & Johnston, O. (1981). Disney Animation: The Illusion of Life. Abbeville Press. ISBN 978-0-89659-232-4.
  2. [2] Williams, R. (2009). The Animator’s Survival Kit. Faber & Faber. ISBN 978-0-571-23834-2.
  3. [3] Adobe. (2024). After Effects User Guide: Animation Workflows. helpx.adobe.com
  4. [4] Lottie. (2024). Lottie Animation Format Documentation. airbnb.io/lottie
  5. [5] Penner, R. (2002). Motion, Tweening, and Easing. In Robert Penner’s Programming Macromedia Flash MX. McGraw-Hill. ISBN 978-0-07-222356-2.
  6. [6] Pillow Contributors. (2024). Image.paste and alpha compositing. pillow.readthedocs.io
  7. [7] Lasseter, J. (1987). Principles of traditional animation applied to 3D computer animation. Proceedings of SIGGRAPH ‘87, 35–44. doi:10.1145/37402.37407