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>
66 lines
2.2 KiB
YAML
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;"]
|