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 01/10] 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 @@ def __call__(self, semi_transparent: bool = False, allow_recompile: bool = False 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 @@ def resolve_cell_defines(which: str, src: str) -> str: 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) From 2e05b205fdae5dfcaaf6d1587e212848704e9f5d Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Tue, 11 Mar 2025 17:07:33 +0100 Subject: [PATCH 02/10] chore: add hsluv port from williammalo/hsluv-glsl --- kitty/hsluv.glsl | 290 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 kitty/hsluv.glsl diff --git a/kitty/hsluv.glsl b/kitty/hsluv.glsl new file mode 100644 index 000000000..da9164038 --- /dev/null +++ b/kitty/hsluv.glsl @@ -0,0 +1,290 @@ +/* +HSLUV-GLSL v4.2 +HSLUV is a human-friendly alternative to HSL. ( http://www.hsluv.org ) +GLSL port by William Malo ( https://github.com/williammalo ) +Put this code in your fragment shader. +*/ + +vec3 hsluv_intersectLineLine(vec3 line1x, vec3 line1y, vec3 line2x, vec3 line2y) { + return (line1y - line2y) / (line2x - line1x); +} + +vec3 hsluv_distanceFromPole(vec3 pointx,vec3 pointy) { + return sqrt(pointx*pointx + pointy*pointy); +} + +vec3 hsluv_lengthOfRayUntilIntersect(float theta, vec3 x, vec3 y) { + vec3 len = y / (sin(theta) - x * cos(theta)); + if (len.r < 0.0) {len.r=1000.0;} + if (len.g < 0.0) {len.g=1000.0;} + if (len.b < 0.0) {len.b=1000.0;} + return len; +} + +float hsluv_maxSafeChromaForL(float L){ + mat3 m2 = mat3( + 3.2409699419045214 ,-0.96924363628087983 , 0.055630079696993609, + -1.5373831775700935 , 1.8759675015077207 ,-0.20397695888897657 , + -0.49861076029300328 , 0.041555057407175613, 1.0569715142428786 + ); + float sub0 = L + 16.0; + float sub1 = sub0 * sub0 * sub0 * .000000641; + float sub2 = sub1 > 0.0088564516790356308 ? sub1 : L / 903.2962962962963; + + vec3 top1 = (284517.0 * m2[0] - 94839.0 * m2[2]) * sub2; + vec3 bottom = (632260.0 * m2[2] - 126452.0 * m2[1]) * sub2; + vec3 top2 = (838422.0 * m2[2] + 769860.0 * m2[1] + 731718.0 * m2[0]) * L * sub2; + + vec3 bounds0x = top1 / bottom; + vec3 bounds0y = top2 / bottom; + + vec3 bounds1x = top1 / (bottom+126452.0); + vec3 bounds1y = (top2-769860.0*L) / (bottom+126452.0); + + vec3 xs0 = hsluv_intersectLineLine(bounds0x, bounds0y, -1.0/bounds0x, vec3(0.0) ); + vec3 xs1 = hsluv_intersectLineLine(bounds1x, bounds1y, -1.0/bounds1x, vec3(0.0) ); + + vec3 lengths0 = hsluv_distanceFromPole( xs0, bounds0y + xs0 * bounds0x ); + vec3 lengths1 = hsluv_distanceFromPole( xs1, bounds1y + xs1 * bounds1x ); + + return min(lengths0.r, + min(lengths1.r, + min(lengths0.g, + min(lengths1.g, + min(lengths0.b, + lengths1.b))))); +} + +float hsluv_maxChromaForLH(float L, float H) { + + float hrad = radians(H); + + mat3 m2 = mat3( + 3.2409699419045214 ,-0.96924363628087983 , 0.055630079696993609, + -1.5373831775700935 , 1.8759675015077207 ,-0.20397695888897657 , + -0.49861076029300328 , 0.041555057407175613, 1.0569715142428786 + ); + float sub1 = pow(L + 16.0, 3.0) / 1560896.0; + float sub2 = sub1 > 0.0088564516790356308 ? sub1 : L / 903.2962962962963; + + vec3 top1 = (284517.0 * m2[0] - 94839.0 * m2[2]) * sub2; + vec3 bottom = (632260.0 * m2[2] - 126452.0 * m2[1]) * sub2; + vec3 top2 = (838422.0 * m2[2] + 769860.0 * m2[1] + 731718.0 * m2[0]) * L * sub2; + + vec3 bound0x = top1 / bottom; + vec3 bound0y = top2 / bottom; + + vec3 bound1x = top1 / (bottom+126452.0); + vec3 bound1y = (top2-769860.0*L) / (bottom+126452.0); + + vec3 lengths0 = hsluv_lengthOfRayUntilIntersect(hrad, bound0x, bound0y ); + vec3 lengths1 = hsluv_lengthOfRayUntilIntersect(hrad, bound1x, bound1y ); + + return min(lengths0.r, + min(lengths1.r, + min(lengths0.g, + min(lengths1.g, + min(lengths0.b, + lengths1.b))))); +} + +float hsluv_fromLinear(float c) { + return c <= 0.0031308 ? 12.92 * c : 1.055 * pow(c, 1.0 / 2.4) - 0.055; +} +vec3 hsluv_fromLinear(vec3 c) { + return vec3( hsluv_fromLinear(c.r), hsluv_fromLinear(c.g), hsluv_fromLinear(c.b) ); +} + +float hsluv_toLinear(float c) { + return c > 0.04045 ? pow((c + 0.055) / (1.0 + 0.055), 2.4) : c / 12.92; +} + +vec3 hsluv_toLinear(vec3 c) { + return vec3( hsluv_toLinear(c.r), hsluv_toLinear(c.g), hsluv_toLinear(c.b) ); +} + +float hsluv_yToL(float Y){ + return Y <= 0.0088564516790356308 ? Y * 903.2962962962963 : 116.0 * pow(Y, 1.0 / 3.0) - 16.0; +} + +float hsluv_lToY(float L) { + return L <= 8.0 ? L / 903.2962962962963 : pow((L + 16.0) / 116.0, 3.0); +} + +vec3 xyzToRgb(vec3 tuple) { + const mat3 m = mat3( + 3.2409699419045214 ,-1.5373831775700935 ,-0.49861076029300328 , + -0.96924363628087983 , 1.8759675015077207 , 0.041555057407175613, + 0.055630079696993609,-0.20397695888897657, 1.0569715142428786 ); + + return hsluv_fromLinear(tuple*m); +} + +vec3 rgbToXyz(vec3 tuple) { + const mat3 m = mat3( + 0.41239079926595948 , 0.35758433938387796, 0.18048078840183429 , + 0.21263900587151036 , 0.71516867876775593, 0.072192315360733715, + 0.019330818715591851, 0.11919477979462599, 0.95053215224966058 + ); + return hsluv_toLinear(tuple) * m; +} + +vec3 xyzToLuv(vec3 tuple){ + float X = tuple.x; + float Y = tuple.y; + float Z = tuple.z; + + float L = hsluv_yToL(Y); + + float div = 1./dot(tuple,vec3(1,15,3)); + + return vec3( + 1., + (52. * (X*div) - 2.57179), + (117.* (Y*div) - 6.08816) + ) * L; +} + + +vec3 luvToXyz(vec3 tuple) { + float L = tuple.x; + + float U = tuple.y / (13.0 * L) + 0.19783000664283681; + float V = tuple.z / (13.0 * L) + 0.468319994938791; + + float Y = hsluv_lToY(L); + float X = 2.25 * U * Y / V; + float Z = (3./V - 5.)*Y - (X/3.); + + return vec3(X, Y, Z); +} + +vec3 luvToLch(vec3 tuple) { + float L = tuple.x; + float U = tuple.y; + float V = tuple.z; + + float C = length(tuple.yz); + float H = degrees(atan(V,U)); + if (H < 0.0) { + H = 360.0 + H; + } + + return vec3(L, C, H); +} + +vec3 lchToLuv(vec3 tuple) { + float hrad = radians(tuple.b); + return vec3( + tuple.r, + cos(hrad) * tuple.g, + sin(hrad) * tuple.g + ); +} + +vec3 hsluvToLch(vec3 tuple) { + tuple.g *= hsluv_maxChromaForLH(tuple.b, tuple.r) * .01; + return tuple.bgr; +} + +vec3 lchToHsluv(vec3 tuple) { + tuple.g /= hsluv_maxChromaForLH(tuple.r, tuple.b) * .01; + return tuple.bgr; +} + +vec3 hpluvToLch(vec3 tuple) { + tuple.g *= hsluv_maxSafeChromaForL(tuple.b) * .01; + return tuple.bgr; +} + +vec3 lchToHpluv(vec3 tuple) { + tuple.g /= hsluv_maxSafeChromaForL(tuple.r) * .01; + return tuple.bgr; +} + +vec3 lchToRgb(vec3 tuple) { + return xyzToRgb(luvToXyz(lchToLuv(tuple))); +} + +vec3 rgbToLch(vec3 tuple) { + return luvToLch(xyzToLuv(rgbToXyz(tuple))); +} + +vec3 hsluvToRgb(vec3 tuple) { + return lchToRgb(hsluvToLch(tuple)); +} + +vec3 rgbToHsluv(vec3 tuple) { + return lchToHsluv(rgbToLch(tuple)); +} + +vec3 hpluvToRgb(vec3 tuple) { + return lchToRgb(hpluvToLch(tuple)); +} + +vec3 rgbToHpluv(vec3 tuple) { + return lchToHpluv(rgbToLch(tuple)); +} + +vec3 luvToRgb(vec3 tuple){ + return xyzToRgb(luvToXyz(tuple)); +} + +// allow vec4's +vec4 xyzToRgb(vec4 c) {return vec4( xyzToRgb( vec3(c.x,c.y,c.z) ), c.a);} +vec4 rgbToXyz(vec4 c) {return vec4( rgbToXyz( vec3(c.x,c.y,c.z) ), c.a);} +vec4 xyzToLuv(vec4 c) {return vec4( xyzToLuv( vec3(c.x,c.y,c.z) ), c.a);} +vec4 luvToXyz(vec4 c) {return vec4( luvToXyz( vec3(c.x,c.y,c.z) ), c.a);} +vec4 luvToLch(vec4 c) {return vec4( luvToLch( vec3(c.x,c.y,c.z) ), c.a);} +vec4 lchToLuv(vec4 c) {return vec4( lchToLuv( vec3(c.x,c.y,c.z) ), c.a);} +vec4 hsluvToLch(vec4 c) {return vec4( hsluvToLch( vec3(c.x,c.y,c.z) ), c.a);} +vec4 lchToHsluv(vec4 c) {return vec4( lchToHsluv( vec3(c.x,c.y,c.z) ), c.a);} +vec4 hpluvToLch(vec4 c) {return vec4( hpluvToLch( vec3(c.x,c.y,c.z) ), c.a);} +vec4 lchToHpluv(vec4 c) {return vec4( lchToHpluv( vec3(c.x,c.y,c.z) ), c.a);} +vec4 lchToRgb(vec4 c) {return vec4( lchToRgb( vec3(c.x,c.y,c.z) ), c.a);} +vec4 rgbToLch(vec4 c) {return vec4( rgbToLch( vec3(c.x,c.y,c.z) ), c.a);} +vec4 hsluvToRgb(vec4 c) {return vec4( hsluvToRgb( vec3(c.x,c.y,c.z) ), c.a);} +vec4 rgbToHsluv(vec4 c) {return vec4( rgbToHsluv( vec3(c.x,c.y,c.z) ), c.a);} +vec4 hpluvToRgb(vec4 c) {return vec4( hpluvToRgb( vec3(c.x,c.y,c.z) ), c.a);} +vec4 rgbToHpluv(vec4 c) {return vec4( rgbToHpluv( vec3(c.x,c.y,c.z) ), c.a);} +vec4 luvToRgb(vec4 c) {return vec4( luvToRgb( vec3(c.x,c.y,c.z) ), c.a);} +// allow 3 floats +vec3 xyzToRgb(float x, float y, float z) {return xyzToRgb( vec3(x,y,z) );} +vec3 rgbToXyz(float x, float y, float z) {return rgbToXyz( vec3(x,y,z) );} +vec3 xyzToLuv(float x, float y, float z) {return xyzToLuv( vec3(x,y,z) );} +vec3 luvToXyz(float x, float y, float z) {return luvToXyz( vec3(x,y,z) );} +vec3 luvToLch(float x, float y, float z) {return luvToLch( vec3(x,y,z) );} +vec3 lchToLuv(float x, float y, float z) {return lchToLuv( vec3(x,y,z) );} +vec3 hsluvToLch(float x, float y, float z) {return hsluvToLch( vec3(x,y,z) );} +vec3 lchToHsluv(float x, float y, float z) {return lchToHsluv( vec3(x,y,z) );} +vec3 hpluvToLch(float x, float y, float z) {return hpluvToLch( vec3(x,y,z) );} +vec3 lchToHpluv(float x, float y, float z) {return lchToHpluv( vec3(x,y,z) );} +vec3 lchToRgb(float x, float y, float z) {return lchToRgb( vec3(x,y,z) );} +vec3 rgbToLch(float x, float y, float z) {return rgbToLch( vec3(x,y,z) );} +vec3 hsluvToRgb(float x, float y, float z) {return hsluvToRgb( vec3(x,y,z) );} +vec3 rgbToHsluv(float x, float y, float z) {return rgbToHsluv( vec3(x,y,z) );} +vec3 hpluvToRgb(float x, float y, float z) {return hpluvToRgb( vec3(x,y,z) );} +vec3 rgbToHpluv(float x, float y, float z) {return rgbToHpluv( vec3(x,y,z) );} +vec3 luvToRgb(float x, float y, float z) {return luvToRgb( vec3(x,y,z) );} +// allow 4 floats +vec4 xyzToRgb(float x, float y, float z, float a) {return xyzToRgb( vec4(x,y,z,a) );} +vec4 rgbToXyz(float x, float y, float z, float a) {return rgbToXyz( vec4(x,y,z,a) );} +vec4 xyzToLuv(float x, float y, float z, float a) {return xyzToLuv( vec4(x,y,z,a) );} +vec4 luvToXyz(float x, float y, float z, float a) {return luvToXyz( vec4(x,y,z,a) );} +vec4 luvToLch(float x, float y, float z, float a) {return luvToLch( vec4(x,y,z,a) );} +vec4 lchToLuv(float x, float y, float z, float a) {return lchToLuv( vec4(x,y,z,a) );} +vec4 hsluvToLch(float x, float y, float z, float a) {return hsluvToLch( vec4(x,y,z,a) );} +vec4 lchToHsluv(float x, float y, float z, float a) {return lchToHsluv( vec4(x,y,z,a) );} +vec4 hpluvToLch(float x, float y, float z, float a) {return hpluvToLch( vec4(x,y,z,a) );} +vec4 lchToHpluv(float x, float y, float z, float a) {return lchToHpluv( vec4(x,y,z,a) );} +vec4 lchToRgb(float x, float y, float z, float a) {return lchToRgb( vec4(x,y,z,a) );} +vec4 rgbToLch(float x, float y, float z, float a) {return rgbToLch( vec4(x,y,z,a) );} +vec4 hsluvToRgb(float x, float y, float z, float a) {return hsluvToRgb( vec4(x,y,z,a) );} +vec4 rgbToHslul(float x, float y, float z, float a) {return rgbToHsluv( vec4(x,y,z,a) );} +vec4 hpluvToRgb(float x, float y, float z, float a) {return hpluvToRgb( vec4(x,y,z,a) );} +vec4 rgbToHpluv(float x, float y, float z, float a) {return rgbToHpluv( vec4(x,y,z,a) );} +vec4 luvToRgb(float x, float y, float z, float a) {return luvToRgb( vec4(x,y,z,a) );} + +/* +END HSLUV-GLSL +*/ From f67aa23d4f8d0b9d0bdb2ad7bb2b179a1177120a Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Tue, 11 Mar 2025 17:09:22 +0100 Subject: [PATCH 03/10] perf: apply min contrast using hsluv instead of rgb --- kitty/cell_fragment.glsl | 55 +++++++++++----------------------------- 1 file changed, 15 insertions(+), 40 deletions(-) diff --git a/kitty/cell_fragment.glsl b/kitty/cell_fragment.glsl index f03d8b797..f201a1457 100644 --- a/kitty/cell_fragment.glsl +++ b/kitty/cell_fragment.glsl @@ -1,4 +1,5 @@ #pragma kitty_include_shader +#pragma kitty_include_shader #pragma kitty_include_shader #pragma kitty_include_shader @@ -89,47 +90,21 @@ vec3 fg_override(float under_luminance, float over_lumininace, vec3 over) { #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); + return clamp((max(under_luminance, over_luminance) + 0.05f) / (min(under_luminance, over_luminance) + 0.05f), 1.f, 21.f); } -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 +vec3 apply_min_contrast_ratio(float under_luminance, float over_luminance, vec3 under, vec3 over) { 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; - } + vec3 diff = abs(under - over); + vec3 over_hsluv = rgbToHsluv(over); + float target_lum_a = clamp((under_luminance + 0.05f) * MIN_CONTRAST_RATIO - 0.05f, 0.f, 1.f); + float target_lum_b = clamp((under_luminance + 0.05f) / MIN_CONTRAST_RATIO - 0.05f, 0.f, 1.f); + vec3 result_a = clamp(hsluvToRgb(vec3(over_hsluv.x, over_hsluv.y, target_lum_a * 100.f)), 0.f, 1.f); + vec3 result_b = clamp(hsluvToRgb(vec3(over_hsluv.x, over_hsluv.y, target_lum_b * 100.f)), 0.f, 1.f); + float result_a_ratio = contrast_ratio(under_luminance, dot(result_a, Y)); + float result_b_ratio = contrast_ratio(under_luminance, dot(result_b, Y)); + vec3 result = mix(result_a, result_b, step(result_a_ratio, result_b_ratio)); + return mix(result, over, diff.r + diff.g + diff.g < 0.001f); } return over; } @@ -144,7 +119,7 @@ vec4 foreground_contrast(vec4 over, vec3 under) { 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.rgb = apply_min_contrast_ratio(under_luminance, over_lumininace, under, over.rgb); over_lumininace = dot(over.rgb, Y); #endif @@ -164,7 +139,7 @@ vec4 foreground_contrast_incorrect(vec4 over, vec3 under) { 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.rgb = apply_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: From 167aa4c270ac159084c9d7cf978cc80acc3a7125 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Tue, 11 Mar 2025 17:22:45 +0100 Subject: [PATCH 04/10] perf: branchless hsluv conversion --- kitty/hsluv.glsl | 107 +++++------------------------------------------ 1 file changed, 10 insertions(+), 97 deletions(-) diff --git a/kitty/hsluv.glsl b/kitty/hsluv.glsl index da9164038..0b8fdaeaa 100644 --- a/kitty/hsluv.glsl +++ b/kitty/hsluv.glsl @@ -5,6 +5,8 @@ GLSL port by William Malo ( https://github.com/williammalo ) Put this code in your fragment shader. */ +// stripped down and optimized (branchless) version + vec3 hsluv_intersectLineLine(vec3 line1x, vec3 line1y, vec3 line2x, vec3 line2y) { return (line1y - line2y) / (line2x - line1x); } @@ -15,9 +17,7 @@ vec3 hsluv_distanceFromPole(vec3 pointx,vec3 pointy) { vec3 hsluv_lengthOfRayUntilIntersect(float theta, vec3 x, vec3 y) { vec3 len = y / (sin(theta) - x * cos(theta)); - if (len.r < 0.0) {len.r=1000.0;} - if (len.g < 0.0) {len.g=1000.0;} - if (len.b < 0.0) {len.b=1000.0;} + len = mix(len, vec3(1000.0), step(len, vec3(0.0))); return len; } @@ -29,7 +29,7 @@ float hsluv_maxSafeChromaForL(float L){ ); float sub0 = L + 16.0; float sub1 = sub0 * sub0 * sub0 * .000000641; - float sub2 = sub1 > 0.0088564516790356308 ? sub1 : L / 903.2962962962963; + float sub2 = mix(L / 903.2962962962963, sub1, step(0.0088564516790356308, sub1)); vec3 top1 = (284517.0 * m2[0] - 94839.0 * m2[2]) * sub2; vec3 bottom = (632260.0 * m2[2] - 126452.0 * m2[1]) * sub2; @@ -65,7 +65,7 @@ float hsluv_maxChromaForLH(float L, float H) { -0.49861076029300328 , 0.041555057407175613, 1.0569715142428786 ); float sub1 = pow(L + 16.0, 3.0) / 1560896.0; - float sub2 = sub1 > 0.0088564516790356308 ? sub1 : L / 903.2962962962963; + float sub2 = mix(L / 903.2962962962963, sub1, step(0.0088564516790356308, sub1)); vec3 top1 = (284517.0 * m2[0] - 94839.0 * m2[2]) * sub2; vec3 bottom = (632260.0 * m2[2] - 126452.0 * m2[1]) * sub2; @@ -88,27 +88,20 @@ float hsluv_maxChromaForLH(float L, float H) { lengths1.b))))); } -float hsluv_fromLinear(float c) { - return c <= 0.0031308 ? 12.92 * c : 1.055 * pow(c, 1.0 / 2.4) - 0.055; -} vec3 hsluv_fromLinear(vec3 c) { - return vec3( hsluv_fromLinear(c.r), hsluv_fromLinear(c.g), hsluv_fromLinear(c.b) ); -} - -float hsluv_toLinear(float c) { - return c > 0.04045 ? pow((c + 0.055) / (1.0 + 0.055), 2.4) : c / 12.92; + return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c)); } vec3 hsluv_toLinear(vec3 c) { - return vec3( hsluv_toLinear(c.r), hsluv_toLinear(c.g), hsluv_toLinear(c.b) ); + return mix(c / 12.92, pow((c + 0.055) / (1.0 + 0.055), vec3(2.4)), step(0.04045, c)); } float hsluv_yToL(float Y){ - return Y <= 0.0088564516790356308 ? Y * 903.2962962962963 : 116.0 * pow(Y, 1.0 / 3.0) - 16.0; + return mix(Y * 903.2962962962963, 116.0 * pow(Y, 1.0 / 3.0) - 16.0, step(0.0088564516790356308, Y)); } float hsluv_lToY(float L) { - return L <= 8.0 ? L / 903.2962962962963 : pow((L + 16.0) / 116.0, 3.0); + return mix(L / 903.2962962962963, pow((L + 16.0) / 116.0, 3.0), step(8.0, L)); } vec3 xyzToRgb(vec3 tuple) { @@ -116,7 +109,6 @@ vec3 xyzToRgb(vec3 tuple) { 3.2409699419045214 ,-1.5373831775700935 ,-0.49861076029300328 , -0.96924363628087983 , 1.8759675015077207 , 0.041555057407175613, 0.055630079696993609,-0.20397695888897657, 1.0569715142428786 ); - return hsluv_fromLinear(tuple*m); } @@ -166,9 +158,7 @@ vec3 luvToLch(vec3 tuple) { float C = length(tuple.yz); float H = degrees(atan(V,U)); - if (H < 0.0) { - H = 360.0 + H; - } + H += 360.0 * step(H, 0.0); return vec3(L, C, H); } @@ -192,16 +182,6 @@ vec3 lchToHsluv(vec3 tuple) { return tuple.bgr; } -vec3 hpluvToLch(vec3 tuple) { - tuple.g *= hsluv_maxSafeChromaForL(tuple.b) * .01; - return tuple.bgr; -} - -vec3 lchToHpluv(vec3 tuple) { - tuple.g /= hsluv_maxSafeChromaForL(tuple.r) * .01; - return tuple.bgr; -} - vec3 lchToRgb(vec3 tuple) { return xyzToRgb(luvToXyz(lchToLuv(tuple))); } @@ -218,73 +198,6 @@ vec3 rgbToHsluv(vec3 tuple) { return lchToHsluv(rgbToLch(tuple)); } -vec3 hpluvToRgb(vec3 tuple) { - return lchToRgb(hpluvToLch(tuple)); -} - -vec3 rgbToHpluv(vec3 tuple) { - return lchToHpluv(rgbToLch(tuple)); -} - vec3 luvToRgb(vec3 tuple){ return xyzToRgb(luvToXyz(tuple)); } - -// allow vec4's -vec4 xyzToRgb(vec4 c) {return vec4( xyzToRgb( vec3(c.x,c.y,c.z) ), c.a);} -vec4 rgbToXyz(vec4 c) {return vec4( rgbToXyz( vec3(c.x,c.y,c.z) ), c.a);} -vec4 xyzToLuv(vec4 c) {return vec4( xyzToLuv( vec3(c.x,c.y,c.z) ), c.a);} -vec4 luvToXyz(vec4 c) {return vec4( luvToXyz( vec3(c.x,c.y,c.z) ), c.a);} -vec4 luvToLch(vec4 c) {return vec4( luvToLch( vec3(c.x,c.y,c.z) ), c.a);} -vec4 lchToLuv(vec4 c) {return vec4( lchToLuv( vec3(c.x,c.y,c.z) ), c.a);} -vec4 hsluvToLch(vec4 c) {return vec4( hsluvToLch( vec3(c.x,c.y,c.z) ), c.a);} -vec4 lchToHsluv(vec4 c) {return vec4( lchToHsluv( vec3(c.x,c.y,c.z) ), c.a);} -vec4 hpluvToLch(vec4 c) {return vec4( hpluvToLch( vec3(c.x,c.y,c.z) ), c.a);} -vec4 lchToHpluv(vec4 c) {return vec4( lchToHpluv( vec3(c.x,c.y,c.z) ), c.a);} -vec4 lchToRgb(vec4 c) {return vec4( lchToRgb( vec3(c.x,c.y,c.z) ), c.a);} -vec4 rgbToLch(vec4 c) {return vec4( rgbToLch( vec3(c.x,c.y,c.z) ), c.a);} -vec4 hsluvToRgb(vec4 c) {return vec4( hsluvToRgb( vec3(c.x,c.y,c.z) ), c.a);} -vec4 rgbToHsluv(vec4 c) {return vec4( rgbToHsluv( vec3(c.x,c.y,c.z) ), c.a);} -vec4 hpluvToRgb(vec4 c) {return vec4( hpluvToRgb( vec3(c.x,c.y,c.z) ), c.a);} -vec4 rgbToHpluv(vec4 c) {return vec4( rgbToHpluv( vec3(c.x,c.y,c.z) ), c.a);} -vec4 luvToRgb(vec4 c) {return vec4( luvToRgb( vec3(c.x,c.y,c.z) ), c.a);} -// allow 3 floats -vec3 xyzToRgb(float x, float y, float z) {return xyzToRgb( vec3(x,y,z) );} -vec3 rgbToXyz(float x, float y, float z) {return rgbToXyz( vec3(x,y,z) );} -vec3 xyzToLuv(float x, float y, float z) {return xyzToLuv( vec3(x,y,z) );} -vec3 luvToXyz(float x, float y, float z) {return luvToXyz( vec3(x,y,z) );} -vec3 luvToLch(float x, float y, float z) {return luvToLch( vec3(x,y,z) );} -vec3 lchToLuv(float x, float y, float z) {return lchToLuv( vec3(x,y,z) );} -vec3 hsluvToLch(float x, float y, float z) {return hsluvToLch( vec3(x,y,z) );} -vec3 lchToHsluv(float x, float y, float z) {return lchToHsluv( vec3(x,y,z) );} -vec3 hpluvToLch(float x, float y, float z) {return hpluvToLch( vec3(x,y,z) );} -vec3 lchToHpluv(float x, float y, float z) {return lchToHpluv( vec3(x,y,z) );} -vec3 lchToRgb(float x, float y, float z) {return lchToRgb( vec3(x,y,z) );} -vec3 rgbToLch(float x, float y, float z) {return rgbToLch( vec3(x,y,z) );} -vec3 hsluvToRgb(float x, float y, float z) {return hsluvToRgb( vec3(x,y,z) );} -vec3 rgbToHsluv(float x, float y, float z) {return rgbToHsluv( vec3(x,y,z) );} -vec3 hpluvToRgb(float x, float y, float z) {return hpluvToRgb( vec3(x,y,z) );} -vec3 rgbToHpluv(float x, float y, float z) {return rgbToHpluv( vec3(x,y,z) );} -vec3 luvToRgb(float x, float y, float z) {return luvToRgb( vec3(x,y,z) );} -// allow 4 floats -vec4 xyzToRgb(float x, float y, float z, float a) {return xyzToRgb( vec4(x,y,z,a) );} -vec4 rgbToXyz(float x, float y, float z, float a) {return rgbToXyz( vec4(x,y,z,a) );} -vec4 xyzToLuv(float x, float y, float z, float a) {return xyzToLuv( vec4(x,y,z,a) );} -vec4 luvToXyz(float x, float y, float z, float a) {return luvToXyz( vec4(x,y,z,a) );} -vec4 luvToLch(float x, float y, float z, float a) {return luvToLch( vec4(x,y,z,a) );} -vec4 lchToLuv(float x, float y, float z, float a) {return lchToLuv( vec4(x,y,z,a) );} -vec4 hsluvToLch(float x, float y, float z, float a) {return hsluvToLch( vec4(x,y,z,a) );} -vec4 lchToHsluv(float x, float y, float z, float a) {return lchToHsluv( vec4(x,y,z,a) );} -vec4 hpluvToLch(float x, float y, float z, float a) {return hpluvToLch( vec4(x,y,z,a) );} -vec4 lchToHpluv(float x, float y, float z, float a) {return lchToHpluv( vec4(x,y,z,a) );} -vec4 lchToRgb(float x, float y, float z, float a) {return lchToRgb( vec4(x,y,z,a) );} -vec4 rgbToLch(float x, float y, float z, float a) {return rgbToLch( vec4(x,y,z,a) );} -vec4 hsluvToRgb(float x, float y, float z, float a) {return hsluvToRgb( vec4(x,y,z,a) );} -vec4 rgbToHslul(float x, float y, float z, float a) {return rgbToHsluv( vec4(x,y,z,a) );} -vec4 hpluvToRgb(float x, float y, float z, float a) {return hpluvToRgb( vec4(x,y,z,a) );} -vec4 rgbToHpluv(float x, float y, float z, float a) {return rgbToHpluv( vec4(x,y,z,a) );} -vec4 luvToRgb(float x, float y, float z, float a) {return luvToRgb( vec4(x,y,z,a) );} - -/* -END HSLUV-GLSL -*/ From a6379d9003e049ec3c64547ef60e6a46f8ccf9c6 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Thu, 13 Mar 2025 15:01:43 +0100 Subject: [PATCH 05/10] fix: prevent nan/inf in hsluv conversion --- kitty/hsluv.glsl | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/kitty/hsluv.glsl b/kitty/hsluv.glsl index 0b8fdaeaa..14c13e935 100644 --- a/kitty/hsluv.glsl +++ b/kitty/hsluv.glsl @@ -7,6 +7,14 @@ Put this code in your fragment shader. // stripped down and optimized (branchless) version +float divide(float num, float denom) { + return num / (abs(denom) + 1e-15) * sign(denom); +} + +vec3 divide(vec3 num, vec3 denom) { + return num / (abs(denom) + 1e-15) * sign(denom); +} + vec3 hsluv_intersectLineLine(vec3 line1x, vec3 line1y, vec3 line2x, vec3 line2y) { return (line1y - line2y) / (line2x - line1x); } @@ -16,7 +24,7 @@ vec3 hsluv_distanceFromPole(vec3 pointx,vec3 pointy) { } vec3 hsluv_lengthOfRayUntilIntersect(float theta, vec3 x, vec3 y) { - vec3 len = y / (sin(theta) - x * cos(theta)); + vec3 len = divide(y, sin(theta) - x * cos(theta)); len = mix(len, vec3(1000.0), step(len, vec3(0.0))); return len; } @@ -89,19 +97,19 @@ float hsluv_maxChromaForLH(float L, float H) { } vec3 hsluv_fromLinear(vec3 c) { - return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c)); + return mix(c * 12.92, 1.055 * pow(max(c, vec3(0)), vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c)); } vec3 hsluv_toLinear(vec3 c) { - return mix(c / 12.92, pow((c + 0.055) / (1.0 + 0.055), vec3(2.4)), step(0.04045, c)); + return mix(c / 12.92, pow(max((c + 0.055) / (1.0 + 0.055), vec3(0)), vec3(2.4)), step(0.04045, c)); } float hsluv_yToL(float Y){ - return mix(Y * 903.2962962962963, 116.0 * pow(Y, 1.0 / 3.0) - 16.0, step(0.0088564516790356308, Y)); + return mix(Y * 903.2962962962963, 116.0 * pow(max(Y, 0), 1.0 / 3.0) - 16.0, step(0.0088564516790356308, Y)); } float hsluv_lToY(float L) { - return mix(L / 903.2962962962963, pow((L + 16.0) / 116.0, 3.0), step(8.0, L)); + return mix(L / 903.2962962962963, pow((max(L, 0) + 16.0) / 116.0, 3.0), step(8.0, L)); } vec3 xyzToRgb(vec3 tuple) { @@ -127,8 +135,7 @@ vec3 xyzToLuv(vec3 tuple){ float Z = tuple.z; float L = hsluv_yToL(Y); - - float div = 1./dot(tuple,vec3(1,15,3)); + float div = 1. / max(dot(tuple, vec3(1, 15, 3)), 1e-15); return vec3( 1., @@ -141,8 +148,8 @@ vec3 xyzToLuv(vec3 tuple){ vec3 luvToXyz(vec3 tuple) { float L = tuple.x; - float U = tuple.y / (13.0 * L) + 0.19783000664283681; - float V = tuple.z / (13.0 * L) + 0.468319994938791; + float U = divide(tuple.y, 13.0 * L) + 0.19783000664283681; + float V = divide(tuple.z, 13.0 * L) + 0.468319994938791; float Y = hsluv_lToY(L); float X = 2.25 * U * Y / V; @@ -178,7 +185,7 @@ vec3 hsluvToLch(vec3 tuple) { } vec3 lchToHsluv(vec3 tuple) { - tuple.g /= hsluv_maxChromaForLH(tuple.r, tuple.b) * .01; + tuple.g = divide(tuple.g, hsluv_maxChromaForLH(tuple.r, tuple.b) * .01); return tuple.bgr; } From d09327581e7f1228026c55aceeebbe41548bc3f6 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Thu, 13 Mar 2025 15:10:11 +0100 Subject: [PATCH 06/10] docs: add min contrast ratio --- kitty/options/definition.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/kitty/options/definition.py b/kitty/options/definition.py index eeb8ea77b..624c4f814 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -266,20 +266,30 @@ ''') opt('text_fg_override_threshold', 0, option_type='float', long_text=''' -The minimum accepted difference in luminance between the foreground and background +A setting to prevent low contrast scenarios, configurable in two different modes (positive and negative value). +The default value is :code:`0`, which means no overriding is performed. Useful when working with applications +that use colors that do not contrast well with your preferred color scheme. + +A positive value represents the minimum accepted difference in luminance between the foreground and background color, below which kitty will override the foreground color. It is percentage ranging from :code:`0` to :code:`100`. If the difference in luminance of the foreground and background is below this threshold, the foreground color will be set -to white if the background is dark or black if the background is light. The default -value is :code:`0`, which means no overriding is performed. Useful when working with applications -that use colors that do not contrast well with your preferred color scheme. +to white if the background is dark or black if the background is light. + +A negative value represents the minimum accepted contrast ratio between the foreground and background color. +Possible values range from :code:`-0.0` to :code:`-21.0`. +To for example meet :link:`WCAG level AA ` +a value of :code:`-4.5` can be provided. +The algorithm is implemented using :link:`HSLuv ` which enables it to change +the perceived lightness of a color just as much as needed without really changing its hue and saturation. WARNING: Some programs use characters (such as block characters) for graphics display and may expect to be able to set the foreground and background to the same color (or similar colors). If you see unexpected stripes, dots, lines, incorrect color, no color where you expect color, or any kind of graphic display problem try setting :opt:`text_fg_override_threshold` to :code:`0` to -see if this is the cause of the problem. +see if this is the cause of the problem or consider the minimum contrast ratio (negative value) +over the minimum difference as it implements a basic workaround for this scenario. ''') egr() # }}} From a6fcdf19646120fdb22dc348f8630ecfb8cd5a93 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Fri, 14 Mar 2025 12:32:57 +0100 Subject: [PATCH 07/10] minor: replace boolean in `mix` with `step` --- kitty/cell_fragment.glsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kitty/cell_fragment.glsl b/kitty/cell_fragment.glsl index f201a1457..4dbff97db 100644 --- a/kitty/cell_fragment.glsl +++ b/kitty/cell_fragment.glsl @@ -104,7 +104,7 @@ vec3 apply_min_contrast_ratio(float under_luminance, float over_luminance, vec3 float result_a_ratio = contrast_ratio(under_luminance, dot(result_a, Y)); float result_b_ratio = contrast_ratio(under_luminance, dot(result_b, Y)); vec3 result = mix(result_a, result_b, step(result_a_ratio, result_b_ratio)); - return mix(result, over, diff.r + diff.g + diff.g < 0.001f); + return mix(result, over, step(diff.r + diff.g + diff.g, 0.001f)); } return over; } From 40ef6b4f37ae54975cf4c95e31834d2db42c8ffd Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Fri, 14 Mar 2025 14:05:34 +0100 Subject: [PATCH 08/10] config: number with unit for min contrast ratio --- kitty/conf/utils.py | 14 ++++++++++++++ kitty/options/definition.py | 14 +++++++------- kitty/options/parse.py | 17 ++++++++++++++--- kitty/options/types.py | 2 +- kitty/shaders.py | 25 ++++++++++++++++--------- 5 files changed, 52 insertions(+), 20 deletions(-) diff --git a/kitty/conf/utils.py b/kitty/conf/utils.py index 72d925559..ab9018808 100644 --- a/kitty/conf/utils.py +++ b/kitty/conf/utils.py @@ -22,6 +22,7 @@ from ..utils import expandvars, log_error, shlex_split key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$') +number_unit_pat = re.compile(r'\s*([-+]?\d+\.?\d*)\s*([^\d\s]*)?') ItemParser = Callable[[str, str, dict[str, Any]], bool] T = TypeVar('T') @@ -66,6 +67,19 @@ def unit_float(x: ConvertibleToNumbers) -> float: return max(0, min(float(x), 1)) +def number_with_unit(x: str, default_unit: str = '') -> tuple[float, str]: + mat = number_unit_pat.match(x) + exception = None + if mat is not None: + try: + value, unit = float(mat.group(1)), mat.group(2) + unit = default_unit if not unit else unit + return value, unit + except Exception as e: + exception = e + raise ValueError(f'Invalid number with unit config option provided: {x if exception is None else exception}') + + def to_bool(x: str) -> bool: return x.lower() in ('y', 'yes', 'true') diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 624c4f814..7a28d1764 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -265,21 +265,21 @@ and adjust the first parameter until the perceived thickness matches the dark theme. ''') -opt('text_fg_override_threshold', 0, option_type='float', long_text=''' -A setting to prevent low contrast scenarios, configurable in two different modes (positive and negative value). +opt('text_fg_override_threshold', '0 %', option_type='number_with_unit', long_text=''' +A setting to prevent low contrast scenarios, configurable in two different modes (suffix :code:` %` and suffix :code:` ratio`). The default value is :code:`0`, which means no overriding is performed. Useful when working with applications that use colors that do not contrast well with your preferred color scheme. -A positive value represents the minimum accepted difference in luminance between the foreground and background +A value with the suffix :code:` %` represents the minimum accepted difference in luminance between the foreground and background color, below which kitty will override the foreground color. It is percentage -ranging from :code:`0` to :code:`100`. If the difference in luminance of the +ranging from :code:`0 %` to :code:`100 %`. If the difference in luminance of the foreground and background is below this threshold, the foreground color will be set to white if the background is dark or black if the background is light. -A negative value represents the minimum accepted contrast ratio between the foreground and background color. -Possible values range from :code:`-0.0` to :code:`-21.0`. +A value with the suffix :code:` ratio` represents the minimum accepted contrast ratio between the foreground and background color. +Possible values range from :code:`0.0 ratio` to :code:`21.0 ratio`. To for example meet :link:`WCAG level AA ` -a value of :code:`-4.5` can be provided. +a value of :code:`4.5 ratio` can be provided. The algorithm is implemented using :link:`HSLuv ` which enables it to change the perceived lightness of a color just as much as needed without really changing its hue and saturation. diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 25d6c0232..a45b730b4 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -4,8 +4,16 @@ import typing import collections.abc # noqa: F401, RUF100 from kitty.conf.utils import ( - merge_dicts, positive_float, positive_int, python_string, to_bool, to_cmdline, to_color, - to_color_or_none, unit_float + merge_dicts, + positive_float, + positive_int, + python_string, + number_with_unit, + to_bool, + to_cmdline, + to_color, + to_color_or_none, + unit_float, ) from kitty.options.utils import ( action_alias, active_tab_title_template, allow_hyperlinks, bell_on_tab, box_drawing_scale, @@ -1324,7 +1332,10 @@ def text_composition_strategy(self, val: str, ans: dict[str, typing.Any]) -> Non ans['text_composition_strategy'] = str(val) def text_fg_override_threshold(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['text_fg_override_threshold'] = float(val) + value, unit = number_with_unit(val, default_unit='%') + if unit not in ['%', 'ratio']: + raise ValueError(f'The unit {unit} is not a valid choice for text_fg_override_threshold') + ans['text_fg_override_threshold'] = value, unit def touch_scroll_multiplier(self, val: str, ans: dict[str, typing.Any]) -> None: ans['touch_scroll_multiplier'] = float(val) diff --git a/kitty/options/types.py b/kitty/options/types.py index ae04efde0..1fef0f042 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -612,7 +612,7 @@ class Options: term: str = 'xterm-kitty' terminfo_type: choices_for_terminfo_type = 'path' text_composition_strategy: str = 'platform' - text_fg_override_threshold: float = 0.0 + text_fg_override_threshold: tuple[float, str] = 0.0, '%' touch_scroll_multiplier: float = 1.0 transparent_background_colors: tuple[tuple[kitty.fast_data_types.Color, float], ...] = () undercurl_style: choices_for_undercurl_style = 'thin-sparse' diff --git a/kitty/shaders.py b/kitty/shaders.py index b75c3ef7e..92b1cebff 100644 --- a/kitty/shaders.py +++ b/kitty/shaders.py @@ -131,8 +131,8 @@ def __call__(self, src: str) -> str: class LoadShaderPrograms: - text_fg_override_threshold: float = 0 + text_fg_override_threshold_unit: str = '%' text_old_gamma: bool = False semi_transparent: bool = False cell_program_replacer: MultiReplacer = null_replacer @@ -140,7 +140,10 @@ class LoadShaderPrograms: @property def needs_recompile(self) -> bool: opts = get_options() - return opts.text_fg_override_threshold != self.text_fg_override_threshold or (opts.text_composition_strategy == 'legacy') != self.text_old_gamma + return ( + opts.text_fg_override_threshold != (self.text_fg_override_threshold, self.text_fg_override_threshold_unit) + or (opts.text_composition_strategy == 'legacy') != self.text_old_gamma + ) def recompile_if_needed(self) -> None: if self.needs_recompile: @@ -150,10 +153,14 @@ def __call__(self, semi_transparent: bool = False, allow_recompile: bool = False self.semi_transparent = semi_transparent opts = get_options() self.text_old_gamma = opts.text_composition_strategy == 'legacy' - 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 + + self.text_fg_override_threshold, self.text_fg_override_threshold_unit = opts.text_fg_override_threshold + match self.text_fg_override_threshold_unit: + case '%': + self.text_fg_override_threshold = max(0, min(self.text_fg_override_threshold, 100.0)) * 0.01 + case 'ratio': + self.text_fg_override_threshold = max(0, min(self.text_fg_override_threshold, 21.0)) + cell = program_for('cell') if self.cell_program_replacer is null_replacer: self.cell_program_replacer = MultiReplacer( @@ -171,9 +178,9 @@ def resolve_cell_defines(which: str, src: str) -> str: 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['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['FG_OVERRIDE'] = str(int(bool(self.text_fg_override_threshold_unit == '%'))) + r['MIN_CONTRAST_RATIO'] = str(self.text_fg_override_threshold) + r['APPLY_MIN_CONTRAST_RATIO'] = str(int(bool(self.text_fg_override_threshold_unit == 'ratio'))) r['TEXT_NEW_GAMMA'] = '0' if self.text_old_gamma else '1' return self.cell_program_replacer(src) From e1b454e49e004fa82433a58ad5641ec384e19a86 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Sat, 15 Mar 2025 13:28:36 +0100 Subject: [PATCH 09/10] fix: generate config min contrast --- kitty/options/definition.py | 2 +- kitty/options/parse.py | 25 +++++++------------------ kitty/options/types.py | 2 +- kitty/options/utils.py | 8 ++++++++ 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 7a28d1764..feb06e23a 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -265,7 +265,7 @@ and adjust the first parameter until the perceived thickness matches the dark theme. ''') -opt('text_fg_override_threshold', '0 %', option_type='number_with_unit', long_text=''' +opt('text_fg_override_threshold', '0 %', option_type='text_fg_override_threshold', long_text=''' A setting to prevent low contrast scenarios, configurable in two different modes (suffix :code:` %` and suffix :code:` ratio`). The default value is :code:`0`, which means no overriding is performed. Useful when working with applications that use colors that do not contrast well with your preferred color scheme. diff --git a/kitty/options/parse.py b/kitty/options/parse.py index a45b730b4..2f3fcf4a3 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -4,16 +4,8 @@ import typing import collections.abc # noqa: F401, RUF100 from kitty.conf.utils import ( - merge_dicts, - positive_float, - positive_int, - python_string, - number_with_unit, - to_bool, - to_cmdline, - to_color, - to_color_or_none, - unit_float, + merge_dicts, positive_float, positive_int, python_string, to_bool, to_cmdline, to_color, + to_color_or_none, unit_float ) from kitty.options.utils import ( action_alias, active_tab_title_template, allow_hyperlinks, bell_on_tab, box_drawing_scale, @@ -27,10 +19,10 @@ pointer_shape_when_dragging, remote_control_password, resize_debounce_time, scrollback_lines, scrollback_pager_history_size, shell_integration, store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge, tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, - tab_title_template, titlebar_color, to_cursor_shape, to_cursor_unfocused_shape, to_font_size, - to_layout_names, to_modifiers, transparent_background_colors, underline_exclusion, url_prefixes, - url_style, visual_bell_duration, visual_window_select_characters, window_border_width, - window_logo_scale, window_size + tab_title_template, text_fg_override_threshold, titlebar_color, to_cursor_shape, + to_cursor_unfocused_shape, to_font_size, to_layout_names, to_modifiers, + transparent_background_colors, underline_exclusion, url_prefixes, url_style, visual_bell_duration, + visual_window_select_characters, window_border_width, window_logo_scale, window_size ) @@ -1332,10 +1324,7 @@ def text_composition_strategy(self, val: str, ans: dict[str, typing.Any]) -> Non ans['text_composition_strategy'] = str(val) def text_fg_override_threshold(self, val: str, ans: dict[str, typing.Any]) -> None: - value, unit = number_with_unit(val, default_unit='%') - if unit not in ['%', 'ratio']: - raise ValueError(f'The unit {unit} is not a valid choice for text_fg_override_threshold') - ans['text_fg_override_threshold'] = value, unit + ans['text_fg_override_threshold'] = text_fg_override_threshold(val) def touch_scroll_multiplier(self, val: str, ans: dict[str, typing.Any]) -> None: ans['touch_scroll_multiplier'] = float(val) diff --git a/kitty/options/types.py b/kitty/options/types.py index 1fef0f042..1e6aff7df 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -612,7 +612,7 @@ class Options: term: str = 'xterm-kitty' terminfo_type: choices_for_terminfo_type = 'path' text_composition_strategy: str = 'platform' - text_fg_override_threshold: tuple[float, str] = 0.0, '%' + text_fg_override_threshold: tuple[float, str] = (0.0, '%') touch_scroll_multiplier: float = 1.0 transparent_background_colors: tuple[tuple[kitty.fast_data_types.Color, float], ...] = () undercurl_style: choices_for_undercurl_style = 'thin-sparse' diff --git a/kitty/options/utils.py b/kitty/options/utils.py index cf90c7503..91691ab0f 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -26,6 +26,7 @@ KeyAction, KeyFuncWrapper, currently_parsing, + number_with_unit, percent, positive_float, positive_int, @@ -752,6 +753,13 @@ def active_tab_title_template(x: str) -> str | None: return None if x == 'none' else x +def text_fg_override_threshold(x: str) -> tuple[float, str]: + value, unit = number_with_unit(x, default_unit='%') + if unit not in ['%', 'ratio']: + raise ValueError(f'The unit {unit} is not a valid choice for text_fg_override_threshold') + return value, unit + + ClearOn = Literal['next', 'focus'] default_clear_on: tuple[ClearOn, ...] = 'focus', 'next' all_clear_on = get_args(ClearOn) From 99016720c33c9cb797de8add11ef2f812d473a69 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Sat, 15 Mar 2025 13:42:31 +0100 Subject: [PATCH 10/10] perf: branchless min contrast ratio --- kitty/cell_fragment.glsl | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/kitty/cell_fragment.glsl b/kitty/cell_fragment.glsl index 4dbff97db..df908a008 100644 --- a/kitty/cell_fragment.glsl +++ b/kitty/cell_fragment.glsl @@ -94,19 +94,16 @@ float contrast_ratio(float under_luminance, float over_luminance) { } vec3 apply_min_contrast_ratio(float under_luminance, float over_luminance, vec3 under, vec3 over) { float ratio = contrast_ratio(under_luminance, over_luminance); - if (ratio < MIN_CONTRAST_RATIO) { - vec3 diff = abs(under - over); - vec3 over_hsluv = rgbToHsluv(over); - float target_lum_a = clamp((under_luminance + 0.05f) * MIN_CONTRAST_RATIO - 0.05f, 0.f, 1.f); - float target_lum_b = clamp((under_luminance + 0.05f) / MIN_CONTRAST_RATIO - 0.05f, 0.f, 1.f); - vec3 result_a = clamp(hsluvToRgb(vec3(over_hsluv.x, over_hsluv.y, target_lum_a * 100.f)), 0.f, 1.f); - vec3 result_b = clamp(hsluvToRgb(vec3(over_hsluv.x, over_hsluv.y, target_lum_b * 100.f)), 0.f, 1.f); - float result_a_ratio = contrast_ratio(under_luminance, dot(result_a, Y)); - float result_b_ratio = contrast_ratio(under_luminance, dot(result_b, Y)); - vec3 result = mix(result_a, result_b, step(result_a_ratio, result_b_ratio)); - return mix(result, over, step(diff.r + diff.g + diff.g, 0.001f)); - } - return over; + vec3 diff = abs(under - over); + vec3 over_hsluv = rgbToHsluv(over); + float target_lum_a = clamp((under_luminance + 0.05f) * MIN_CONTRAST_RATIO - 0.05f, 0.f, 1.f); + float target_lum_b = clamp((under_luminance + 0.05f) / MIN_CONTRAST_RATIO - 0.05f, 0.f, 1.f); + vec3 result_a = clamp(hsluvToRgb(vec3(over_hsluv.x, over_hsluv.y, target_lum_a * 100.f)), 0.f, 1.f); + vec3 result_b = clamp(hsluvToRgb(vec3(over_hsluv.x, over_hsluv.y, target_lum_b * 100.f)), 0.f, 1.f); + float result_a_ratio = contrast_ratio(under_luminance, dot(result_a, Y)); + float result_b_ratio = contrast_ratio(under_luminance, dot(result_b, Y)); + vec3 result = mix(result_a, result_b, step(result_a_ratio, result_b_ratio)); + return mix(result, over, max(step(diff.r + diff.g + diff.g, 0.001f), step(MIN_CONTRAST_RATIO, ratio))); } #endif