Skip to content

Commit dd1c613

Browse files
committed
docs(ebru): add marbling tray design spec
1 parent 11b4c6f commit dd1c613

2 files changed

Lines changed: 205 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ yarn-debug.log*
2828
yarn-error.log*
2929

3030
/templates
31+
32+
# Superpowers brainstorming visual companion
33+
.superpowers/
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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

Comments
 (0)