|
| 1 | +# Ebru — the Marbling Tray · Design Spec |
| 2 | + |
| 3 | +- **Date:** 2026-06-08 |
| 4 | +- **Status:** Approved (ready for implementation plan) |
| 5 | +- **Type:** New app for the fezcodex `/apps` collection |
| 6 | +- **Category:** Whimsical Tools |
| 7 | + |
| 8 | +## Summary |
| 9 | + |
| 10 | +A hands-on Turkish paper-marbling (*ebru*) simulator. The user floats pigments |
| 11 | +on a water surface, combs and swirls them with authentic tools, then "lays the |
| 12 | +paper" to lift and save the print. Every plate is made **by hand** — there is no |
| 13 | +randomize or auto-generate. It sits beside Fractal Flora as a generative-art |
| 14 | +toy, but uses a completely different engine (fluid marbling, not recursive |
| 15 | +trees) and a distinct **Ottoman illuminated-manuscript** aesthetic. |
| 16 | + |
| 17 | +## Goals |
| 18 | + |
| 19 | +- A faithful, tactile ebru *craft* experience: drop → comb → lay paper. |
| 20 | +- Authentic results: the three core tools must be enough to hand-make the |
| 21 | + classic figures (battal, gel-git, taraklı, tulip/lale, bülbül yuvası). |
| 22 | +- Crisp, high-resolution, exportable output. |
| 23 | +- Self-contained, single-screen, matches existing app conventions. |
| 24 | + |
| 25 | +## Non-Goals (out of scope for v1 — YAGNI) |
| 26 | + |
| 27 | +- Randomize / one-click "generate a pattern." |
| 28 | +- Save/load sessions or multi-page galleries. |
| 29 | +- A custom palette editor. |
| 30 | +- Ox-gall / surface-tension physical realism. |
| 31 | +- Pressure-sensitive pens / dedicated mobile gestures beyond basic |
| 32 | + pointer drag. |
| 33 | + |
| 34 | +## User Experience |
| 35 | + |
| 36 | +Single screen, three zones: |
| 37 | + |
| 38 | +1. **Header band** — illuminated title "Ebru", subtitle "THE MARBLING TRAY · SU |
| 39 | + EBRUSU", gold double-rule, small Ottoman flourishes. |
| 40 | +2. **Left rail** — pigment palette (traditional ebru colors), tool selector |
| 41 | + (Dropper / Needle / Comb), and contextual sliders (drop size; comb tine |
| 42 | + count & spacing). |
| 43 | +3. **The tray** — the dominant central canvas (the water surface), framed like a |
| 44 | + manuscript plate, with an editable caption ("LEVHA I · BATTAL EBRUSU · |
| 45 | + MMXXVI"). |
| 46 | +4. **Action bar** — Undo, New Tray, and "Lay Paper & Save". |
| 47 | + |
| 48 | +Interaction loop: pick a pigment → choose a tool → act on the tray (tap to drop, |
| 49 | +drag to comb/swirl) → repeat → Lay Paper to export. |
| 50 | + |
| 51 | +## Engine — Geometric Mathematical Marbling |
| 52 | + |
| 53 | +The approved model is **geometric** (Jaffer/Lu "mathematical marbling"), not a |
| 54 | +raster fluid sim. Ink is represented as **colored polygons**; tools apply |
| 55 | +closed-form, area-preserving transforms to every existing vertex. This yields |
| 56 | +crisp, authentic ebru, runs fast, is deterministic, and is vector-exportable. |
| 57 | + |
| 58 | +### Geometry & data model |
| 59 | + |
| 60 | +- A **tray** is an ordered list of **ink polygons** (back-to-front z-order). |
| 61 | + Each polygon = `{ color, points: [{x,y}, …] }` in normalized tray space. |
| 62 | +- The authoritative state is an **operation log**: an ordered list of ops |
| 63 | + - `{ type: 'drop', c:{x,y}, r, color }` |
| 64 | + - `{ type: 'comb', path:[…], tines, spacing, sharpness, magnitude, axis }` |
| 65 | + - `{ type: 'needle', path:[…], magnitude }` |
| 66 | +- Geometry is derived by **replaying** the op log from an empty tray. This makes |
| 67 | + state deterministic, undo trivial, and SVG export possible. |
| 68 | + |
| 69 | +### Transforms |
| 70 | + |
| 71 | +**Ink drop** — a new circular drop, center `c`, radius `r`. Every existing |
| 72 | +vertex `p` is pushed radially outward: |
| 73 | + |
| 74 | +``` |
| 75 | +p' = c + (p − c) · sqrt(1 + r² / |p − c|²) |
| 76 | +``` |
| 77 | + |
| 78 | +This is area-preserving and is the defining ebru operation. Invariant: |
| 79 | +`|p' − c| = sqrt(|p − c|² + r²)`. The new drop is then appended as a fresh |
| 80 | +circular polygon (color = current pigment) on top. |
| 81 | + |
| 82 | +**Tine line (comb tooth / needle)** — drags fluid along a direction. For a tine |
| 83 | +line with unit direction `u`, displacement magnitude `M`, sharpness `z ∈ (0,1)`, |
| 84 | +and decay length `c`: each vertex `p` at perpendicular distance `d` from the tine |
| 85 | +line is translated along `u`: |
| 86 | + |
| 87 | +``` |
| 88 | +p' = p + (M · z^(|d| / c)) · u |
| 89 | +``` |
| 90 | + |
| 91 | +- A **comb** is an array of parallel tines (count = `tines`, gap = `spacing`); |
| 92 | + dragging it applies the tine transform incrementally along the drag path. |
| 93 | +- A **needle (biz)** is effectively a single sharp tine with a small decay |
| 94 | + length, used for swirls, tulips, and hearts. |
| 95 | +- Constants (`z`, `c`, default `M`) are tunable; defaults chosen to match |
| 96 | + reference ebru visuals. |
| 97 | + |
| 98 | +### Edge subdivision |
| 99 | + |
| 100 | +After each transform, polygon edges stretch. To keep circles round and combed |
| 101 | +lines smooth, **adaptively resample**: split any edge longer than a max-edge |
| 102 | +threshold by inserting midpoints. Cap total vertices (target < ~50k) by merging |
| 103 | +where density is excessive, to protect frame rate. |
| 104 | + |
| 105 | +### Live preview vs. committed state |
| 106 | + |
| 107 | +- Keep a **materialized polygon list** for the committed state (replayed log). |
| 108 | +- During an in-progress drag, apply incremental transforms to a working copy for |
| 109 | + 60fps live preview. |
| 110 | +- On pointer-up, push **one** op to the log and re-commit. |
| 111 | +- **Undo** pops the last op and replays the committed ops (cost acceptable on the |
| 112 | + undo action only). |
| 113 | + |
| 114 | +### Rendering |
| 115 | + |
| 116 | +- HTML5 **Canvas 2D**. Fill each polygon path with its color, back to front, |
| 117 | + over the water/base color. Rely on canvas antialiasing. |
| 118 | +- **Export**: re-rasterize the op log to an offscreen canvas at high resolution |
| 119 | + (e.g. 2000px on the long edge) for PNG; optionally serialize polygons to |
| 120 | + **SVG** for vector export. |
| 121 | + |
| 122 | +## Tools & Controls |
| 123 | + |
| 124 | +| Tool | Turkish | Action | Controls | |
| 125 | +| --- | --- | --- | --- | |
| 126 | +| Dropper | Damlalık | Tap to drop pigment | drop size | |
| 127 | +| Needle | Biz | Drag to swirl a single point | (fixed sharp decay) | |
| 128 | +| Comb | Tarak | Drag a multi-tine rake across | tine count, spacing | |
| 129 | + |
| 130 | +Plus: pigment palette (select current color), Undo, New Tray. |
| 131 | + |
| 132 | +## Aesthetic — Ottoman Illuminated Manuscript |
| 133 | + |
| 134 | +Deliberately distinct from Fractal Flora's "pressed-fern herbarium." Design |
| 135 | +tokens: |
| 136 | + |
| 137 | +``` |
| 138 | +paper #ECE3CD paperDeep #D8CBA6 |
| 139 | +gold #B08D3E goldDeep #9A7A32 |
| 140 | +indigo #1C3A5E ink #2B2417 |
| 141 | +``` |
| 142 | + |
| 143 | +Traditional ebru pigments (default palette): |
| 144 | + |
| 145 | +``` |
| 146 | +indigo #1F4E6B madder #B23A2A ochre #D3A13A sap green #3F7A4E |
| 147 | +lampblk #14202A turkuaz #1F8F9A mor #6B3F8F havva #ECE3CD |
| 148 | +``` |
| 149 | + |
| 150 | +- Double gold rule border around the whole app; illuminated header band with |
| 151 | + ۞ / ﷽ flourishes. |
| 152 | +- Elegant serif display with letter-spaced small-caps Turkish labels |
| 153 | + (BOYALAR · PIGMENTS, ALETLER · TOOLS). Avoid Fractal Flora's exact |
| 154 | + Playfair + Space Mono pairing. |
| 155 | +- Tray framed in indigo with an offset gold outline; caption in italic small |
| 156 | + caps with Roman-numeral plate number and year. |
| 157 | + |
| 158 | +## File Structure (matches existing conventions) |
| 159 | + |
| 160 | +- `src/pages/apps/EbruPage.jsx` — single-file app page: design tokens, UI, |
| 161 | + tool/pigment state, pointer handling, SEO (`Seo`), toast (`useToast`). |
| 162 | +- `src/pages/apps/ebru/marblingEngine.js` (or co-located module) — the engine as |
| 163 | + its own unit: polygon model, `applyDrop`, `applyTine`, `subdivide`, `replay`, |
| 164 | + `render(ctx)`, `exportPNG`, `exportSVG`. Pure functions, no React — so it is |
| 165 | + unit-testable in isolation. |
| 166 | +- Register route consistent with other apps (lazy-loaded, e.g. alongside |
| 167 | + `FractalFloraPage`). |
| 168 | +- Add entry to `public/apps/apps.json` under **Whimsical Tools** |
| 169 | + (`slug: "ebru"`, `to: "/apps/ebru"`, `title: "Ebru"`, Phosphor icon |
| 170 | + `DropIcon`, `created_at` = build date). |
| 171 | +- Regenerate `public/rss.xml` and `public/sitemap.xml` via |
| 172 | + `npm run generate-rss` / `npm run generate-sitemap`. |
| 173 | + |
| 174 | +## Output |
| 175 | + |
| 176 | +"Lay Paper" plays a brief reveal animation (paper descends onto the tray and |
| 177 | +lifts with the pattern), then downloads a high-res PNG. Optional SVG export |
| 178 | +since the model is vector. Filename includes the plate caption/slug. |
| 179 | + |
| 180 | +## Testing Strategy |
| 181 | + |
| 182 | +Project uses **vitest**. Engine is pure → unit-testable. |
| 183 | + |
| 184 | +- **Ink drop:** a vertex at distance `d` from center maps to distance |
| 185 | + `sqrt(d² + r²)` (the core invariant). Verify for several `d`, `r`. |
| 186 | +- **Drop displacement direction:** points move radially outward from `c`. |
| 187 | +- **Tine line:** points on the line translate by `M`; far points ≈ 0; |
| 188 | + displacement decays monotonically with distance. |
| 189 | +- **Subdivision:** after transforms, max edge length stays under threshold; |
| 190 | + vertex count stays under the cap. |
| 191 | +- **Replay determinism:** identical op logs produce identical geometry. |
| 192 | +- **Component smoke test:** page renders; switching tools updates state; Undo |
| 193 | + pops the last op; New Tray clears. |
| 194 | +- **Manual visual check:** battal and gel-git sequences look authentic; export |
| 195 | + PNG is crisp at target resolution. |
| 196 | + |
| 197 | +## Resolved Decisions |
| 198 | + |
| 199 | +- Engine: **geometric / vector mathematical marbling** (not raster fluid sim). |
| 200 | +- Category: **Whimsical Tools** (tactile experience, like *Paper & Ink*). |
| 201 | +- Aesthetic: **manuscript plate**, rendered as an **Ottoman illuminated** |
| 202 | + treatment to stay distinct from Fractal Flora. |
0 commit comments