Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@
- Every user-facing string must be localized through Flutter l10n (`AppLocalizations`) unless there is a technical reason not to localize.
- Non-user-facing tokens (e.g., protocol values, file format identifiers, action IDs, binding/runtime markers) must be declared as named `const String` values, not inline literals.

## Tool Design: Brushes & Effects

- Every pixel-changing tool lives in one **Brush** section and is used the same
way — pick it, then paint. **Effects are brushes too**: tapping one arms it as
a brush (size + strength) and you paint it on. In the panel an effect is a
brush only; whole-layer/selection apply lives on the on-canvas selection
overlay, not the panel. Selection is an orthogonal modifier that clips any
tool and gates none.
- Do not add a separate section, an "effect mode" toggle, or a whole-region
"Apply" button to the panel, and do not make one capability selectable in two
places.
- **Read [BRUSHES_AND_EFFECTS.md](BRUSHES_AND_EFFECTS.md) before adding, moving,
or renaming any tool or effect.** It is the canonical spec and holds the full
invariants (one active tool at a time, region-bounded painting, and the
bipolar-effect no-op commit guard — a strength-0 effect returns the same image
instance, so any commit that disposes its source must check
`identical(processed, source)` before drawing).

## Current Lint Context

- Magic number currently reported: `lib/main.dart` line 108 value `0.35`.
Expand Down
163 changes: 163 additions & 0 deletions BRUSHES_AND_EFFECTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Brushes & Effects — the tool model

This is the single source of truth for how fPaint organizes the tools that
change pixels. Both the end-user help and the contributor rules point here.

If you are adding, moving, or renaming a tool, read the
[Design rules for contributors](#design-rules-for-contributors) section first.

---

## The core idea

**Everything is a brush. Selection is a mask.**

Every pixel-changing tool lives in one **Brush** section in the side panel and
is used the same way: pick it, then paint. This includes the effects — a blur
or a brightness adjustment is just a brush you paint onto the canvas.

The interaction shape, not the algorithm, is what unifies them:

- **Gesture tools** deposit or move pixels along your stroke (pencil, brush,
smudge, shapes, fill, eraser, text).
- **Effects** transform pixels (blur, sharpness, brightness, …). Pick one and
it **arms as a brush**; paint to apply it along your stroke.

In the left panel an effect *is* a brush — nothing more. There is no separate
"effect mode", no mode toggle, and no per-effect "Apply" button — one flat
palette, one interaction. (Applying an effect to a whole region at once lives
on the on-canvas selection overlay; see [Whole-region apply](#whole-region-apply).)

---

## The one Brush section

The rail is a single flat grid, in this order:

**Gesture tools** — Pencil · Brush · Smudge · Line · Rectangle · Circle · Fill ·
Eraser · Text.

**Effects** — Blur · Sharpness · Brightness · Contrast · Grayscale ·
Hue/Saturation · Noise · Pixelate · Shadow · Vignette.

| | Gesture tool | Effect |
|---|---|---|
| Backed by | `ActionType` | `SelectionEffect` |
| Tapping it | selects it as the active tool | arms it as a brush (tap again to disarm) |
| Options shown | that tool's params | brush **size** and **strength** |
| How it applies | paint the stroke | paint the stroke |

Exactly **one tool is active at a time**: arming an effect deselects the
gesture tool (and swaps in the effect's controls); picking a gesture tool
disarms the effect. The highlight, the options panel, and the actual stroke
behavior always agree on one active tool.

---

## Selection is a modifier, not a mode

The selection is **orthogonal**: it clips any tool and gates none.

- With a selection visible, both gesture strokes **and** effects are confined
to it. This holds even while the **selector tool** is active: an armed effect
brush paints (clipped to the selection) instead of starting a new marquee —
the brush wins, the selection just clips it.
- With **no** selection, a stroke paints freely and a whole-region apply targets
the **whole active layer** — an effect is never a silent no-op waiting for a
selection.
- Switching tools never clears the selection. The visible selection marquee is
the cue that the clip is live, and the toolbar's **Cancel Selection** button
(the selector toggle, which flips to a cancel state whenever a selection
exists) clears it.

---

## Painting an effect

An armed effect is laid down by **painting** it onto a band, the same as any
brush stroke. Painting stays **region-bounded** (cost scales with the brushed
area, not the whole canvas), so it stays off the slow full-canvas image-transfer
path.

## Whole-region apply

To filter an entire layer or selection at once (rather than brushing it), use
the **on-canvas selection overlay**: make a selection, then pick the effect from
the overlay popup. That opens a live-preview flow with Apply/Cancel
(`startEffectPreview` / `EffectIntensityControls`, presented in a bottom sheet on
compact layouts). The left-panel Brush section deliberately does **not** carry a
whole-region "Apply" button — in the panel, effects are brushes you paint.

---

## Effect strength: bipolar vs unipolar

Each effect declares its slider polarity:

- **Unipolar** (`0 → 1`): Blur, Grayscale, Noise, Pixelate, Shadow, Vignette.
Zero means none; the slider only adds the effect.
- **Bipolar** (centered `− … 0 … +`): **Sharpness, Brightness, Contrast,
Hue/Saturation.** The center (0) is no change and the **sign picks the
direction** — darken/brighten, less/more contrast, hue ±, and for Sharpness,
soften (blur) vs. sharpen. Bipolar effects default to a positive value so the
brush does something out of the box; drag through 0 to reverse.

Sharpness folds the former separate "Sharpen" and "Edge Soften" into one signed
axis. Heavy blur stays the dedicated **Blur** effect.

---

## Design rules for contributors

These are the invariants. Breaking one is a design regression, not just a code
change.

1. **One Brush section, one interaction.** Every pixel-changing tool goes in the
single flat grid and is used by picking-then-painting. Do not add a separate
section, mode, or "effect mode" toggle.

2. **In the panel, an effect is a brush — full stop.** A new effect must arm as
a brush (size + strength controls) and be applied by painting. Do not add a
whole-region "Apply" button to the panel; whole-region apply is the selection
overlay's job (`startEffectPreview`), keeping the panel purely a brush
palette.

3. **One capability, one place.** A given effect must not be selectable twice.
(Blur is an effect only. The `blurBrush` `ActionType` and its pixel-brush
engine are kept in code for rendering/export/prefs and a possible fast
Apply-paint path, but it is **not** in the rail.)

4. **Selection clips, never gates.** Any new tool must honor a visible selection
and must still work with no selection (targeting the whole active layer).

5. **One active tool at a time.** Arming an effect must disarm the gesture tool
and vice-versa; highlight + options panel + stroke behavior must stay in
agreement.

6. **Paint stays region-bounded.** Never route a painted effect stroke through a
full-canvas transform — that hits the CPU↔GPU transfer wall. Bound it to the
brushed region.

7. **Guard the no-op commit.** A bipolar effect at strength 0 returns the *same*
image instance. Any commit path that disposes its captured source **must**
check `identical(processed, source)` and bail before drawing, or it crashes
with "non-genuine Image" (see `commitEffectBrushStroke`).

8. **Localize and constant-ize.** New tool/effect labels go through
`AppLocalizations`; no inline strings or magic numbers (see
[RULES.md](RULES.md) / [AGENTS.md](AGENTS.md)).

---

## Code map

| Concern | File |
|---|---|
| The unified rail (one Brush section: grid + active-tool controls) | [lib/panels/tools/tool_family_rail.dart](lib/panels/tools/tool_family_rail.dart) |
| Rail entries + gesture order (`toolRail`, `kGestureToolOrder`) | [lib/models/tool_descriptor.dart](lib/models/tool_descriptor.dart) |
| Gesture tool labels | [lib/models/tool_family.dart](lib/models/tool_family.dart) |
| Effect definitions, polarity, `apply()` | [lib/models/selection_effect.dart](lib/models/selection_effect.dart) |
| Effect display labels | [lib/models/effect_labels.dart](lib/models/effect_labels.dart) |
| Armed effect-brush state | [lib/models/effect_brush_model.dart](lib/models/effect_brush_model.dart) |
| Gesture actions | [lib/models/user_action_drawing.dart](lib/models/user_action_drawing.dart) |
| Paint commit, overlay preview flow, selection clipping | [lib/providers/app_provider_selection_effects.dart](lib/providers/app_provider_selection_effects.dart) |
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@

All notable changes to this project will be documented in this file.

## [1.9.0] - 2026-07-05

### Update

- Unified brushes and effects into one experience: every pixel tool now lives in a single Brush section and works the same way — pick it, then paint. Effects such as Blur, Brightness, and Sharpness now arm as brushes with their own size and strength and are painted directly onto the canvas.
- Effects are no longer gated by a selection — with nothing selected, an effect targets the whole active layer instead of waiting for a region.
- Selection is now a persistent modifier that clips any tool: a "Clipped to selection" badge with one-tap clear appears while a selection is active, and switching tools no longer clears it.
- Streamlined the tool groups into Draw and Adjust, with Smudge now sitting beside the paint tools.
- Signed sliders for Sharpness, Brightness, Contrast, and Hue/Saturation — the center is no change and the sign picks the direction. Sharpness now folds the former Sharpen and Edge Soften into a single axis.
- Adjust paint mode now has its own brush-size slider instead of borrowing the ambient brush size.
- Reworked the magic-wand ("Edge Detection") selection with a clearer on-canvas workflow and simpler Add/Subtract selection math.

### Add

- Toggle to show the top 20 colors used in the current image
- Documents now remember the last selected layer when reopened

## [1.8.9] - 2026-07-03

### Update
Expand Down
23 changes: 22 additions & 1 deletion FEATURES.MD
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,33 @@ This document lists the features currently available in fPaint.
- Localized UI support (via Flutter localization)
- Automatic recovery/restore flow for unsaved artwork on startup

## Tools: Brushes & Effects

Every pixel-changing tool lives in one **Brush** section and is used the same
way — pick it, then paint. **Effects are brushes too.**

- **Gesture tools** — Pencil/Freehand, Brush, Smudge, Line, Rectangle, Circle,
Fill, Eraser, Text.
- **Effects** — Blur, Sharpness, Brightness, Contrast, Grayscale,
Hue/Saturation, Noise, Pixelate, Shadow, Vignette. Tapping one **arms it as a
brush** (with size + strength); paint it on. (To filter a whole layer or
selection at once, pick the effect from the on-canvas selection overlay.)

The **selection** is a modifier that clips any tool and gates none. Bipolar
effects (Sharpness, Brightness, Contrast, Hue/Saturation) use a centered
slider where the sign chooses the direction (e.g. darken ↔ brighten).

📖 Full design rules and rationale:
**[BRUSHES_AND_EFFECTS.md](BRUSHES_AND_EFFECTS.md).**

## Drawing and Painting Tools

- Freehand painting workflow with adjustable brush settings
- Smudge tool (paint-driven)
- Line tool
- Rectangle tool
- Circle tool
- Eraser tool
- Text tool
- Fill tool

Expand Down Expand Up @@ -100,7 +121,7 @@ This document lists the features currently available in fPaint.
- Haptic feedback on scale and rotate gestures
- Display dimensions while drawing selection rectangles
- Updated icon set using SVG assets
- Effects (Blur, Sharpen, Pixelate, Grayscale, Noise, Soften, Vignette) accessible from the selector tool panel without requiring an active selection
- Effects (Blur, Sharpness, Brightness, Contrast, Grayscale, Hue/Saturation, Noise, Pixelate, Shadow, Vignette) live in the Brush section, armed as a brush and painted on; whole-layer/selection apply is available from the on-canvas selection overlay — see [BRUSHES_AND_EFFECTS.md](BRUSHES_AND_EFFECTS.md)

## Quality and Testing Support (Project Capability)

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ To create a **free alternative to expensive commercial graphics software** throu
## ✨ Features

- **Professional Drawing Tools** - Advanced brushes, pressure sensitivity, and precision controls
- **Brushes & Effects** - A unified tool model: *everything is a brush, selection is a mask* — even effects arm as brushes. See [BRUSHES_AND_EFFECTS.md](BRUSHES_AND_EFFECTS.md)
- **Layer Management** - Full layer system with blending modes, opacity, and organization
- **Selection Tools** - Magic Wand, lasso, rectangle, and advanced selection capabilities
- **Color Management** - Professional color picker with palettes, gradients, and color harmony
Expand Down
15 changes: 15 additions & 0 deletions RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@
- Make sure the tests are passing.
- Do not regress code coverage; improving it is encouraged.

## Tool design: Brushes & Effects

Every pixel-changing tool lives in one **Brush** section and is used the same
way — pick it, then paint. **Effects are brushes too**: tapping one arms it as a
brush (size + strength) and you paint it on. In the panel an effect is a brush
only — no whole-region "Apply" button; whole-layer/selection apply lives on the
on-canvas selection overlay. Selection is an orthogonal modifier that clips any
tool and gates none. Do not add a separate section or mode toggle, and do not
make one capability selectable in two places.

**Read [BRUSHES_AND_EFFECTS.md](BRUSHES_AND_EFFECTS.md) before adding, moving,
or renaming any tool or effect** — it holds the full invariants (one active
tool at a time, bipolar-effect no-op commit guard, region-bounded painting,
etc.).

## Platform UX

- The app is used primarily on desktop with the Side Panel visible. Effect controls, intensity sliders, and Apply/Cancel actions live in the Side Panel for that context.
Expand Down
9 changes: 6 additions & 3 deletions assets/icons/selector_wand.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions assets/icons/smudge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading