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’s occluded (a declared fact, written by whoever KNOWS the geometry, like colour_roles).

  • ``face.brows`` is the capability the character analyser (cutan.library) derives with brow_affordance(): two brow slots with art on an overlay face, and nothing recorded over them. The cut-out genre’s expression aspect 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

HEAD_ART_SIZE

The offline head's drawing is this many units square (its viewBox).

BROW_CANVAS

its canvas (view-box units at head scale 1), the stroke, and how far each end tilts from the level.

BROWS_FEATURE

The face features a drawing can be recorded as covering (occluded keys).

HAT_BROW_CLEARANCE

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.

HAT_CROWN_MIN_Y

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.

HAT_MIN_FLATTEN

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.

BROW_SLOTS

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.

Functions

brow_affordance(desc, drawn)

face.brows's params for a descriptor whose drawn slots are drawn, or None.

brow_cover(desc)

What covers the brows' acting range, or None (an#284).

brow_slots(desc)

The slots the character's own expression binding moves on a brow axis.

brow_path_d(side)

The path of the factory's brow on side (l or r) in its canvas.

brow_range(*[, head_scale, views, presets])

{view: {column: top}}: the highest the brows' ink reaches, per head-unit column.

ink_columns(discs)

{column: (top, bottom)} of ink, per head-unit column (round(x)).

seat_above_brows(fragments, *[, head_scale, ...])

Seat a hat, drawn as {view: svg fragment} in head units, above the brows' range.

seat_overlap(fragments, seat, *[, head_scale])

How far a hat, drawn as {view: svg fragment} and worn at seat (a transform seat_above_brows() wrote, None: where it is drawn), dips into the brows' acting range: positive means it covers them (an#284).

Classes

Seat([transform, covers])

Where a hat is drawn: transform (None: where it was drawn) and whether it still covers the brows' range there (covers).

cutan.characters.brows.BROWS_FEATURE: str = 'brows'

The face features a drawing can be recorded as covering (occluded keys).

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: object

Where 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 are drawn, or None.

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 declared occluded, 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
Return type:

dict[str, Any] | None

cutan.characters.brows.brow_cover(desc)[source]

What covers the brows’ acting range, or None (an#284).

The descriptor’s declared occluded entry (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 against brow_range()). One answer for the capability, the compiler’s record and an validate.

Return type:

str | None

cutan.characters.brows.brow_path_d(side)[source]

The path of the factory’s brow on side (l or r) in its canvas.

>>> brow_path_d("l")
'M 8 16 Q 40 4 72 8'
Return type:

str

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: an axes: override at the corner (height 1, a full tilt) or two summed spans can reach past it (review-278 L1).

Return type:

dict[str, dict[int, float]]

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 declared expression_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 validate reports it).

>>> from cutan.characters.schema import CharacterDescriptor
>>> brow_slots(CharacterDescriptor(name="c"))
['left_brow', 'right_brow']
Return type:

list[str]

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)}
Return type:

dict[int, tuple[float, float]]

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 below min_flatten of its height; one transform for every view. covers says the seat found still overlaps the range (it never claims a clearance it did not measure).

Return type:

Seat

cutan.characters.brows.seat_overlap(fragments, seat, *, head_scale=1.0)[source]

How far a hat, drawn as {view: svg fragment} and worn at seat (a transform seat_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:

float