Shape library
Catalog of 35 named shapes + morphing contract for Material 3 Expressive parity. Sibling of `shape.kmd` (radius scale) — this spec covers polygon shapes (Pill, Cookie, Burst, Flower, etc.) and the interpolation contract that animates between two shapes via spring physics (`motion.kmd` R9). Source-of-truth for shape paths: androidx.graphics.shapes (vendored per-surface).
Spec — Shape library
Facet *isual*do Koder Design. Material parity: https://m3.material.io/styles/shape. Source of polygon paths: androidx.graphics.shapes.
Companion to shape.kmd:
shape.kmddefines the *adius scale*(xssmmdlgxl/full) boundto corners of rectangular surfaces (cards, buttons, sheets).
shape-library.kmddefines the *5 named polygon shapes*usedfor non-rectangular surfaces (loading indicators, FAB morphs, Hero carousels, expressive decorations).
Both coexist: a Card uses shape.kmd radius tokens; a Loading indicator (#065) uses shape-library.kmd polygon shapes.
R1 — Catalog (35 shapes)
| # | Name | Vertices | Tier | Canonical use |
|---|---|---|---|---|
| 1 | Pill | — (capsule) | baseline | Buttons, chips, FAB |
| 2 | Square | 4 | baseline | Cards, surfaces |
| 3 | Rounded-square | 4 (squircle) | baseline | Icon containers, FAB at rest |
| 4 | Circle | — | baseline | Avatars, FABs, loading dots |
| 5 | Oval | — | baseline | Hero carousel items |
| 6 | Triangle | 3 | expressive | Decorative, alert glyphs |
| 7 | Pentagon | 5 | expressive | Loading indicator phase |
| 8 | Hexagon | 6 | expressive | Loading indicator phase, badge backing |
| 9 | Slanted | 4 (parallelogram) | expressive | Stylized cards |
| 10 | Arch | — (semi + flat) | expressive | Hero sections |
| 11 | Semicircle | — | expressive | Hero footer cap |
| 12 | Heart | — | expressive | Like/favorite action |
| 13 | Diamond | 4 (rotated square) | expressive | Loading phase |
| 14 | Clover-4 | 4-lobe | expressive | Loading phase, decoration |
| 15 | Clover-6 | 6-lobe | expressive | Decoration |
| 16 | Clover-8 | 8-lobe | expressive | Decoration |
| 17 | Burst | 12-spike | expressive | Loading peak, "ta-da" reveal |
| 18 | Flower | 8-petal | expressive | Loading mid, decoration |
| 19 | Sunny | 8-ray | expressive | Decoration |
| 20 | Very-sunny | 16-ray | expressive | Decoration, hero accent |
| 21 | Cookie-4 | 4-notch | expressive | Loading start state |
| 22 | Cookie-6 | 6-notch | expressive | Loading phase |
| 23 | Cookie-7 | 7-notch | expressive | Loading phase |
| 24 | Cookie-9 | 9-notch | expressive | Loading phase |
| 25 | Cookie-12 | 12-notch | expressive | Loading peak |
| 26 | Boom | irregular star | expressive | Decoration, badge |
| 27 | Bun | 2-lobe | expressive | Toggle backing |
| 28 | Cubic | rounded square 3D-feel | expressive | Hero element |
| 29 | Fan | quarter-arc | expressive | Decoration |
| 30 | Gem | 6 (rotated hex) | expressive | Badge, premium-tier indicator |
| 31 | Loafer | rounded-rectangle stretched | expressive | Pill-variant for wider buttons |
| 32 | Pixel | 4 (sharp corners) | expressive | Code/dev surfaces, terminal |
| 33 | Puffy | 8-bump | expressive | Decoration |
| 34 | Pillar | tall rounded-rect | expressive | Vertical hero element |
| 35 | Star-12 | 12-point | expressive | Achievement, milestone |
*ier:*
baselineshapes (#1-5) MUST be present in every preset.expressiveshapes (#635) are optin per preset; presets thatdisable Expressive (e.g.,
terminal_classic,brutalist) MAY declare them as no-ops mapping to Square or Pill.
*ource of truth:*polygon vertex coordinates and SVG paths come from androidx.graphics.shapes (vendored under engines/sdk/koder-design-compose/shapes/ once #062.G11 ships). Other surfaces (Flutter, SwiftUI, Web) re-export the same vertex data — no persurface rederivation.
R2 — Morphing contract
Two named shapes A → B can morph if they share the same number of *orph vertices*(vertex correspondence map). Where vertex counts differ, the library MUST subdivide the lower-count shape into the higher count before interpolation:
morph(Square, Cookie-12)
→ subdivide(Square) from 4 → 12 vertices (each edge into 3 segments)
→ interpolate(square_12v_i, cookie_12v_i) for i in 0..11
→ output frameSubdivision is deterministic per androidx.graphics.shapes algorithm — same A,B always yields same intermediate frames.
R2.1 — Required morph pairs
| Source | Destination | Use |
|---|---|---|
| Pill | Squircle | Button group hover/press |
| Rounded-square | Cookie-12 | FAB pressed → menu open |
| Cookie-4 | Burst | Loading indicator (low→high progress) |
| Square | Pill | Card → snackbar transition |
| Circle | Heart | Like button toggle |
| Square | Oval | Carousel item peek↔hero |
Components that animate shape MUST declare their morph pair in their spec (e.g., loading-indicator.kmd R2 declares Cookie-4 → Burst → Flower → Cookie-4 cycle).
R2.2 — Animation driver
Shape morph MUST be driven by motion.kmd R9.1 spring tokens (spatial). Duration-driven morphs are forbidden because spring overshoot is part of the perceptual signature.
| Morph context | Token |
|---|---|
| Hover/press | motion-spatial-fast |
| State change (FAB morph) | motion-spatial-default |
| Hero/decorative | motion-spatial-slow |
R3 — Surface bindings
| Surface | API |
|---|---|
| Compose (Android, Wear) | androidx.graphics.shapes.RoundedPolygon + Morph |
| Flutter | KoderMorphableShape(from: KoderShape.cookie4, to: KoderShape.burst, t: progress) (wraps androidx.graphics.shapes via FFI for Android, custom polygon math elsewhere) |
| SwiftUI | Custom Shape conforming protocol + AnimatableData for vertex array interpolation |
| Web | SVG <path> with d attribute animated via Web Animations API; polygon paths precomputed at build time per preset |
For Web, precompute morph frames at build time (e.g., 30 keyframes between A and B) and serve them as a CSS variable lookup table — do NOT compute polygon math at runtime in JS.
R4 — Accessibility
- Shape morph honors
prefers-reduced-motion: when set, MUST snapdirectly to end state (no intermediate frames). Same contract as
motion.kmdR6. - Decorative
only shapes (#1435 used purely for decoration) MUSTcarry
aria-hidden="true"or platform equivalent. - Functional shapes (Loading indicator, toggle states) MUST announce
state changes via the host component, not the shape itself.
R5 — Per-preset variation
| Preset | Shape behavior |
|---|---|
material3 / material_expressive |
All 35 shapes; morphing enabled |
material2 |
Baseline only (#1-5); expressive shapes map to Square |
terminal_classic |
All shapes → Square (no rounded, no curves) |
brutalist |
All shapes → Pixel (#32) — sharp corners only |
cyberpunk_neon |
All 35 enabled; morph timing -20% (snappier) |
glassmorphism |
All 35 enabled; outline +1px blur during morph |
minimalist_mono |
Baseline + Pill + Squircle only; expressive collapse to Squircle |
Per-preset shape disables MUST be declared explicitly in ui-style.kmd preset definition. Default fallback: missing preset config = all 35 enabled.
R6 — Forbidden patterns
- ❌ Inline polygon vertex math in component code (use the library
binding)
- ❌ Shape morph without spring driver (per R2.2)
- ❌ Decorative shapes that animate continuously (battery drain,
motion
sensitivity hazard) — only statedriven morphs allowed - ❌ Shape morph between shapes whose semantic meaning differs
(e.g., Heart → Triangle) — morphs MUST be within a semantic family (loading, like, button-state)
Cross-link
shape.kmd— radius scale for rectangular surfacesmotion.kmdR9 — spring tokens driving morphsthemes/ui-style.kmd— per-preset shape configcomponents/loading-indicator.kmd— primary morph consumer (#065)components/carousels.kmd— Hero variant morphs (#076)components/buttons.kmd— FAB morph, Split button trailing morph (#066)