Files

181 lines
7.7 KiB
Markdown
Raw Permalink Normal View History

# Watercolor Brush Category — Implementation Plan
## Goal
Add a new **Watercolor** brush category to the iced GUI's Brushes panel. Each brush has a watercolor-splat preview (not the standard sine-wave stroke). The brushes simulate realistic watercolor effects. Implemented as a new crate with zero modification to existing brush/engine crates.
## Architecture Overview
### Existing brush flow
1. `hcie-brush-engine` defines `BrushStyle` enum (33 variants including `Watercolor`) and `BrushPreset` struct
2. `hcie-engine-api` re-exports `BrushPreset` and provides `draw_specialized_stroke` → dispatches to `draw_watercolor_brush` for `BrushStyle::Watercolor`
3. `hcie-iced-gui/src/panels/brushes.rs` defines `BrushCategory` enum, `BRUSH_STYLES` static array, and `generate_brush_preview()` for thumbnails
4. When a preset is selected → `Message::BrushPresetSelected(preset)` → sets `tool_state.brush_style`, `brush_size`, `brush_opacity`, `brush_hardness`, `brush_spacing`, `active_brush_preset`
### Key constraint
Engine crates (`hcie-brush-engine`, `hcie-protocol`, `hcie-engine-api`, `hcie-draw`) are **locked** — agents must not modify them. The new crate lives in the GUI layer and reuses the engine's existing `BrushStyle::Watercolor` rendering path.
### New crate strategy
- Define watercolor presets using existing `BrushPreset` (from `hcie-engine-api`) with `style: BrushStyle::Watercolor`
- Provide a **watercolor splat preview renderer** that generates RGBA pixels for thumbnails
- The brushes panel integrates the new crate's presets under a "Watercolor" category tab
- Actual drawing uses the engine's existing watercolor brush rendering — no engine changes needed
---
## Step 1: Create the new crate `hcie-watercolor-brushes`
**Path:** `hcie-iced-app/crates/hcie-watercolor-brushes/`
### `Cargo.toml`
```toml
[package]
name = "hcie-watercolor-brushes"
version.workspace = true
edition.workspace = true
[dependencies]
hcie-engine-api = { path = "../../../hcie-engine-api" }
```
### `src/lib.rs` — Presets + Preview Renderer
Exports:
- `pub fn watercolor_presets() -> Vec<BrushPreset>` — returns 8 watercolor brush presets
- `pub fn render_watercolor_splat(style: BrushStyle, base_color: [u8; 3]) -> Vec<u8>` — generates 64×64 RGBA watercolor splat preview
#### Presets (8 brushes)
| ID | Name | Size | Opacity | Hardness | Spacing | Description |
|----|------|------|---------|----------|---------|-------------|
| `wc_wash` | Watercolor Wash | 80 | 0.25 | 0.0 | 0.08 | Large, transparent wash |
| `wc_wet` | Wet Watercolor | 60 | 0.35 | 0.0 | 0.06 | Medium, wet-on-wet |
| `wc_dry` | Dry Brush | 30 | 0.70 | 0.6 | 0.12 | Small, textured, high opacity |
| `wc_glaze` | Glazing | 50 | 0.15 | 0.0 | 0.10 | Very transparent, layered |
| `wc_splat` | Splatter | 40 | 0.50 | 0.2 | 0.30 | Scattered droplets |
| `wc_bleed` | Color Bleed | 70 | 0.30 | 0.0 | 0.07 | Heavy diffusion edges |
| `wc_grain` | Granulation | 45 | 0.40 | 0.3 | 0.09 | Visible paper grain texture |
| `wc_bloom` | Bloom | 90 | 0.20 | 0.0 | 0.05 | Soft, blooming edges |
All use `BrushStyle::Watercolor` as the engine style — the engine's existing `draw_watercolor_brush` handles the actual stroke rendering.
#### Watercolor Splat Preview Algorithm (64×64 RGBA)
The splat is generated procedurally using multi-octave value noise:
1. **Base shape:** Radial gradient from center, `alpha = (1 - dist/radius)^1.5`
2. **Edge displacement:** Displace the radius at each angle using 3-octave value noise → irregular organic boundary
3. **Multiple layers:** 3-5 overlapping semi-transparent blobs at slightly offset positions
4. **Color variation:** Shift hue slightly per layer (±10°) for color separation
5. **Granulation:** High-frequency noise modulates alpha in the outer 40% of the radius
6. **Wet edge:** Slightly darker/more saturated ring near the boundary (wet-edge accumulation effect)
The noise function is a simple hash-based value noise (no external deps):
```rust
fn value_noise(x: f32, y: f32) -> f32 {
// hash-based pseudo-random, returns 0.0..1.0
}
fn fbm(x: f32, y: f32, octaves: u32) -> f32 {
// fractal Brownian motion using value_noise
}
```
---
## Step 2: Register the crate in the workspace
**File:** `Cargo.toml` (workspace root)
Add to `[workspace] members`:
```toml
"hcie-iced-app/crates/hcie-watercolor-brushes",
```
---
## Step 3: Add `hcie-watercolor-brushes` dependency to `hcie-iced-gui`
**File:** `hcie-iced-app/crates/hcie-iced-gui/Cargo.toml`
Add:
```toml
hcie-watercolor-brushes = { path = "../hcie-watercolor-brushes" }
```
---
## Step 4: Update the Brushes panel
**File:** `hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs`
### 4a. Add `Watercolor` variant to `BrushCategory`
```rust
pub enum BrushCategory {
All,
Drawing,
Painting,
Watercolor, // ← NEW
Effects,
Custom,
Imported,
}
```
### 4b. Update `category_matches_style` for the new variant
```rust
BrushCategory::Watercolor => style_category == BrushCategory::Watercolor,
```
### 4c. Add Watercolor tab to the category tabs array
```rust
(BrushCategory::Watercolor, "Watercolor", "icons/brush.svg"),
```
### 4d. Render watercolor presets when Watercolor category is active
When `active_category == BrushCategory::Watercolor`:
- Call `hcie_watercolor_brushes::watercolor_presets()` to get the preset list
- For each preset, call `hcie_watercolor_brushes::render_watercolor_splat(...)` for the thumbnail
- Render as a grid (same 2-column layout as existing brushes)
- Each cell sends `Message::BrushPresetSelected(preset)` on click
### 4e. Update `view` function signature
The function already accepts `imported_presets: &[BrushPreset]`. No signature change needed — the watercolor presets are fetched internally from the new crate.
### 4f. Update test `catalog_contains_every_engine_brush_style`
The test asserts `BRUSH_STYLES.len() == 34`. This doesn't change (watercolor presets are separate from `BRUSH_STYLES`). The test remains valid.
---
## Step 5: Add "Watercolor" to the egui GUI category filter (optional, for parity)
**File:** `hcie-egui-app/crates/hcie-gui-egui/src/app/tools/brushes_panel.rs`
Add "Watercolor" to the categories array and update `matches_style_category` to map it to `BrushStyle::Watercolor`. This is a minimal change for parity — the watercolor presets themselves would need separate integration in the egui panel if desired.
**Note:** This step is optional for the initial implementation. The user's request focuses on the iced GUI.
---
## Files Changed
| File | Change |
|------|--------|
| `hcie-iced-app/crates/hcie-watercolor-brushes/Cargo.toml` | **New** — crate manifest |
| `hcie-iced-app/crates/hcie-watercolor-brushes/src/lib.rs` | **New** — presets + splat renderer |
| `Cargo.toml` (workspace root) | Add member `hcie-iced-app/crates/hcie-watercolor-brushes` |
| `hcie-iced-app/crates/hcie-iced-gui/Cargo.toml` | Add dependency `hcie-watercolor-brushes` |
| `hcie-iced-app/crates/hcie-iced-gui/src/panels/brushes.rs` | Add `Watercolor` category, tab, and grid rendering |
**Files NOT modified:** All engine crates (`hcie-brush-engine`, `hcie-protocol`, `hcie-engine-api`, `hcie-draw`), existing brush definitions, existing `BRUSH_STYLES` array.
---
## Validation
1. `cargo check -p hcie-watercolor-brushes` — new crate compiles
2. `cargo check -p hcie-iced-gui` — GUI compiles with new dependency
3. `cargo test -p hcie-iced-gui` — all existing tests pass
4. `cargo test -p hcie-watercolor-brushes` — new crate tests pass
5. `cargo test --test feature_scorecard` — scorecard passes
6. Visual: launch the app, open Brushes panel, click "Watercolor" tab → 8 watercolor presets with splat previews appear
7. Visual: select a watercolor brush → draw on canvas → realistic watercolor stroke rendered
8. Regression: existing Drawing/Painting/Effects brushes unchanged