an.stage.text#
Words on screen: title cards, labels, and text you can animate word by word.
Epic #9 Wave 8, first slice; the design question was an#155. A text block is a
prop whose document kind is TextDescriptor — the shape stroked paths
took (an.stage.paths) — so a scene names it with an ordinary
AssetRef(kind="prop", ...), its overrides supply the per-shot string,
and nothing in the scene IR changed (no field, no migration).
>>> title = TextDescriptor(name="title", text="Words on screen", layer="overlay")
>>> title.kind, title.unit, title.layer
('TextDescriptor', 'word', 'overlay')
Typesetting is not done here. The sibling package tituli owns it —
shaping, real glyph metrics, wrapping, alignment, the title-safe area — and
emits a Layout of placed Run``s, one per unit. :func:`layout_text` asks it
for that layout and for each run's glyph contours (``tituli.run_outline), and
the compiler turns each unit into one node whose visual is an SVG sprite
converted at compile time. The runtime never learns what text is, so fonts
never reach the frame path and the determinism perimeter is unchanged.
Units are addressable, so text animates with ordinary tweens. A block of
unit="word" builds <id>/word_0, <id>/word_1, …; "glyph" and
"line" likewise. index counts DRAWN units in reading order (spaces are
not units). reveal_units() is a Python-side generator of ordinary actions
for a staggered reveal — a preset, not a new IR node (the general combinator is
an.ir.compose.stagger()):
>>> from an.ir.compose import flatten
>>> reveal = reveal_units("title", 3, "alpha", to=1.0, from_=0.0, duration=0.3, step=0.1)
>>> [(f.action.target, round(f.start, 3)) for a in reveal for f in flatten(a)
... if f.action.kind == "tween"]
[('title/word_0', 0.0), ('title/word_1', 0.1), ('title/word_2', 0.2)]
Two layers. layer="world" is in the scene and moves with the camera, like
any prop. layer="overlay" is drawn in a second top-level container the
camera never touches — a title card stays exactly where it is through a
push-in while an in-world label grows with the scene.
Fonts fail loudly and never depend on the machine. font=None is the
face Pillow embeds (Aileron, CC0 — “No Rights Reserved”), requested through
tituli.EMBEDDED so the installed fonts are never consulted: the same bytes
on every machine with the same Pillow, and no typeface is vendored in this
package. font otherwise names a font FILE (.ttf/.otf/.ttc),
absolute or relative to the text document’s directory in the props store;
a path that is not a file, or a file the typesetter could not use, RAISES
(TextFontError). A family NAME is refused on purpose: resolving one
would make the picture depend on what a machine has installed. A character
the face has no glyph for raises too — an is English-first and refuses
loudly rather than drawing a box. The face’s identity (sha256 of its bytes) is
recorded in the compiled document.
Module Attributes
Its own versioned document kind, registered from the module that owns the schema (the rule |
|
0.06 is 65 px at 1080p, and the same block reads the same at 720p and at 4K. |
|
Ink when the document names none — a near-black that reads on the default white background. |
|
the runtime indexes the scene's container as |
|
The unit that typesets the whole string as ONE node, |
|
The swap set a text block with |
Functions
|
The text block an entity draws: its stored document with |
|
What a relative |
|
What is wrong with WHERE this entity puts its block, or |
|
Set |
|
The node names a block builds — what |
|
Every string of a block's replacement set, set once: |
|
|
|
Tween |
Classes
|
The on-disk text schema, saved as a prop's |
|
A counter block's number-to-text rule (an#342): |
|
One addressable unit: its node name, its string, its box and its ink. |
|
A placed block: its units, its reference point, and the face that set it. |
|
Which face drew a block — by its bytes, not its name. |
Exceptions
A text block's font cannot be used: not a file, not a font, or not the face the typesetter actually used. |
|
The text cannot be set as asked (a glyph the face lacks, nothing to draw). |
|
A unit's outline is wide enough to cover its neighbour's glyphs. |
- an.stage.text.BLOCK_UNIT: str = 'block'#
The unit that typesets the whole string as ONE node,
block_0(an#341): what a replacement set swaps, since per-word units of different strings do not line up.
- class an.stage.text.Counter(**data)[source]#
Bases:
BaseModelA counter block’s number-to-text rule (an#342):
formatin the declared d3-format subset (an.formats), and the value shown before anyvalueaction (start;None= the first action’s value).>>> Counter(format="Day {d}", start=1).format 'Day {d}'
- an.stage.text.DFLT_TEXT_COLOUR: str = '#1a1a1a'#
Ink when the document names none — a near-black that reads on the default white background.
- an.stage.text.DFLT_TEXT_SIZE: float = 0.06#
0.06 is 65 px at 1080p, and the same block reads the same at 720p and at 4K.
- Type:
Type size as a FRACTION OF FRAME HEIGHT (tituli’s convention)
- class an.stage.text.FontIdentity(family, style, sha256, embedded, layout_engine='basic', features=())[source]#
Bases:
objectWhich face drew a block — by its bytes, not its name.
- features: tuple[str, ...] = ()#
a tag asked for and missing here was not in the font. Labelled only when there is one, so a block without features labels as before.
- Type:
The requested OpenType features this face applied (an#362)
- label()[source]#
"Aileron Regular (embedded) sha256:6985… layout:basic"— what the compiled document records.- Return type:
- layout_engine: str = 'basic'#
Pillow’s layout engine for this face (
basicorraqm). Recorded because it moves glyph ADVANCES: RAQM (HarfBuzz) applies kerning and ligatures and is used for a font file whenever libraqm can be loaded, so the same bytes can set differently on two machines. The embedded face is alwaysbasic.
- an.stage.text.RESERVED_TEXT_IDS: frozenset[str] = frozenset({'overlay', 'root'})#
the runtime indexes the scene’s container as
root(the camera’s target), and the overlay container is namedoverlay. An overlay block calledrootwould overwrite the camera’s node in the runtime’s index and take the push-in with it.- Type:
Entity ids a text block may not take
- an.stage.text.TEXT_DOCUMENT_KIND: DocumentKind = DocumentKind(name='TextDescriptor', version_field='schema_version', current_version='0.1.0')#
Its own versioned document kind, registered from the module that owns the schema (the rule
PathDescriptorandPropDescriptorfollow).
- class an.stage.text.TextDescriptor(**data)[source]#
Bases:
BaseModelThe on-disk text schema, saved as a prop’s
prop.json.extra="forbid"(thePathDescriptorprecedent): a text block is a precise instruction, and a misspeltcolourthat silently did nothing is the defect class this package refuses. Set-but-inert combinations raise too:>>> TextDescriptor(name="t", text="hi", anchor="top") Traceback (most recent call last): ... pydantic_core._pydantic_core.ValidationError: 1 validation error for TextDescriptor Value error, `anchor` places a block in the title-safe area of the FRAME, which only an overlay has; a world-layer block is placed with the entity's `stage.at` [type=value_error, input_value=..., input_type=dict] ...
- anchor: str | None#
one of tituli’s nine anchors (
"top","bottom-left","center", …) inside the title-safe area.Nonecentres the block on the node origin (the frame centre, orstage.at).- Type:
Overlay only
- property content: str#
its
start, else zero, until the compiler lowers itsvaluechannel).- Type:
The string the block draws at rest (a counter
- counter: Counter | None#
tween <id> value a→bandset <id> value vare lowered at compile to a replacement set of the strings the frames show. Requiresunit="block"; notext/texts.- Type:
A number that reads as text (an#342)
- features: list[str] | None#
["tnum"]sets tabular figures.None= the block’s default: acounterasks forCOUNTER_FEATURES, anything else for none. Whether the face has them is recorded with it (meta.fonts,features:).- Type:
OpenType features to apply, by tag (an#362)
- model_config: ClassVar[ConfigDict] = {'extra': 'forbid'}#
Configuration for the model, should be a dictionary conforming to [
ConfigDict][pydantic.config.ConfigDict].
- property rest_key: str | None#
The key a block with
textsshows at rest (rest, else the first key);Nonefor a single-string block.
- stroke_width: float#
An outline around every glyph (an#313, OverSimplified’s white labels edged in black): its visible thickness OUTSIDE the glyph, in scene pixels, drawn under the fill in the same texture, so a per-unit reveal shows outline and fill together.
0= none.
- text: str | None#
The words. Explicit newlines break lines;
max_widthwraps. Exactly one oftextandtextsis given.
- texts: dict[str, str] | None#
{key: string}, every string one drawing of the block, swapped byset <id> text <key>— replacement animation applied to text. Keys obey the swap-key rules (no/, no::). Requiresunit="block".- Type:
A replacement set (an#341)
- tracking: float#
tituli sets a tracked string one run per glyph, so a word unit would not exist.
- Type:
Extra advance per glyph, in em. Only with
unit="glyph"
- property typeset_features: tuple[str, ...]#
its own, else a counter’s tabular figures (an#362), so a number does not change width as it counts.
>>> TextDescriptor(name="n", counter={"format": "{d}"}, unit="block").typeset_features ('tnum',) >>> TextDescriptor(name="t", text="x").typeset_features ()
- Type:
The features the block is set with
- exception an.stage.text.TextFontError[source]#
Bases:
ValueErrorA text block’s font cannot be used: not a file, not a font, or not the face the typesetter actually used. Raised instead of falling back, because a fallback face is a different picture wearing the right one’s clothes.
- class an.stage.text.TextLayout(units, origin, font, bounds=(0.0, 0.0, 0.0, 0.0))[source]#
Bases:
objectA placed block: its units, its reference point, and the face that set it.
originis the block’s reference point in frame pixels — the frame centre, or its title-safe anchor position — which is where the block’s own node sits; each unit’s node is placed relative to it.
- exception an.stage.text.TextLayoutError[source]#
Bases:
ValueErrorThe text cannot be set as asked (a glyph the face lacks, nothing to draw).
- exception an.stage.text.TextOutlineWarning[source]#
Bases:
UserWarningA unit’s outline is wide enough to cover its neighbour’s glyphs.
- class an.stage.text.TextUnit(name, text, box, d)[source]#
Bases:
objectOne addressable unit: its node name, its string, its box and its ink.
boxis(x0, y0, x1, y1)in frame pixels, snapped OUTWARD to whole pixels and containing both the layout box and the ink, so the sprite’s corners sit on the pixel grid and nothing is clipped.dis the unit’s glyph contours as SVG path data in frame pixels.
- an.stage.text.font_base_dir(props_store, ref)[source]#
What a relative
fontpath resolves against: the text document’s own directory in an on-disk props store —Nonefor an in-memory one, where a relative path then RAISES rather than resolving against the working directory (which would make the picture depend on where you ran it).
- an.stage.text.layout_text(desc, *, width, height, base_dir=None)[source]#
Set
descon awidthxheightframe and take each unit’s contours.base_diris what a relativefontpath resolves against — the text document’s own directory in the props store. A block withtextsis set with its rest string (layout_text_set()sets every one).- Return type:
>>> lay = layout_text(TextDescriptor(name="t", text="Hello big world"), width=1920, height=1080) >>> [u.name for u in lay.units] ['word_0', 'word_1', 'word_2'] >>> lay.origin (960.0, 540.0) >>> lay.font.family, lay.font.embedded ('Aileron', True) >>> [u.name for u in layout_text(TextDescriptor(name="t", text="two\nlines", unit="block"), width=1920, height=1080).units] ['block_0']
- an.stage.text.layout_text_set(desc, *, width, height, base_dir=None)[source]#
Every string of a block’s replacement set, set once:
{key: layout}.Each is set exactly as a single-string block would be, so each is centred (or anchored) on its own; lining their
alignedges up is the builder’s job (an.stage.text_layout.build_text_subtree()). A block with notextsreturns{}.- Return type:
>>> lays = layout_text_set(TextDescriptor(name="t", texts={"a": "1", "b": "22"}, unit="block"), width=320, height=240) >>> sorted(lays), [lay.units[0].name for lay in lays.values()] (['a', 'b'], ['block_0', 'block_0'])
- an.stage.text.resolve_text(document, overrides=None)[source]#
The text block an entity draws: its stored document with
overrideson top.Validated strictly AFTER the merge, so a stored document may be a reusable style with no
textof its own, and each entity supplies the words:- Return type:
>>> style = {"kind": "TextDescriptor", "name": "label", "size": 0.04} >>> resolve_text(style, {"text": "Paris"}).text 'Paris'
- an.stage.text.reveal_units(entity_id, count, property, *, to, from_, duration, step, start=0.0, unit='word', easing='ease_out')[source]#
Tween
propertyfromfrom_totoon units0..count-1of a text block, eachstepseconds after the last — a word-by-word (or letter-by-letter) reveal. Unitistarts atstart + i*step.A unit whose tween starts later also gets a
settofrom_at 0, which HOLDS until its tween begins (the compiler’s set-hold rule) — without it the word would show its built value until its turn and then snap tofrom_. That is also whyfrom_is required.Returns a LIST of top-level actions —
set, a baretween, or thesequence(delay(start), tween)wrapper thescene.mdparser itself produces for astart:key — soshot.actions.extend(stagger(...))round-trips throughscene.mdin its short form, one entry per unit.Named
staggeruntil an#241 gave the core a general combinator of that name (an.ir.compose.stagger(), any actions, oneparallel);an.stage.text.staggerstays as an alias of this function so old imports keep working.- Return type:
>>> [a.kind for a in reveal_units("t", 2, "alpha", to=1, from_=0, duration=0.2, step=0.1)] ['tween', 'set', 'sequence']
- an.stage.text.text_entity_problem(entity, desc)[source]#
What is wrong with WHERE this entity puts its block, or
None.The one statement of the placement rules, called by the compiler (which raises) and by
an validate(which reports) so the two agree:a reserved id (
RESERVED_TEXT_IDS) —rootwould hijack the camera’s node in the runtime’s index;anchorandstage.attogether — two answers to one question.