cutan.characters.brows¶
Brow acting: where the brows can go, what may not draw there, and the face.brows capability.
An expression acts largely with the brows (raised in surprise, knitted in anger, tilted in sorrow). A drawing that sits where the brows go — a hat brim pulled down over the forehead — leaves them nothing to read against, and the expression falls to the lids, the gaze and the mouth (an#252). So:
The brows’ acting range (
brow_range()) is measured, not assumed: the factory’s brow drawing, posed by every shipped expression preset through the default expression binding (its travel and its signs), on every view that shows a brow, at the character’s head scale — in the offline head’s drawing units, so it can be compared with what the head draws.A hat is seated above that range (
seat_above_brows()): lifted, and if its crown would leave the drawing, flattened toward its crown, by one transform shared by every view so the hat does not change shape as the character turns. When even the flattest seat still overlaps — a small head under a tall hat — the overlap is not hidden: the factory records it in the descriptor’soccluded(a declared fact, written by whoever KNOWS the geometry, likecolour_roles).``face.brows`` is the capability the character analyser (
cutan.library) derives withbrow_affordance(): two brow slots with art on an overlay face, and nothing recorded over them. The cut-out genre’sexpressionaspect requires it for its full-face method (cutan.characters.methods).
Hair is not measured here: since cutan#61 the brows rest below the default
hairline (BROW_DROP; before, their outer ends
ran into the fringe and read as hair), so a hair style is held to the default
hairline instead (tests), never to the brows’ range.
Units: the offline head is drawn in an HEAD_ART_SIZE square; the rig
draws it REFERENCE_HEAD_HEIGHT × head_scale
view-box units tall, hung from the neck at
HEAD_ANCHOR. The face offsets scale with the head
(_scale_face), so a brow’s rest place in head units does not depend on the
head scale; its expression travel does (BROW_HEIGHT_TRAVEL is a view-box
length), which is why a small head’s brows reach higher up its forehead.
A surprised brow reaches about three head units above its rest, less on a bigger head; a band drawn across the forehead is lifted clear of it:
>>> round(BROW_REST_TOP, 1)
25.1
>>> [round(min(brow_range(head_scale=s)["front"].values()), 1) for s in (1.0, 1.7)]
[21.8, 22.6]
>>> seat_above_brows({"front": '<rect x="20" y="20" width="40" height="4"/>'}, head_scale=1.0)
Seat(transform='translate(0 -3.18)', covers=False)
Module Attributes
The offline head's drawing is this many units square (its |
|
its canvas (view-box units at head scale 1), the stroke, and how far each end tilts from the level. |
|
The face features a drawing can be recorded as covering ( |
|
Head units left between a hat's lowest ink and the brows' highest reach — about the outline's width, so a raised brow never touches the brim's line. |
|
How high a lifted hat's crown may go (head units from the drawing's top edge): above it the crown would be clipped by the head's canvas. |
|
The flattest a hat is drawn (its height kept, as a fraction) to seat it above the brows; past it the hat keeps this shape and the overlap is recorded. |
|
The factory's brow slots, and how far below the default face layout ( |
Functions
|
|
|
What covers the brows' acting range, or |
|
The slots the character's own expression binding moves on a brow axis. |
|
The path of the factory's brow on |
|
|
|
|
|
Seat a hat, drawn as |
|
How far a hat, drawn as |
Classes
|
Where a hat is drawn: |
- cutan.characters.brows.BROWS_FEATURE: str = 'brows'¶
The face features a drawing can be recorded as covering (
occludedkeys).
- cutan.characters.brows.BROW_CANVAS: tuple[int, int] = (80, 24)¶
its canvas (view-box units at head scale 1), the stroke, and how far each end tilts from the level.
- Type:
The factory’s brow drawing
- cutan.characters.brows.BROW_SLOTS: tuple[str, ...] = ('left_brow', 'right_brow')¶
The factory’s brow slots, and how far below the default face layout (
FACE_OFFSETS) the factory hangs them on its own head, in view_box units (cutan#61): about 2.5 of the head drawing’s 80 units, so their outer ends clear the hairline — the fringe ran into them, and a brow in the hair’s colour over the hair is a brow nobody sees — and they keep their gap above the eyes. Only new factory heads take it (a descriptor stores its offsets); a promoted or DiceBear head keeps the layout.
- cutan.characters.brows.HAT_BROW_CLEARANCE: float = 1.0¶
Head units left between a hat’s lowest ink and the brows’ highest reach — about the outline’s width, so a raised brow never touches the brim’s line.
- cutan.characters.brows.HAT_CROWN_MIN_Y: float = 0.5¶
How high a lifted hat’s crown may go (head units from the drawing’s top edge): above it the crown would be clipped by the head’s canvas.
- cutan.characters.brows.HAT_MIN_FLATTEN: float = 0.6¶
The flattest a hat is drawn (its height kept, as a fraction) to seat it above the brows; past it the hat keeps this shape and the overlap is recorded.
- cutan.characters.brows.HEAD_ART_SIZE: float = 80.0¶
The offline head’s drawing is this many units square (its
viewBox).
- class cutan.characters.brows.Seat(transform=None, covers=False)[source]¶
Bases:
objectWhere a hat is drawn:
transform(None: where it was drawn) and whether it still covers the brows’ range there (covers).
- cutan.characters.brows.brow_affordance(desc, drawn)[source]¶
face.brows’s params for a descriptor whose drawn slots aredrawn, orNone.Afforded when the face is an overlay (
face_overlay), the binding moves a brow (brow_slots()) and every slot it moves on a brow axis has art, and nothing covers them (brow_cover(): a declaredoccluded, or a factory hat measured over them, an#284). The slots are the solver’s own binding’s, so the capability and the solver cannot disagree about which slots act.>>> from cutan.characters.schema import CharacterDescriptor >>> d = CharacterDescriptor(name="c") >>> brow_affordance(d, {"left_brow": {"brow_l"}, "right_brow": {"brow_r"}}) {'slots': ['left_brow', 'right_brow']} >>> d.occluded = {"brows": "a helmet"} >>> brow_affordance(d, {"left_brow": {"brow_l"}, "right_brow": {"brow_r"}}) is None True
- cutan.characters.brows.brow_cover(desc)[source]¶
What covers the brows’ acting range, or
None(an#284).The descriptor’s declared
occludedentry (an illustrator’s override, for drawn art), else what is DERIVED from a factory head’s recorded knobs (cutan.characters.factory.derived_brow_cover(): its hat, as drawn, measured againstbrow_range()). One answer for the capability, the compiler’s record andan validate.
- cutan.characters.brows.brow_path_d(side)[source]¶
The path of the factory’s brow on
side(lorr) in its canvas.>>> brow_path_d("l") 'M 8 16 Q 40 4 72 8'- Return type:
- cutan.characters.brows.brow_range(*, head_scale=1.0, views=('front', 'three_quarter', 'side'), presets=None)[source]¶
{view: {column: top}}: the highest the brows’ ink reaches, per head-unit column.Every shipped expression preset (or
presets), at full intensity, posed through the default binding on each brow the view shows (its turnaround pose: shifted, narrowed, or hidden) — the region a cover must stay above for the presets to read. The range is bounded by the PRESETS, not by the axes’ full box: anaxes:override at the corner (height 1, a full tilt) or two summed spans can reach past it (review-278 L1).
- cutan.characters.brows.brow_slots(desc)[source]¶
The slots the character’s own expression binding moves on a brow axis.
The binding the solver uses (
binding_for(): the declaredexpression_binding, else the default one), so a rig whose brows live on slots of its own naming is read as having brows, and one whose declared binding moves no brow is read as having none. A binding that does not resolve moves nothing (an validatereports it).>>> from cutan.characters.schema import CharacterDescriptor >>> brow_slots(CharacterDescriptor(name="c")) ['left_brow', 'right_brow']
- cutan.characters.brows.ink_columns(discs)[source]¶
{column: (top, bottom)}of ink, per head-unit column (round(x)).>>> ink_columns([(10.2, 5.0, 0.0), (10.4, 7.0, 0.0)]) {10: (5.0, 7.0)}
- cutan.characters.brows.seat_above_brows(fragments, *, head_scale=1.0, clearance=1.0, crown_min_y=0.5, min_flatten=0.6)[source]¶
Seat a hat, drawn as
{view: svg fragment}in head units, above the brows’ range.No transform when it already clears them by
clearance. Else it is lifted — as far as its crown may go (crown_min_y) — and, if that is not enough, flattened toward its crown, never belowmin_flattenof its height; one transform for every view.coverssays the seat found still overlaps the range (it never claims a clearance it did not measure).- Return type:
- cutan.characters.brows.seat_overlap(fragments, seat, *, head_scale=1.0)[source]¶
How far a hat, drawn as
{view: svg fragment}and worn atseat(a transformseat_above_brows()wrote,None: where it is drawn), dips into the brows’ acting range: positive means it covers them (an#284).The inverse of
seat_above_brows()on its own transforms, so a character’s recorded seat is measured, not re-chosen.>>> seat_overlap({"front": ""}, None) -inf- Return type: