an.stage.paths#
Stroked paths: routes, invasion arrows, borders, timelines, connectors.
One drawable covers the whole map-and-infographic motif family (epic #9, Wave 9; an#160): a stroke along a polyline or a chain of cubic Béziers, with an animatable trim — the visible span, as fractions of arc length — and an optional arrowhead that rides the moving tip, oriented along the path.
>>> arrow = PathDescriptor(
... name="route",
... points=[(-300.0, 0.0), (0.0, 0.0), (0.0, 200.0)],
... arrowhead=True,
... )
>>> arrow.kind, arrow.curve, arrow.trim_end
('PathDescriptor', 'polyline', 1.0)
>>> arrow.head_length_px, arrow.head_width_px # defaults scale with the stroke
(28.0, 24.0)
Where it lives, and why there. A path is a prop — a drawable that is not a
person — so its document sits in the props store beside
an.stage.props.PropDescriptor and a scene names it with an ordinary
AssetRef(kind="prop", ...). The compiler dispatches on the document’s
kind. That keeps the scene IR unchanged (no field, no migration) and gives
a path stage placement (at, scale) and entity ordering for free.
Geometry is often per-shot (the same arrow style, a different route), so an
entity’s overrides are merged over the stored document and the result is
validated strictly — resolve_path() is the one place that happens,
and both the compiler and an validate call it, so their verdicts agree.
Trim is an ordinary property. trim_start / trim_end are numeric
node properties in an.base.TRANSFORM_PROPERTIES, animated by ordinary
set/tween actions and flattened to the canonical timeline like
alpha: tween route trim_end 0 -> 1 is a draw-on. The fields below are
only the values the path shows before anything animates it.
Why a separate document rather than a PropDescriptor field: a
PropDescriptor is a rig (bones, slots, skins, swap sets) whose art is
SVG; a path has none of those, and its colour is decided by the compiler
(which is what will let a an.styles.StylePack reach it), not by art.
Module Attributes
Its own versioned document kind, registered from the module that owns the schema — the rule |
|
The stroke colour when the document names none. |
|
The shortest dash period (dash + gap), scene pixels. |
Functions
|
The path an entity draws: its stored document with |
|
The polyline the compiler puts on the wire for entity |
|
A draw-on whose tip reaches each authored point at its own time (an#161). |
Classes
|
The on-disk path schema, saved as a prop's |
- an.stage.paths.DFLT_STROKE_COLOUR: str = '#c0392b'#
The stroke colour when the document names none. A
StylePack’sstrokerole replaces it (an#161) — but only this default: a document that setscoloritself is art, not a default, and a pack does not rewrite art (the same linean.stylesdraws for SVG). A per-entitystrokeoverride in the pack wins over both.
- an.stage.paths.MIN_DASH_PERIOD: float = 1.0#
The shortest dash period (dash + gap), scene pixels. Bounds the number of dashes a path can ask the runtime to redraw every frame: a path a few thousand pixels long is a few thousand dashes at most.
- an.stage.paths.PATH_DOCUMENT_KIND: DocumentKind = DocumentKind(name='PathDescriptor', version_field='schema_version', current_version='0.1.0')#
Its own versioned document kind, registered from the module that owns the schema — the rule
PropDescriptorandCharacterDescriptorfollow.
- class an.stage.paths.PathDescriptor(**data)[source]#
Bases:
BaseModelThe on-disk path schema, saved as a prop’s
prop.json.extra="forbid", unlike the store documents around it: a path is a precise drawing instruction, and a misspelttrim_endsthat silently did nothing is the defect class this package refuses (thePlaneprecedent, an#110).curve="cubic"readspointsas chained cubic Béziers,p0 c1 c2 p1 c1 c2 p2 ...—3n + 1points, the SVGCcommand chained:>>> PathDescriptor(name="s", curve="cubic", points=[(0, 0), (1, 1), (2, 1), (3, 0)]).curve 'cubic' >>> PathDescriptor(name="s", curve="cubic", points=[(0, 0), (1, 1), (3, 0)]) Traceback (most recent call last): ... pydantic_core._pydantic_core.ValidationError: 1 validation error for PathDescriptor Value error, a cubic path takes 3n + 1 points (p0, then c1 c2 p per segment); got 3 [type=value_error, input_value=..., input_type=dict] ...
- closed: bool#
the path returns to its first point (a straight closing leg is added when the last point is elsewhere) and the stroke joins there instead of ending in two caps. Trim still runs from the first point round to it again.
- Type:
A closed shape (an#161)
- dash: float | None#
dashon,gapoff, repeating along the path from ITS start — anchored to the path, not to the trimmed span, so a draw-on reveals dashes in place instead of making them crawl.gapdefaults todash.None= a solid stroke.- Type:
A dash pattern, scene pixels
- dash_offset: float#
Shifts the pattern along the path (positive = forward). An ordinary numeric node property like
trim_end, sotween route dash_offsetis the “marching ants” route; only a dashed path has one.
- fill: str | None#
The region a closed path encloses,
#rrggbb;None= unfilled. Drawn under the stroke and NOT trimmed: a draw-on draws the border and the fill is there throughout. To fade a region in separately, make it its own entity (width: 0, filled) and tween that node’salpha.
- model_config: ClassVar[ConfigDict] = {'extra': 'forbid'}#
Configuration for the model, should be a dictionary conforming to [
ConfigDict][pydantic.config.ConfigDict].
- sampling: Literal['parameter', 'arclength']#
"parameter"(uniform in the curve’s parameter, the default) or"arclength"(evenly along it).- Type:
How a cubic’s samples are spaced (an#161)
- tail_arrowhead: bool#
An arrowhead at the START too, pointing back along the path (an#161): with
arrowhead, a double-headed arrow. Same size as the end’s.
- trim_start: float#
The visible span before anything animates it, as fractions of arc length.
trim_end=0starts a draw-on hidden, and a trim tween with nofrom_valuestarts from these values (not the global rest).
- width: float#
Stroke width, scene px.
0= no stroke, only for a ``fill``ed shape (a region without a border).
- width_profile: list[tuple[float, float]] | None#
[[t, factor], ...]along the WHOLE path’s arc length (tfrom 0 to 1, increasing;factortimeswidth, linear between stops), so[[0, 1], [1, 0]]tapers to a point and a trim never makes the width crawl. The stroke becomes a filled shape: butt ends, mitred corners (nocap/join).- Type:
A variable width (an#161)
- wobble: float#
the stroke wanders up to this many scene px either side of its line, by seeded smooth noise applied at compile (
an.stage.path_wobble), its ends left where they are.0= a ruled line.- Type:
A hand-drawn wobble (an#161)
- an.stage.paths.draw_on_through(entity_id, path, arrivals, *, start=0.0, easing='ease_in_out')[source]#
A draw-on whose tip reaches each authored point at its own time (an#161).
arrivals[k]is when the tip reaches authored pointk + 1(the tip is at point 0, hidden, atstart): a route that reaches each city on a beat, or slows into the last turn. Onetweenoftrim_endper leg, from the arc fraction of one point to the next on the polyline the compiler draws (drawn_polyline(), wobble included, so the tip is ON the point), each eased byeasing, preceded by asetoftrim_endto 0 at 0.arrivalsare absolute shot times, increasing.Returns a list of top-level actions for
shot.actions.extend(...), asan.stage.text.reveal_units()does.- Return type:
>>> acts = draw_on_through("r", {"kind": "PathDescriptor", "name": "r", ... "points": [[0, 0], [30, 0], [30, 10]]}, [1.0, 3.0]) >>> [(a.kind, getattr(a, "to_value", getattr(a, "value", None))) for a in acts] [('set', 0.0), ('tween', 0.75), ('sequence', None)]
- an.stage.paths.drawn_polyline(desc, entity_id)[source]#
The polyline the compiler puts on the wire for entity
entity_iddrawingdesc— flattened, closed, wobbled (the wobble is seeded by the entity) — and the index in it of each AUTHORED on-path point: every point of a polyline,p0 p1 p2 ...of a cubic chain (not its controls), and the closing return to the first point whenclosedadded one.>>> pts, anchors = drawn_polyline(PathDescriptor(name="r", points=[(0, 0), (10, 0), (10, 10)], closed=True), "r") >>> pts[anchors[-1]], anchors ((0.0, 0.0), [0, 1, 2, 3])
- an.stage.paths.resolve_path(document, overrides=None)[source]#
The path an entity draws: its stored document with
overrideson top.Validated strictly after the merge, so an override key the schema does not know raises instead of vanishing (an environment override silently drops unknown keys; a path’s does not).
- Return type:
>>> doc = {"kind": "PathDescriptor", "name": "a", "points": [[0, 0], [10, 0]]} >>> resolve_path(doc, {"points": [[0, 0], [0, 50]]}).points [(0.0, 0.0), (0.0, 50.0)] >>> try: ... resolve_path(doc, {"colour": "#000000"}) ... except ValueError as e: ... print(str(e).split("Value error, ")[1].split(". The fields")[0]) unknown PathDescriptor field(s): 'colour' (did you mean 'color', 'curve', 'source'?)