an.motion#
Motion presets: a named vocabulary of moves, as authoring macros.
pop_in, hop, shake, slide_in, slide_out,
squash_stretch and crawl each EXPAND to ordinary tween (and
set) actions on transform properties, composed with
sequence() and parallel(). Called from
Python, nothing downstream learns a preset exists: the flat timeline, an
validate, the verifiers and the renderer see the same tweens an author could
have written by hand. No runtime change, and no compiled document that does not
use a preset moves by a byte.
These are the core’s presets: each moves the node it is given (an entity
container, a text block, a plane) and needs no rig. The moves that name a
rig’s parts or swap its views — nod, point, turn, walk,
waddle, speech_pulse — are the cut-out genre’s, in cutan.motion
(an#322); their old names here are live aliases while anything still imports
them. This module also holds what every preset is built from: the rest pose
(rest_pose(), stage_poses()), the landing set, and
as_leaves().
>>> from an.ir.compose import flatten, sequence
>>> leaves = _tweens(sequence(pop_in("charlie"), hop("charlie"), shake("charlie")))
>>> [(f.action.target, f.action.property) for f in leaves][:3]
[('charlie', 'scale_x'), ('charlie', 'scale_y'), ('charlie', 'y')]
>>> round(leaves[-1].end, 3)
1.35
Targets. A preset targets the node it is given — usually the entity
container ("charlie"), the node a descriptor’s bone:root track animates
too. A target the built scene does not carry makes the render raise (the
runtime refuses an unknown node, naming the known ones); rest_pose()
raises for it up front, before any browser starts.
Landing. Every preset ends each property it moves with a set at the
value it ends on, so the move lands exactly whatever the frame rate or
step_hz (a tween ending between two frames otherwise leaves the property
where the last sampled frame had it). The set holds until the next tween
on that property.
Rest. A tween’s value is ABSOLUTE, and every preset writes its from
explicitly so presets chain without a jump. Each preset therefore needs the
target node’s rest value for the properties it moves; rest=None means the
identity pose (x = y = rotation = 0, scale = 1), which is right for
every rotation, and for y/scale of any entity without a stage
placement — but an entity’s x is laid out across the shot (-110 and
110 for two characters), so a move on x (shake, slide_in,
slide_out) in a shot with more than one character
wants rest=rest_pose(shot, "charlie"), which reads the value off the
compiler’s own scene builder rather than restating its layout.
scene.md: play a preset by name (an#166; play is the cut-out genre’s
action kind, and cutan.motion.PRESETS lists these presets with its own). {kind: play, target:
charlie, animation: hop, args: {height: 30}, start: 1.0} expands to exactly
this module’s tweens at compile, with rest the pose the node HAS at the
play’s start — the built scene’s (stage placement, layout) overridden by the
sets and tweens before it (an#212) — so no rest=, and a preset after a move
starts where the move left it (an entrance in HOME_PRESETS lands on the
built pose instead); args are the preset’s keyword arguments. A character
descriptor animation of the same name WINS; an validate and the compiler
decide both through cutan.characters.play.play_problems(). duration
stretches the move and speed divides it; loop is refused. In a
sequence a play without a duration occupies the preset’s own
length divided by speed, so two in a row run one after the other.
as_leaves() remains for a preset composed in Python and written into
scene.md as plain, hand-editable tweens (a composition tree round-trips
too since an#241, but verbatim, as its JSON form).
Module Attributes
A cubic-Bézier that overshoots its target by about 10% and settles back (CSS "easeOutBack"). |
|
the moves that need no rig. |
|
bump a preset's version in the SAME change that makes it expand differently for the same args, so every shot that plays it re-renders visibly instead of silently ( |
|
Presets whose |
Functions
|
|
|
An opening crawl: lay |
|
Jump up by |
|
Grow from nothing to full size, overshooting and settling (an entrance). |
|
The rest values of |
|
Tremble side to side |
|
|
|
Whip in from |
|
Exit |
|
Squash (wide and short), stretch (narrow and tall), then settle. |
- an.motion.HOME_PRESETS: frozenset[str] = frozenset({'pop_in', 'slide_in'})#
Presets whose
restis the node’s HOME — where an entrance LANDS — rather than where the node is when the move starts. Played by name these read the BUILT pose (slide_outthenslide_incomes back home;pop_inafter asetof the scales to 0 grows to full size); every other preset moves relative to where the node IS at its start (an#212).
- an.motion.OVERSHOOT: tuple[float, float, float, float] = (0.34, 1.56, 0.64, 1.0)#
A cubic-Bézier that overshoots its target by about 10% and settles back (CSS “easeOutBack”). The compiler and both evaluators take any 4-point Bézier on a numeric channel, and nothing clamps
yto[0, 1].
- an.motion.PRESETS: dict[str, Callable[[...], Annotated[Annotated[SetAction, Tag(tag=set)] | Annotated[TweenAction, Tag(tag=tween)] | Annotated[SequenceAction, Tag(tag=sequence)] | Annotated[ParallelAction, Tag(tag=parallel)] | Annotated[DelayAction, Tag(tag=delay)] | Annotated[LoopAction, Tag(tag=loop)] | Annotated[ExtensionAction, SerializeAsAny(), Tag(tag=extension)], Discriminator(discriminator=_action_tag, custom_error_type=None, custom_error_message=None, custom_error_context=None)]]] = {'crawl': <function crawl>, 'hop': <function hop>, 'pop_in': <function pop_in>, 'shake': <function shake>, 'slide_in': <function slide_in>, 'slide_out': <function slide_out>, 'squash_stretch': <function squash_stretch>}#
the moves that need no rig. A genre’s presets (the cut-out ones:
cutan.motion.PRESETS, an#322) are listed beside these by the genre, which owns theplaythat names them.- Type:
The core’s presets by name
- an.motion.PRESET_VERSIONS: dict[str, str] = {'crawl': '1', 'hop': '1', 'pop_in': '1', 'shake': '1', 'slide_in': '1', 'slide_out': '1', 'squash_stretch': '1'}#
bump a preset’s version in the SAME change that makes it expand differently for the same args, so every shot that plays it re-renders visibly instead of silently (
an.semanticfolds it into the shot’s vocabulary digest). Versioning starts here (an#248).- Type:
Each preset’s vocabulary version (ADR 0003 decision 2)
- an.motion.as_leaves(action, *, start=0.0)[source]#
actionas top-level leaves thatscene.mdcan round-trip.The markdown writer spells a leaf and the
sequence(delay(start), leaf)wrapper the parser produces for astart:key in their short form, and writes any other composition tree verbatim (its JSON form, an#241). This flattens a preset (or any tree) into the short form, with the same absolute times, which is what a person editingscene.mdwants.A
setkeeps its absolute time inatinstead of a wrapper.- Return type:
list[Union[SetAction,TweenAction,SequenceAction,ParallelAction,DelayAction,LoopAction,ExtensionAction]]
>>> leaves = as_leaves(hop("charlie"), start=1.0) >>> [type(a).__name__ for a in leaves] ['SequenceAction', 'SequenceAction', 'SetAction'] >>> [round(f.start, 3) for a in leaves for f in flatten(a)] # each from 0 [1.0, 1.25, 1.5]
- an.motion.crawl(target, *, distance=2400.0, duration=30.0, start=None, tilt=0.96, perspective=1.0, fade=(700.0, 1500.0), y=None, easing='linear', rest=None)[source]#
An opening crawl: lay
targeton a plane tilted away, and slide it up and away.Sets the plane (
rotation_x=tilt,perspective, the farfade, and the hinge’sywhen given) at the start, then ONE tween:pivot_yfromstart(default: where the pivot rests) tostart + distance. On a tilted node the pivot is the point of the plane on the hinge (an#314), so the content travelsdistancescene px along the plane; the slowing and shrinking as it recedes are the projection’s, not the tween’s. A block centred on its origin starts with its middle on the hinge: a negativestart(half the block’s height and more) has it enter from below.fade=Nonedraws the plane to the horizon.- Return type:
Union[SetAction,TweenAction,SequenceAction,ParallelAction,DelayAction,LoopAction,ExtensionAction]
>>> leaves = flatten(crawl("crawl", distance=1000, duration=20, start=-300, fade=None)) >>> sorted((f.action.property, getattr(f.action, "value", None)) for f in leaves ... if isinstance(f.action, SetAction) and f.start == 0) [('perspective', 1.0), ('rotation_x', 0.96)] >>> [(f.action.from_value, f.action.to_value, f.end) for f in _tweens(crawl("crawl", ... distance=1000, duration=20, start=-300))] [(-300.0, 700.0, 20.0)]
- an.motion.hop(target, *, height=40.0, duration=0.5, rest=None)[source]#
Jump up by
heightscene pixels and land back where it started.- Return type:
Union[SetAction,TweenAction,SequenceAction,ParallelAction,DelayAction,LoopAction,ExtensionAction]
>>> [(f.action.from_value, f.action.to_value) for f in _tweens(hop("charlie", height=30))] [(0.0, -30.0), (-30.0, 0.0)]
- an.motion.pop_in(target, *, duration=0.45, easing=(0.34, 1.56, 0.64, 1.0), rest=None)[source]#
Grow from nothing to full size, overshooting and settling (an entrance).
Scales the target from 0 to its rest scale. Before the preset starts the target shows at its rest pose: to keep it hidden until it pops, start the preset at the target’s first frame (or hold
scale_x/scale_yat 0 with asetbefore it).- Return type:
Union[SetAction,TweenAction,SequenceAction,ParallelAction,DelayAction,LoopAction,ExtensionAction]
>>> [(f.action.property, f.action.from_value, f.action.to_value) ... for f in _tweens(pop_in("charlie"))] [('scale_x', 0.0, 1.0), ('scale_y', 0.0, 1.0)]
- an.motion.rest_pose(shot, target, *, mall=None)[source]#
The rest values of
target’s node as the compiler buildsshot.Compiles the shot’s STAGE — its entities, without actions, dialogue or camera — through the cutout compiler’s own scene builder, so the layout (
-110/110for two characters), astageplacement and a stage scale are read, never restated. Pass the samemallyou render with: a descriptor rig is built from its character store.(These examples build character entities, the cut-out genre’s; they are not run here.)
>>> from an.ir.schema import AssetRef >>> two = Shot(id="s", entities=[ ... AssetRef(kind="character", id=n, store="characters", ref=n) for n in ("a", "b")]) >>> rest_pose(two, "a")["x"], rest_pose(two, "b")["x"] (-110.0, 110.0) >>> rest_pose(two, "a/head")["y"] -55.0
- an.motion.shake(target, *, amplitude=8.0, duration=0.4, cycles=3, rest=None)[source]#
Tremble side to side
cyclestimes and come back to rest (onx).- Return type:
Union[SetAction,TweenAction,SequenceAction,ParallelAction,DelayAction,LoopAction,ExtensionAction]
>>> [f.action.to_value for f in _tweens(shake("charlie", amplitude=5, cycles=2))] [5.0, -5.0, 5.0, -5.0, 0.0] >>> [f.action.to_value for f in _tweens(shake("charlie", cycles=1, rest={"x": -110}))] [-102.0, -118.0, -110.0]
- an.motion.slide_in(target, *, from_side='left', distance=600.0, duration=0.35, easing=(0.34, 1.56, 0.64, 1.0), rest=None)[source]#
Whip in from
distancepixels off to one side, overshoot, and settle.- Return type:
Union[SetAction,TweenAction,SequenceAction,ParallelAction,DelayAction,LoopAction,ExtensionAction]
>>> [(f.action.from_value, f.action.to_value) for f in _tweens(slide_in("charlie", distance=400))] [(-400.0, 0.0)]
- an.motion.slide_out(target, *, to_side='right', distance=600.0, duration=0.35, easing='ease_in', rest=None)[source]#
Exit
distancepixels off to one side, accelerating (an exit).- Return type:
Union[SetAction,TweenAction,SequenceAction,ParallelAction,DelayAction,LoopAction,ExtensionAction]
>>> [(f.action.from_value, f.action.to_value) for f in _tweens(slide_out("charlie", to_side="left"))] [(0.0, -600.0)]
- an.motion.squash_stretch(target, *, amount=0.2, duration=0.36, rest=None)[source]#
Squash (wide and short), stretch (narrow and tall), then settle.
Scales about the target’s own origin (for the procedural rig, the torso’s centre). Volume is roughly kept: one axis grows by what the other loses.
- Return type:
Union[SetAction,TweenAction,SequenceAction,ParallelAction,DelayAction,LoopAction,ExtensionAction]
>>> [[round(f.action.to_value, 2) for f in _tweens(squash_stretch("c")) ... if f.action.property == p] for p in ("scale_x", "scale_y")] [[1.2, 0.9, 1.0], [0.8, 1.1, 1.0]]
- an.motion.stage_poses(shot, *, mall=None, width=None, height=None)[source]#
{node path: rest pose}for every node the compiler builds forshot’s stage — whatrest_pose()reads one entry of, and whatan validatechecks a presetplay’s node and everyset/tweentarget against (an#166, an#193).width/height(default: the compiler’s) matter to text, whose line breaks depend on the frame.(These examples build character entities, the cut-out genre’s; they are not run here.)
>>> from an.ir.schema import AssetRef >>> one = Shot(id="s", entities=[AssetRef(kind="character", id="c", store="characters", ref="c")]) >>> poses = stage_poses(one) >>> "c/right_arm" in poses, poses["c/head"]["y"] (True, -55.0)