Animating Primitives & Color Transitions

Low-level drawing primitives (drawlib.shapes, drawlib.lines, drawlib.text, drawlib.icons) are stateless functions that draw directly onto the canvas. To animate them, use Pattern A (In-Frame Build) with clear=True (the default of with anim.frame():), computing per-frame coordinates, sizes, or styles inside a loop.

primitives_1
Multi-Frame Primitive Animation: Trajectory Motion, Color Fade, and Dynamic Scaling
Source Code
from drawlib.anim import Animation
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line
from drawlib.math import get_intermediate_points
from drawlib.shapes import arrow, circle, rectangle
from drawlib.styles import Colors, Styles, get_intermediate_colors
from drawlib.text import text

setup(width=124, height=40)
anim = Animation(fps=10.0)

pkt_points = get_intermediate_points((33.5, 20), (43.5, 20), num=3, include_ends=True)
arrow_tips = [*get_intermediate_points((73, 20), (93, 20), num=3), (93, 20)]
fade_colors = get_intermediate_colors(Colors.White, Colors.Primary, num=3, include_ends=True)
scales = [1.0, 1.05, 1.10, 1.05, 1.0]

# Phase 1: Packet travels from Producer to Router
for pkt_xy in pkt_points:
    with anim.frame(duration=0.11):
        rectangle((17, 20), width=23, height=16, style=Styles.Neutral.patch(shape_r=2.0), text="\nProducer", text_style=Styles.DarkBold.patch(text_size=11.0))
        phosphor.globe((17, 23.2), width=4.4, style=Styles.Dark)
        rectangle((59, 20), width=26, height=16, style=Styles.PrimaryNeutral.patch(shape_r=2.0), text="\nStream Router", text_style=Styles.DarkBold.patch(text_size=11.0))
        phosphor.git_merge((59, 23.2), width=4.4, style=Styles.Primary)
        rectangle((107, 20), width=23, height=16, style=Styles.Neutral.patch(shape_r=2.0), text="\nReplica DB", text_style=Styles.DarkBold.patch(text_size=11.0))
        phosphor.database((107, 23.2), width=4.4, style=Styles.Dark)
        line((28.5, 20), (46, 20), style=Styles.MutedDashed, arrow_head="->")
        line((72, 20), (95.5, 20), style=Styles.MutedDashed, arrow_head="->")
        circle(pkt_xy, radius=2.3, style=Styles.PrimaryFlat)
        text((pkt_xy[0], 27.5), "event", style=Styles.PrimaryBold.patch(text_size=10.5))

# Phase 2: Block arrow grows from Router to Replica DB
for tip_xy in arrow_tips:
    with anim.frame(duration=0.11):
        rectangle((17, 20), width=23, height=16, style=Styles.Neutral.patch(shape_r=2.0), text="\nProducer", text_style=Styles.DarkBold.patch(text_size=11.0))
        phosphor.globe((17, 23.2), width=4.4, style=Styles.Dark)
        rectangle((59, 20), width=26, height=16, style=Styles.PrimaryNeutral.patch(shape_r=2.0), text="\nStream Router", text_style=Styles.DarkBold.patch(text_size=11.0))
        phosphor.git_merge((59, 23.2), width=4.4, style=Styles.Primary)
        rectangle((107, 20), width=23, height=16, style=Styles.Neutral.patch(shape_r=2.0), text="\nReplica DB", text_style=Styles.DarkBold.patch(text_size=11.0))
        phosphor.database((107, 23.2), width=4.4, style=Styles.Dark)
        line((28.5, 20), (46, 20), style=Styles.DarkBold, arrow_head="->")
        arrow((73, 20), tip_xy, tail_width=2.6, head_width=6.0, head_length=4.5, style=Styles.PrimaryFlat)

# Phase 3: Replica DB scales and smoothly fades into Primary active state
for i, (bg, sc) in enumerate(zip(fade_colors, scales)):
    is_last = (i == len(fade_colors) - 1)
    with anim.frame(duration=2.0 if is_last else 0.12):
        rectangle((17, 20), width=23, height=16, style=Styles.Neutral.patch(shape_r=2.0), text="\nProducer", text_style=Styles.DarkBold.patch(text_size=11.0))
        phosphor.globe((17, 23.2), width=4.4, style=Styles.Dark)
        rectangle((59, 20), width=26, height=16, style=Styles.PrimaryNeutral.patch(shape_r=2.0), text="\nStream Router", text_style=Styles.DarkBold.patch(text_size=11.0))
        phosphor.git_merge((59, 23.2), width=4.4, style=Styles.Primary)
        line((28.5, 20), (46, 20), style=Styles.DarkBold, arrow_head="->")
        arrow((73, 20), (93, 20), tail_width=2.6, head_width=6.0, head_length=4.5, style=Styles.PrimaryFlat)
        dst_style = Styles.PrimaryOutline.patch(shape_fill_color=bg, shape_r=2.0)
        dst_text = (Styles.WhiteBold if i >= 2 else Styles.DarkBold).patch(text_size=11.0)
        ic_style = Styles.White if i >= 2 else Styles.Dark
        rectangle(
            (107, 20),
            width=23 * sc,
            height=16 * sc,
            style=dst_style,
            text="\nReplica DB",
            text_style=dst_text,
        )
        phosphor.database((107, 23.2), width=4.4 * sc, style=ic_style)
        if is_last:
            text((83, 28.0), "Synced", style=Styles.PrimaryBold.patch(text_size=10.5))

save()

1. Coordinate & Path Interpolation (get_intermediate_points & get_intermediate_paths)

To move shapes or progressively extend block arrows and polylines along straight segments or multi-point trajectories (such as L-shaped or U-shaped paths), use the pure coordinate and path interpolation helpers in drawlib.math:

from drawlib.math import (
    get_intermediate_path_point,
    get_intermediate_path_points,
    get_intermediate_paths,
    get_intermediate_point,
    get_intermediate_points,
)
Function Signature Description
get_intermediate_point (xy1: Coordinate, xy2: Coordinate) -> tuple[float, float] Returns the single 50% midpoint (mx, my) between xy1 and xy2.
get_intermediate_points (xy1: Coordinate, xy2: Coordinate, num: int = 1, *, include_ends: bool = False) -> list[tuple[float, float]] Returns num evenly spaced intermediate (x, y) points along the segment from xy1 to xy2. Set include_ends=True to return [xy1, ..., xy2] (num + 2 points total).
get_intermediate_path_point (xys: Coordinates) -> tuple[float, float] Returns the coordinate at 50% of the total arc length along a multi-point polyline xys (e.g., the center of the bottom bar of a U-shaped path).
get_intermediate_path_points (xys: Coordinates, num: int = 1, *, include_ends: bool = False) -> list[tuple[float, float]] Returns num evenly spaced (x, y) coordinates along the arc length of xys (or num + 2 points with include_ends=True).
get_intermediate_paths (xys: Coordinates, num: int = 1, *, include_ends: bool = False) -> list[list[tuple[float, float]]] Returns num progressive prefix sub-paths [(x0, y0), ..., (xt, yt)] along xys (plus the complete path xys at the end when include_ends=True), ready to pass directly to lines(), lines_curved(), or arrow_polyline().

Animating a Growing Block Arrow (arrow)

By iterating over intermediate tip coordinates from start_xy to end_xy, you can smoothly grow a block arrow across frames:

from drawlib.anim import Animation
from drawlib.canvas import save, setup
from drawlib.math import get_intermediate_points
from drawlib.shapes import arrow, rectangle
from drawlib.styles import Styles
from drawlib.text import text

setup(width=110, height=40)
anim = Animation(fps=10.0)

start_xy = (33, 20)
end_xy = (77, 20)
tip_points = [*get_intermediate_points(start_xy, end_xy, num=5), end_xy]

for i, tip_xy in enumerate(tip_points):
    is_last = (i == len(tip_points) - 1)
    with anim.frame(duration=1.8 if is_last else 0.12):
        rectangle((20, 20), width=22, height=15, style=Styles.Neutral.patch(shape_r=2), text="Source")
        dst_style = Styles.PrimaryFlat if is_last else Styles.Neutral
        dst_text = Styles.WhiteBold if is_last else Styles.Dark
        rectangle((90, 20), width=22, height=15, style=dst_style.patch(shape_r=2), text="Target", text_style=dst_text)

        arrow(
            start_xy,
            tip_xy,
            tail_width=3.0,
            head_width=7.0,
            head_length=5.0,
            style=Styles.PrimaryFlat,
        )
        if is_last:
            text((55, 28), "Replicated", style=Styles.PrimaryBold.patch(text_size=10.5))

save()
primitives_2
Growing Block Arrow with get_intermediate_points()

Animating a Growing U-Shaped Polyline Arrow (get_intermediate_paths & get_intermediate_path_point)

For L-shaped or U-shaped routes, get_intermediate_paths(u_path, num=..., include_ends=True) generates progressive partial polylines that preserve all corners passed so far, while get_intermediate_path_point(u_path) computes the true midpoint along the trajectory:

from drawlib.anim import Animation
from drawlib.canvas import save, setup
from drawlib.lines import lines_curved
from drawlib.math import get_intermediate_path_point, get_intermediate_paths
from drawlib.shapes import arrow_polyline, rectangle
from drawlib.styles import Styles
from drawlib.text import text

setup(width=110, height=52)
anim = Animation(fps=12.0)

u_path = [(24, 30), (24, 12), (86, 12), (86, 30)]
mid_xy = get_intermediate_path_point(u_path)
sub_paths = get_intermediate_paths(u_path, num=8, include_ends=True)

for i, sub_xys in enumerate(sub_paths):
    is_last = (i == len(sub_paths) - 1)
    with anim.frame(duration=1.8 if is_last else 0.12):
        # Guide track & service cards
        lines_curved(u_path, r=6, style=Styles.MutedDashed)
        rectangle((24, 39), width=26, height=14, style=Styles.Neutral.patch(shape_r=2), text="Primary DB")
        rectangle((55, 39), width=22, height=14, style=Styles.MutedDashed.patch(shape_r=2), text="Firewall")
        dst_style = Styles.PrimaryFlat if is_last else Styles.Neutral
        dst_text = Styles.WhiteBold if is_last else Styles.Dark
        rectangle((86, 39), width=26, height=14, style=dst_style.patch(shape_r=2), text="DR Replica", text_style=dst_text)

        # Growing U-shaped arrow with rounded corners
        arrow_polyline(
            sub_xys,
            tail_width=2.5,
            head_width=6.0,
            head_length=4.5,
            style=Styles.PrimaryFlat.patch(shape_r=6),
        )
        if is_last:
            text((mid_xy[0], mid_xy[1] + 5.5), "Bypass Tunnel", style=Styles.PrimaryBold.patch(text_size=10.5))

save()
primitives_3
Growing U-Shaped Arrow and Trajectory Midpoint Label

Progressively Drawing Primitive Lines (line, lines, lines_curved)

The same interpolation functions apply directly to primitive line connectors in drawlib.lines:

  • Straight line(start_xy, tip_xy, arrow_head="->"): Iterate tip_xy over [*get_intermediate_points(start_xy, end_xy, num=5), end_xy] so the line and its "->" arrowhead extend smoothly toward the target.
  • Multi-segment lines(sub_xys, arrow_head="->") or lines_curved(sub_xys, r=..., arrow_head="->"): Iterate sub_xys over get_intermediate_paths(full_path, num=8, include_ends=True) to draw an orthogonal or rounded-corner connector progressively around bends:
from drawlib.anim import Animation
from drawlib.canvas import save, setup
from drawlib.lines import line, lines_curved
from drawlib.math import get_intermediate_paths, get_intermediate_points
from drawlib.shapes import rectangle
from drawlib.styles import Styles

setup(width=110, height=45)
anim = Animation(fps=12.0)

# 1. Straight line growing from (31, 33) to (79, 33)
straight_tips = get_intermediate_points((31, 33), (79, 33), num=5, include_ends=True)[1:]

# 2. Multi-segment curved line growing along an orthogonal Z-bend path
curved_path = [(31, 14), (55, 14), (55, 22), (79, 22)]
curved_subpaths = get_intermediate_paths(curved_path, num=5, include_ends=True)

for i, (tip_xy, sub_path) in enumerate(zip(straight_tips, curved_subpaths)):
    is_last = (i == len(straight_tips) - 1)
    with anim.frame(duration=2.0 if is_last else 0.1):
        rectangle((19, 33), width=24, height=11, style=Styles.Neutral, text="Ingress")
        rectangle((19, 14), width=24, height=11, style=Styles.SecondaryNeutral, text="Stream")

        dst_style = Styles.PrimaryFlat if is_last else Styles.Neutral
        dst_text = Styles.WhiteBold if is_last else Styles.Dark
        rectangle((91, 27.5), width=24, height=22, style=dst_style, text="Core Hub", text_style=dst_text)

        line((31, 33), tip_xy, arrow_head="->", style=Styles.PrimaryBold)
        lines_curved(sub_path, r=4.0, arrow_head="->", style=Styles.DarkBold)

save()
primitives_4
Progressively Extending Straight and Curved Polylines Across Frames

Animating a Moving Packet Along a Line

from drawlib.anim import Animation
from drawlib.canvas import save, setup
from drawlib.lines import line
from drawlib.math import get_intermediate_points
from drawlib.shapes import circle, rectangle
from drawlib.styles import Styles
from drawlib.text import text

setup(width=110, height=40)
anim = Animation(fps=10.0)

pts = get_intermediate_points((32, 20), (77, 20), num=4, include_ends=True)

for i, pkt_xy in enumerate(pts):
    is_last = (i == len(pts) - 1)
    with anim.frame(duration=1.8 if is_last else 0.12):
        # Static baseline endpoints
        rectangle((20, 20), width=22, height=15, style=Styles.Neutral, text="Producer")
        recv_style = Styles.PrimaryFlat if is_last else Styles.Neutral
        recv_text = Styles.WhiteBold if is_last else Styles.Dark
        rectangle((90, 20), width=22, height=15, style=recv_style, text="Consumer", text_style=recv_text)

        # Wire and moving packet
        line((31, 20), (79, 20), style=Styles.MutedDashed, arrow_head="->")
        if not is_last:
            circle(pkt_xy, radius=2.8, style=Styles.PrimaryFlat)
            text((pkt_xy[0], 27), "msg", style=Styles.PrimaryBold.patch(text_size=10.5))

save()
primitives_5
Packet Transmission Between Services

2. Smooth Color Interpolation (get_intermediate_color & get_intermediate_colors)

When you want a shape's fill, border, or text color to transition smoothly from Color A to Color B across frames, use the color interpolation helpers in drawlib.styles (also available in drawlib.preset_colors):

from drawlib.styles import Colors, get_intermediate_color, get_intermediate_colors
Function Signature Description
get_intermediate_color (color1: ColorType, color2: ColorType) -> Color Returns the single 50% midpoint Color between color1 and color2 (wrapper around get_intermediate_colors(color1, color2, num=1)[0]).
get_intermediate_colors (color1: ColorType, color2: ColorType, num: int = 1, *, include_ends: bool = False) -> list[Color] Returns num evenly spaced intermediate Color objects in RGBA space. Set include_ends=True to return [color1, ..., color2] (num + 2 colors total).

Both functions accept any ColorType (Color instance, (R, G, B) / (R, G, B, A) tuple, or hex string like "#3B82F6").

Animating Style Fades with Style.patch()

Generate the color sequence once before the loop with include_ends=True, and apply each frame's color via .patch(shape_fill_color=bg):

from drawlib.anim import Animation
from drawlib.canvas import save, setup
from drawlib.shapes import rectangle
from drawlib.styles import Colors, Styles, get_intermediate_colors

setup(width=110, height=38)
anim = Animation(fps=10.0)

# Generate 7 colors from White -> Primary (including both endpoints)
fade_in = get_intermediate_colors(Colors.White, Colors.Primary, num=5, include_ends=True)

for i, bg_color in enumerate(fade_in):
    is_last = (i == len(fade_in) - 1)
    with anim.frame(duration=2.0 if is_last else 0.12):
        # Static neutral peers
        rectangle((22, 19), width=26, height=16, style=Styles.Neutral, text="Standby A")
        rectangle((88, 19), width=26, height=16, style=Styles.Neutral, text="Standby B")

        # Hero node smoothly fading from White to Primary
        active_style = Styles.PrimaryOutline.patch(shape_fill_color=bg_color)
        active_text = Styles.WhiteBold if i >= 3 else Styles.DarkBold
        rectangle((55, 19), width=28, height=16, style=active_style, text="Leader Node", text_style=active_text)

save()
primitives_6
Smooth Color Interpolation with get_intermediate_colors()

3. Combining Motion, Scaling, Rotation & Color Transitions

You can combine coordinate interpolation (get_intermediate_points()), dynamic geometric scaling (width, height, radius), rotation (Style.patch(angle=...)), and smooth color transitions (get_intermediate_colors()) in multi-phase animations—for example, moving and rotating a payload token across a link, and then pulsing (width/height scale) and fading the receiver node into its active state:

from drawlib.anim import Animation
from drawlib.canvas import save, setup
from drawlib.lines import line
from drawlib.shapes import rectangle, regularpolygon
from drawlib.styles import Colors, Styles, get_intermediate_colors

setup(width=105, height=40)
anim = Animation(fps=10.0)

packet_xs = [34, 44, 54, 64]
packet_angles = [0, 30, 60, 90]
packet_radii = [2.6, 3.0, 3.4, 3.0]
fade_colors = get_intermediate_colors(Colors.White, Colors.Primary, num=3, include_ends=True)
scale_factors = [1.0, 1.06, 1.12, 1.06, 1.0]

# Phase 1: Rotating & scaling hexagon token travels from API to Worker
for pkt_x, ang, rad in zip(packet_xs, packet_angles, packet_radii):
    with anim.frame(duration=0.1):
        rectangle((20, 20), width=22, height=15, style=Styles.PrimaryNeutral, text="API")
        rectangle((82, 20), width=26, height=15, style=Styles.Neutral, text="Worker")
        line((31, 20), (69, 20), style=Styles.DarkBold, arrow_head="->")
        regularpolygon((pkt_x, 20), radius=rad, num_vertex=6, style=Styles.PrimaryFlat.patch(angle=ang))

# Phase 2: Worker pulses in scale (width/height) while smoothly transitioning from White to Primary
for i, (bg, s) in enumerate(zip(fade_colors, scale_factors)):
    is_last = (i == len(fade_colors) - 1)
    with anim.frame(duration=2.0 if is_last else 0.12):
        rectangle((20, 20), width=22, height=15, style=Styles.PrimaryNeutral, text="API")
        worker_style = Styles.PrimaryOutline.patch(shape_fill_color=bg)
        worker_text = Styles.WhiteBold if i >= 2 else Styles.DarkBold
        rectangle(
            (82, 20),
            width=26 * s,
            height=15 * s,
            style=worker_style,
            text="Worker",
            text_style=worker_text,
        )
        line((31, 20), (69, 20), style=Styles.DarkBold, arrow_head="->")

save()
primitives_7
Combining Motion, Rotation (angle), Scaling (width/height), and Color Transition