# Layout & text

> Boxes, flex layout, shapes, fonts, GPU text, rich text, measurement, colors, gradients and track mattes.

## Boxes and flex

```ts
import { Layer } from "framefields";

Layer.flex({
  width: 1920, height: 1080,
  dir: "column",            // "row" | "column"
  justify: "center",        // start | center | end | space-between | space-around
  align: "center",          // start | center | end | stretch
  gap: 24, padding: 80,
  wrap: false,
  background: "#0d0f12",
  children: [
    Layer.box({ padding: 16, background: "#1e232d", borderRadius: 20, children: [/* … */] }),
  ],
});
```

| Prop | Notes |
| --- | --- |
| `width`, `height` | px, `"auto"` (intrinsic), `"fit"` (shrink-wrap) or `"fill"` (parent extent) |
| `position: "absolute"` + `x`, `y` | take the layer out of flow; `x`/`y` alone also make it absolute |
| `background` | a color or a CSS gradient string |
| `borderRadius`, `borderColor`, `borderWidth` | no CSS `border` shorthand |
| `overflow: "hidden"` | clip children to the box |
| `opacity`, `scale`, `rotation` | static or animated |
| `startFrame`, `durationFrames` | when the layer is on screen |
| `id` | stable name; used by lint/QA messages and chart children |

## Shapes

```ts
Layer.shape("circle", { width: 200, height: 200, fill: "#ff5a1f" });
Layer.shape("star", { width: 240, height: 240, fill: "radial-gradient(#fff, #ffb800)" });
Layer.shape("path", { d: "M0 0 L200 0 A100 100 0 0 1 0 0Z", strokeColor: "#fff", strokeWidth: 4, trimEnd: 0 })
  .animate(LayerAnimation.create().fromTo("trimEnd", 0, 1, { start: 0, end: 30 })); // draw-on
```

Types: `rect`, `circle`, `ellipse`, `polygon`, `star`, `arrow`, `path`. Animate `trimStart` / `trimEnd` / `trimOffset` for stroke draw-ons.

## Fonts

There are no system fonts in headless WebGPU. Register every family before building any text layer.

```ts
import { FontManager } from "framefields";

await FontManager.register({ family: "Inter", source: "assets/fonts/Inter.ttf" });
await FontManager.register({ family: "Inter", source: "assets/fonts/Inter-Bold.ttf", weight: 700 });
await FontManager.registerEmojiFont("assets/fonts/NotoColorEmoji.ttf"); // emoji need a CBDT/CBLC color font
```

## Text

```ts
Layer.text("PRECISION MOTION", {
  fontFamily: "Inter",
  fontSize: 96,
  fontWeight: 700,
  fill: "#ffffff",
  letterSpacing: 2,
  lineHeight: 104,
  width: 1400,          // wrap width; set it or text may clip
  align: "center",      // "start" | "center" | "end" (follows text direction)
  verticalAlign: "middle", // "top" | "middle" | "bottom" inside the box height
});
```

Text is drawn by the Slug algorithm: glyph curves are evaluated per pixel in the shader, so it stays sharp at any size and under 3D camera moves. Arabic, Hebrew and Persian shape right-to-left automatically (`direction: "auto"`; force with `"rtl"` / `"ltr"`).

### Rich text

```ts
import { Layer, bold, color, mark, rich } from "framefields";

Layer.text(
  rich`Scale ${bold("10x", { fill: "#6366f1" })} faster with ${mark("120 FPS", { bg: "#10b98133" })}`,
  { fontFamily: "Inter", fontSize: 56, fill: "#f8fafc" },
);
```

Span helpers: `span`, `bold`, `italic`, `color`, `mark`, `size`, `sub`, `sup`. A span takes `fontFamily`, `fontSize`, `fontWeight`, `fill`, `letterSpacing`, `baselineShift`, `opacity`.

### Measure with the real font

```ts
const m = FontManager.measure("Hello, world", { fontFamily: "Inter", fontSize: 64 });
m.width; m.height; m.lines; m.caretX(5); m.glyphs[0].x; // px, relative to the text box
```

Use it to place carets, underline a word, or size a pill to its label instead of guessing.

## Color helpers

Every helper returns a hex string.

```ts
import { Color } from "framefields";

Color.oklch(0.72, 0.16, 250);           // equal L = equal perceived brightness
Color.mix("#ff5a1f", "#1f6bff", 0.5);   // perceptual (OKLab) mix
Color.ramp(["#3b82f6", "#8b5cf6", "#ec4899"])(0.3);
Color.steps(["#0e0d0c", "#ff5a1f"], 5); // 5 swatches
Color.lighten(c, 0.1); Color.darken(c, 0.1); Color.alpha("#000", 0.4);
Color.contrast(fg, bg);                 // WCAG ratio: ≥ 4.5 body, ≥ 3 large
Color.readableOn(bg);                   // black or white, whichever reads
```

## Gradients

CSS strings, usable in any `background` and any shape `fill`. Linear, radial and conic, up to 8 stops, hard stops allowed.

```ts
import { Gradient } from "framefields";

Layer.box({ background: "linear-gradient(135deg, #ff5a1f, #ffb800 60%, #7c3aed)" });
Layer.shape("circle", { fill: Gradient.radial(["#fff", "transparent"]) });
Layer.box({ background: Gradient.conic(["red", "yellow", "lime", "aqua", "blue", "magenta", "red"]) });
```

A gradient that doesn't parse draws nothing; `comp.lint()` reports it as `invalid_gradient`.

## Track mattes

`node.withMatte(matte, mode)` shows `node` only where the matte is opaque (`"alpha"`, default), transparent (`"alpha-inverted"`), bright (`"luma"`) or dark (`"luma-inverted"`). The matte is laid out like a sibling and is never drawn itself.

```ts
// Text revealed by a sliding bar
Layer.text("LAUNCH", { position: "absolute", x: 560, y: 480, width: 800, height: 120, fontSize: 120 })
  .withMatte(
    Layer.box({ position: "absolute", x: 560, y: 480, width: 800, height: 120, background: "#fff" })
      .animate(LayerAnimation.keys("x", [[0, -240], [24, 560, "expo.out"]])),
  );

// Video inside type
Layer.video("assets/footage/surf.mp4", { position: "absolute", x: 0, y: 0, width: 1920, height: 1080, fit: "cover" })
  .withMatte(Layer.text("OCEAN", { position: "absolute", x: 0, y: 340, width: 1920, fontSize: 380, align: "center", fill: "#fff" }));
```
