an.timing#
The timing kernel: what is on screen at time t, as a pure function.
The heart of the core (core study §3, layer K; ADR 0001 decisions 10-11). Every
genre that can answer “what is the value of this property at time t” does it the
same way: addressed properties (address), each of a
declared field kind (kinds) chosen by its entity kind’s
property space (spaces), keyed over time with easing
(easing), flattened to absolute times
(flat), compiled to tracks of placed clips of channels
(channel, clip, timeline)
and evaluated by evaluate_timeline(timeline, t) — sparse: a property nothing
has started writing is absent, and the entity’s rest state supplies it.
The kernel is a cross-language contract before it is code: five JSON files
in an/data/timing/ generated from these registries (contract),
which a second implementation (TypeScript) asserts. runtime.js — the stage
engine’s evaluator — is a bit-exact port of this package’s value-typed rule, and
its parity tests hold it there.
The frame clock is an’s: frame k at k / fps, frame_count = max(1,
round(duration * fps)) (an.frame_clock).
>>> from an.timing import Channel, Keyframe, evaluate_channel, get_space
>>> ch = Channel("charlie", "x", [Keyframe(0.0, 0.0, "ease_in_out"), Keyframe(1.0, 10.0)])
>>> evaluate_channel(ch, 0.25, kind=get_space("stage.node").kind_of("x"))
1.25
Functions
|
The address string of a compiled |
|
|
|
Why |
|
Evaluate |
|
Merge multiple poses with override semantics (later wins per key). |
|
The public name of the loop rule (the contract's; |
|
Evaluate |
|
Apply an easing spec to a normalised parameter |
|
Every registered entry in registration order; only |
|
The registered entry called |
|
Add |
|
The curve an easing spec names. |
|
A kind instance from its contract spec ( |
|
The registered kind names in registration order; only |
|
Register a field kind under |
|
|
|
Register |
|
A space from its JSON form (what |
|
The registered space names in registration order; only |
|
One compiled animation ( |
|
Evaluate |
The compiled document's |
Classes
|
A parsed address. |
|
Sorted keyframes for one property of one target. |
|
One keyframe: time, value, optional per-segment easing. |
|
Named animation: a duration + a bundle of channels. |
|
How a clip behaves past its natural duration. |
|
One named easing: its curve, where it comes from, and how it is solved. |
|
An angle: the shortest arc when |
|
A colour (hex or |
|
Holds |
Base of every field kind: a frozen, hashable, JSON-describable interpolator. |
|
|
A continuous scalar. |
|
A camera orbiting a target: |
A unit quaternion |
|
|
Where an evaluation sits: time |
A fixed-length list of numbers, interpolated componentwise. |
|
|
One declaration of a property space: pattern -> kind, write group, unit. |
|
An entity kind's properties: declarations, matched exact-first then by glob. |
|
A clip placed at an absolute time on a track. |
|
A duration + ordered list of tracks. |
|
A sequence of placed clips that share a common purpose / target prefix. |
Exceptions
A string is not an address under the grammar. |
|
An easing spec names no registered curve and parses as no parametrised one. |
|
A kind spec is malformed, or names no registered kind. |
|
A property space is malformed, unknown, or already registered. |
- class an.timing.Address(entity, nodes=(), field='', qualifier=None)[source]#
Bases:
objectA parsed address.
str(address)writes it back unchanged.
- exception an.timing.AddressError[source]#
Bases:
ValueErrorA string is not an address under the grammar.
- class an.timing.AngleKind(unit='deg', wrap=True)[source]#
Bases:
FieldKindAn angle: the shortest arc when
wrap, else the raw numbers. Interpolated angles are not re-normalised (350 -> 10 passes through 360).
- class an.timing.Channel(target, property, keyframes=<factory>)[source]#
Bases:
objectSorted keyframes for one property of one target.
Construction validates that
keyframesis non-empty and sorted.
- class an.timing.Clip(name, duration, channels=<factory>, loop_mode=LoopMode.ONCE)[source]#
Bases:
objectNamed animation: a duration + a bundle of channels.
- class an.timing.ColorKind(space='oklab')[source]#
Bases:
FieldKindA colour (hex or
[r, g, b, a?]in 0..1), written back in the form of the start value.oklabmixes perceptually with premultiplied alpha;srgblerps the encoded channels (Manim’s and the stage tint’s rule).
- class an.timing.DiscreteKind(switch_at=0.5)[source]#
Bases:
FieldKindHolds
aand switches tobon TIME (see the module docstring).switch_atlies in(0, 1]. The default, 0.5, is the switch point of an undeclared property; a replacement drawing (a swap set) declares 1, so it never appears before its own key.
- class an.timing.EasingEntry(name, curve, family, solver, description, version=1, params=<factory>)[source]#
Bases:
objectOne named easing: its curve, where it comes from, and how it is solved.
versionfollows ADR 0003: an entry whose meaning changes gets a new version, so a shot that names it re-renders visibly instead of silently.
- class an.timing.FieldDecl(pattern, kind, unit=None, writes=None, description='')[source]#
Bases:
objectOne declaration of a property space: pattern -> kind, write group, unit.
- class an.timing.FieldKind[source]#
Bases:
objectBase of every field kind: a frozen, hashable, JSON-describable interpolator.
Subclasses set
nameand implementinterpolate(); the dataclass fields are the kind’s parameters, andto_spec()writes them as the contract does ({"kind": name, **non-default params}).
- exception an.timing.FieldKindError[source]#
Bases:
ValueErrorA kind spec is malformed, or names no registered kind.
- class an.timing.Keyframe(time, value, easing=None)[source]#
Bases:
objectOne keyframe: time, value, optional per-segment easing.
The easing on a keyframe describes the curve leaving that keyframe toward the next one. The last keyframe’s easing is therefore unused.
- class an.timing.NumberKind(space='linear')[source]#
Bases:
FieldKindA continuous scalar.
space="log"interpolates the logarithm (zoom, scale).Linear interpolation is
a + (b - a) * uwith no special case at the ends, which is the stage runtime’s arithmetic: a declarednumberchannel is bit-identical to whatruntime.jsdraws.
- class an.timing.OrbitKind(unit='deg')[source]#
Bases:
FieldKindA camera orbiting a target:
{azimuth, elevation, distance, target?}.Azimuth takes the shortest arc, elevation is lerped and clamped to the poles, distance is lerped in log, target componentwise. Any other member is discrete and switches at
DEFAULT_SWITCH_AT(on time). One value: a dotted address never reaches inside it.
- class an.timing.PlacedClip(clip, start_time=0.0, duration=None, speed=1.0, blend_in=0.0, blend_out=0.0)[source]#
Bases:
objectA clip placed at an absolute time on a track.
- class an.timing.PropertySpace(name, fields=(), version=1, description='', undeclared=<factory>)[source]#
Bases:
objectAn entity kind’s properties: declarations, matched exact-first then by glob.
- class an.timing.QuaternionKind[source]#
Bases:
FieldKindA unit quaternion
[x, y, z, w], interpolated by slerp after a sign fix.
- class an.timing.Segment(t, start, end)[source]#
Bases:
objectWhere an evaluation sits: time
tinside the segment[start, end).
- exception an.timing.SpaceError[source]#
Bases:
ValueErrorA property space is malformed, unknown, or already registered.
- class an.timing.Timeline(duration, tracks=<factory>, space=None)[source]#
Bases:
objectA duration + ordered list of tracks. The canonical playback structure.
spaceis the property space the timeline’s DOCUMENT declares for its targets (an#245): one space, a registered name, or atarget -> spaceresolver. It is whatevaluate_timeline()uses when the caller passes none, so every caller of the default evaluates a target in the space its entity’s kind declares.None(every document that declares nothing) is the kernel default,an.timing.spaces.DFLT_TIMELINE_SPACE.
- class an.timing.Track(target_root='', clips=<factory>)[source]#
Bases:
objectA sequence of placed clips that share a common purpose / target prefix.
target_rootis informational metadata for downstream tools (the JS runtime can use it to scope rendering); evaluation does not filter by it.
- exception an.timing.UnknownEasingError[source]#
Bases:
ValueErrorAn easing spec names no registered curve and parses as no parametrised one.
- class an.timing.VectorKind[source]#
Bases:
FieldKindA fixed-length list of numbers, interpolated componentwise.
- an.timing.apply_easing(spec, t, *, names=None)[source]#
Apply an easing spec to a normalised parameter
tin[0, 1].namesrestricts the string specs accepted to that collection — what an engine that implements only part of the registry passes (the stage runtime implements the legacy names; seean.stage.easing). Sequences always take the legacy Bézier solver.- Return type:
>>> apply_easing(None, 0.25) 0.25 >>> apply_easing("step", 0.99), apply_easing("step", 1.0) (0.0, 1.0) >>> apply_easing("smooth", 0.5, names={"linear"}) Traceback (most recent call last): ... an.timing.easing.UnknownEasingError: unknown easing preset 'smooth'; known: ['linear']
- an.timing.check_channel(channel, kind)[source]#
Why
channel’s keyframe values do not fitkind(empty if they do).
- an.timing.clip_from_json(anim, *, name=None)[source]#
One compiled animation (
compiled.schema.json’sanimation) as aClip.Accepts the JSON mapping or any object with the same attributes (the stage’s
AnimationClipJSON). Two fields are carried rather than defaulted, and both have cost a bug:loop_mode(without it every loop evaluated asonce— an#7) and a list-valuedeasing, which is a cubic-bezier control quadruple and must stay a tuple forKeyframe. The stage compiler reads a from-less tween’s start through this too (an#212), so it evaluates exactly what the runtime will.- Return type:
- an.timing.easing_entries(*, owner=None)[source]#
Every registered entry in registration order; only
owner’s when given.- Return type:
- an.timing.easing_entry(name)[source]#
The registered entry called
name.- Return type:
>>> easing_entry("ease").family 'legacy'
- an.timing.evaluate_channel(channel, t, *, kind=None)#
Evaluate
channelat timet(see the module docstring forkind).- Return type:
- an.timing.evaluate_clip(clip, t, *, kind_of=None)#
Evaluate
clipat timet, returning aPose.kind_ofdeclares each channel’s field kind;Noneinterpolates by value type, asruntime.jsdoes (seean.timing.channel).
- an.timing.evaluate_timeline(timeline, t, *, space=None)[source]#
Evaluate
timelineat timet, merging poses across tracks/clips.The result is a PURE function of
t(an#185): what a node shows attnever depends on which instants were evaluated before it. Per(target, property):Active — some clip writing it is playing at
t(inclusive end: a clip at[s, e]is active att == etoo, so the final frame of “play this from 0 to 1 s” is visible at 1.0). Later wins: track order, then clip order within a track. Written att.Held — no clip writing it is playing, but one has ended: the value the clip reached AT ITS END holds. The latest end wins; a tie goes to the later clip, the same “later wins” as above. Written at that end.
At rest — nothing writing it has started yet. The key is ABSENT from the pose, and its value is the node’s own (the entity’s rest state;
runtime.jsrestores what it built).
Keys that write the same thing on one node (a write group: the swap sets of one visual,
rotation/rotation_rad) keep only the most recently WRITTEN — an endedviseme@happyspan does not outlive thevisemetrack that took the mouth back.spacesays what each property is (an.timing.spaces): one space, a registered space’s name, or atarget -> spaceresolver. Its field kinds interpolate and its write groups resolve.Noneis the default space,an.timing.spaces.DFLT_TIMELINE_SPACE(the stage node’s declared kinds, by name at call time).space=VALUE_TYPEDis the ruleruntime.jsimplements — interpolation by value type, thestage.nodewrite groups — which gives the same pose on every timeline the compiler emits (it refuses a value that fails its field kind).Forward-order rendering used to show the value at the clip’s last SAMPLED frame instead (the runtime kept whatever it last applied). The two agree whenever a clip ends on the frame grid — true of every golden-corpus clip — and differ when it ends between frames: a 0.37 s tween to 10 at 24 fps used to stop at 9.80 and now lands on 10, as authored. That landing is deliberate (it is the bug the motion presets’ settling
setpatched one preset at a time), and it is what makes the pose independent of the grid. Also deliberate: a clip shorter than a frame that no frame lands in now leaves its end value, and a held descendant tint stays on top of an ancestor’s later tint (the more specific target wins, as it always did while both played).runtime.js::evaluateTimelineis a port of this function andcutan’stests/test_pure_pose.pyholds the two to it.>>> from an.timing.channel import Channel, Keyframe >>> from an.timing.clip import Clip >>> ch = Channel("a", "x", [Keyframe(0.0, 0.0), Keyframe(1.0, 10.0)]) >>> tl = Timeline(2.0, [Track("a", [PlacedClip(Clip("m", 1.0, [ch]), 0.5)])]) >>> evaluate_timeline(tl, 0.0) # not started: at rest, so absent {} >>> evaluate_timeline(tl, 1.0)[("a", "x")] # active 5.0 >>> evaluate_timeline(tl, 1.75)[("a", "x")] # ended: its end value holds 10.0
- an.timing.format_address(target, prop)[source]#
The address string of a compiled
(target, property)pair, unvalidated (the fast path for keying states;Address.of()validates).- Return type:
- an.timing.kind_from_spec(spec)[source]#
A kind instance from its contract spec (
{"kind": name, **params}), a bare kind name, or an instance (returned as is).- Return type:
>>> kind_from_spec("discrete") DiscreteKind(switch_at=0.5) >>> kind_from_spec({"kind": "points"}) Traceback (most recent call last): ... an.timing.kinds.FieldKindError: unknown field kind 'points'; known: ['number', 'angle', 'vector', 'quaternion', 'color', 'orbit', 'discrete']
- an.timing.kind_names(*, owner=None)[source]#
The registered kind names in registration order; only
owner’s when given.
- an.timing.merge_poses(*poses)[source]#
Merge multiple poses with override semantics (later wins per key).
Used by the timeline to combine concurrent clips on the same target.
>>> merge_poses({("a", "x"): 1.0}, {("a", "x"): 2.0, ("a", "y"): 3.0}) {('a', 'x'): 2.0, ('a', 'y'): 3.0}
- an.timing.parse_address(text)[source]#
textas anAddress; raisesAddressErrornaming the fault.- Return type:
>>> parse_address("charlie/left_arm:rotation").target 'charlie/left_arm' >>> parse_address("a::b") Traceback (most recent call last): ... an.timing.address.AddressError: 'a::b': an address has exactly one ':', found 2
- an.timing.register_easing(entry, *, replace=False, owner=None)[source]#
Add
entryto the registry (a genre’s own curves register here).Re-registering a name is refused unless
replace=True, and a replacement must carry a HIGHER version: a name’s meaning changing under a scene that uses it is exactly what entry versions exist to make visible, so it must be deliberate and visible.ownernames who registered it (core entries:CORE_OWNER); only core entries reach the contract files.- Return type:
- an.timing.register_kind(name, factory, *, replace=False, owner=None)[source]#
Register a field kind under
name(how a genre adds one).ownernames who registered it; onlyCORE_OWNER’s kinds reachan’s contract files.>>> sorted(kind_names())[:3] ['angle', 'color', 'discrete']
- an.timing.register_space(space, *, replace=False, owner=None)[source]#
Register
spaceunder its name (how an entity kind declares its properties).ownernames who registered it; onlyCORE_OWNER’s spaces reachan’s contract files, so an installed genre never edits the core contract.- Return type:
- an.timing.resolve_easing(spec)[source]#
The curve an easing spec names.
None-> linear;a registered name,
cubic-bezier(x1, y1, x2, y2)orsteps(n[, position]);a 4-sequence
[cx1, cy1, cx2, cy2]-> the legacy Bézier solver.
Raises
UnknownEasingError(aValueError) for an unknown name or a malformed sequence,TypeErrorfor any other type.
- an.timing.space_from_json(doc)[source]#
A space from its JSON form (what
PropertySpace.to_json()writes).- Return type:
- an.timing.space_names(*, owner=None)[source]#
The registered space names in registration order; only
owner’s when given.
- an.timing.timeline_from_compiled(doc)[source]#
The compiled document’s
timeline/animationsas an evaluableTimeline.docis the JSON mapping ofcompiled.schema.jsonor any object with the same attributes (the stage’sCutoutSceneJSON). This is the Python side of the parity contract:evaluate_timelineover what it returns is the executable specruntime.jsis tested against.target_rootand the blend ramps are carried although nothing reads them yet: a reader that quietly drops a field it was handed is a lossy “rebuilds the evaluable form”.- Return type:
>>> doc = {"timeline": {"duration": 1.0, "tracks": [{"clips": [ ... {"animation_id": "m", "start_time": 0.0}]}]}, ... "animations": {"m": {"duration": 1.0, "channels": [{"target": "a", ... "property": "x", "keyframes": [{"time": 0.0, "value": 0.0}, ... {"time": 1.0, "value": 4.0}]}]}}} >>> evaluate_timeline(timeline_from_compiled(doc), 0.25) {('a', 'x'): 1.0}
- an.timing.wrap_time(t, duration, loop_mode)#
Apply the loop mode to
t, returning a time within[0, duration].- Return type:
Modules
The address grammar: one way to name an animatable value, in every genre. |
|
Channel: keyframes for a single (target, property) pair, evaluated at time t. |
|
Clip: a named bundle of channels with a duration and loop mode. |
|
The timing kernel's cross-language contract: five JSON files, generated from the registries. |
|
The easing registry: every named timing curve, each with the solver that computes it. |
|
The authored flat timeline: |
|
Field kinds: the declared interpolator of each animatable property. |
|
Property spaces: what an entity kind's properties ARE, declared once. |
|
Timeline: tracks of placed clips with absolute times — the compiled evaluation form. |