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(luma), c, 1.0 + strength); c = max(c, vec3(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;"]