From 2a1b8c4fcc93616a182b2b026643853590be6a14 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 10 Aug 2024 12:48:12 -0400 Subject: [PATCH 1/7] gdk: Add oklab and oklch color states These are new default color states. Tests for the tf and matrices included. --- gdk/gdkcolordefs.h | 50 +++++ gdk/gdkcolorstate.c | 321 ++++++++++++++++++++++------ gdk/gdkcolorstate.h | 6 + gdk/gdkcolorstateprivate.h | 4 + testsuite/gdk/colorstate-internal.c | 3 + 5 files changed, 317 insertions(+), 67 deletions(-) diff --git a/gdk/gdkcolordefs.h b/gdk/gdkcolordefs.h index 2736353502..d356f30c9a 100644 --- a/gdk/gdkcolordefs.h +++ b/gdk/gdkcolordefs.h @@ -233,3 +233,53 @@ static const float srgb_to_rec2020[9] = { 0.069108, 0.919519, 0.011360, 0.016394, 0.088011, 0.895380, }; + +/* oklab conversion */ + +static float +from_oklab_nl (float v) +{ + return v * v * v; +} + +static float +to_oklab_nl (float v) +{ + return cbrtf (v); +} + +static const float oklab_to_lms[9] = { + 1, 0.3963377774, 0.2158037573, + 1, -0.1055613458, -0.0638541728, + 1, -0.0894841775, -1.2914855480 +}; + +static const float lms_to_srgb[9] = { + 4.0767416621, -3.3077115913, 0.2309699292, + -1.2684380046, 2.6097574011, -0.3413193965, + -0.0041960863, -0.7034186147, 1.7076147010, +}; + +static const float srgb_to_lms[9] = { + 0.4122214708, 0.5363325363, 0.0514459929, + 0.2119034982, 0.6806995451, 0.1073969566, + 0.0883024619, 0.2817188376, 0.6299787005, +}; + +static const float lms_to_oklab[9] = { + 0.2104542553, 0.7936177850, -0.0040720468, + 1.9779984951, -2.4285922050, 0.4505937099, + 0.0259040371, 0.7827717662, -0.8086757660, +}; + +static const float rec2020_to_lms[9] = { + 0.616645, 0.360250, 0.023064, + 0.265075, 0.635874, 0.099059, + 0.100076, 0.203907, 0.696161, +}; + +static const float lms_to_rec2020[9] = { + 2.140325, -1.246734, 0.106491, + -0.884665, 2.163141, -0.278489, + -0.048559, -0.454366, 1.502711, +}; diff --git a/gdk/gdkcolorstate.c b/gdk/gdkcolorstate.c index 35f5c23402..b2df7b1538 100644 --- a/gdk/gdkcolorstate.c +++ b/gdk/gdkcolorstate.c @@ -172,6 +172,18 @@ gdk_color_state_get_rec2100_linear (void) return GDK_COLOR_STATE_REC2100_LINEAR; } +GdkColorState * +gdk_color_state_get_oklab (void) +{ + return GDK_COLOR_STATE_OKLAB; +} + +GdkColorState * +gdk_color_state_get_oklch (void) +{ + return GDK_COLOR_STATE_OKLCH; +} + /** * gdk_color_state_equal: * @self: a `GdkColorState` @@ -223,56 +235,171 @@ gdk_color_state_create_cicp_params (GdkColorState *self) /* {{{ Conversion functions */ typedef float (* GdkTransferFunc) (float v); +typedef void (* GdkConvertFunc) (GdkColorState *self, + float values[4]); typedef const float GdkColorMatrix[9]; #define IDENTITY ((float*)0) #define NONE ((GdkTransferFunc)0) -#define TRANSFORM(name, eotf, matrix, oetf) \ +#define CONVERT_FUNC(name) \ static void \ -name (GdkColorState *self, \ - float (*values)[4], \ - gsize n_values) \ +gdk_convert_ ## name (GdkColorState *self, \ + float (*values)[4], \ + gsize n_values) \ { \ for (gsize i = 0; i < n_values; i++) \ { \ - if (eotf != NONE) \ - { \ - values[i][0] = eotf (values[i][0]); \ - values[i][1] = eotf (values[i][1]); \ - values[i][2] = eotf (values[i][2]); \ - } \ - if (matrix != IDENTITY) \ - { \ - float res[3]; \ - res[0] = matrix[0] * values[i][0] + matrix[1] * values[i][1] + matrix[2] * values[i][2]; \ - res[1] = matrix[3] * values[i][0] + matrix[4] * values[i][1] + matrix[5] * values[i][2]; \ - res[2] = matrix[6] * values[i][0] + matrix[7] * values[i][1] + matrix[8] * values[i][2]; \ - values[i][0] = res[0]; \ - values[i][1] = res[1]; \ - values[i][2] = res[2]; \ - } \ - if (oetf != NONE) \ - { \ - values[i][0] = oetf (values[i][0]); \ - values[i][1] = oetf (values[i][1]); \ - values[i][2] = oetf (values[i][2]); \ - } \ + name (self, values[i]); \ } \ } -TRANSFORM(gdk_default_srgb_to_srgb_linear, srgb_eotf, IDENTITY, NONE); -TRANSFORM(gdk_default_srgb_linear_to_srgb, NONE, IDENTITY, srgb_oetf) -TRANSFORM(gdk_default_rec2100_pq_to_rec2100_linear, pq_eotf, IDENTITY, NONE) -TRANSFORM(gdk_default_rec2100_linear_to_rec2100_pq, NONE, IDENTITY, pq_oetf) -TRANSFORM(gdk_default_srgb_linear_to_rec2100_linear, NONE, srgb_to_rec2020, NONE) -TRANSFORM(gdk_default_rec2100_linear_to_srgb_linear, NONE, rec2020_to_srgb, NONE) -TRANSFORM(gdk_default_srgb_to_rec2100_linear, srgb_eotf, srgb_to_rec2020, NONE) -TRANSFORM(gdk_default_rec2100_pq_to_srgb_linear, pq_eotf, rec2020_to_srgb, NONE) -TRANSFORM(gdk_default_srgb_linear_to_rec2100_pq, NONE, srgb_to_rec2020, pq_oetf) -TRANSFORM(gdk_default_rec2100_linear_to_srgb, NONE, rec2020_to_srgb, srgb_oetf) -TRANSFORM(gdk_default_srgb_to_rec2100_pq, srgb_eotf, srgb_to_rec2020, pq_oetf) -TRANSFORM(gdk_default_rec2100_pq_to_srgb, pq_eotf, rec2020_to_srgb, srgb_oetf) +#define TRANSFORM(name, eotf, matrix, nonlinear, matrix2, oetf) \ +static inline void \ +name (GdkColorState *self, \ + float values[4]) \ +{ \ + if (eotf != NONE) \ + { \ + values[0] = eotf (values[0]); \ + values[1] = eotf (values[1]); \ + values[2] = eotf (values[2]); \ + } \ + if (matrix != IDENTITY) \ + { \ + float res[3]; \ + res[0] = matrix[0] * values[0] + matrix[1] * values[1] + matrix[2] * values[2]; \ + res[1] = matrix[3] * values[0] + matrix[4] * values[1] + matrix[5] * values[2]; \ + res[2] = matrix[6] * values[0] + matrix[7] * values[1] + matrix[8] * values[2]; \ + values[0] = res[0]; \ + values[1] = res[1]; \ + values[2] = res[2]; \ + } \ + if (nonlinear != NONE) \ + { \ + values[0] = nonlinear (values[0]); \ + values[1] = nonlinear (values[1]); \ + values[2] = nonlinear (values[2]); \ + } \ + if (matrix2 != IDENTITY) \ + { \ + float res[3]; \ + res[0] = matrix2[0] * values[0] + matrix2[1] * values[1] + matrix2[2] * values[2]; \ + res[1] = matrix2[3] * values[0] + matrix2[4] * values[1] + matrix2[5] * values[2]; \ + res[2] = matrix2[6] * values[0] + matrix2[7] * values[1] + matrix2[8] * values[2]; \ + values[0] = res[0]; \ + values[1] = res[1]; \ + values[2] = res[2]; \ + } \ + if (oetf != NONE) \ + { \ + values[0] = oetf (values[0]); \ + values[1] = oetf (values[1]); \ + values[2] = oetf (values[2]); \ + } \ +} \ +CONVERT_FUNC (name) + +#define TRANSFORM_PAIR(name, func1, func2) \ +static inline void \ +name (GdkColorState *self, \ + float values[4]) \ +{ \ + func1 (self, values); \ + func2 (self, values); \ +} \ +CONVERT_FUNC (name) + +TRANSFORM(srgb_to_srgb_linear, srgb_eotf, IDENTITY, NONE, IDENTITY, NONE) +TRANSFORM(srgb_linear_to_srgb, NONE, IDENTITY, NONE, IDENTITY, srgb_oetf) +TRANSFORM(rec2100_pq_to_rec2100_linear, pq_eotf, IDENTITY, NONE, IDENTITY, NONE) +TRANSFORM(rec2100_linear_to_rec2100_pq, NONE, IDENTITY, NONE, IDENTITY, pq_oetf) +TRANSFORM(srgb_linear_to_rec2100_linear, NONE, srgb_to_rec2020, NONE, IDENTITY, NONE) +TRANSFORM(rec2100_linear_to_srgb_linear, NONE, rec2020_to_srgb, NONE, IDENTITY, NONE) +TRANSFORM(srgb_to_rec2100_linear, srgb_eotf, srgb_to_rec2020, NONE, IDENTITY, NONE) +TRANSFORM(rec2100_pq_to_srgb_linear, pq_eotf, rec2020_to_srgb, NONE, IDENTITY, NONE) +TRANSFORM(srgb_linear_to_rec2100_pq, NONE, srgb_to_rec2020, NONE, IDENTITY, pq_oetf) +TRANSFORM(rec2100_linear_to_srgb, NONE, rec2020_to_srgb, NONE, IDENTITY, srgb_oetf) +TRANSFORM(srgb_to_rec2100_pq, srgb_eotf, srgb_to_rec2020, NONE, IDENTITY, pq_oetf) +TRANSFORM(rec2100_pq_to_srgb, pq_eotf, rec2020_to_srgb, NONE, IDENTITY, srgb_oetf) + +TRANSFORM(oklab_to_srgb_linear, NONE, oklab_to_lms, from_oklab_nl, lms_to_srgb, NONE) +TRANSFORM(oklab_to_srgb, NONE, oklab_to_lms, from_oklab_nl, lms_to_srgb, srgb_oetf) +TRANSFORM(oklab_to_rec2100_linear, NONE, oklab_to_lms, from_oklab_nl, lms_to_rec2020, NONE) +TRANSFORM(oklab_to_rec2100_pq, NONE, oklab_to_lms, from_oklab_nl, lms_to_rec2020, pq_oetf) +TRANSFORM(srgb_linear_to_oklab, NONE, srgb_to_lms, to_oklab_nl, lms_to_oklab, NONE) +TRANSFORM(srgb_to_oklab, srgb_eotf, srgb_to_lms, to_oklab_nl, lms_to_oklab, NONE) +TRANSFORM(rec2100_linear_to_oklab, NONE, rec2020_to_lms, to_oklab_nl, lms_to_oklab, NONE) +TRANSFORM(rec2100_pq_to_oklab, pq_eotf, rec2020_to_lms, to_oklab_nl, lms_to_oklab, NONE) + +#define DEG_TO_RAD(x) ((x) * G_PI / 180) +#define RAD_TO_DEG(x) ((x) * 180 / G_PI) + +static inline void +_sincosf (float angle, + float *out_s, + float *out_c) +{ +#ifdef HAVE_SINCOSF + sincosf (angle, out_s, out_c); +#else + *out_s = sinf (angle); + *out_c = cosf (angle); +#endif +} + +static void +oklch_to_oklab (GdkColorState *self, + float values[4]) +{ + float L, C, H, a, b; + + L = values[0]; + C = values[1]; + H = values[2]; + + _sincosf (DEG_TO_RAD (H), &b, &a); + a *= C; + b *= C; + + values[0] = L; + values[1] = a; + values[2] = b; +} + +static void +oklab_to_oklch (GdkColorState *self, + float values[4]) +{ + float L, a, b, C, H; + + L = values[0]; + a = values[1]; + b = values[2]; + + C = hypotf (a, b); + H = RAD_TO_DEG (atan2 (b, a)); + + H = fmod (H, 360); + if (H < 0) + H += 360; + + values[0] = L; + values[1] = C; + values[2] = H; +} + +CONVERT_FUNC (oklch_to_oklab) +CONVERT_FUNC (oklab_to_oklch) + +TRANSFORM_PAIR (srgb_to_oklch, srgb_to_oklab, oklab_to_oklch) +TRANSFORM_PAIR (srgb_linear_to_oklch, srgb_linear_to_oklab, oklab_to_oklch) +TRANSFORM_PAIR (rec2100_pq_to_oklch, rec2100_pq_to_oklab, oklab_to_oklch) +TRANSFORM_PAIR (rec2100_linear_to_oklch, rec2100_linear_to_oklab, oklab_to_oklch) +TRANSFORM_PAIR (oklch_to_srgb, oklch_to_oklab, oklab_to_srgb) +TRANSFORM_PAIR (oklch_to_srgb_linear, oklch_to_oklab, oklab_to_srgb_linear) +TRANSFORM_PAIR (oklch_to_rec2100_pq, oklch_to_oklab, oklab_to_rec2100_pq) +TRANSFORM_PAIR (oklch_to_rec2100_linear, oklch_to_oklab, oklab_to_rec2100_pq) /* }}} */ /* {{{ Default implementation */ @@ -328,6 +455,9 @@ gdk_default_color_state_get_cicp (GdkColorState *color_state) { GdkDefaultColorState *self = (GdkDefaultColorState *) color_state; + if (self->cicp.color_primaries == 0) + return NULL; + return &self->cicp; } @@ -419,9 +549,11 @@ GdkDefaultColorState gdk_default_color_states[] = { .name = "srgb", .no_srgb = GDK_COLOR_STATE_SRGB_LINEAR, .convert_to = { - [GDK_COLOR_STATE_ID_SRGB_LINEAR] = gdk_default_srgb_to_srgb_linear, - [GDK_COLOR_STATE_ID_REC2100_PQ] = gdk_default_srgb_to_rec2100_pq, - [GDK_COLOR_STATE_ID_REC2100_LINEAR] = gdk_default_srgb_to_rec2100_linear, + [GDK_COLOR_STATE_ID_SRGB_LINEAR] = gdk_convert_srgb_to_srgb_linear, + [GDK_COLOR_STATE_ID_REC2100_PQ] = gdk_convert_srgb_to_rec2100_pq, + [GDK_COLOR_STATE_ID_REC2100_LINEAR] = gdk_convert_srgb_to_rec2100_linear, + [GDK_COLOR_STATE_ID_OKLAB] = gdk_convert_srgb_to_oklab, + [GDK_COLOR_STATE_ID_OKLCH] = gdk_convert_srgb_to_oklch, }, .clamp = gdk_color_state_clamp_0_1, .cicp = { 1, 13, 0, 1 }, @@ -437,9 +569,11 @@ GdkDefaultColorState gdk_default_color_states[] = { .name = "srgb-linear", .no_srgb = NULL, .convert_to = { - [GDK_COLOR_STATE_ID_SRGB] = gdk_default_srgb_linear_to_srgb, - [GDK_COLOR_STATE_ID_REC2100_PQ] = gdk_default_srgb_linear_to_rec2100_pq, - [GDK_COLOR_STATE_ID_REC2100_LINEAR] = gdk_default_srgb_linear_to_rec2100_linear, + [GDK_COLOR_STATE_ID_SRGB] = gdk_convert_srgb_linear_to_srgb, + [GDK_COLOR_STATE_ID_REC2100_PQ] = gdk_convert_srgb_linear_to_rec2100_pq, + [GDK_COLOR_STATE_ID_REC2100_LINEAR] = gdk_convert_srgb_linear_to_rec2100_linear, + [GDK_COLOR_STATE_ID_OKLAB] = gdk_convert_srgb_linear_to_oklab, + [GDK_COLOR_STATE_ID_OKLCH] = gdk_convert_srgb_linear_to_oklch, }, .clamp = gdk_color_state_clamp_0_1, .cicp = { 1, 8, 0, 1 }, @@ -455,9 +589,11 @@ GdkDefaultColorState gdk_default_color_states[] = { .name = "rec2100-pq", .no_srgb = NULL, .convert_to = { - [GDK_COLOR_STATE_ID_SRGB] = gdk_default_rec2100_pq_to_srgb, - [GDK_COLOR_STATE_ID_SRGB_LINEAR] = gdk_default_rec2100_pq_to_srgb_linear, - [GDK_COLOR_STATE_ID_REC2100_LINEAR] = gdk_default_rec2100_pq_to_rec2100_linear, + [GDK_COLOR_STATE_ID_SRGB] = gdk_convert_rec2100_pq_to_srgb, + [GDK_COLOR_STATE_ID_SRGB_LINEAR] = gdk_convert_rec2100_pq_to_srgb_linear, + [GDK_COLOR_STATE_ID_REC2100_LINEAR] = gdk_convert_rec2100_pq_to_rec2100_linear, + [GDK_COLOR_STATE_ID_OKLAB] = gdk_convert_rec2100_pq_to_oklab, + [GDK_COLOR_STATE_ID_OKLCH] = gdk_convert_rec2100_pq_to_oklch, }, .clamp = gdk_color_state_clamp_0_1, .cicp = { 9, 16, 0, 1 }, @@ -473,16 +609,54 @@ GdkDefaultColorState gdk_default_color_states[] = { .name = "rec2100-linear", .no_srgb = NULL, .convert_to = { - [GDK_COLOR_STATE_ID_SRGB] = gdk_default_rec2100_linear_to_srgb, - [GDK_COLOR_STATE_ID_SRGB_LINEAR] = gdk_default_rec2100_linear_to_srgb_linear, - [GDK_COLOR_STATE_ID_REC2100_PQ] = gdk_default_rec2100_linear_to_rec2100_pq, + [GDK_COLOR_STATE_ID_SRGB] = gdk_convert_rec2100_linear_to_srgb, + [GDK_COLOR_STATE_ID_SRGB_LINEAR] = gdk_convert_rec2100_linear_to_srgb_linear, + [GDK_COLOR_STATE_ID_REC2100_PQ] = gdk_convert_rec2100_linear_to_rec2100_pq, + [GDK_COLOR_STATE_ID_OKLAB] = gdk_convert_rec2100_linear_to_oklab, + [GDK_COLOR_STATE_ID_OKLCH] = gdk_convert_rec2100_linear_to_oklch, }, .clamp = gdk_color_state_clamp_unbounded, .cicp = { 9, 8, 0, 1 }, }, + [GDK_COLOR_STATE_ID_OKLAB] = { + .parent = { + .klass = &GDK_DEFAULT_COLOR_STATE_CLASS, + .ref_count = 0, + .depth = GDK_MEMORY_FLOAT16, + .rendering_color_state = GDK_COLOR_STATE_SRGB, + }, + .name = "oklab", + .no_srgb = NULL, + .convert_to = { + [GDK_COLOR_STATE_ID_SRGB] = gdk_convert_oklab_to_srgb, + [GDK_COLOR_STATE_ID_SRGB_LINEAR] = gdk_convert_oklab_to_srgb_linear, + [GDK_COLOR_STATE_ID_REC2100_PQ] = gdk_convert_oklab_to_rec2100_pq, + [GDK_COLOR_STATE_ID_REC2100_LINEAR] = gdk_convert_oklab_to_rec2100_linear, + [GDK_COLOR_STATE_ID_OKLCH] = gdk_convert_oklab_to_oklch, + }, + .cicp = { 0, 0, 0, 0 }, + }, + [GDK_COLOR_STATE_ID_OKLCH] = { + .parent = { + .klass = &GDK_DEFAULT_COLOR_STATE_CLASS, + .ref_count = 0, + .depth = GDK_MEMORY_FLOAT16, + .rendering_color_state = GDK_COLOR_STATE_SRGB, + }, + .name = "oklch", + .no_srgb = NULL, + .convert_to = { + [GDK_COLOR_STATE_ID_SRGB] = gdk_convert_oklch_to_srgb, + [GDK_COLOR_STATE_ID_SRGB_LINEAR] = gdk_convert_oklch_to_srgb_linear, + [GDK_COLOR_STATE_ID_REC2100_PQ] = gdk_convert_oklch_to_rec2100_pq, + [GDK_COLOR_STATE_ID_REC2100_LINEAR] = gdk_convert_oklch_to_rec2100_linear, + [GDK_COLOR_STATE_ID_OKLAB] = gdk_convert_oklch_to_oklab, + }, + .cicp = { 0, 0, 0, 0 }, + }, }; - /* }}} */ +/* }}} */ /* {{{ Cicp implementation */ typedef struct _GdkCicpColorState GdkCicpColorState; @@ -509,17 +683,22 @@ struct _GdkCicpColorState #define cicp ((GdkCicpColorState *)self) -TRANSFORM(gdk_cicp_to_srgb, cicp->eotf, cicp->to_srgb, srgb_oetf) -TRANSFORM(gdk_cicp_to_srgb_linear, cicp->eotf, cicp->to_srgb, NONE) -TRANSFORM(gdk_cicp_to_rec2100_pq, cicp->eotf, cicp->to_rec2020, pq_oetf) -TRANSFORM(gdk_cicp_to_rec2100_linear, cicp->eotf, cicp->to_rec2020, NONE) -TRANSFORM(gdk_cicp_from_srgb, srgb_eotf, cicp->from_srgb, cicp->oetf) -TRANSFORM(gdk_cicp_from_srgb_linear, NONE, cicp->from_srgb, cicp->oetf) -TRANSFORM(gdk_cicp_from_rec2100_pq, pq_eotf, cicp->from_rec2020, cicp->oetf) -TRANSFORM(gdk_cicp_from_rec2100_linear, NONE, cicp->from_rec2020, cicp->oetf) +TRANSFORM(cicp_to_srgb, cicp->eotf, cicp->to_srgb, NONE, IDENTITY, srgb_oetf) +TRANSFORM(cicp_to_srgb_linear, cicp->eotf, cicp->to_srgb, NONE, IDENTITY, NONE) +TRANSFORM(cicp_to_rec2100_pq, cicp->eotf, cicp->to_rec2020, NONE, IDENTITY, pq_oetf) +TRANSFORM(cicp_to_rec2100_linear, cicp->eotf, cicp->to_rec2020, NONE, IDENTITY, NONE) +TRANSFORM(cicp_from_srgb, srgb_eotf, cicp->from_srgb, NONE, IDENTITY, cicp->oetf) +TRANSFORM(cicp_from_srgb_linear, NONE, cicp->from_srgb, NONE, IDENTITY, cicp->oetf) +TRANSFORM(cicp_from_rec2100_pq, pq_eotf, cicp->from_rec2020, NONE, IDENTITY, cicp->oetf) +TRANSFORM(cicp_from_rec2100_linear, NONE, cicp->from_rec2020, NONE, IDENTITY, cicp->oetf) #undef cicp +TRANSFORM_PAIR (cicp_to_oklab, cicp_to_srgb_linear, srgb_linear_to_oklab) +TRANSFORM_PAIR (cicp_from_oklab, oklab_to_srgb_linear, cicp_from_srgb_linear) +TRANSFORM_PAIR (cicp_to_oklch, cicp_to_srgb_linear, srgb_linear_to_oklch) +TRANSFORM_PAIR (cicp_from_oklch, oklch_to_srgb_linear, cicp_from_srgb_linear) + /* }}} */ /* {{{ Vfuncs */ @@ -572,13 +751,17 @@ gdk_cicp_color_state_get_convert_to (GdkColorState *self, switch (GDK_DEFAULT_COLOR_STATE_ID (target)) { case GDK_COLOR_STATE_ID_SRGB: - return gdk_cicp_to_srgb; + return gdk_convert_cicp_to_srgb; case GDK_COLOR_STATE_ID_SRGB_LINEAR: - return gdk_cicp_to_srgb_linear; + return gdk_convert_cicp_to_srgb_linear; case GDK_COLOR_STATE_ID_REC2100_PQ: - return gdk_cicp_to_rec2100_pq; + return gdk_convert_cicp_to_rec2100_pq; case GDK_COLOR_STATE_ID_REC2100_LINEAR: - return gdk_cicp_to_rec2100_linear; + return gdk_convert_cicp_to_rec2100_linear; + case GDK_COLOR_STATE_ID_OKLAB: + return gdk_convert_cicp_to_oklab; + case GDK_COLOR_STATE_ID_OKLCH: + return gdk_convert_cicp_to_oklch; case GDK_COLOR_STATE_N_IDS: default: @@ -598,13 +781,17 @@ gdk_cicp_color_state_get_convert_from (GdkColorState *self, switch (GDK_DEFAULT_COLOR_STATE_ID (source)) { case GDK_COLOR_STATE_ID_SRGB: - return gdk_cicp_from_srgb; + return gdk_convert_cicp_from_srgb; case GDK_COLOR_STATE_ID_SRGB_LINEAR: - return gdk_cicp_from_srgb_linear; + return gdk_convert_cicp_from_srgb_linear; case GDK_COLOR_STATE_ID_REC2100_PQ: - return gdk_cicp_from_rec2100_pq; + return gdk_convert_cicp_from_rec2100_pq; case GDK_COLOR_STATE_ID_REC2100_LINEAR: - return gdk_cicp_from_rec2100_linear; + return gdk_convert_cicp_from_rec2100_linear; + case GDK_COLOR_STATE_ID_OKLAB: + return gdk_convert_cicp_from_oklab; + case GDK_COLOR_STATE_ID_OKLCH: + return gdk_convert_cicp_from_oklch; case GDK_COLOR_STATE_N_IDS: default: diff --git a/gdk/gdkcolorstate.h b/gdk/gdkcolorstate.h index 6743349b71..1145eb4158 100644 --- a/gdk/gdkcolorstate.h +++ b/gdk/gdkcolorstate.h @@ -49,6 +49,12 @@ GdkColorState * gdk_color_state_get_rec2100_pq (void); GDK_AVAILABLE_IN_4_16 GdkColorState * gdk_color_state_get_rec2100_linear (void); +GDK_AVAILABLE_IN_4_16 +GdkColorState * gdk_color_state_get_oklab (void); + +GDK_AVAILABLE_IN_4_16 +GdkColorState * gdk_color_state_get_oklch (void); + GDK_AVAILABLE_IN_4_16 gboolean gdk_color_state_equal (GdkColorState *self, GdkColorState *other); diff --git a/gdk/gdkcolorstateprivate.h b/gdk/gdkcolorstateprivate.h index cea7bc82fd..0a0f2bb95c 100644 --- a/gdk/gdkcolorstateprivate.h +++ b/gdk/gdkcolorstateprivate.h @@ -13,6 +13,8 @@ typedef enum GDK_COLOR_STATE_ID_SRGB_LINEAR, GDK_COLOR_STATE_ID_REC2100_PQ, GDK_COLOR_STATE_ID_REC2100_LINEAR, + GDK_COLOR_STATE_ID_OKLAB, + GDK_COLOR_STATE_ID_OKLCH, GDK_COLOR_STATE_N_IDS } GdkColorStateId; @@ -73,6 +75,8 @@ extern GdkDefaultColorState gdk_default_color_states[GDK_COLOR_STATE_N_IDS]; #define GDK_COLOR_STATE_SRGB_LINEAR ((GdkColorState *) &gdk_default_color_states[GDK_COLOR_STATE_ID_SRGB_LINEAR]) #define GDK_COLOR_STATE_REC2100_PQ ((GdkColorState *) &gdk_default_color_states[GDK_COLOR_STATE_ID_REC2100_PQ]) #define GDK_COLOR_STATE_REC2100_LINEAR ((GdkColorState *) &gdk_default_color_states[GDK_COLOR_STATE_ID_REC2100_LINEAR]) +#define GDK_COLOR_STATE_OKLAB ((GdkColorState *) &gdk_default_color_states[GDK_COLOR_STATE_ID_OKLAB]) +#define GDK_COLOR_STATE_OKLCH ((GdkColorState *) &gdk_default_color_states[GDK_COLOR_STATE_ID_OKLCH]) #define GDK_IS_DEFAULT_COLOR_STATE(c) ((GdkDefaultColorState *) (c) >= &gdk_default_color_states[0] && \ (GdkDefaultColorState *) (c) < &gdk_default_color_states[GDK_COLOR_STATE_N_IDS]) diff --git a/testsuite/gdk/colorstate-internal.c b/testsuite/gdk/colorstate-internal.c index 9c90fc6915..8794bd6306 100644 --- a/testsuite/gdk/colorstate-internal.c +++ b/testsuite/gdk/colorstate-internal.c @@ -23,6 +23,7 @@ TransferTest transfers[] = { { "hlg", hlg_oetf, hlg_eotf, { 0, 1}, { 0, 1} }, { "gamma22", gamma22_oetf, gamma22_eotf, { 0, 1 }, { 0, 1 } }, { "gamma28", gamma28_oetf, gamma28_eotf, { 0, 1 }, { 0, 1 } }, + { "oklab", to_oklab_nl, from_oklab_nl,{ 0, 1 }, { 0, 1 } }, }; #define LERP(t, a, b) ((a) + (t) * ((b) - (a))) @@ -103,6 +104,8 @@ static MatrixTest matrices[] = { { "ntsc", ntsc_to_xyz, xyz_to_ntsc }, { "p3", p3_to_xyz, xyz_to_p3 }, { "srgb<>rec2020", rec2020_to_srgb, srgb_to_rec2020 }, + { "oklab<>lms", oklab_to_lms, lms_to_oklab }, + { "lms<>srgb", lms_to_srgb, srgb_to_lms }, }; #define IDX(i,j) 3*i+j From 808345659932294c273b9f665a8676472155b83f Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 10 Aug 2024 13:32:10 -0400 Subject: [PATCH 2/7] gsk: Handle oklab and oklch color states --- gsk/gpu/shaders/color.glsl | 135 +++++++++++++++++++++++++++++++++++-- gsk/gpu/shaders/enums.glsl | 2 + 2 files changed, 131 insertions(+), 6 deletions(-) diff --git a/gsk/gpu/shaders/color.glsl b/gsk/gpu/shaders/color.glsl index 4364392b13..95ed12635d 100644 --- a/gsk/gpu/shaders/color.glsl +++ b/gsk/gpu/shaders/color.glsl @@ -111,6 +111,54 @@ const mat3 rec2020_from_srgb = mat3( 0.043303, 0.011360, 0.895380 ); +const mat3 oklab_to_lms = mat3( + 1.0, 1.0, 1.0, + 0.3963377774, -0.1055613458, -0.0894841775, + 0.2158037573, -0.0638541728, -1.2914855480 +); + +const mat3 lms_to_srgb = mat3( + 4.0767416621, -1.2684380046, -0.0041960863, + -3.3077115913, 2.6097574011, -0.7034186147, + 0.2309699292, -0.3413193965, 1.7076147010 +); + +const mat3 srgb_to_lms = mat3( + 0.4122214708, 0.2119034982, 0.0883024619, + 0.5363325363, 0.6806995451, 0.2817188376, + 0.0514459929, 0.1073969566, 0.6299787005 +); + +const mat3 lms_to_oklab = mat3( + 0.2104542553, 1.9779984951, 0.0259040371, + 0.7936177850, -2.4285922050, 0.7827717662, + -0.0040720468, 0.4505937099, -0.8086757660 +); + +vec3 +oklab_to_srgb_linear (vec3 color) +{ + vec3 lms = oklab_to_lms * color; + + lms = vec3 (pow (lms.r, 3.0), + pow (lms.g, 3.0), + pow (lms.b, 3.0)); + + return lms_to_srgb * lms; +} + +vec3 +srgb_linear_to_oklab (vec3 color) +{ + vec3 lms = srgb_to_lms * color; + + lms = vec3 (pow (lms.r, 1.0/3.0), + pow (lms.g, 1.0/3.0), + pow (lms.b, 1.0/3.0)); + + return lms_to_oklab * lms; +} + vec3 apply_eotf (vec3 color, uint cs) @@ -131,6 +179,9 @@ apply_eotf (vec3 color, case GDK_COLOR_STATE_ID_REC2100_LINEAR: return color; + case GDK_COLOR_STATE_ID_OKLAB: + return oklab_to_srgb_linear (color); + default: return vec3(1.0, 0.0, 0.8); } @@ -156,6 +207,9 @@ apply_oetf (vec3 color, case GDK_COLOR_STATE_ID_REC2100_LINEAR: return color; + case GDK_COLOR_STATE_ID_OKLAB: + return srgb_linear_to_oklab (color); + default: return vec3(0.0, 1.0, 0.8); } @@ -183,10 +237,71 @@ linear_color_space (uint cs) case GDK_COLOR_STATE_ID_SRGB_LINEAR: return GDK_COLOR_STATE_ID_SRGB_LINEAR; case GDK_COLOR_STATE_ID_REC2100_PQ: return GDK_COLOR_STATE_ID_REC2100_LINEAR; case GDK_COLOR_STATE_ID_REC2100_LINEAR: return GDK_COLOR_STATE_ID_REC2100_LINEAR; + case GDK_COLOR_STATE_ID_OKLAB: return GDK_COLOR_STATE_ID_SRGB_LINEAR; + case GDK_COLOR_STATE_ID_OKLCH: return GDK_COLOR_STATE_ID_SRGB_LINEAR; default: return 0u; }; } +uint +rectangular_color_space (uint cs) +{ + if (cs == GDK_COLOR_STATE_ID_OKLCH) + return GDK_COLOR_STATE_ID_OKLAB; + else + return cs; +} + +#define M_PI 3.14159265358979323846 +#define RAD_TO_DEG(x) ((x)*180.0/M_PI) +#define DEG_TO_RAD(x) ((x)*M_PI/180.0) + +float +normalize_hue (float h) +{ + while (h < 0.0) + h += 360.0; + while (h > 360.0) + h -= 360.0; + return h; +} + +vec3 +oklch_to_oklab (vec3 color) +{ + color.z = normalize_hue (color.z); + + return vec3 (color.x, + color.y * cos (DEG_TO_RAD (color.z)), + color.y * sin (DEG_TO_RAD (color.z))); +} + +vec3 +oklab_to_oklch (vec3 color) +{ + return vec3 (color.x, + length (color.yz), + RAD_TO_DEG (atan (color.z, color.y))); +} + +vec3 +to_rect (vec3 color, uint from) +{ + if (from == GDK_COLOR_STATE_ID_OKLCH) + return oklch_to_oklab (color); + else + return vec3(1, 0, 0.5); +} + +vec3 +to_polar (vec3 color, uint to) +{ + if (to == GDK_COLOR_STATE_ID_OKLCH) + return oklab_to_oklch (color); + else + return vec3(1, 0, 0.5); +} + vec4 convert_color (vec4 color, uint from, @@ -199,17 +314,25 @@ convert_color (vec4 color, if (from != to) { - uint from_linear = linear_color_space (from); - uint to_linear = linear_color_space (to); + uint from_rectangular = rectangular_color_space (from); + uint to_rectangular = rectangular_color_space (to); + uint from_linear = linear_color_space (from_rectangular); + uint to_linear = linear_color_space (to_rectangular); - if (from_linear != from) - color.rgb = apply_eotf (color.rgb, from); + if (from_rectangular != from) + color.rgb = to_rect (color.rgb, from); + + if (from_linear != from_rectangular) + color.rgb = apply_eotf (color.rgb, from_rectangular); if (from_linear != to_linear) color.rgb = convert_linear (color.rgb, from_linear, to_linear); - if (to_linear != to) - color.rgb = apply_oetf (color.rgb, to); + if (to_linear != to_rectangular) + color.rgb = apply_oetf (color.rgb, to_rectangular); + + if (to_rectangular != to) + color.rgb = to_polar (color.rgb, to); } if (to_premul && (!from_premul || from != to)) diff --git a/gsk/gpu/shaders/enums.glsl b/gsk/gpu/shaders/enums.glsl index e78db57f5d..e2816ffbcc 100644 --- a/gsk/gpu/shaders/enums.glsl +++ b/gsk/gpu/shaders/enums.glsl @@ -56,6 +56,8 @@ #define GDK_COLOR_STATE_ID_SRGB_LINEAR 1u #define GDK_COLOR_STATE_ID_REC2100_PQ 2u #define GDK_COLOR_STATE_ID_REC2100_LINEAR 3u +#define GDK_COLOR_STATE_ID_OKLAB 4u +#define GDK_COLOR_STATE_ID_OKLCH 5u #define TOP 0u #define RIGHT 1u From 3bce60c433138159bdf582c1f60e9b13fdfb090d Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 10 Aug 2024 18:56:26 -0400 Subject: [PATCH 3/7] gsk: Handle hue-interpolation in ops Make all our gradient ops adjust the hue according to the hue interpolation. This is currently modifying the values in the vertex array. If reading those values back is bad, we may need to change that. --- gsk/gpu/gskgpuconicgradientop.c | 8 +++ gsk/gpu/gskgpulineargradientop.c | 79 +++++++++++++++++++++++++ gsk/gpu/gskgpulineargradientopprivate.h | 5 ++ gsk/gpu/gskgpuradialgradientop.c | 8 +++ 4 files changed, 100 insertions(+) diff --git a/gsk/gpu/gskgpuconicgradientop.c b/gsk/gpu/gskgpuconicgradientop.c index 3232047e21..69aa6b8ff5 100644 --- a/gsk/gpu/gskgpuconicgradientop.c +++ b/gsk/gpu/gskgpuconicgradientop.c @@ -1,6 +1,7 @@ #include "config.h" #include "gskgpuconicgradientopprivate.h" +#include "gskgpulineargradientopprivate.h" #include "gskgpuframeprivate.h" #include "gskgpuprintprivate.h" @@ -97,4 +98,11 @@ gsk_gpu_conic_gradient_op (GskGpuFrame *frame, instance->offsets0[1] = stops[1].offset; gsk_gpu_color_to_float (&stops[0].color, ics, opacity, instance->color0); instance->offsets0[0] = stops[0].offset; + + gsk_adjust_hue (ics, hue_interp, instance->color0, instance->color1); + gsk_adjust_hue (ics, hue_interp, instance->color1, instance->color2); + gsk_adjust_hue (ics, hue_interp, instance->color2, instance->color3); + gsk_adjust_hue (ics, hue_interp, instance->color3, instance->color4); + gsk_adjust_hue (ics, hue_interp, instance->color4, instance->color5); + gsk_adjust_hue (ics, hue_interp, instance->color5, instance->color6); } diff --git a/gsk/gpu/gskgpulineargradientop.c b/gsk/gpu/gskgpulineargradientop.c index 8b4668e50a..aa17a170d5 100644 --- a/gsk/gpu/gskgpulineargradientop.c +++ b/gsk/gpu/gskgpulineargradientop.c @@ -52,6 +52,78 @@ static const GskGpuShaderOpClass GSK_GPU_LINEAR_GRADIENT_OP_CLASS = { gsk_gpu_lineargradient_setup_vao }; +void +gsk_adjust_hue (GdkColorState *ics, + GskHueInterpolation interp, + const float color1[4], + float color2[4]) +{ + float h1, h2; + float d; + + if (!gdk_color_state_equal (ics, GDK_COLOR_STATE_OKLCH)) + return; + + h1 = color1[2]; + h2 = color2[2]; + d = h2 - h1; + + while (d > 360) + { + h2 -= 360; + d = h2 - h1; + } + while (d < -360) + { + h2 += 360; + d = h2 - h1; + } + + g_assert (fabsf (d) <= 360); + + switch (interp) + { + case GSK_HUE_INTERPOLATION_SHORTER: + { + if (d > 180) + h2 -= 360; + else if (d < -180) + h2 += 360; + } + g_assert (fabsf (h2 - h1) <= 180); + break; + + case GSK_HUE_INTERPOLATION_LONGER: + { + if (0 < d && d < 180) + h2 -= 360; + else if (-180 < d && d <= 0) + h2 += 360; + g_assert (fabsf (h2 - h1) >= 180); + } + break; + + case GSK_HUE_INTERPOLATION_INCREASING: + if (h2 < h1) + h2 += 360; + d = h2 - h1; + g_assert (h1 <= h2); + break; + + case GSK_HUE_INTERPOLATION_DECREASING: + if (h1 < h2) + h2 -= 360; + d = h2 - h1; + g_assert (h1 >= h2); + break; + + default: + g_assert_not_reached (); + } + + color2[2] = h2; +} + void gsk_gpu_linear_gradient_op (GskGpuFrame *frame, GskGpuShaderClip clip, @@ -102,4 +174,11 @@ gsk_gpu_linear_gradient_op (GskGpuFrame *frame, instance->offsets0[1] = stops[1].offset; gsk_gpu_color_to_float (&stops[0].color, ics, opacity, instance->color0); instance->offsets0[0] = stops[0].offset; + + gsk_adjust_hue (ics, hue_interp, instance->color0, instance->color1); + gsk_adjust_hue (ics, hue_interp, instance->color1, instance->color2); + gsk_adjust_hue (ics, hue_interp, instance->color2, instance->color3); + gsk_adjust_hue (ics, hue_interp, instance->color3, instance->color4); + gsk_adjust_hue (ics, hue_interp, instance->color4, instance->color5); + gsk_adjust_hue (ics, hue_interp, instance->color5, instance->color6); } diff --git a/gsk/gpu/gskgpulineargradientopprivate.h b/gsk/gpu/gskgpulineargradientopprivate.h index ee1c0a4872..5bc4343807 100644 --- a/gsk/gpu/gskgpulineargradientopprivate.h +++ b/gsk/gpu/gskgpulineargradientopprivate.h @@ -23,5 +23,10 @@ void gsk_gpu_linear_gradient_op (GskGpuF gsize n_stops); +void gsk_adjust_hue (GdkColorState *ics, + GskHueInterpolation interp, + const float color1[4], + float color2[4]); + G_END_DECLS diff --git a/gsk/gpu/gskgpuradialgradientop.c b/gsk/gpu/gskgpuradialgradientop.c index 5015f2886a..b54186b2ba 100644 --- a/gsk/gpu/gskgpuradialgradientop.c +++ b/gsk/gpu/gskgpuradialgradientop.c @@ -1,6 +1,7 @@ #include "config.h" #include "gskgpuradialgradientopprivate.h" +#include "gskgpulineargradientopprivate.h" #include "gskgpuframeprivate.h" #include "gskgpuprintprivate.h" @@ -106,4 +107,11 @@ gsk_gpu_radial_gradient_op (GskGpuFrame *frame, instance->offsets0[1] = stops[1].offset; gsk_gpu_color_to_float (&stops[0].color, ics, opacity, instance->color0); instance->offsets0[0] = stops[0].offset; + + gsk_adjust_hue (ics, hue_interp, instance->color0, instance->color1); + gsk_adjust_hue (ics, hue_interp, instance->color1, instance->color2); + gsk_adjust_hue (ics, hue_interp, instance->color2, instance->color3); + gsk_adjust_hue (ics, hue_interp, instance->color3, instance->color4); + gsk_adjust_hue (ics, hue_interp, instance->color4, instance->color5); + gsk_adjust_hue (ics, hue_interp, instance->color5, instance->color6); } From 965fd476a5b75d25d9b8e43d9ee476ca66715e27 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 10 Aug 2024 13:32:28 -0400 Subject: [PATCH 4/7] node parser: handle oklab and oklch --- gsk/gskrendernodeparser.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gsk/gskrendernodeparser.c b/gsk/gskrendernodeparser.c index ef4dc87f89..268fdb933e 100644 --- a/gsk/gskrendernodeparser.c +++ b/gsk/gskrendernodeparser.c @@ -628,6 +628,10 @@ parse_color_state (GtkCssParser *parser, cs = gdk_color_state_get_rec2100_pq (); else if (gtk_css_parser_try_ident (parser, "rec2100-linear")) cs = gdk_color_state_get_rec2100_linear (); + else if (gtk_css_parser_try_ident (parser, "oklab")) + cs = gdk_color_state_get_oklab (); + else if (gtk_css_parser_try_ident (parser, "oklch")) + cs = gdk_color_state_get_oklch (); else if (gtk_css_token_is (gtk_css_parser_get_token (parser), GTK_CSS_TOKEN_STRING)) { char *name = gtk_css_parser_consume_string (parser); From 6d878bd21c2e2eec24e99fabdff3c28ec4c8ae5c Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 10 Aug 2024 13:32:42 -0400 Subject: [PATCH 5/7] css: Pass oklab and oklch color states through --- gtk/gtkcsscolor.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/gtk/gtkcsscolor.c b/gtk/gtkcsscolor.c index 34606e5c4f..5096f15d1e 100644 --- a/gtk/gtkcsscolor.c +++ b/gtk/gtkcsscolor.c @@ -1188,10 +1188,14 @@ gtk_css_color_space_get_color_state (GtkCssColorSpace color_space) case GTK_CSS_COLOR_SPACE_SRGB: case GTK_CSS_COLOR_SPACE_HSL: case GTK_CSS_COLOR_SPACE_HWB: - case GTK_CSS_COLOR_SPACE_OKLAB: - case GTK_CSS_COLOR_SPACE_OKLCH: return GDK_COLOR_STATE_SRGB; + case GTK_CSS_COLOR_SPACE_OKLAB: + return GDK_COLOR_STATE_OKLAB; + + case GTK_CSS_COLOR_SPACE_OKLCH: + return GDK_COLOR_STATE_OKLCH; + case GTK_CSS_COLOR_SPACE_SRGB_LINEAR: return GDK_COLOR_STATE_SRGB_LINEAR; @@ -1225,10 +1229,17 @@ gtk_css_color_to_color (const GtkCssColor *css, gdk_color_init (color, GDK_COLOR_STATE_REC2100_PQ, css->values); break; + case GTK_CSS_COLOR_SPACE_OKLAB: + gdk_color_init (color, GDK_COLOR_STATE_OKLAB, css->values); + break; + + case GTK_CSS_COLOR_SPACE_OKLCH: + gdk_color_init (color, GDK_COLOR_STATE_OKLCH, css->values); + break; + + case GTK_CSS_COLOR_SPACE_HSL: case GTK_CSS_COLOR_SPACE_HWB: - case GTK_CSS_COLOR_SPACE_OKLAB: - case GTK_CSS_COLOR_SPACE_OKLCH: { GtkCssColor tmp; gtk_css_color_convert (css, GTK_CSS_COLOR_SPACE_SRGB, &tmp); From 4de67b2fe5cb132cc6f0cc9c37f0a4761809fc2e Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 10 Aug 2024 18:56:49 -0400 Subject: [PATCH 6/7] gtk: Don't optimize gradients away too eagerly Even if the stops are the same color, with hue interpolation, it might still make a beautiful rainbow. --- gtk/gtksnapshot.c | 48 +++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/gtk/gtksnapshot.c b/gtk/gtksnapshot.c index 7416190b92..9eeb46eea3 100644 --- a/gtk/gtksnapshot.c +++ b/gtk/gtksnapshot.c @@ -2571,7 +2571,7 @@ gtk_snapshot_append_linear_gradient2 (GtkSnapshot *snapshot, graphene_rect_t real_bounds; float scale_x, scale_y, dx, dy; const GdkColor *first_color; - gboolean need_gradient = FALSE; + gboolean need_gradient = TRUE; g_return_if_fail (snapshot != NULL); g_return_if_fail (start_point != NULL); @@ -2585,13 +2585,17 @@ gtk_snapshot_append_linear_gradient2 (GtkSnapshot *snapshot, &dx, &dy); gtk_graphene_rect_scale_affine (bounds, scale_x, scale_y, dx, dy, &real_bounds); - first_color = &stops[0].color; - for (gsize i = 1; i < n_stops; i ++) + if (hue_interpolation != GSK_HUE_INTERPOLATION_LONGER) { - if (!gdk_color_equal (first_color, &stops[i].color)) + need_gradient = FALSE; + first_color = &stops[0].color; + for (gsize i = 1; i < n_stops; i ++) { - need_gradient = TRUE; - break; + if (!gdk_color_equal (first_color, &stops[i].color)) + { + need_gradient = TRUE; + break; + } } } @@ -2685,7 +2689,7 @@ gtk_snapshot_append_repeating_linear_gradient2 (GtkSnapshot *snapsho GskRenderNode *node; graphene_rect_t real_bounds; float scale_x, scale_y, dx, dy; - gboolean need_gradient = FALSE; + gboolean need_gradient = TRUE; const GdkColor *first_color; g_return_if_fail (snapshot != NULL); @@ -2697,13 +2701,17 @@ gtk_snapshot_append_repeating_linear_gradient2 (GtkSnapshot *snapsho gtk_snapshot_ensure_affine (snapshot, &scale_x, &scale_y, &dx, &dy); gtk_graphene_rect_scale_affine (bounds, scale_x, scale_y, dx, dy, &real_bounds); - first_color = &stops[0].color; - for (gsize i = 1; i < n_stops; i ++) + if (hue_interpolation != GSK_HUE_INTERPOLATION_LONGER) { - if (!gdk_color_equal (first_color, &stops[i].color)) + need_gradient = FALSE; + first_color = &stops[0].color; + for (gsize i = 1; i < n_stops; i ++) { - need_gradient = TRUE; - break; + if (!gdk_color_equal (first_color, &stops[i].color)) + { + need_gradient = TRUE; + break; + } } } @@ -2800,7 +2808,7 @@ gtk_snapshot_append_conic_gradient2 (GtkSnapshot *snapshot, graphene_rect_t real_bounds; float dx, dy; const GdkColor *first_color; - gboolean need_gradient = FALSE; + gboolean need_gradient = TRUE; int i; g_return_if_fail (snapshot != NULL); @@ -2811,13 +2819,17 @@ gtk_snapshot_append_conic_gradient2 (GtkSnapshot *snapshot, gtk_snapshot_ensure_translate (snapshot, &dx, &dy); graphene_rect_offset_r (bounds, dx, dy, &real_bounds); - first_color = &stops[0].color; - for (i = 1; i < n_stops; i ++) + if (hue_interpolation != GSK_HUE_INTERPOLATION_LONGER) { - if (!gdk_color_equal (first_color, &stops[i].color)) + need_gradient = FALSE; + first_color = &stops[0].color; + for (i = 1; i < n_stops; i ++) { - need_gradient = TRUE; - break; + if (!gdk_color_equal (first_color, &stops[i].color)) + { + need_gradient = TRUE; + break; + } } } From df31bbf9e5b63a0448891beac58b36e956bfa837 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sun, 11 Aug 2024 22:35:07 -0400 Subject: [PATCH 7/7] gsk: Support hue interpolation in cairo Since cairos gradient code isn't flexible enough to let us interpolate in oklch, add additional color stops and let cairo interpolate in the ccs. This isn't as accurate as interpolating in oklch, but it gets an ok result for fallback situations. --- gsk/gskrendernodeimpl.c | 261 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 250 insertions(+), 11 deletions(-) diff --git a/gsk/gskrendernodeimpl.c b/gsk/gskrendernodeimpl.c index 98fb65d55a..ba13b4ff2e 100644 --- a/gsk/gskrendernodeimpl.c +++ b/gsk/gskrendernodeimpl.c @@ -358,6 +358,140 @@ gsk_linear_gradient_node_finalize (GskRenderNode *node) parent_class->finalize (node); } +static float +adjust_hue (GskHueInterpolation interp, + float h1, + float h2) +{ + float d; + + d = h2 - h1; + while (d > 360) + { + h2 -= 360; + d = h2 - h1; + } + while (d < -360) + { + h2 += 360; + d = h2 - h1; + } + + g_assert (fabsf (d) <= 360); + + switch (interp) + { + case GSK_HUE_INTERPOLATION_SHORTER: + { + if (d > 180) + h2 -= 360; + else if (d < -180) + h2 += 360; + } + g_assert (fabsf (h2 - h1) <= 180); + break; + + case GSK_HUE_INTERPOLATION_LONGER: + { + if (0 < d && d < 180) + h2 -= 360; + else if (-180 < d && d <= 0) + h2 += 360; + g_assert (fabsf (h2 - h1) >= 180); + } + break; + + case GSK_HUE_INTERPOLATION_INCREASING: + if (h2 < h1) + h2 += 360; + d = h2 - h1; + g_assert (h1 <= h2); + break; + + case GSK_HUE_INTERPOLATION_DECREASING: + if (h1 < h2) + h2 -= 360; + d = h2 - h1; + g_assert (h1 >= h2); + break; + + default: + g_assert_not_reached (); + } + + return h2; +} + +#define lerp(t,a,b) ((a) + (t) * ((b) - (a))) + +typedef void (* ColorStopCallback) (float offset, + GdkColorState *ccs, + float values[4], + gpointer data); + +static void +interpolate_color_stops (GdkColorState *ccs, + GdkColorState *interpolation, + GskHueInterpolation hue_interpolation, + float offset1, + GdkColor *color1, + float offset2, + GdkColor *color2, + ColorStopCallback callback, + gpointer data) +{ + float values1[4]; + float values2[4]; + int n; + + gdk_color_to_float (color1, interpolation, values1); + gdk_color_to_float (color2, interpolation, values2); + + if (gdk_color_state_equal (interpolation, GDK_COLOR_STATE_OKLCH)) + { + values2[2] = adjust_hue (hue_interpolation, values1[2], values2[2]); + /* don't make hue steps larger than 30° */ + n = ceilf (fabsf (values2[2] - values1[2]) / 30); + } + else + { + /* just some steps */ + n = 4; + } + + for (int k = 1; k < n; k++) + { + float f = k / (float) n; + float values[4]; + float offset; + GdkColor c; + + values[0] = lerp (f, values1[0], values2[0]); + values[1] = lerp (f, values1[1], values2[1]); + values[2] = lerp (f, values1[2], values2[2]); + values[3] = lerp (f, values1[3], values2[3]); + offset = lerp (f, offset1, offset2); + + gdk_color_init (&c, interpolation, values); + gdk_color_to_float (&c, ccs, values); + + callback (offset, ccs, values, data); + + gdk_color_finish (&c); + } +} + +static void +add_color_stop_to_pattern (float offset, + GdkColorState *ccs, + float values[4], + gpointer data) +{ + cairo_pattern_t *pattern = data; + + cairo_pattern_add_color_stop_rgba (pattern, offset, values[0], values[1], values[2], values[3]); +} + static void gsk_linear_gradient_node_draw (GskRenderNode *node, cairo_t *cr, @@ -378,18 +512,42 @@ gsk_linear_gradient_node_draw (GskRenderNode *node, ccs, 0.0, &self->stops[0].color); + for (i = 0; i < self->n_stops; i++) { + if (!gdk_color_state_equal (self->interpolation, ccs)) + interpolate_color_stops (ccs, + self->interpolation, self->hue_interpolation, + i > 0 ? self->stops[i-1].offset : 0, + i > 0 ? &self->stops[i-1].color : &self->stops[i].color, + self->stops[i].offset, + &self->stops[i].color, + add_color_stop_to_pattern, + pattern); + gdk_cairo_pattern_add_color_stop_color (pattern, ccs, self->stops[i].offset, &self->stops[i].color); } + if (self->stops[self->n_stops-1].offset < 1.0) - gdk_cairo_pattern_add_color_stop_color (pattern, - ccs, - 1.0, - &self->stops[self->n_stops - 1].color); + { + if (!gdk_color_state_equal (self->interpolation, ccs)) + interpolate_color_stops (ccs, + self->interpolation, self->hue_interpolation, + self->stops[self->n_stops-1].offset, + &self->stops[self->n_stops-1].color, + 1, + &self->stops[self->n_stops-1].color, + add_color_stop_to_pattern, + pattern); + + gdk_cairo_pattern_add_color_stop_color (pattern, + ccs, + 1.0, + &self->stops[self->n_stops-1].color); + } cairo_set_source (cr, pattern); cairo_pattern_destroy (pattern); @@ -925,18 +1083,42 @@ gsk_radial_gradient_node_draw (GskRenderNode *node, ccs, 0.0, &self->stops[0].color); + for (i = 0; i < self->n_stops; i++) { + if (!gdk_color_state_equal (self->interpolation, ccs)) + interpolate_color_stops (ccs, + self->interpolation, self->hue_interpolation, + i > 0 ? self->stops[i-1].offset : 0, + i > 0 ? &self->stops[i-1].color : &self->stops[i].color, + self->stops[i].offset, + &self->stops[i].color, + add_color_stop_to_pattern, + pattern); + gdk_cairo_pattern_add_color_stop_color (pattern, ccs, self->stops[i].offset, &self->stops[i].color); } + if (self->stops[self->n_stops-1].offset < 1.0) - gdk_cairo_pattern_add_color_stop_color (pattern, - ccs, - 1.0, - &self->stops[self->n_stops-1].color); + { + if (!gdk_color_state_equal (self->interpolation, ccs)) + interpolate_color_stops (ccs, + self->interpolation, self->hue_interpolation, + self->stops[self->n_stops-1].offset, + &self->stops[self->n_stops-1].color, + 1, + &self->stops[self->n_stops-1].color, + add_color_stop_to_pattern, + pattern); + + gdk_cairo_pattern_add_color_stop_color (pattern, + ccs, + 1.0, + &self->stops[self->n_stops-1].color); + } gdk_cairo_rect (cr, &node->bounds); cairo_translate (cr, self->center.x, self->center.y); @@ -1623,6 +1805,29 @@ gdk_rgba_color_interpolate (GdkRGBA *dest, } } +static void +add_color_stop_to_array (float offset, + GdkColorState *ccs, + float values[4], + gpointer data) +{ + GArray *stops = data; + GskColorStop2 stop; + + stop.offset = offset; + gdk_color_init (&stop.color, ccs, values); + + g_array_append_val (stops, stop); +} + +static void +clear_stop (gpointer data) +{ + GskColorStop2 *stop = data; + + gdk_color_finish (&stop->color); +} + static void gsk_conic_gradient_node_draw (GskRenderNode *node, cairo_t *cr, @@ -1633,6 +1838,7 @@ gsk_conic_gradient_node_draw (GskRenderNode *node, graphene_point_t corner; float radius; gsize i; + GArray *stops; pattern = cairo_pattern_create_mesh (); graphene_rect_get_top_right (&node->bounds, &corner); @@ -1644,10 +1850,41 @@ gsk_conic_gradient_node_draw (GskRenderNode *node, graphene_rect_get_top_left (&node->bounds, &corner); radius = MAX (radius, graphene_point_distance (&self->center, &corner, NULL, NULL)); - for (i = 0; i <= self->n_stops; i++) + stops = g_array_new (FALSE, TRUE, sizeof (GskColorStop2)); + g_array_set_clear_func (stops, clear_stop); + + if (gdk_color_state_equal (self->interpolation, ccs)) { - GskColorStop2 *stop1 = &self->stops[MAX (i, 1) - 1]; - GskColorStop2 *stop2 = &self->stops[MIN (i, self->n_stops - 1)]; + for (i = 0; i < self->n_stops; i++) + { + GskColorStop2 *stop = &self->stops[i]; + g_array_append_val (stops, *stop); + /* take a ref, since clear_stop removes one */ + gdk_color_state_ref (stop->color.color_state); + } + } + else + { + g_array_append_val (stops, self->stops[0]); + + for (i = 1; i < self->n_stops; i++) + { + interpolate_color_stops (ccs, + self->interpolation, self->hue_interpolation, + self->stops[i-1].offset, &self->stops[i-1].color, + self->stops[i].offset, &self->stops[i].color, + add_color_stop_to_array, + stops); + g_array_append_val (stops, self->stops[i]); + /* take a ref, since clear_stop removes one */ + gdk_color_state_ref (self->stops[i].color.color_state); + } + } + + for (i = 0; i <= stops->len; i++) + { + GskColorStop2 *stop1 = &g_array_index (stops, GskColorStop2, MAX (i, 1) - 1); + GskColorStop2 *stop2 = &g_array_index (stops, GskColorStop2, MIN (i, stops->len - 1)); double offset1 = i > 0 ? stop1->offset : 0; double offset2 = i < self->n_stops ? stop2->offset : 1; double start_angle, end_angle; @@ -1683,6 +1920,8 @@ gsk_conic_gradient_node_draw (GskRenderNode *node, } } + g_array_unref (stops); + cairo_pattern_set_extend (pattern, CAIRO_EXTEND_PAD); gdk_cairo_rect (cr, &node->bounds);