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

70 lines
2.3 KiB
YAML

id: brilliance
label: op.brilliance
order: 70
attributes: [tone]
doc: |
Brilliance — shadows up and highlights down at once.
Apple's control, and a genuinely different idea from contrast: it applies
the opposite correction at each end of the range while leaving mid-tones
alone. The result reads as "more light in the scene" rather than "less
contrast", because local relationships survive where a plain contrast
reduction flattens them.
It overlaps with highlights/shadows deliberately — one control doing both
in a fixed relationship is easier to reach for than two controls needing
to be balanced against each other.
placement: |
After the tone curve has had the final word on tone, and before the colour
controls, which should act on the tones the photographer has settled.
params:
brilliance:
label: param.brilliance
kind: amount
uniforms:
amount:
value: brilliance / 100 * 0.5
doc: |
Half a stop at each end at full travel — the two ends move apart by a
stop in total, which is a strong but not destructive flattening.
helpers: [luminance, tone_position, apply_tone_gain]
wgsl: |
let luma = luminance(c);
let pos = tone_position(luma);
// Opposite corrections at the two ends: shadows up, highlights down, both
// tapering to nothing at the mid-point. This is what separates brilliance
// from a contrast control — mid-tones keep their local relationships, so
// the image gains apparent light rather than losing structure.
let lift = (1.0 - smoothstep(0.0, 0.5, pos)) * amount;
let pull = smoothstep(0.5, 1.0, pos) * amount;
// A mild saturation compensation. Flattening the tonal range washes colour
// out; without this, brilliance looks faded at useful settings.
let gain = exp2(lift - pull);
c = c * gain;
let luma_after = luminance(c);
c = mix(vec3<f32>(luma_after), c, 1.0 + max(amount, 0.0) * 0.15);
c = max(c, vec3<f32>(0.0));
tests:
- name: it_starts_neutral
expect_active: false
- name: full_travel_is_half_a_stop_at_each_end
set: { brilliance: 100 }
expect: { amount: 0.5 }
- name: the_two_ends_move_in_opposite_directions
why: |
The property that makes this brilliance rather than a contrast slider.
Both terms scaling the same way would flatten without lifting.
expect_wgsl: ["let gain = exp2(lift - pull);"]