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

83 lines
2.6 KiB
YAML

id: highlights_shadows
label: op.highlights_shadows
order: 40
attributes: [tone]
doc: |
Highlights and shadows — broad, overlapping recovery at both ends.
Weights are built from smoothstep rather than a hard threshold: a sharp
boundary produces visible banding on a gradient — a sky is the worst case,
and it is also the most common subject for these controls.
Broad where [`blacks_whites`](blacks_whites.yaml) is narrow. These are the
controls used to tame a contrasty scene; those are the ones used to place
the endpoints.
placement: |
After the broad tonal statement contrast makes, so these refine it.
params:
highlights:
label: param.highlights
kind: amount
doc: |
Negative recovers highlights, the overwhelmingly common direction,
matching the convention every other developer uses.
shadows:
label: param.shadows
kind: amount
uniforms:
hi_amount:
value: highlights / 100
doc: |
A full stop at the extreme; enough to recover a bright sky without
inverting the tonal relationship.
lo_amount: shadows / 100
helpers: [luminance, tone_position, apply_tone_gain]
wgsl: |
let luma = luminance(c);
let pos = tone_position(luma);
// Broad, overlapping weights. Highlights ramp in over the upper half,
// shadows out over the lower half, so a mid-tone is barely touched by
// either and the two controls blend rather than fighting at the join.
let hi_w = smoothstep(0.5, 1.0, pos);
let lo_w = 1.0 - smoothstep(0.0, 0.5, pos);
// Each control contributes up to a stop of gain at full deflection.
// exp2 keeps the effect symmetric: -100 and +100 are inverse.
let hi_gain = exp2(hi_amount * hi_w);
let lo_gain = exp2(lo_amount * lo_w);
c = apply_tone_gain(c, hi_gain * lo_gain);
tests:
- name: it_starts_neutral
expect_active: false
- name: one_parameter_is_enough_to_activate
set: { highlights: -50 }
expect_active: true
- name: highlight_recovery_is_the_negative_direction
why: The convention users expect — dragging left recovers. Full travel is one stop.
set: { highlights: -100 }
expect: { hi_amount: -1.0 }
- name: tonal_amounts_are_symmetric
why: |
exp2 of equal and opposite exponents multiplies to 1, so +100 and -100
have to be exact negations for the two directions to cancel.
set: { shadows: 100 }
expect: { lo_amount: 1.0 }
- name: the_weights_are_smooth_not_thresholded
why: |
A hard boundary bands visibly on a gradient, and a sky is both the worst
case and the most common subject for this control.
expect_wgsl: ["smoothstep(0.5, 1.0, pos)", "smoothstep(0.0, 0.5, pos)"]