Let a mask start from a tone or a colour, not only a shape
Every local adjustment began from a shape: painted, drawn with a handle, or found by a model. So the only way to hold back a sky was to draw a line near where it ended, and the only way to warm skin was to paint round it — both of which put the edit's edge where the photographer put a gesture rather than where the picture changes. A gradient across a treeline halos, and an adjustment traced round a face stops on the outline of a hand. MaskSource grows two variants that select by what a pixel *is*. Luminance carries two bounds on the perceptual tone scale plus a softness; Colour carries an arc of hue, a range of chroma, and one softness for every edge of both. Five floats and three, so they diff, sync and merge per field under FR-NC-9 exactly as a gradient's geometry does — the property a stored raster has none of, and the reason the model's coverage had to sit beside its source rather than inside it. The pixels are the shader's business and nowhere else's. `mask.wgsl` takes the demosaiced source as a sixth binding and two new modes read it: decode, balance, pull a clipped photosite back to neutral, apply the camera matrix, then weigh the band. Nothing crosses to the CPU but the numbers and the matrix, and each mask texel averages its own footprint in the source, so a band lands on the tone an area is rather than on whichever texel a proxy grid happened to land on. The photograph it measures is the one the camera recorded, before this edit. A band over the edited result would slide out from under the edit as the edit was made — raising the highlights would change which pixels counted as highlights, and the slider would chase its own mask. Feather, falloff and morphology stay off a range layer, which is what `shapeable` already meant. All three are functions of the signed distance from a boundary, and a range has no boundary to be at a distance from; its edge is the softness of its own band, in the band's units. Offering them would be four controls that move and change nothing.
This commit is contained in:
+84
-17
@@ -178,23 +178,37 @@ pub(crate) fn sync(window: &AppWindow, session: &Rc<RefCell<Option<DevelopSessio
|
||||
let rows: Vec<MaskRow> = s
|
||||
.mask_layers()
|
||||
.into_iter()
|
||||
.map(|(id, label, enabled, selected)| MaskRow {
|
||||
stale: s.mask_is_stale(&id),
|
||||
adjusted: s.mask_is_adjusted(&id),
|
||||
kind: s.mask_kind(&id).into(),
|
||||
inverted: s.mask_inverted(&id),
|
||||
opacity: s.mask_opacity(&id),
|
||||
feather: s.mask_feather(&id),
|
||||
falloff: s.mask_falloff(&id) as i32,
|
||||
morphology: s.mask_morphology(&id) as i32,
|
||||
morph_radius: s.mask_morph_radius(&id),
|
||||
refine: s.mask_refine(&id),
|
||||
refinable: s.mask_is_refinable(&id),
|
||||
shapeable: s.mask_is_shapeable(&id),
|
||||
id: id.into(),
|
||||
label: label.into(),
|
||||
enabled,
|
||||
selected,
|
||||
.map(|(id, label, enabled, selected)| {
|
||||
// Read once each: both answer with a tuple, and asking three
|
||||
// times for three fields of one would walk the stack three times
|
||||
// per row per sync.
|
||||
let (band_lo, band_hi, band_softness) = s.mask_band(&id);
|
||||
let (hue, hue_width) = s.mask_hue(&id);
|
||||
MaskRow {
|
||||
stale: s.mask_is_stale(&id),
|
||||
adjusted: s.mask_is_adjusted(&id),
|
||||
kind: s.mask_kind(&id).into(),
|
||||
inverted: s.mask_inverted(&id),
|
||||
opacity: s.mask_opacity(&id),
|
||||
feather: s.mask_feather(&id),
|
||||
falloff: s.mask_falloff(&id) as i32,
|
||||
morphology: s.mask_morphology(&id) as i32,
|
||||
morph_radius: s.mask_morph_radius(&id),
|
||||
refine: s.mask_refine(&id),
|
||||
refinable: s.mask_is_refinable(&id),
|
||||
shapeable: s.mask_is_shapeable(&id),
|
||||
ranged: s.mask_is_ranged(&id),
|
||||
chromatic: s.mask_is_chromatic(&id),
|
||||
band_lo,
|
||||
band_hi,
|
||||
band_softness,
|
||||
hue,
|
||||
hue_width,
|
||||
id: id.into(),
|
||||
label: label.into(),
|
||||
enabled,
|
||||
selected,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
window.set_mask_rows(ModelRc::new(VecModel::from(rows)));
|
||||
@@ -611,6 +625,43 @@ pub(crate) fn wire(
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
// TRACES: FR-DEV-10
|
||||
// A range mask's band, and a colour range's arc.
|
||||
//
|
||||
// Two handlers rather than five, because a band is one control: moving its
|
||||
// lower bound past its upper swaps them, and a callback per field would
|
||||
// have to answer that with a third of the answer. Each carries the whole
|
||||
// of the thing it changes and the model puts it back in order.
|
||||
//
|
||||
// Cheap, unlike the refine below: a band is three floats in a uniform, so
|
||||
// the mask redraws in the rasterisation the redraw was going to run
|
||||
// anyway and nothing is rebuilt on the CPU at all.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
window.on_mask_band_changed(move |id, lo, hi, softness| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
s.set_mask_band(&id, lo, hi, softness);
|
||||
}
|
||||
sync(&w, &session);
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
window.on_mask_hue_changed(move |id, hue, width| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
s.set_mask_hue(&id, hue, width);
|
||||
}
|
||||
sync(&w, &session);
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
// The refine control. Same shape as the feather's handler, and a separate
|
||||
// one for the same reason every other mask control has its own: the
|
||||
// callback carries the layer id, so there is nothing to share.
|
||||
@@ -687,6 +738,22 @@ pub(crate) fn wire(
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
// TRACES: FR-DEV-10
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
let rows = rows.clone();
|
||||
window.on_add_range_mask(move |chromatic| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
s.add_range_mask(chromatic);
|
||||
}
|
||||
sync(&w, &session);
|
||||
sync_rows(&w, &rows, &session);
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
|
||||
Reference in New Issue
Block a user