Emphasis
Drawing attention: indications, highlights and hand-drawn notes.
Indications
Indicate, wiggle, blink, passing flashes, broadcast ripples, flashing frames and underlines, and the emphasis of one word inside a text.

View on GitHub
gaanim examples/emphasis/indications.pyCode · 175 lines
"""Indications: transient animations that point at a drawable, a frame or a word.
Each cell or row plays one emphasis of ``drawable.animate`` and is labelled
with the call; the drawable is back at rest when it ends. Segments cover
pulses and shakes, blinks and passing flashes, bursts of rays and drawn
frames, ripples, flashing frames and underlines, and the emphasis of a word
inside a text.
"""
import os
from gaanim import Scene, Zones
DURATION = 1.6
HOLD = 0.5
scene = Scene()
success = scene.canvas.color("success")
foreground = scene.canvas.color("foreground")
geometry = scene.geometry
def cells(labels, rows=2, columns=3):
"""A grid of cells, each label above its stage; return the stages."""
grid = scene.layout.zones(Zones.grid(rows=rows, columns=columns, gap="48px", padding="page_padding"))
stages = []
for cell, label in zip(grid, labels):
top, stage = cell.split(Zones.rows(["88px", "1fr"]))
scene.text(label, role="code").place(top, anchor="top_left")
stages.append(stage)
return stages
def lines(labels, tracks=("1fr", "1fr")):
"""One row per label, the label to the left; return each row's stage."""
grid = scene.layout.zones(
Zones.grid(rows=len(labels), columns=list(tracks), column_gap="48px", padding="page_padding")
)
for index, label in enumerate(labels):
scene.text(label, role="code").place(grid[2 * index], anchor="right")
return [grid[2 * index + 1] for index in range(len(labels))]
def play(entries, stages):
"""Play one emphasis per stage: ``entries`` are (label, make, emphasis).
``make(stage)`` builds the drawable in its stage and
``emphasis(drawable.animate)`` returns the animation.
"""
moves = [emphasis(make(stage).animate).duration(DURATION) for stage, (_, make, emphasis) in zip(stages, entries)]
scene.wait(HOLD)
scene.play(moves)
scene.wait(HOLD)
def circle(stage):
return geometry.circle(0.75).move_to(*stage.center)
def square(stage):
return geometry.square(1.3).move_to(*stage.center)
def star(stage):
return geometry.star(5, 0.5, 0.22).move_to(*stage.center)
def card(stage):
return geometry.rounded_rect(2.6, 1.2, 0.12).move_to(*stage.center)
def dot(stage):
return geometry.dot(0.3).move_to(*stage.center)
def wave(stage):
"""A zigzag stroke across ``stage``."""
(x, y), half = stage.center, stage.width / 2 - 0.3
points = [(x - half + index * half / 4, y + (0.5 if index % 2 else -0.5)) for index in range(9)]
return geometry.polyline(points).stroke(foreground, 0.1)
def word(stage, text="Notice", anchor="center"):
return scene.text(text, size=0.9).place(stage, anchor=anchor)
# indicate swells the drawable for a moment and tints it: with the theme's
# accent, its foreground for a drawable already in the accent, or color.
# wiggle shakes it sideways, count times amplitude units each way.
scene.segment("pulses")
ENTRIES = [
("indicate()", circle, lambda a: a.indicate()),
("indicate(color=success,\n scale_factor=1.5)", circle, lambda a: a.indicate(color=success, scale_factor=1.5)),
("wiggle()", square, lambda a: a.wiggle()),
("wiggle(amplitude=0.3, count=2)", square, lambda a: a.wiggle(amplitude=0.3, count=2)),
]
play(ENTRIES, cells([label for label, _, _ in ENTRIES], columns=2))
# blink switches the drawable off and on, three times by default.
# show_passing_flash sweeps a window along a stroke, which is hidden before
# and after; time_width is the window's share of the path.
scene.segment("blinks")
ENTRIES = [
("blink()", card, lambda a: a.blink()),
("blink(6)", card, lambda a: a.blink(6)),
("show_passing_flash()", wave, lambda a: a.show_passing_flash()),
("show_passing_flash(time_width=0.6)", wave, lambda a: a.show_passing_flash(time_width=0.6)),
]
play(ENTRIES, cells([label for label, _, _ in ENTRIES], columns=2))
# flash bursts rays out of the drawable: lines of them, length long, gap
# beyond it. circumscribe draws a frame around it and lets it go.
scene.segment("flashes")
ENTRIES = [
("flash()", star, lambda a: a.flash()),
("flash(lines=6, length=0.6,\n gap=0.4, color=success)", star, lambda a: a.flash(lines=6, length=0.6, gap=0.4, color=success)),
("circumscribe()", word, lambda a: a.circumscribe()),
("circumscribe(padding=0.3,\n corner_radius=0.3)", word, lambda a: a.circumscribe(padding=0.3, corner_radius=0.3)),
]
play(ENTRIES, cells([label for label, _, _ in ENTRIES], columns=2))
# broadcast sends copies of the drawable out as ripples (stars, from a
# star): count of them, growing to max_scale times its size, each lag of a
# ripple after the last.
scene.segment("ripples")
ENTRIES = [
("broadcast()", dot, lambda a: a.broadcast()),
("broadcast(count=2, max_scale=5.0,\n lag=0.0)", dot, lambda a: a.broadcast(count=2, max_scale=5.0, lag=0.0)),
("broadcast(count=8, lag=0.6)", dot, lambda a: a.broadcast(count=8, lag=0.6)),
("broadcast(max_scale=2.5)", star, lambda a: a.broadcast(max_scale=2.5)),
]
play(ENTRIES, cells([label for label, _, _ in ENTRIES], columns=2))
# flash_around and flash_under sweep a bright window around a new frame or
# along a new underline, both invisible outside the flash. color and width
# restyle them, padding, corner_radius, gap and overhang shape them, and
# time_width sets the window's length.
scene.segment("frames")
ENTRIES = [
("flash_around()", word, lambda a: a.flash_around()),
("flash_around(color=success, width=0.08)", word, lambda a: a.flash_around(color=success, width=0.08)),
("flash_around(padding=0.3,\n corner_radius=0.3)", word, lambda a: a.flash_around(padding=0.3, corner_radius=0.3)),
("flash_around(time_width=1.0)", word, lambda a: a.flash_around(time_width=1.0)),
("flash_under()", word, lambda a: a.flash_under()),
("flash_under(gap=0.2, overhang=0.5,\n time_width=1.0)", word, lambda a: a.flash_under(gap=0.2, overhang=0.5, time_width=1.0)),
]
play(ENTRIES, lines([label for label, _, _ in ENTRIES], ("3fr", "2fr")))
# A selection of a text takes indications of its own: indicate swells and
# tints its glyphs, pulse swells them twice, highlight tints them, wiggle
# shakes them, wave lifts them one after another, and focus dims the rest of
# the text.
scene.segment("words")
ENTRIES = [
('line["gap"].animate.indicate()', lambda a: a["gap"].animate.indicate()),
('line["gap"].animate.pulse()', lambda a: a["gap"].animate.pulse()),
('line["gap"].animate.highlight()', lambda a: a["gap"].animate.highlight()),
('line["gap"].animate.wiggle()', lambda a: a["gap"].animate.wiggle()),
('line["gap"].animate.wave()', lambda a: a["gap"].animate.wave()),
('line["gap"].animate.focus()', lambda a: a["gap"].animate.focus()),
("line.words[0:2].animate.indicate()", lambda a: a.words[0:2].animate.indicate()),
]
stages = lines([label for label, _ in ENTRIES])
texts = [scene.text("Mind the gap", size=0.8).place(stage, anchor="left") for stage in stages]
scene.wait(HOLD)
scene.play([emphasis(line).duration(DURATION) for (_, emphasis), line in zip(ENTRIES, texts)])
scene.wait(HOLD)
SEGMENT = 2 * HOLD + DURATION
if snapshots := os.environ.get("GAANIM_SNAPSHOTS"):
# Every segment early, at its peak and late in the emphasis.
scene.snapshots(snapshots, [SEGMENT * index + HOLD + t for index in range(6) for t in (0.4, 0.8, 1.2)])
else:
scene.render()Highlights
Live surrounding frames that follow and retarget, color-cycling boundaries, highlighter marker bands and spotlights that dim everything else.

View on GitHub
gaanim examples/emphasis/highlights.pyCode · 217 lines
"""Highlights: frames, color-cycling borders, highlighter bands and spotlights that stay on a target.
``scene.geometry.surrounding_rect`` draws a live frame that follows its
targets and can move to others; ``animated_boundary`` draws a border whose
color cycles; ``marker`` sweeps a highlighter band behind words; and
``scene.fx.spotlight`` dims everything but its target. Each label is the
code that builds or animates its row or card.
"""
import os
from gaanim import Easing, Scene, Zones, part
DRAW = 1.2
HOLD = 0.6
LIGHT = 1.6
scene = Scene()
accent = scene.canvas.color("accent")
success = scene.canvas.color("success")
warning = scene.canvas.color("warning")
danger = scene.canvas.color("danger")
foreground = scene.canvas.color("foreground")
background = scene.canvas.color("background")
panel = scene.canvas.color("panel")
rule = scene.canvas.color("rule")
geometry = scene.geometry
def rows(labels, tracks=("3fr", "2fr")):
"""One row per label, the label to the left; return each row's stage."""
grid = scene.layout.zones(
Zones.grid(rows=len(labels), columns=list(tracks), column_gap="48px", padding="page_padding")
)
for index, label in enumerate(labels):
scene.text(label, role="code").place(grid[2 * index], anchor="right")
return [grid[2 * index + 1] for index in range(len(labels))]
def card(stage, width=3.0, height=0.9):
"""A panel card on the left of ``stage``, a little in from its edge."""
return geometry.rounded_rect(width, height, 0.1).fill(panel).stroke(rule, 0.02).place(stage.inset((0, "32px")), anchor="left")
def line(stage, text="Mind the gap", size=0.8):
"""A line of text on the left of ``stage``, a little in from its edge."""
return scene.text(text, size=size).place(stage.inset((0, "32px")), anchor="left")
# surrounding_rect outlines its targets' bounds, padding away (0.12 by
# default), in the theme's foreground; several targets share one frame and
# a text selection frames just its glyphs. Restyle it like any drawable: a
# translucent fill behind the text makes a background highlight. Each label
# abbreviates scene.geometry.surrounding_rect(...).
scene.segment("frames")
stages = rows([
"surrounding_rect(box)",
"surrounding_rect(box, padding=0.3,\n corner_radius=0.3)",
"surrounding_rect([box, dot])",
'surrounding_rect(line["gap"])',
'surrounding_rect(line["gap"])\n .stroke(accent, 0.06)',
"surrounding_rect(line)\n .fill(accent).opacity(0.35).z_index(-1)",
], ("2fr", "1fr"))
boxed = card(stages[0])
padded = card(stages[1])
pair = card(stages[2], 2.0)
spot = geometry.dot(0.22).fill(accent).move_to(pair.bounds().right + 1.2, stages[2].center[1])
selected = line(stages[3])
styled = line(stages[4])
shaded = line(stages[5])
frames = [
geometry.surrounding_rect(boxed),
geometry.surrounding_rect(padded, padding=0.3, corner_radius=0.3),
geometry.surrounding_rect([pair, spot]),
geometry.surrounding_rect(selected["gap"]),
geometry.surrounding_rect(styled["gap"]).stroke(accent, 0.06),
geometry.surrounding_rect(shaded).fill(accent).opacity(0.35).z_index(-1),
]
scene.play([frame.animate.create() for frame in frames], duration=DRAW)
scene.wait(HOLD)
# A frame follows its targets wherever they go, and retarget tweens its four
# edges to new targets, which it follows from then on.
def energy(stage):
"""E = m c^2, with the parts mass and light, on the left of ``stage``."""
return scene.text.equation("E = ", part("mass", "m"), part("light", "c^2"), size=1.1).place(
stage.inset((0, "32px")), anchor="left"
)
scene.segment("retarget")
stages = rows([
'frame.retarget(eq["light"])',
'frame.retarget([eq["mass"], eq["light"]])',
"eq.animate.shift_by(1.5, 0)",
])
equations = [energy(stage) for stage in stages]
frames = [geometry.surrounding_rect(eq["mass"]).stroke(accent, 0.05) for eq in equations]
scene.wait(HOLD)
scene.play(
[
frames[0].retarget(equations[0]["light"]),
frames[1].retarget([equations[1]["mass"], equations[1]["light"]]),
equations[2].animate.shift_by(1.5, 0),
],
duration=DRAW,
)
scene.wait(HOLD)
# animated_boundary frames a drawable with a stroke that cycles through
# colors, cycle_rate turns of the list per second (the last color blends
# back into the first). It is visible at once: fade it in like any
# drawable. warm is [accent, danger] and three is [accent, success, warning].
scene.segment("boundaries")
warm, three = [accent, danger], [accent, success, warning]
stages = rows([
"box.animated_boundary(warm)",
"box.animated_boundary(three, cycle_rate=2.0)",
"box.animated_boundary(warm, width=0.1,\n padding=0.3, corner_radius=0.3)",
"box.animated_boundary(three)\n .animate.fade_in()",
], ("2fr", "1fr"))
boxes = [card(stage, 3.0, 1.1) for stage in stages]
boxes[0].animated_boundary(warm)
boxes[1].animated_boundary(three, cycle_rate=2.0)
boxes[2].animated_boundary(warm, width=0.1, padding=0.3, corner_radius=0.3)
faded = boxes[3].animated_boundary(three)
scene.play([faded.animate.fade_in()], duration=DRAW)
scene.wait(2 * HOLD)
# marker sweeps a highlighter band behind the selected glyphs, one band per
# line, and leaves it there; the static form draws it at once. color,
# opacity, skew (radians) and padding style the band; blend="multiply" inks
# it into a light card under the text. gap is line["gap"].
scene.segment("marker")
stages = rows([
"gap.animate.marker()",
"gap.animate.marker(accent)",
"gap.animate.marker(accent, skew=0.2)",
"gap.animate.marker(accent, opacity=0.9,\n padding=0.15)",
"gap.marker(success)",
'gap.animate.marker(accent,\n blend="multiply")',
"lines.words[1:5].animate.marker(accent)",
])
texts = [line(stage, size=0.72) for stage in stages[:5]]
paper = geometry.rounded_rect(4.8, 0.95, 0.1).fill(foreground).place(stages[5].inset((0, "32px")), anchor="left")
inked = scene.text("Mind the gap", size=0.72, color=background).move_to(paper)
wrapped = line(stages[6], "Mind the gap\nbetween lines", size=0.5)
texts[4]["gap"].marker(success)
scene.wait(HOLD)
scene.play(
[
texts[0]["gap"].animate.marker(),
texts[1]["gap"].animate.marker(accent),
texts[2]["gap"].animate.marker(accent, skew=0.2),
texts[3]["gap"].animate.marker(accent, opacity=0.9, padding=0.15),
inked["gap"].animate.marker(accent, blend="multiply"),
wrapped.words[1:5].animate.marker(accent),
],
duration=DRAW,
)
scene.wait(HOLD)
# scene.fx.spotlight dims everything outside its target, to dim at most,
# and lights it all again: padding and corner_radius shape the hole, which
# can frame a text selection, and several targets get a hole each. With
# Easing.SMOOTH the dim stays at the end. Each card is labelled with the
# call that lights it, abbreviating scene.fx.spotlight(...).
scene.segment("spotlight")
grid = scene.layout.zones(Zones.grid(rows=3, columns=2, gap="64px", padding="page_padding"))
CALLS = [
"spotlight(card)",
"spotlight(card, dim=0.95)",
"spotlight(card, padding=0.5,\n corner_radius=0.5)",
'spotlight(line["gap"])',
"spotlight([card, cards[5]])",
"spotlight(card)\n .easing(Easing.SMOOTH)",
]
cards = []
for zone, call in zip(grid, CALLS):
box = zone.inset("24px")
cards.append(geometry.rounded_rect(box.width, box.height, 0.12).fill(panel).stroke(rule, 0.02).place(box))
scene.text(call, role="code").place(box.inset("24px"), anchor="top_left")
gap_line = scene.text("Mind the gap", size=0.7).place(grid[3].inset("48px"), anchor="bottom_right")
fx = scene.fx
for light in [
fx.spotlight(cards[0]),
fx.spotlight(cards[1], dim=0.95),
fx.spotlight(cards[2], padding=0.5, corner_radius=0.5),
fx.spotlight(gap_line["gap"]),
fx.spotlight([cards[4], cards[5]]),
fx.spotlight(cards[5]).easing(Easing.SMOOTH),
]:
scene.play([light], duration=LIGHT)
scene.wait(HOLD)
RETARGET = DRAW + HOLD
BOUNDARIES = RETARGET + HOLD + DRAW + HOLD
MARKER = BOUNDARIES + DRAW + 2 * HOLD
SPOTLIGHT = MARKER + HOLD + DRAW + HOLD
if snapshots := os.environ.get("GAANIM_SNAPSHOTS"):
# Frames drawn; retargets midway and done; boundaries at three moments;
# bands midway and done; every spotlight at its darkest, and the end.
scene.snapshots(
snapshots,
[DRAW / 2, RETARGET - 0.1]
+ [RETARGET + HOLD + DRAW / 2, BOUNDARIES - 0.1]
+ [BOUNDARIES + t for t in (0.3, 0.9, 2.0)]
+ [MARKER + HOLD + DRAW / 2, SPOTLIGHT - 0.1]
+ [SPOTLIGHT + index * LIGHT + LIGHT / 2 for index in range(6)] + [SPOTLIGHT + 6 * LIGHT + HOLD - 0.1],
)
else:
scene.render()Hand drawn
Hand-drawn underlines, boxes, circles, strikes and brackets with every roughness, pass count, seed and style, marks that follow moving targets, and chalk shapes.

View on GitHub
gaanim examples/emphasis/hand_drawn.pyCode · 198 lines
"""Hand-drawn notes: rough boxes, circles, underlines, strikes and brackets that draw themselves.
``drawable.annotate`` returns a mark drawn as if by hand around the drawable
or a text selection; ``mark.animate.create()`` draws it on, one pass after
another, and the mark follows its target from then on. Segments cover every
kind of mark, the sides of a bracket, roughness, passes, seeds and style,
marks on moving targets, and the chalk look of a whole drawable.
"""
import os
from gaanim import Scene, Zones
DRAW = 1.2
HOLD = 0.6
scene = Scene()
accent = scene.canvas.color("accent")
success = scene.canvas.color("success")
danger = scene.canvas.color("danger")
panel = scene.canvas.color("panel")
rule = scene.canvas.color("rule")
geometry = scene.geometry
def cells(labels, rows=2, columns=3):
"""A grid of cells, each label above its stage; return the stages."""
grid = scene.layout.zones(Zones.grid(rows=rows, columns=columns, gap="48px", padding="page_padding"))
stages = []
for cell, label in zip(grid, labels):
top, stage = cell.split(Zones.rows(["48px", "1fr"]))
scene.text(label, role="code").place(top, anchor="top_left")
stages.append(stage)
return stages
def word(stage, text="Notice", size=0.9):
return scene.text(text, size=size).move_to(*stage.center)
def draw(marks):
"""Draw the marks on together, then hold."""
scene.wait(0.3)
scene.play([mark.animate.create() for mark in marks], duration=DRAW)
scene.wait(HOLD)
# Six kinds of mark. Without a color they take the theme's stroke; each
# label abbreviates word.annotate.<mark>.
scene.segment("kinds")
KINDS = [
("underline()", lambda notes: notes.underline()),
("box()", lambda notes: notes.box()),
("circle()", lambda notes: notes.circle()),
("strike_through()", lambda notes: notes.strike_through()),
("crossed_off()", lambda notes: notes.crossed_off()),
("bracket()", lambda notes: notes.bracket()),
]
draw([mark(word(stage).annotate) for stage, (_, mark) in zip(cells([label for label, _ in KINDS]), KINDS)])
# bracket draws square brackets on one side or several: "left", "right",
# "top" and "bottom" (left and right by default).
scene.segment("brackets")
SIDES = ['bracket("left")', 'bracket("right")', 'bracket("top")', 'bracket("bottom")', 'bracket(["top", "bottom"])',
'bracket(["left", "top"])']
SIDE_ARGS = ["left", "right", "top", "bottom", ["top", "bottom"], ["left", "top"]]
draw([
word(stage, "Two lines\nof notes", 0.7).annotate.bracket(sides, color=accent)
for stage, sides in zip(cells(SIDES, rows=3, columns=2), SIDE_ARGS)
])
def table(rows):
"""Mark a word in every cell: a row per option, a column per kind of mark.
``rows`` are (label, options) pairs; each row draws a box, a circle and
an underline with ``options``, so the cell under box(...) in the row
padding=0.3 is word.annotate.box(padding=0.3).
"""
kinds = ["box", "circle", "underline"]
grid = scene.layout.zones(
Zones.grid(rows=len(rows) + 1, columns=["5fr", "2fr", "2fr", "2fr"], column_gap="48px", padding="page_padding")
)
for index, name in enumerate(kinds, start=1):
scene.text(f"{name}(...)", role="code").place(grid[index])
marks = []
for row_index, (label, options) in enumerate(rows, start=1):
first = row_index * 4
scene.text(label, role="code").place(grid[first], anchor="right")
for index, name in enumerate(kinds, start=1):
target = scene.text("Notice", size=0.6).move_to(*grid[first + index].center)
marks.append(getattr(target.annotate, name)(**options))
draw(marks)
# roughness scales the wobble: 0 draws clean geometry, 1 (the default) the
# usual hand-drawn look. The same seed always draws the same scribble.
scene.segment("roughness")
table([
("roughness=0", {"roughness": 0}),
("roughness=0.5", {"roughness": 0.5}),
("roughness=1.0", {"roughness": 1.0}),
("roughness=2.0", {"roughness": 2.0}),
("roughness=3.0", {"roughness": 3.0}),
])
# passes draws the shape over itself 1 to 8 times, each with fresh
# randomness (2 by default); another seed draws another scribble.
scene.segment("passes")
table([
("passes=1", {"passes": 1}),
("passes=2", {"passes": 2}),
("passes=4", {"passes": 4}),
("passes=8", {"passes": 8}),
("seed=1, roughness=2.0", {"seed": 1, "roughness": 2.0}),
("seed=7, roughness=2.0", {"seed": 7, "roughness": 2.0}),
])
# color and width restyle the stroke (a width needs a color); padding grows
# the target's bounds first, by one value, (vertical, horizontal) or
# (top, right, bottom, left).
scene.segment("style")
table([
("color=accent", {"color": accent}),
("color=success, width=0.08", {"color": success, "width": 0.08}),
("padding=0.3", {"padding": 0.3}),
("padding=(0, 0.5)", {"padding": (0, 0.5)}),
("padding=(0.3, 0, 0, 0)", {"padding": (0.3, 0, 0, 0)}),
])
# A mark follows its target's bounds every frame and keeps its scribble
# while the target moves or grows; marks go on text selections too, and
# fade or restyle like any drawable.
def rows(labels):
"""One row per label, the label to the left; return each row's stage."""
grid = scene.layout.zones(
Zones.grid(rows=len(labels), columns=["4fr", "3fr"], column_gap="48px", padding="page_padding")
)
for index, label in enumerate(labels):
scene.text(label, role="code").place(grid[2 * index], anchor="right")
return [grid[2 * index + 1] for index in range(len(labels))]
def card(stage):
return geometry.rounded_rect(1.8, 0.7, 0.1).fill(panel).stroke(rule, 0.02).place(stage.inset((0, "48px")), anchor="left")
scene.segment("follow")
stages = rows([
"card.annotate.box(color=accent)\ncard.animate.shift_by(2.5, 0)",
"card.annotate.circle(color=accent)\ncard.animate.scale_to(1.3)",
'line["gap"].annotate\n .underline(color=accent)',
"line.words[0].annotate\n .crossed_off(color=danger)",
"mark.animate.fade_out()",
])
moving, growing = card(stages[0]), card(stages[1])
lines = [scene.text("Mind the gap", size=0.72).place(stage.inset((0, "48px")), anchor="left") for stage in stages[2:]]
marks = [
moving.annotate.box(color=accent),
growing.annotate.circle(color=accent),
lines[0]["gap"].annotate.underline(color=accent),
lines[1].words[0].annotate.crossed_off(color=danger),
lines[2].annotate.box(),
]
draw(marks)
scene.play([moving.animate.shift_by(2.5, 0), growing.animate.scale_to(1.3), marks[4].animate.fade_out()], duration=DRAW)
scene.wait(HOLD)
# chalk draws a whole drawable as chalk on a blackboard: a trembling outline
# up to roughness units and a grain, both following seed.
scene.segment("chalk")
CHALK = [
("star.chalk()", {}),
("star.chalk(seed=4)", {"seed": 4}),
("star.chalk(roughness=0.05)", {"roughness": 0.05}),
("star.chalk(seed=4, roughness=0.1)", {"seed": 4, "roughness": 0.1}),
]
stars = [
geometry.star(5, 1.35, 0.6).move_to(*stage.center).chalk(**options)
for stage, (_, options) in zip(cells([label for label, _ in CHALK], columns=2, rows=2), CHALK)
]
draw(stars)
MARKS = 0.3 + DRAW + HOLD
FOLLOW = 5 * MARKS
if snapshots := os.environ.get("GAANIM_SNAPSHOTS"):
# Every set of marks midway and drawn; the moving targets midway and
# settled; the chalk stars midway and drawn.
scene.snapshots(
snapshots,
[MARKS * index + t for index in range(5) for t in (0.3 + DRAW / 2, MARKS - 0.1)]
+ [FOLLOW + MARKS - 0.1, FOLLOW + MARKS + DRAW / 2, FOLLOW + MARKS + DRAW + HOLD - 0.1]
+ [FOLLOW + MARKS + DRAW + HOLD + t for t in (0.3 + DRAW / 2, MARKS - 0.1)],
)
else:
scene.render()