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

8.2.1 Flower Assembly

Slice an image into tiles, scatter the tiles to random offscreen positions, then animate them flying back into place — the canonical reverse-explosion technique.

Duration20–25 min
Levelintermediate
Load
Prereqs8.1.3 (interpolation), 8.1.2 (easing)

Overview

The flower assembly is the canonical “reverse-explosion” animation: an image is divided into a grid of tiles, the tiles are scattered to random positions off the canvas, and then they fly back into their correct positions to assemble the original image. It looks like a complicated effect; mechanically it is just interpolation between two known positions for each of $N^2$ tiles, with an easing curve to make the arrival look organic.

The technique is the cousin of every “logo reveal” animation in motion graphics, every text-by-text assembly in title sequences, and the visual basis of the explosion-played-backwards sequence in Christopher Nolan’s Tenet (2020). For us, it’s the first time in Module 08 that we animate many things at once — each tile is a small animation, and the composition is a render pass that composes all of them every frame.

Learning objectives

  1. Slice an image into a regular grid of tiles using NumPy array indexing.
  2. Generate per-tile random start positions in polar coordinates (angle + radius) and convert to Cartesian offsets.
  3. Interpolate each tile’s position from its scatter point to its home position via an ease-out cubic curve.
  4. Render the composite frame by blitting each translated tile into a fresh canvas.

Quick start — a 16×16 flower assembly

python · quick_start.py
import numpy as np
from PIL import Image

N_TILES, N_FRAMES, SCATTER_RADIUS = 16, 60, 350

flower = np.array(Image.open('flower.png').convert('RGB'))
H, W, _ = flower.shape
th, tw = H // N_TILES, W // N_TILES

# Per-tile arrays: home position + scatter offset
home_yx = np.array([(r * th, c * tw) for r in range(N_TILES) for c in range(N_TILES)])
rng = np.random.default_rng(7)
angles = rng.uniform(0, 2 * np.pi, len(home_yx))
radii = rng.uniform(SCATTER_RADIUS * 0.5, SCATTER_RADIUS, len(home_yx))
scatter_dyx = np.stack([(radii * np.sin(angles)).astype(int),
                        (radii * np.cos(angles)).astype(int)], axis=1)

frames = []
for f in range(N_FRAMES):
    t = f / (N_FRAMES - 1)
    progress = 1 - (1 - t) ** 3                       # ease-out cubic
    offsets = (scatter_dyx * (1 - progress)).astype(int)
    canvas = np.full(flower.shape, 12, dtype=np.uint8)
    for (cy, cx), (dy, dx) in zip(home_yx, offsets):
        y, x = cy + dy, cx + dx
        if 0 <= y <= H - th and 0 <= x <= W - tw:
            canvas[y:y+th, x:x+tw] = flower[cy:cy+th, cx:cx+tw]
    frames.append(Image.fromarray(canvas))

frames[0].save('flower_assembly.gif', save_all=True,
               append_images=frames[1:], duration=50, loop=0)
An animation of small coloured tiles flying inward from off-screen and gradually assembling into a six-petalled pink rose curve flower with a yellow centre — by the end of the loop the full flower image is reconstructed
Fig. 1 60 frames + a 15-frame hold. 256 tiles each fly from a random scatter position into their home cell with an ease-out-cubic time curve.

Core concepts

Concept 1 — Slicing the image into tiles

A grid of N × N tiles is non-overlapping rectangular crops:

tile[r, c] = image[r * th : (r+1) * th, c * tw : (c+1) * tw]

where th = H // N and tw = W // N. NumPy slicing returns views, not copies, so storing all $N²$ tiles costs nothing in memory until you write into them. For a 512×512 image at $N = 16$, each tile is 32×32 — small enough to render thousands per frame at interactive rates.

Concept 2 — Scatter offsets in polar coordinates

Random positions tend to clump in the corners of a bounding rectangle. Random directions are uniformly distributed around the circle. The polar form gives the more even-looking scatter:

python · polar_scatter.py
angles = rng.uniform(0, 2 * np.pi, n_tiles)
radii = rng.uniform(R_min, R_max, n_tiles)
scatter_dy = (radii * np.sin(angles)).astype(int)
scatter_dx = (radii * np.cos(angles)).astype(int)

The R_min cushion prevents tiles from starting too close to their home positions (which would look static); R_max controls how far they travel. For an off-canvas scatter, R_max should exceed the canvas half-diagonal.

Concept 3 — Per-tile interpolation with ease-out

At each frame, every tile’s offset from home shrinks from scatter_offset (at t = 0) to 0 (at t = 1):

offset(t) = scatter_offset * (1 - easing(t))

Ease-out cubic — $1 - (1 - t)^3$ — is the natural choice: the tiles fly in fast and decelerate as they arrive, which looks like they have inertia. Ease-in would be wrong (slow arrival followed by sudden snap). Linear works but feels mechanical. The whole animation hinges on this easing call.

Exercises

Three exercises in Execute → Modify → Create order: render the default assembly, sweep parameters, then add per-tile rotation.

EXECUTE I.

Run the flower assembly

Run flower_assembly.py. The script generates a synthetic flower if no flower.png is found, then renders the 60-frame assembly.

flower_assembly.py — full reference implementation

Reflection questions

  • What happens with N_TILES = 4 instead of 16? With N_TILES = 64?
  • Replace ease_out_cubic with linear and re-render. How does the arrival feel?
  • The script holds the assembled image for 15 frames after the assembly completes. Why?
MODIFY II.

Parameter sweep

Modify the script to produce three variants.

Goals

  1. Vertical-only scatter. Force angles to be either π/2 or 3π/2 so tiles only scatter up or down.
  2. Per-tile delay. Add a small per-tile delay so tiles arrive in a wave from one corner to the other. Use delay = (cy + cx) / (H + W) as the per-tile delay fraction.
  3. Longer animation. Double N_FRAMES to 120 with the same easing.
CREATE III.

Add per-tile rotation

Each tile currently arrives flat. Add a rotation that starts at a random angle and rotates to 0° as the tile arrives. Use PIL’s Image.rotate per tile.

python · exercise3_starter.py
# inside the frame loop, after computing progress and offsets:
for i, ((cy, cx), (dy, dx)) in enumerate(zip(home_yx, offsets)):
    y, x = cy + dy, cx + dx
    if not (0 <= y <= H - th and 0 <= x <= W - tw):
        continue

    tile_arr = flower[cy:cy+th, cx:cx+tw]
    tile_img = Image.fromarray(tile_arr)

    # TODO 1: pick a per-tile random start_rotation (e.g., uniform in [-180, 180])
    #         (store these outside the frame loop, computed once)

    # TODO 2: current rotation = start_rotation * (1 - progress)

    # TODO 3: rotate the tile and paste into the canvas (canvas is now a PIL image)

Make it your own

  • Logo reveal. Use your own company logo as the input image. 256 tiles assembling a company brand is the canonical “logo sting” animation.
  • Backward play. Reverse the frame list and play frames[::-1] — the assembly becomes an explosion. Useful as the “exit” transition matching an earlier “enter” assembly.
  • Wipe scatter. Replace random angles with angles[i] = π (all left) and a cosine-of-row scatter — the assembly becomes a sliding curtain that pulls the image into focus from one side.

Downloads

flower_assembly.py — full reference flower.png — synthetic input

Summary

Common pitfalls to avoid

  • Random Cartesian scatter. Looks rectangular; biases toward the diagonals. Always use polar (angle + radius) for circular scatter.
  • Ease-in for arrival. Wrong direction — the tiles slow at the start and snap home. Always ease-out for arriving motion.
  • No bounds check. Tiles flying off-canvas during animation would crash the index assignment. Skip blits with out-of-range destinations.
  • No held frames at the end. The viewer can’t tell the animation finished without a pause to see the completed image.

References

  1. [1] Nolan, C. (Director). (2020). Tenet [Film]. Warner Bros. Pictures.
  2. [2] Thomas, F., & Johnston, O. (1981). Disney Animation: The Illusion of Life. Abbeville Press. ISBN 978-0-89659-232-4.
  3. [3] Pearson, M. (2011). Generative Art: A Practical Guide Using Processing. Manning. ISBN 978-1-935182-62-5.
  4. [4] Penner, R. (2002). Motion, Tweening, and Easing. In Robert Penner’s Programming Macromedia Flash MX. McGraw-Hill. ISBN 978-0-07-222356-2.
  5. [5] Foley, J. D., van Dam, A., Feiner, S. K., & Hughes, J. F. (1990). Computer Graphics: Principles and Practice (2nd ed.). Addison-Wesley. ISBN 978-0-201-12110-0.
  6. [6] Lasseter, J. (1987). Principles of traditional animation applied to 3D computer animation. Proceedings of SIGGRAPH ‘87, 35–44. doi:10.1145/37402.37407
  7. [7] Pillow Contributors. (2024). Pillow Documentation: ImageDraw and Image.paste. pillow.readthedocs.io