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.
62 lines
1.9 KiB
YAML
62 lines
1.9 KiB
YAML
id: saturation
|
|
label: op.saturation
|
|
order: 90
|
|
attributes: [colour]
|
|
|
|
doc: |
|
|
Saturation — every colour's distance from grey, scaled equally.
|
|
|
|
The blunt instrument beside [`vibrance`](vibrance.yaml). Both are offered
|
|
because they fail differently: this one is predictable and even, which is
|
|
what a landscape wants, and ruinous on faces, which is what vibrance is for.
|
|
|
|
placement: |
|
|
Last of the per-colour controls, so it has the final word if both it and
|
|
vibrance are in play.
|
|
|
|
params:
|
|
saturation:
|
|
label: param.saturation
|
|
kind: amount
|
|
|
|
uniforms:
|
|
factor:
|
|
value: max(1 + saturation / 100, 0)
|
|
doc: |
|
|
-100 reaches exactly monochrome; +100 doubles the distance from grey.
|
|
The floor at zero matters: a negative factor would push a colour past
|
|
grey into its complement, inverting hues.
|
|
|
|
helpers: [luminance, tone_position, apply_tone_gain]
|
|
|
|
wgsl: |
|
|
// Interpolate away from the luminance-preserving grey. A factor of 0 is
|
|
// monochrome, 1 is unchanged, above 1 is more saturated.
|
|
let luma = luminance(c);
|
|
c = mix(vec3<f32>(luma), c, factor);
|
|
c = max(c, vec3<f32>(0.0));
|
|
|
|
tests:
|
|
- name: it_starts_unchanged
|
|
expect: { factor: 1.0 }
|
|
expect_active: false
|
|
|
|
- name: full_negative_saturation_reaches_monochrome
|
|
why: |
|
|
The property that makes -100 meaningful: it must land exactly on grey,
|
|
not merely near it.
|
|
set: { saturation: -100 }
|
|
expect: { factor: 0.0 }
|
|
|
|
- name: positive_saturation_increases_the_factor
|
|
set: { saturation: 100 }
|
|
expect: { factor: 2.0 }
|
|
|
|
- name: the_factor_never_goes_negative
|
|
why: |
|
|
A negative factor pushes a colour past grey into its complement, which
|
|
inverts hues rather than desaturating them. The clamp is what makes the
|
|
bottom of the slider's travel monochrome instead of a solarised image.
|
|
set: { saturation: -100 }
|
|
expect_range: { factor: [0.0, 2.0] }
|