Files
DarkRoom/core/dr-pipeline/ops/vibrance.yaml
T
dtourolle 7421837c8a Let an operation say what it is about, so the panel can group without naming
Tool tabs need a taxonomy, and the taxonomy was the problem: a table in
`ui/` mapping operation to tab breaks FR-DEV-3a, and a `group:` field risks
what `ui-refinement.md` condemned `starts-group` for — the core deciding
where the panel draws things.

`Attribute` threads the needle. It says what an operation *is* — tone,
colour, detail, optics, geometry, effect — which is the same category as
`ParamKind` and squarely on the core's side of ARCH §4.3a's line. What is
drawn, where it sits and whether it is visible stay the frontend's. There is
no attribute for "the third tab", the enum's order is declaration order
rather than screen order, and a frontend may render these as tabs, as
headings, or ignore them.

The payoff is that a tab strip can be *derived*: the groups are the
attributes present in the capability list, so the interface names no
operation and needs no table to keep in step. An operation joins the right
group by declaring what it is, which is the one thing its author is well
placed to say.

Plural, because the tone curve is genuinely both — an RGB curve is tonal and
the per-channel curves are chromatic, and filing it under one would hide it
from half the people looking for it.

Required and non-empty, enforced in `build.rs`, and the failure was checked
by removing the line rather than assumed. An operation with no attribute is
invisible to a panel that groups by them; a build that stops costs ten
seconds, a control nobody can find costs more. The vocabulary is closed for
the same reason: a typo would otherwise invent a category holding exactly one
operation, which looks like a deliberate one until somebody counts.

Six tests over the real chain, including the hand-written operations that
`build.rs` never sees and so cannot check.
2026-08-22 10:10:00 +02:00

67 lines
2.2 KiB
YAML

id: vibrance
label: op.vibrance
order: 80
attributes: [colour]
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;"]