Design brush family - orlandomenke/Lightbox GitHub Wiki
The smudge/blur family, and aligning brush settings with Photoshop
Status: design. Nothing here is implemented yet except where noted.
1. Four brushes, two behaviours
| Brush | Reads | Writes | Deposits colour |
|---|---|---|---|
| Smudge | active layer | active layer | no |
| Blur | active layer | active layer | no |
| Smudge (all layers) | composite of visible layers | active layer | no |
| Blur (all layers) | composite of visible layers | active layer | no |
Reading is fixed per brush, not a setting — picking the tool picks the behaviour, as in Photoshop where Sample All Layers is a per-tool option that presets carry. Each of the four gets its own preset and its own stored settings, so tuning the sampled smudge never disturbs the isolated one.
What already works
The isolated pair is correct today. BrushEngine.StampSmudge and
StampBlur never read stroke.Color, so they are colourless by
construction, and the committed render samples the active layer's own
pixels. A live-preview bug where dragging with Smudge stamped flat
foreground-coloured dabs for the whole stroke — only snapping to the real
smear on pen-up — is fixed (MainViewModel.BeginStroke now gives smudge the
same real-pixel copy blur already had).
What the sampled pair needs, and the decision it hinges on
FrameRasterizer.Materialize renders one layer in isolation: baseline
PNG, then that layer's strokes, with targetPixels being that layer's own
bitmap. Layers are composited afterwards, in SceneRenderer.ComposeInto.
Nothing at rasterization time can see another layer.
So a sampled brush needs a backdrop — the composite of the other visible
layers — threaded from the caller into BrushEngine. Mechanically that is
an extra optional parameter on StampStroke, Materialize, Append and
AppendDraft, plus a backdrop argument on FrameBitmapCache.Get. The
mechanical part is easy. The consequence is not:
A layer's cached bitmap stops being a function of that layer alone. Edit a lower layer, and every sampled stroke above it must re-render. The cache key has to include the backdrop's identity, and invalidation has to cascade upward through the stack.
That is a genuine design fork, and it is Q6 below. It is the only thing blocking the sampled pair; everything else is plumbing.
2. Photoshop alignment — the selection
Our BrushSettings currently carries: size, hardness, opacity, flow,
spacing, scatter, granulation, wet edge, tip id, angle, roundness,
smoothing, anti-aliasing, and the pressure curves. Photoshop's brush panel
is far larger, and the .abr importer necessarily drops what we cannot
represent.
Rather than chase the whole panel, here is the selection worth having — the
ones that change how a mark reads, that .abr and .kpp files actually
carry, and that our dab-stamping engine can honour without a rewrite.
Decided (Q7 = b): tier 1 plus Texture and Colour Dynamics. Grouping follows Photoshop's own (Q8 = a): Shape Dynamics, Scattering, Texture, Dual Brush, Transfer.
Tier 1 — take these first
| Photoshop name | What it does | Why it earns its place |
|---|---|---|
| Size Jitter + Minimum Diameter | dab size varies per dab | The single biggest difference between a "digital" and a "natural" mark. We have scatter but not size variation. |
| Angle Jitter / Roundness Jitter | dab orientation and squash vary | Makes a custom tip read as bristles rather than a repeated stamp. We store angle and roundness but never vary them. |
| Shape Dynamics → Direction | angle follows the stroke direction | Required for any flat/chisel tip to behave like one. Cheap: we already compute segment direction for spacing. |
| Dual Brush | a second tip masks the first | This is what makes Photoshop's textured brushes look textured; it is also the single most common thing lost on .abr import. |
| Transfer → Flow Jitter | flow varies per dab | Gives the wash/build-up variation that currently only pressure can produce. |
All five are per-dab modulations of values we already have, and all five can
be seeded from dab position through Hash01, so they cost nothing in
determinism (invariant 2) and nothing in re-render divergence.
Tier 2 — Texture and Colour Dynamics are in scope
- Texture (paper grain multiplied into the dab, with depth and scale) —
we have granulation, which is a fixed-frequency version of this; making
the tile and depth settable is most of the way there, and
PaperFieldalready supplies the tile. - Colour Dynamics (foreground/background jitter, hue/sat/brightness jitter) — natural-media colour variation. This is the one item in the agreed set that needs a model change: it wants a second colour on the stroke, since jittering toward the background colour is what makes it read as natural media rather than as noise.
Not in scope:
- Noise and Wet Edges as independent toggles — we have wet edge.
- Brush Pose (tilt, rotation from the stylus) — we read tilt from the pen already but do nothing with it.
Tier 3 — deliberately not doing
- Airbrush/build-up timing, bristle qualities, and the full Mixer Brush
reservoir model. Each is a simulation rather than a parameter, and none
survives an
.abrround trip in a form we could honour faithfully.
Where these go in the UI
The brush flyout already has General / Effects / Pressure / Presets pages. Photoshop groups by dynamic, not by parameter, and that grouping is what makes the panel legible: Shape Dynamics, Scattering, Texture, Dual Brush, Transfer. Adopting those names for the Effects page's sections would make imported presets recognisable to anyone coming from Photoshop, which is the point of aligning.
3. Questions this raises
Added to .claude/quality/QUESTIONS.md as Q6–Q8.