12 KiB
| title | type | audience | status | source |
|---|---|---|---|---|
| RFC — Eidos shape engine | rfc | human + agent | implemented — phases 1-5 ✅ | migrated from src/uix/eidos/SHAPE_ENGINE_RFC.md (2026-07-02, docs-book F7.4) |
RFC — Eidos shape engine
Sibling of
rfc-color-engine.md,rfc-typography.mdandrfc-depth.md. Takes the shape channel (shapefrom the book Diseñando lo que ocurre) to reference-grade by breaking the references' model — with the open cage. It is the book's 8th and last expression channel left to elevate (time · motion · presence · depth · shape · color · sound · haptic).
0. Thesis
Shape is not a rounding number an element has; it is a perceptual quality — continuity, family, tension — that the system composes and that, in its moment, happens.
Everybody reduces shape to ONE number: border-radius. But the corner has more
axes than its magnitude: how continuous it is (circular arc vs
superellipse), which family it belongs to (rounded / continuous / cut /
pill), how it relates to nested shapes, and what happens to it when the
element is pressed or commits. Eidos treats shape as a first-class channel:
unified (magnitude + continuity + family + harmony), and eventful (shape can
tense/relax), under the open cage.
1. The survey — how the references do it and where they stop
| Framework | Model | Structural limit |
|---|---|---|
| Tailwind / Bootstrap | rounded-sm…full scale, circular arc |
no continuity, no nested harmony, no family, static; the corner is an arc, period |
| Material 3 | shape scale + per-component mapping + (M3 Expressive) morph + cut family |
arc/cut; the morph is recent and narrow; a closed system (you pick from the scale) |
| Apple / SwiftUI | continuous corners (superellipse) .continuous |
platform-bound, not a portable token; not eventful; manual nesting |
| CSS today | border-radius = elliptical arcs |
no universal native superellipse (corner-shape emerging); squircles via SVG/clip-path hacks |
Common limit: shape = one arc-radius number, a static property you assign, detached from neighboring/nested elements, without real continuity, and disconnected from what happens. SwiftUI is the only one with continuity — but closed and per-platform. Material is the only one with family + morph — but arc-based and closed. Nobody unifies them or makes them open.
2. Where Eidos stands today (coherent, but not novel)
- Radius scale in
--radius-{none·sm·md·lg·xl·xxl·full}(4/6/10/16/20px + pill). ✓ - Assigned per component via the size primitive (
radius: 'md'). ✓ - BUT the model is exactly "one circular arc number": no continuity (squircle), no harmony between nested radii, no perceptual family, and static (it never happens). You are ~on par with Tailwind; below SwiftUI (continuity) and M3 (family + morph).
3. The novel model — 4 pieces
- Continuity (superellipse) — a new axis
--shape-smoothing(0 = circular arc … 1 = iOS-style superellipse). Progressive output (the exact strategy of color's oklch):border-radiusis emitted (universal, the fallback) andcorner-shape: superellipse(…)where supported. Solves at the root the arc-corner everyone drags along, breaking nobody. - Nested-radius harmony — inner radius derived from the outer:
inner = max(0, outer − gap)(concentric), computed incalc(). A--shape-nestconvention so card-inside-card (or input-inside-panel) never misaligns. - Shape family —
rounded · continuous · cut · pill(+sharp). A perceptual axis, not one of magnitude. Eidos canon (like roles/variants): themeable values, canonical set. - Eventful shape (the genuinely new part) — shape tenses/relaxes when
something happens:
contact→squeezes corners,commit→rounds them, via the same signature that already drives motion/depth. Shape-as-happening, coordinated from one single sema event.
3.bis The two moments of shape
Shape respects the framework's two-moments model (motion F1) — it does not reinvent it:
| Moment | Attribute (axis) | For shape | Phase |
|---|---|---|---|
| state | data-state (persistent) |
the resting shape — magnitude (--radius-*) + family + continuity: how the element is |
1 |
| event | data-event-* (transient, during the hold) |
the morph — squeeze/round on contact/commit: what happens. Ordered by sequence, next to the motion/depth signatures |
3 |
A control has a shape (state) and tenses it when pressed (event) — the two moments, never collapsed. The book's same rigor (event ≠ state) carried to shape.
4. Doctrine — strong default, open cage
| Piece | Strong default | Open door |
|---|---|---|
| scale | canonical --radius-* |
config-driven (EidosConfig size/radius); raw border-radius always one step away |
| continuity | --shape-smoothing on by default (subtle) |
global/per-component/per-element override; 0 returns to the pure arc |
| family | canonical set (rounded/continuous/cut/pill/sharp) | the set is canon but themeable; a wrapper composes its own without inventing a family |
| nesting | --shape-nest convention (concentric) |
opt-in; the computation is transparent (calc()), overridable |
| eventful | contact↓squeeze / commit↑round |
opt-in/opt-out (like sound/haptic/depth); sema's open registry; degrades with prefers-reduced-motion; static shape works without the channel |
| whole system | canonical theme | runtime applyShape(seed) (sibling of applyColorScheme/applyTypeScale/applyDepth) |
It is exactly how color/typography/depth already operate: retintable canon + runtime builders + primitives always reachable. Shape inherits the same openness contract.
5. Token contract (additions, frozen)
Bare-prefixed by system (--shape-…). The magnitude stays in --radius-*
(nothing renamed → zero breakage):
--radius-{key} magnitude (existing scale — kept)
--shape-smoothing superellipse EXPONENT (1 = circular arc, 2 ≈ the iOS
squircle — the shipped semantics, `STATIC_SHAPE`;
this RFC's earlier "factor 0..1" draft did not land)
→ corner-shape: superellipse(k)
--shape-nest-gap default gap for the nested radius (Phase 2)
- Continuity:
[style] { border-radius: var(--radius-md) }(universal) + where supportedcorner-shape: superellipse(var(--shape-smoothing-k))(progressive, degrades to the arc). - Family:
data-shape='cut'|'continuous'|…attribute or the component's recipe (not a global token) — selects the appropriatecorner-shape/ clip. - Nesting:
border-radius: max(0px, calc(var(--_outer-radius) - var(--shape-nest-gap)))— acalc()convention, no new output token.
4-corner nesting requires FINITE radii — at
fullonly the top nests. Atfull(--radius-full= 9999px) the radius is clipped to half the smaller dimension of each element. A wide child (e.g. 16:10) cannot be concentric on all 4 corners: its height caps all four at ~½-height, different from the parent. But its top corners CAN becard_radius − gapif the bottom ones stay square (the height constraint stops biting). That is the iOS player geometry: artwork with a concentric rounded top + a straight base meeting the metadata.The
[data-shape-nest]CSS rule does not work here because the parent's computed radius value is still9999px(the cap is a used value, not readable from CSS). The/temas/formademo measures it (min(w,h)/2 − gapviaResizeObserver) and applies it to the artwork's top corners; the bottom ones stay at0. The "concentric radii" toggle is disabled atfull("auto: top only") because there the nesting is automatic and partial by geometry, not optional.
6. Phases (the flexibility is baked into each)
- ✅ Continuity + scale + families —
--shape-smoothing+data-shape→corner-shapewith the 4 families (rounded/continuous/cut/scoop). Progressive emission: universalborder-radius(the magnitude,--radius-*intact) +corner-shapewhere supported (degrades to the arc). Config-driven- validation + test + regen. (Families were brought forward here — they are
the same
corner-shapemechanic.)
- validation + test + regen. (Families were brought forward here — they are
the same
- ✅ Nested harmony —
--shape-nest-gap+[data-shape-nest]rule:border-radius: max(0, var(--shape-outer-radius) − gap)(concentric). - ✅ Eventful shape (event moment) — the corner morph folded into the
press-squeezesignature (cross-modal: one press = scale + shadow + corner) +@property --shape-smoothingso it interpolates. On the existingsignaturessystem; degrades with reduced-motion. Not a parallel system. - ✅ Runtime builder
applyShape(seed)—smoothing/nestGapdial + family override, managed block (sibling ofapplyColorScheme/applyTypeScale/applyDepth). - ✅ Showcase + docs —
/temas/forma(continuity with a dial + families + nesting + eventful, live) + theming reference §30.
7. Composition with the existing
--radius-*scale → each shape's magnitude (reused, not duplicated).signaturessystem (motion/depth) → the eventful morph (Phase 3), not a new engine.- Motion's two moments → the morph is a movement of shape, not a jump.
- Sema event bus → the eventful trigger (Phase 3), like sound/haptic/depth.
- Spacing → the nested-radius gap (Phase 2).
Nothing is reinvented: what is today a loose number gets unified + made continuous, harmonic and eventful, under the book's doctrine.
8. Doctrine (parallel to color / typography / depth)
- Scale + families = eidos canon (like roles/variants and planes): themeable values, but the set is canon.
- Continuity = a default quality, retunable;
0= pure arc (open cage). - Eventful shape = an engine capability, opt-in with degradation (like sound/haptic/depth).
- Theme = retint/retune the perceptually fixed: change HOW MUCH
mdrounds or how much continuity it carries, not WHATcutmeans. - Open cage: a strong opinion that never traps — raw
border-radiusalways reachable.
9. Out of scope
- Blobs / random organic shapes and arbitrary
pathmorphing (shape is perceptual, not a free-geometry engine). - Complex geometry clipping (bespoke per-component clip-path stays in each recipe).
- Asymmetric per-side corners as a system (raw per-corner
border-radiusalready covers them).