Skip to content

Background

A new layer type, distinct from Text Foundation's per-layer props.background padding fix — that fix is about a text layer's own paint-time pill; this is a standalone layer that derives its box from other layers.

background added to the layerSchema discriminated union:

{
  type: "background",
  layout: {
    mode: "derived",                       // the fourth layout mode; background is its only user
    "16:9": Derived, "9:16": Derived, "1:1": Derived,
  },
  props: { fill, opacity?, radius?, padding?, stroke? },   // paint only
}

type Derived = {
  of: LayerId[],                           // the members whose union is this layer's box
  ofBounds: "content" | "extents",         // which of the members' bounds the union takes
}

The relationship lives in layout, not props, for the same reason pinTo and flowIn do: it's what positions the layer, and like them it's per-ratio — a layer hidden in portrait drops out of the portrait of list without touching landscape (Positioning). props keeps only paint. layout.mode: "derived" is what keeps "every layer has a layout.mode" true for the validator and the Builder without pretending a background has a box of its own to author (Positioning); validate.ts rejects derived on any other layer type. Its box is derived (never authored) from the union of its members' ofBounds boxes, plus its own padding and radius — that padding is the background layer's own paint-time inset around its derived box (consistent with the padding-is-paint-only invariant Text Foundation establishes, applied to this layer type too); member layers' own text-background padding never contributes to the union.

Which bounds the union derives from is itself authored

ofBounds picks which of the members' bounds (Coordinate System & Sizing) the union takes — never raster, which is internal-only, the same restriction pinToBounds has. extents is the sensible default, so a background sitting behind its members clears their painted stroke/shadow rather than just their tight ink, but content, hugging only the ink, is a real, wanted case too, not an edge case to rule out.

background declares which layers it references, the opposite direction from pin/flow, where the member declares what it's attached to — not an inconsistency, the same rule applied from the other side: whichever side's own layout depends on the other is the side that declares the reference. A pinned/flowed layer's own position depends on its target/container, so it declares the reference; a background's own box depends on its members, and its members' layout is untouched by the background, so the background declares the reference instead.

of is always a LayerId[], permanently, even once flowBox (Positioning) exists — there is no separate groupId variant, ever. Referencing a flowBox's members as a group is just referencing the flowBox's own id through this same list, since flowBox is a real layer with its own id and resolved box.

Re-derivation is not solve-only. A background reads its members' current nodes from the motion graph every frame — solve alone can't be the final answer for a spanning background any more than it can for an animating pin target or flow member, so a background whose members are themselves animated re-derives every frame in animate, tracking their motion, not just once at authoring-time move/resize.

Validation: an of entry naming a layer id that doesn't exist in the same container fails validation with the same friendly message a dangling pinTo gets — of is an edge in the motion graph like pinTo and flowIn, and it goes through the same reference, cycle, and container-local checks. An empty of list fails validation too, and so does an of list whose members carry no intrinsic size between them (a background spanning only an empty flowBox), the same empty-container rule every container gets — so a mutation stripping a background down to nothing it can derive a box from is rejected where it's proposed. The renderer keeps one defensive fallback for a document that reaches it anyway: derive from whatever members it can find, render nothing if none.

Done when

The spanning-background hard case renders and re-derives when a member moves.