LibGfx: Fix SVG/canvas radial gradients with focal point != start center

Previously, it was assumed the focal point was the center of the start
circle. This is only the case if the start radius is zero or the centers
of the start and end circle are the same.

This was pretty hard to spot until repeating SVG radial gradients where
added, where the maths broke near the focal point.
This commit is contained in:
MacDue 2023-08-21 23:03:42 +01:00 committed by Andreas Kling
parent c171810030
commit bf55b49070
Notes: sideshowbarker 2024-07-16 23:34:49 +09:00

View File

@ -468,28 +468,21 @@ static auto create_radial_gradient_between_two_circles(Gfx::FloatPoint start_cen
// radius r(ω) at position (x(ω), y(ω)), with the color at ω, but only painting on the parts of the
// bitmap that have not yet been painted on by earlier circles in this step for this rendering of the gradient.
auto center_delta = end_center - start_center;
auto center_dist = end_center.distance_from(start_center);
bool inner_contained = ((center_dist + start_radius) < end_radius);
auto start_point = start_center;
if (!inner_contained) {
// The intersection point of the direct common tangents of the start/end circles.
start_point = FloatPoint {
(start_radius * end_center.x() - end_radius * start_center.x()) / (start_radius - end_radius),
(start_radius * end_center.y() - end_radius * start_center.y()) / (start_radius - end_radius)
};
if (start_radius != 0) {
// Set the start point to the focal point.
auto f = end_radius / (end_radius - start_radius);
auto one_minus_f = 1 - f;
start_point = start_center.scaled(f) + end_center.scaled(one_minus_f);
}
// This is just an approximate upperbound (the gradient line class will shorten this if necessary).
int gradient_length = AK::ceil(center_dist + end_radius + start_radius);
GradientLine gradient_line(gradient_length, color_stops, repeat_length, UsePremultipliedAlpha::No);
auto radius2 = end_radius * end_radius;
center_delta = end_center - start_point;
auto dx2_factor = (radius2 - center_delta.y() * center_delta.y());
auto dy2_factor = (radius2 - center_delta.x() * center_delta.x());
// If you can simplify this please do, this is "best guess" implementation due to lack of specification.
// It was implemented to visually match chrome/firefox in all cases:
// - Start circle inside end circle
@ -499,11 +492,29 @@ static auto create_radial_gradient_between_two_circles(Gfx::FloatPoint start_cen
// - Start circle larger than end circle (outside end circle)
// - Start circle or end circle radius == 0
// FIXME: This does not render the SVG spreadMethod=repeat/reflect correctly if
// focal radius > 0 and the focal point is not centered within the end circle.
// It does not render spreadMethod=pad correctly in this case either, but it's really subtle.
// It may be worth trying Skia's approach: https://skia.org/docs/dev/design/conical/
// (Yes, this gradient actually is a two-point conical gradient)
auto circle_distance_finder = [=](auto radius, auto center) {
auto radius2 = radius * radius;
auto delta = center - start_point;
auto dx2_factor = (radius2 - delta.y() * delta.y());
auto dy2_factor = (radius2 - delta.x() * delta.x());
return [=](bool postive_root, auto vec) {
// This works out the distance to the nearest point on the circle
// in the direction of the "vec" vector.
auto dx2 = vec.x() * vec.x();
auto dy2 = vec.y() * vec.y();
auto root = sqrtf(dx2 * dx2_factor + dy2 * dy2_factor
+ 2 * vec.x() * vec.y() * delta.x() * delta.y());
auto dot = vec.x() * delta.x() + vec.y() * delta.y();
return (((postive_root ? root : -root) + dot) / (dx2 + dy2));
};
};
auto end_circle_dist = circle_distance_finder(end_radius, end_center);
auto start_circle_dist = [=, dist = circle_distance_finder(start_radius, start_center)](bool postive_root, auto vec) {
if (start_center == start_point)
return start_radius;
return dist(postive_root, vec);
};
return Gradient {
move(gradient_line),
@ -513,23 +524,17 @@ static auto create_radial_gradient_between_two_circles(Gfx::FloatPoint start_cen
auto dist = point.distance_from(start_point);
if (dist == 0)
return 0.0f;
// The "vec" (unit) vector points from the focal point to the current point.
auto vec = (point - start_point) / dist;
auto dx2 = vec.x() * vec.x();
auto dy2 = vec.y() * vec.y();
// This works out the distance to the nearest point on the end circle in the direction of the "vec" vector.
// The "vec" vector points from the center of the start circle to the current point.
auto root = sqrtf(dx2 * dx2_factor + dy2 * dy2_factor
+ 2 * vec.x() * vec.y() * center_delta.x() * center_delta.y());
auto dot = vec.x() * center_delta.x() + vec.y() * center_delta.y();
// Note: When reversed we always want the farthest point
auto edge_dist = (((inner_contained || reverse_gradient ? root : -root) + dot) / (dx2 + dy2));
auto start_offset = inner_contained ? start_radius : (edge_dist / end_radius) * start_radius;
bool use_postive_root = inner_contained || reverse_gradient;
auto dist_end = end_circle_dist(use_postive_root, vec);
auto dist_start = start_circle_dist(use_postive_root, vec);
// FIXME: Returning nan is a hack for "Don't paint me!"
if (edge_dist < 0)
if (dist_end < 0)
return AK::NaN<float>;
if (edge_dist - start_offset < 0)
if (dist_end - dist_start < 0)
return float(gradient_length);
return ((dist - start_offset) / (edge_dist - start_offset));
return (dist - dist_start) / (dist_end - dist_start);
};
auto loc = get_gradient_location();
if (reverse_gradient)