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.
This commit is contained in:
Matthias Clasen 2024-08-11 22:35:07 -04:00
parent 4de67b2fe5
commit df31bbf9e5

View File

@ -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);