diff --git a/kitty/cell_fragment.glsl b/kitty/cell_fragment.glsl index 247419cc2..599bb0c90 100644 --- a/kitty/cell_fragment.glsl +++ b/kitty/cell_fragment.glsl @@ -20,6 +20,9 @@ in float bg_alpha; #ifdef NEEDS_FOREGROUND uniform sampler2DArray sprites; +uniform int text_old_gamma; +uniform float text_contrast; +uniform float text_gamma_adjustment; in float effective_text_alpha; in vec3 sprite_pos; in vec3 underline_pos; @@ -100,6 +103,8 @@ vec4 vec4_premul(vec4 rgba) { #ifdef NEEDS_FOREGROUND // sRGB luminance values const vec3 Y = vec3(0.2126, 0.7152, 0.0722); +// Scaling factor for the extra text-alpha adjustment for luminance-difference. +const float text_gamma_scaling = 0.5; float linear2srgb(float x) { // Approximation of linear-to-sRGB conversion @@ -116,6 +121,15 @@ float clamp(float x) { return max(min(x, 1.0f), 0.0f); } +vec4 foreground_contrast(vec4 over, vec3 under) { + float underL = dot(under, Y); + float overL = dot(over.rgb, Y); + // 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 sasturation. + over.a = clamp(mix(over.a, pow(over.a, text_gamma_adjustment), (1 - overL + underL) * text_gamma_scaling) * text_contrast); + return over; +} + vec4 foreground_contrast_incorrect(vec4 over, vec3 under) { // Simulation of gamma-incorrect blending float underL = dot(under, Y); @@ -150,8 +164,10 @@ vec4 calculate_foreground() { return foreground_with_decorations(text_fg); } vec4 calculate_foreground(vec3 bg) { + // When rendering on a background we can adjust the alpha channel contrast + // to improve legibility based on the source and destination colors vec4 text_fg = foreground_color(); - text_fg = foreground_contrast_incorrect(text_fg, bg); + text_fg = mix(foreground_contrast(text_fg, bg), foreground_contrast_incorrect(text_fg, bg), text_old_gamma); return foreground_with_decorations(text_fg); } diff --git a/kitty/options/definition.py b/kitty/options/definition.py index fb9a24b6d..7a92ef0b1 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -230,6 +230,41 @@ The style with which undercurls are rendered. This option takes the form :code:`(thin|thick)-(sparse|dense)`. Thin and thick control the thickness of the undercurl. Sparse and dense control how often the curl oscillates. With sparse the curl will peak once per character, with dense twice. +''' + ) + +opt('text_old_gamma', 'no', + option_type='to_bool', ctype='bool', + long_text=''' +If to simulate the old gamma-incorrect blending for the text alpha-channel, this +will make some text appear like the strokes are uneven. Dark text on bright backgrounds +will also look thicker while lighter text on darker backgrounds will look thinner. +''' + ) + +opt('text_gamma_adjustment', '1.0', + option_type='positive_float', ctype='float', + long_text=''' +This setting adjusts the thickness of darker text on lighter backgrounds. Increasing the value +setting will make the text appear thicker while decreasing the value will make it thinner. It +can compensate for some fonts looking too-thin when using the gamma-correct alpha blending. + +The result is scaled based on the luminance difference between the background and the foreground. +Dark text on light backgrounds receive the full impact of the curve while light text on dark +backgrounds are affected very little. + +Range: >=0.01 +MacOS: 1.7 +''' + ) + +opt('text_contrast', '0', + option_type='positive_float', ctype='float', + long_text=''' +Additional multiplicative text contrast as a percentage, will saturate and cause jagged edges if set too high. + +Range: >=0.0 +MacOS: 30 ''' ) egr() # }}} diff --git a/kitty/options/parse.py b/kitty/options/parse.py index bb3180f19..237c19fae 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -1278,6 +1278,15 @@ class Parser: def term(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['term'] = str(val) + def text_contrast(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + ans['text_contrast'] = positive_float(val) + + def text_gamma_adjustment(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + ans['text_gamma_adjustment'] = positive_float(val) + + def text_old_gamma(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + ans['text_old_gamma'] = to_bool(val) + def touch_scroll_multiplier(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['touch_scroll_multiplier'] = float(val) diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index 665a31d61..633f9ab31 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -57,6 +57,45 @@ convert_from_opts_modify_font(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_text_old_gamma(PyObject *val, Options *opts) { + opts->text_old_gamma = PyObject_IsTrue(val); +} + +static void +convert_from_opts_text_old_gamma(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "text_old_gamma"); + if (ret == NULL) return; + convert_from_python_text_old_gamma(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_text_gamma_adjustment(PyObject *val, Options *opts) { + opts->text_gamma_adjustment = PyFloat_AsFloat(val); +} + +static void +convert_from_opts_text_gamma_adjustment(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "text_gamma_adjustment"); + if (ret == NULL) return; + convert_from_python_text_gamma_adjustment(ret, opts); + Py_DECREF(ret); +} + +static void +convert_from_python_text_contrast(PyObject *val, Options *opts) { + opts->text_contrast = PyFloat_AsFloat(val); +} + +static void +convert_from_opts_text_contrast(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "text_contrast"); + if (ret == NULL) return; + convert_from_python_text_contrast(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_cursor_shape(PyObject *val, Options *opts) { opts->cursor_shape = PyLong_AsLong(val); @@ -1042,6 +1081,12 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_modify_font(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_text_old_gamma(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_text_gamma_adjustment(py_opts, opts); + if (PyErr_Occurred()) return false; + convert_from_opts_text_contrast(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_cursor_shape(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_cursor_beam_thickness(py_opts, opts); diff --git a/kitty/options/types.py b/kitty/options/types.py index b37808287..6e380be1b 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -443,6 +443,9 @@ option_names = ( # {{{ 'tab_title_max_length', 'tab_title_template', 'term', + 'text_contrast', + 'text_gamma_adjustment', + 'text_old_gamma', 'touch_scroll_multiplier', 'undercurl_style', 'update_check_interval', @@ -594,6 +597,9 @@ class Options: tab_title_max_length: int = 0 tab_title_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{title}' term: str = 'xterm-kitty' + text_contrast: float = 0 + text_gamma_adjustment: float = 1.0 + text_old_gamma: bool = False touch_scroll_multiplier: float = 1.0 undercurl_style: choices_for_undercurl_style = 'thin-sparse' update_check_interval: float = 24.0 diff --git a/kitty/shaders.c b/kitty/shaders.c index 05bf279be..3af305e7d 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -569,6 +569,12 @@ set_cell_uniforms(float current_inactive_text_alpha, bool force) { S(CELL_PROGRAM, sprites, SPRITE_MAP_UNIT, 1i); S(CELL_FG_PROGRAM, sprites, SPRITE_MAP_UNIT, 1i); S(CELL_PROGRAM, dim_opacity, OPT(dim_opacity), 1f); S(CELL_FG_PROGRAM, dim_opacity, OPT(dim_opacity), 1f); S(CELL_BG_PROGRAM, defaultbg, OPT(background), 1f); + int text_old_gamma = OPT(text_old_gamma) ? 1 : 0; + S(CELL_PROGRAM, text_old_gamma, text_old_gamma, 1i); S(CELL_FG_PROGRAM, text_old_gamma, text_old_gamma, 1i); + float text_contrast = 1.0f + OPT(text_contrast) * 0.01f; + S(CELL_PROGRAM, text_contrast, text_contrast, 1f); S(CELL_FG_PROGRAM, text_contrast, text_contrast, 1f); + float text_gamma_adjustment = OPT(text_gamma_adjustment) < 0.01f ? 1.0f : 1.0f / OPT(text_gamma_adjustment); + S(CELL_PROGRAM, text_gamma_adjustment, text_gamma_adjustment, 1f); S(CELL_FG_PROGRAM, text_gamma_adjustment, text_gamma_adjustment, 1f); #undef S #define SV(prog, name, num, val, type) { bind_program(prog); glUniform##type(glGetUniformLocation(program_id(prog), #name), num, val); } SV(CELL_PROGRAM, gamma_lut, 256, srgb_lut, 1fv); SV(CELL_FG_PROGRAM, gamma_lut, 256, srgb_lut, 1fv); diff --git a/kitty/state.h b/kitty/state.h index 49ee3b51e..0b49b3d5f 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -47,6 +47,8 @@ typedef struct { WindowTitleIn macos_show_window_title_in; char *bell_path; float background_opacity, dim_opacity; + float text_contrast, text_gamma_adjustment; + bool text_old_gamma; char *background_image, *default_window_logo; BackgroundImageLayout background_image_layout;