Files
DarkRoom/core/dr-pipeline/ops/blacks_whites.yaml
T
dtourolleandClaude Opus 5 65e6a96a65 Say what the application is doing, in one bar and one list
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>
2026-08-16 18:32:09 +02:00

87 lines
2.9 KiB
YAML

id: blacks_whites
label: op.blacks_whites
order: 50
doc: |
Blacks and whites — the endpoints, where the image clips.
Narrow where [`highlights_shadows`](highlights_shadows.yaml) is broad:
whites act only in the top quarter, blacks only in the bottom quarter. That
difference in reach is the whole distinction between the two pairs.
placement: |
After the broad recovery controls: the endpoints are placed once the tonal
range they bound has been settled.
params:
blacks:
label: param.blacks
kind: amount
whites:
label: param.whites
kind: amount
uniforms:
white_amount: whites / 100
black_amount:
value: blacks / 100 * 0.02
doc: |
A small linear offset. Scene-referred black sits near zero, so the
useful range here is far smaller than a stop — 0.02 is already a
visible lift on a dark frame.
helpers: [luminance, tone_position, apply_tone_gain]
wgsl: |
let luma = luminance(c);
let pos = tone_position(luma);
// Narrow weights concentrated at each end — this is what separates these
// controls from highlights/shadows, which are broad. Whites act only in the
// top quarter, blacks only in the bottom quarter.
let white_w = smoothstep(0.75, 1.0, pos);
let black_w = 1.0 - smoothstep(0.0, 0.25, pos);
// Whites scale the top end multiplicatively, moving the clipping point.
let white_gain = exp2(white_amount * white_w);
c = apply_tone_gain(c, white_gain);
// Blacks shift the floor. This one is deliberately *additive*: the point of
// a blacks control is to set where the image reaches zero, and a multiply
// can never bring a non-zero value to zero nor lift a true black off it.
c = c + vec3<f32>(black_amount * black_w);
// The subtractive direction can push below zero, which is not light.
c = max(c, vec3<f32>(0.0));
tests:
- name: it_starts_neutral
expect_active: false
- name: one_parameter_is_enough_to_activate
set: { whites: 20 }
expect_active: true
- name: the_blacks_offset_stays_small
why: |
Scene-referred black is near zero; a full-stop control here would be
unusable, moving the image to grey at a fraction of its travel.
set: { blacks: 100 }
expect_range: { black_amount: [0.0, 0.05] }
- name: blacks_are_additive_not_multiplicative
why: |
A multiply can never bring a non-zero value to zero nor lift a true
black off it, which is precisely what this control is for.
expect_wgsl: ["c = c + vec3<f32>(black_amount * black_w);"]
- name: the_result_cannot_go_below_zero
why: A negative radiance is not light, and it poisons everything downstream.
expect_wgsl: ["c = max(c, vec3<f32>(0.0));"]
- name: these_endpoints_are_narrower_than_the_recovery_controls
why: |
The one property distinguishing this node from highlights_shadows. If
these weights widened to match, the two would be the same control twice.
expect_wgsl: ["smoothstep(0.75, 1.0, pos)", "smoothstep(0.0, 0.25, pos)"]