From 59a4cc7b3aed7221ab4f7cb1a8e1b7c5924a5010 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Sun, 9 Mar 2025 16:57:30 +0100 Subject: [PATCH] feat: ensure min contrast ratio as in xterm.js --- kitty/cell_defines.glsl | 2 ++ kitty/cell_fragment.glsl | 55 ++++++++++++++++++++++++++++++++++++++++ kitty/shaders.py | 9 +++++-- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/kitty/cell_defines.glsl b/kitty/cell_defines.glsl index 0bc4f4a34..c6467c795 100644 --- a/kitty/cell_defines.glsl +++ b/kitty/cell_defines.glsl @@ -7,6 +7,8 @@ #define HAS_TRANSPARENCY {TRANSPARENT} #define FG_OVERRIDE {FG_OVERRIDE} #define FG_OVERRIDE_THRESHOLD {FG_OVERRIDE_THRESHOLD} +#define APPLY_MIN_CONTRAST_RATIO {APPLY_MIN_CONTRAST_RATIO} +#define MIN_CONTRAST_RATIO {MIN_CONTRAST_RATIO} #define TEXT_NEW_GAMMA {TEXT_NEW_GAMMA} #define DECORATION_SHIFT {DECORATION_SHIFT} diff --git a/kitty/cell_fragment.glsl b/kitty/cell_fragment.glsl index 5319a0904..f03d8b797 100644 --- a/kitty/cell_fragment.glsl +++ b/kitty/cell_fragment.glsl @@ -87,6 +87,53 @@ vec3 fg_override(float under_luminance, float over_lumininace, vec3 over) { return original_level * over + override_level * vec3(step(under_luminance, 0.5f)); } #endif +#if (APPLY_MIN_CONTRAST_RATIO == 1) +float contrast_ratio(float under_luminance, float over_luminance) { + return (max(under_luminance, over_luminance) + 0.05f) / (min(under_luminance, over_luminance) + 0.05f); +} +vec3 increase_luminance(vec3 over, float ratio, float target_ratio, float under_luminance) { + vec3 one = vec3(1.f); + while (ratio < target_ratio && any(lessThan(over, one))) { + over = clamp(over + (one - over) * 0.1f + 0.02f, 0.f, 1.f); // add 0.02 to converge faster towards 1 + ratio = contrast_ratio(under_luminance, dot(over, Y)); + } + return over; +} +vec3 decrease_luminance(vec3 over, float ratio, float target_ratio, float under_luminance) { + vec3 zero = vec3(0.f); + while (ratio < target_ratio && any(greaterThan(over, zero))) { + over = clamp(over * 0.9f - 0.02f, 0.f, 1.f); // subtract 0.02 to converge faster towards 0 + ratio = contrast_ratio(under_luminance, dot(over, Y)); + } + return over; +} +vec3 ensure_min_contrast_ratio(float under_luminance, float over_luminance, vec3 under, vec3 over) { + // as in https://github.com/xtermjs/xterm.js/blob/2042bb85023714e55c0c2e986b5000e33b17c414/src/common/Color.ts#L288 + float ratio = contrast_ratio(under_luminance, over_luminance); + if (ratio < MIN_CONTRAST_RATIO) { + if (over_luminance < under_luminance) { + vec3 result_a = decrease_luminance(over, ratio, MIN_CONTRAST_RATIO, under_luminance); + float result_a_ratio = contrast_ratio(under_luminance, dot(result_a, Y)); + if (result_a_ratio < ratio) { + vec3 result_b = increase_luminance(over, ratio, MIN_CONTRAST_RATIO, under_luminance); + float result_b_ratio = contrast_ratio(under_luminance, dot(result_b, Y)); + return result_a_ratio > result_b_ratio ? result_a : result_b; + } + return result_a; + } else { + vec3 result_a = increase_luminance(over, ratio, MIN_CONTRAST_RATIO, under_luminance); + float result_a_ratio = contrast_ratio(under_luminance, dot(result_a, Y)); + if (result_a_ratio < ratio) { + vec3 result_b = decrease_luminance(over, ratio, MIN_CONTRAST_RATIO, under_luminance); + float result_b_ratio = contrast_ratio(under_luminance, dot(result_b, Y)); + return result_a_ratio > result_b_ratio ? result_a : result_b; + } + return result_a; + } + } + return over; +} +#endif vec4 foreground_contrast(vec4 over, vec3 under) { float under_luminance = dot(under, Y); @@ -96,6 +143,10 @@ vec4 foreground_contrast(vec4 over, vec3 under) { over.rgb = fg_override(under_luminance, over_lumininace, over.rgb); over_lumininace = dot(over.rgb, Y); #endif +#if (APPLY_MIN_CONTRAST_RATIO == 1) + over.rgb = ensure_min_contrast_ratio(under_luminance, over_lumininace, under, over.rgb); + over_lumininace = dot(over.rgb, Y); +#endif // Apply additional gamma-adjustment scaled by the luminance difference, the darker the foreground the more adjustment we apply. // A multiplicative contrast is also available to increase saturation. @@ -111,6 +162,10 @@ vec4 foreground_contrast_incorrect(vec4 over, vec3 under) { #if (FG_OVERRIDE == 1) over.rgb = fg_override(under_luminance, over_lumininace, over.rgb); over_lumininace = dot(over.rgb, Y); +#endif +#if (APPLY_MIN_CONTRAST_RATIO == 1) + over.rgb = ensure_min_contrast_ratio(under_luminance, over_lumininace, under, over.rgb); + over_lumininace = dot(over.rgb, Y); #endif // This is the original gamma-incorrect rendering, it is the solution of the following equation: // diff --git a/kitty/shaders.py b/kitty/shaders.py index d3f80f73e..b75c3ef7e 100644 --- a/kitty/shaders.py +++ b/kitty/shaders.py @@ -150,7 +150,10 @@ class LoadShaderPrograms: self.semi_transparent = semi_transparent opts = get_options() self.text_old_gamma = opts.text_composition_strategy == 'legacy' - self.text_fg_override_threshold = max(0, min(opts.text_fg_override_threshold, 100)) * 0.01 + if opts.text_fg_override_threshold < 0: + self.text_fg_override_threshold = opts.text_fg_override_threshold + else: + self.text_fg_override_threshold = max(0, min(opts.text_fg_override_threshold, 100)) * 0.01 cell = program_for('cell') if self.cell_program_replacer is null_replacer: self.cell_program_replacer = MultiReplacer( @@ -168,7 +171,9 @@ class LoadShaderPrograms: r['WHICH_PHASE'] = f'PHASE_{which}' r['TRANSPARENT'] = '1' if semi_transparent else '0' r['FG_OVERRIDE_THRESHOLD'] = str(self.text_fg_override_threshold) - r['FG_OVERRIDE'] = '1' if self.text_fg_override_threshold != 0. else '0' + r['FG_OVERRIDE'] = '1' if self.text_fg_override_threshold > 0 else '0' + r['MIN_CONTRAST_RATIO'] = str(-self.text_fg_override_threshold) + r['APPLY_MIN_CONTRAST_RATIO'] = '1' if self.text_fg_override_threshold < 0 else '0' r['TEXT_NEW_GAMMA'] = '0' if self.text_old_gamma else '1' return self.cell_program_replacer(src)