tituli.layout¶
The layout model: placed runs of text, and the engines that place them.
A Layout is the single intermediate every use case produces and the
renderer consumes — a title card, a credits crawl and a calligram are all just
different ways of filling one. A Run is the unit: a string, a baseline
origin in frame pixels, an angle, a resolved face and a colour. A block of prose
is a few runs (one per line); a calligram is one run per glyph. That is the whole
unification — the humble cases are the degenerate calligram, exactly as hoped.
Real font metrics throughout (Face.length), never an average-character
estimate.
>>> from tituli.frame import Frame
>>> from tituli.style import TextStyle
>>> lay = block("Hello\nWorld", TextStyle(size=0.05), Frame.blank((1920, 1080)))
>>> len(lay.runs) # one run per line (a tracked style would give one per glyph)
2
>>> lay.bbox().width > 0
True
Functions
|
Set |
|
Lay out prose as lines from the top-left corner |
|
Pour prose into a silhouette, row by row — the concrete-poem 'shape' case. |
|
Shrink |
|
Upright glyphs stepping down slanted streaks — Apollinaire's Il pleut. |
|
Width in pixels of |
|
Concatenate layouts. |
|
Greedy word wrap on measured widths. |
Classes
|
Everything the renderer needs: runs on top of plates, in frame pixels. |
|
A backdrop drawn under the runs: a scrim, a box, a rule. |
|
A string drawn from a baseline origin, possibly rotated. |
- class tituli.layout.Layout(runs=(), plates=(), meta=<factory>)[source]¶
Bases:
objectEverything the renderer needs: runs on top of plates, in frame pixels.
- class tituli.layout.Plate(box, color, kind='box', radius=0.0, solid=(0.5, 0.5))[source]¶
Bases:
objectA backdrop drawn under the runs: a scrim, a box, a rule.
- class tituli.layout.Run(text, x, y, face, color, angle=0.0, opacity=1.0, tracking=0.0, unit='line', index=0, t_in=None, t_full=None, tags=())[source]¶
Bases:
objectA string drawn from a baseline origin, possibly rotated.
- tituli.layout.along_path(text, path, style, frame, *, start=0.0, align='start', offset=0.0, upright=False, color=None, tags=())[source]¶
Set
textglyph by glyph alongpath(the path is the baseline).Each glyph’s origin is the point at its arc length and it is rotated to the tangent there — unless
upright(a calligram convention: letters stay vertical while their positions follow the shape).offsetshifts the baseline perpendicular to the path (negative = above, in screen terms).alignpositions the whole string on the path fromstart.Text longer than the path keeps its spacing and runs off the end — that is reported in
meta["overflow"]rather than silently squeezed.- Return type:
- tituli.layout.block(text, style, frame, *, max_width=None, x=0.0, y=0.0, color=None, unit='line', tags=(), break_at=None)[source]¶
Lay out prose as lines from the top-left corner
(x, y).textis a string (wrapped tomax_widthwhen given) or pre-broken lines. Alignment followsstyle.alignwithinmax_width(or the widest line when no width is given).unit="glyph"emits one run per character (needed for tracking and for per-glyph reveals).break_at(e.g." · ") wraps a list only between its items — seewrap().- Return type:
- tituli.layout.fill_shape(text, mask, style, frame, *, box, threshold=128, min_span=0.5, color=None, tags=(), repeat=True)[source]¶
Pour prose into a silhouette, row by row — the concrete-poem ‘shape’ case.
maskis any Pillow image (or path): dark pixels are outside, light pixels are the shape (invert your mask if it is the other way round). It is fitted intobox; for each line of text height the horizontal spans of the shape are found and filled with as many words as fit, in reading order. Withrepeatthe text cycles until the shape is full; otherwise leftover spans stay empty and leftover words are reported inmeta["unplaced"].- Return type:
Layout
- tituli.layout.fit_size(text, style, frame_height, *, max_width, max_height=None, min_size=0.012, step=0.9, break_at=None)[source]¶
Shrink
style.sizeuntiltextwraps within the given bounds.Shrinks geometrically by
stepand never belowmin_size(a fraction of frame height, likesizeitself).- Return type:
- tituli.layout.glyph_columns(lines, style, frame, *, box, slants, head_offsets, word_gap_slots=1, size_ratio=0.86, color=None, tags=())[source]¶
Upright glyphs stepping down slanted streaks — Apollinaire’s Il pleut.
- Return type:
Line
kbecomes a streak: glyphisits at ``(u, v) = (head_offsets[k]i * slants[k], i)`` on an abstract slot grid, fitted to
boxin one step, so the shape is aspect-independent. Letters stay upright. This is the solver muvid’scalligramarchetype introduced, generalised over real glyph metrics and any frame — the streaks arePathlines, which is how it stays one preset ofalong_path()rather than a second family.
slantsandhead_offsetsare cycled if shorter thanlines.
- tituli.layout.measure(text, style, frame_height)[source]¶
Width in pixels of
textset instyleon a frame of that height.- Return type:
- tituli.layout.wrap(text, style, frame_height, *, max_width, break_at=None)[source]¶
Greedy word wrap on measured widths. Explicit newlines are honoured.
break_atmakes a list wrap only between its items: with" · ","バス bus · スープ soup"never ends a line onバス. An item wider than the whole line falls back to word wrapping inside that item (it is never cut). A no-break space (U+00A0) binds two words in any mode.>>> from tituli.style import CAPTION >>> lines = wrap("one two three four five six", CAPTION, 1080, max_width=300) >>> len(lines) >= 2 and all(measure(l, CAPTION, 1080) <= 300 for l in lines) True