Every background job reported into a window property of its own — library-thumbs-done, library-pin-total, library-syncing — which only the grid ever read. A pin download that outlived the view it was started from drew nothing at all once the user opened an image, and there was no answer anywhere to "what is this busy with", because the answer was spread across eight properties nothing collected. They report to one register now (ui/dr-ui/src/activity.rs). It publishes an aggregate, which draws a three-pixel bar across the top of the shell in every view, and a row per job, which the settings page lists: scans, thumbnail batches, pin and open downloads, sidecar uploads, the sync and the trash. Failures stay on the list until they are cleared; routine successes do not, or a scroll would bury them. The handle removes a still-running job when it drops, so a worker that dies mid-transfer takes its row with it rather than leaving the bar sweeping for the rest of the session. Also carries in-flight work from a parallel session — the drawn icon set and the dr-pipeline ops split. dr-pipeline's build script does not compile at this commit; ui/dr-ui does, with clippy clean and its tests passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
111 lines
4.0 KiB
YAML
111 lines
4.0 KiB
YAML
id: contrast
|
|
label: op.contrast
|
|
order: 30
|
|
|
|
doc: |
|
|
Contrast — an S-curve about a fixed mid-point.
|
|
|
|
Pushes tones away from middle grey (positive) or toward it (negative),
|
|
pivoting where the eye reads "neither light nor dark". In linear light
|
|
that point is 0.18, not 0.5: a scene-referred value of 0.5 is roughly a
|
|
stop and a half above middle grey, and pivoting there would darken almost
|
|
every photograph.
|
|
|
|
The curve is applied in a perceptual domain rather than directly to linear
|
|
values. Applied linearly, an S-curve crushes shadows far harder than it
|
|
lifts highlights, because linear light devotes most of its range to the
|
|
brightest stop.
|
|
|
|
placement: |
|
|
After the capture corrections, before the region controls: it is the broad
|
|
tonal statement those controls then refine.
|
|
|
|
params:
|
|
contrast:
|
|
label: param.contrast
|
|
kind: amount
|
|
|
|
uniforms:
|
|
amount:
|
|
value: contrast / 100
|
|
doc: The shader's curve expects -1..1; the descriptor speaks -100..100.
|
|
|
|
helpers: [luminance, apply_tone_gain]
|
|
|
|
define:
|
|
contrast_curve: |
|
|
// A symmetric S-curve on a 0..1 perceptual position.
|
|
//
|
|
// `amount` above zero steepens, below zero flattens. The smoothstep form is
|
|
// used for the steepening direction because it has zero gradient at both
|
|
// ends, so the curve cannot invert however hard it is pushed — the failure
|
|
// that makes naive gain-about-a-pivot unusable past moderate settings.
|
|
fn contrast_curve(x: f32, amount: f32) -> f32 {
|
|
let clamped = clamp(x, 0.0, 1.0);
|
|
if (amount >= 0.0) {
|
|
// Blend toward a smoothstep, which is the S.
|
|
let s = clamped * clamped * (3.0 - 2.0 * clamped);
|
|
return mix(clamped, s, amount);
|
|
}
|
|
// Flattening: pull toward the mid-point. At amount = -1 every tone
|
|
// collapses to 0.5, which is the meaningful limit of 'no contrast'.
|
|
return mix(clamped, 0.5, -amount);
|
|
}
|
|
|
|
wgsl: |
|
|
let luma = luminance(c);
|
|
if (luma > 0.0001) {
|
|
// Work on luminance and rescale the colour by the ratio, rather than
|
|
// curving each channel independently. Per-channel contrast shifts hue
|
|
// wherever the channels differ — the classic symptom being skies going
|
|
// cyan as contrast rises.
|
|
//
|
|
// MIDDLE_GREY is 0.18: the linear value the eye reads as mid-tone. The
|
|
// curve operates on luma/(2*0.18) so that middle grey lands at the
|
|
// curve's own 0.5 pivot.
|
|
let pos = clamp(luma / 0.36, 0.0, 1.0);
|
|
let curved = contrast_curve(pos, amount);
|
|
// Not `target`: that is a WGSL reserved keyword, and using it produces a
|
|
// parse error in generated code rather than anywhere a reader would look.
|
|
let curved_luma = curved * 0.36;
|
|
c = apply_tone_gain(c, curved_luma / luma);
|
|
}
|
|
c = max(c, vec3<f32>(0.0));
|
|
|
|
tests:
|
|
- name: neutral_does_nothing
|
|
expect: { amount: 0.0 }
|
|
expect_active: false
|
|
|
|
- name: the_amount_is_normalised_to_unit_range
|
|
set: { contrast: 100 }
|
|
expect: { amount: 1.0 }
|
|
|
|
- name: the_negative_direction_normalises_too
|
|
set: { contrast: -100 }
|
|
expect: { amount: -1.0 }
|
|
|
|
- name: contrast_works_on_luminance_not_per_channel
|
|
why: |
|
|
Curving each channel separately shifts hue; the ratio form is what keeps
|
|
a blue sky blue as contrast rises.
|
|
expect_wgsl: ["luminance(c)", "apply_tone_gain"]
|
|
|
|
- name: the_pivot_is_middle_grey_not_half
|
|
why: |
|
|
Pivoting at 0.5 in linear light would darken nearly every image:
|
|
scene-referred 0.5 is well above what the eye calls mid-tone.
|
|
expect_wgsl: ["0.36"]
|
|
|
|
- name: a_division_by_luminance_is_guarded
|
|
why: |
|
|
A black pixel has zero luminance; dividing by it would produce NaN and
|
|
propagate through everything downstream.
|
|
expect_wgsl: ["luma > 0.0001"]
|
|
|
|
- name: the_curve_cannot_invert
|
|
why: |
|
|
A gain-about-a-pivot form produces a non-monotonic curve past moderate
|
|
settings, which inverts tones. smoothstep cannot.
|
|
expect_helper_wgsl: { contrast_curve: ["3.0 - 2.0 * clamped"] }
|