Files
DarkRoom/core/dr-pipeline/ops/vibrance.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

66 lines
2.2 KiB
YAML

id: vibrance
label: op.vibrance
order: 80
doc: |
Vibrance — saturation weighted toward the muted colours.
Where [`saturation`](saturation.yaml) scales every colour's distance from
grey equally, vibrance scales it *more for muted colours than for already
saturated ones*, and protects skin tones. The difference matters: pushing
saturation on a portrait turns faces orange long before the background
improves, which is precisely the problem vibrance was invented to solve.
placement: |
Before saturation, so the broad control has the last word if both are used.
params:
vibrance:
label: param.vibrance
kind: amount
uniforms:
amount: vibrance / 100
helpers: [luminance, tone_position, colour_saturation]
wgsl: |
let luma = luminance(c);
let sat = colour_saturation(c);
// The vibrance curve: full effect on grey, tapering to nothing on colours
// that are already saturated. Squaring the falloff keeps the mid-range
// responsive while still protecting the extremes.
let falloff = (1.0 - sat) * (1.0 - sat);
// Skin protection. Skin sits in a narrow band of hue where red leads green
// leads blue; pushing it is what makes vibrance look wrong on portraits.
// Detected by channel ordering rather than a hue angle, which costs a
// conversion and buys nothing here.
let is_skin = f32(c.r > c.g && c.g > c.b);
let skin_guard = 1.0 - is_skin * 0.5;
let strength = amount * falloff * skin_guard;
c = mix(vec3<f32>(luma), c, 1.0 + strength);
c = max(c, vec3<f32>(0.0));
tests:
- name: it_starts_neutral
expect_active: false
- name: the_amount_is_normalised_to_unit_range
set: { vibrance: 100 }
expect: { amount: 1.0 }
- name: muted_colours_get_more_than_saturated_ones
why: |
The one property that distinguishes vibrance from saturation. Without
the falloff term this node would be a duplicate of its neighbour.
expect_wgsl: ["let falloff = (1.0 - sat) * (1.0 - sat);"]
- name: skin_tones_are_protected
why: |
The reason vibrance exists. A portrait pushed on plain saturation goes
orange long before the background improves.
expect_wgsl: ["let skin_guard = 1.0 - is_skin * 0.5;"]