summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMacDue <macdue@dueutil.tech>2023-01-17 19:52:02 +0000
committerAndreas Kling <kling@serenityos.org>2023-01-22 18:15:52 +0100
commit1a89d776885dfbde280d50cfea14c99d66823f87 (patch)
tree138a3b57e7838104acde1e80441f1a62a6aa7551
parentf3c0987afe4148d204143f09acade8bd9d178321 (diff)
downloadserenity-1a89d776885dfbde280d50cfea14c99d66823f87.zip
LibGfx: Implement paint styles required for HTML canvas gradients
This implements the gradients for: - CanvasRenderingContext2D.createLinearGradient() - CanvasRenderingContext2D.createConicGradient() - CanvasRenderingContext2D.createRadialGradient() As loosely defined in: https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles (It's really not very well defined for radial gradients) Actual implementation (for radial gradients) was done with a lot of trial and error, then visually comparing to other browsers.
-rw-r--r--Userland/Libraries/LibGfx/GradientPainting.cpp161
-rw-r--r--Userland/Libraries/LibGfx/PaintStyle.h68
2 files changed, 228 insertions, 1 deletions
diff --git a/Userland/Libraries/LibGfx/GradientPainting.cpp b/Userland/Libraries/LibGfx/GradientPainting.cpp
index f016e1ea67..f15ae95b9a 100644
--- a/Userland/Libraries/LibGfx/GradientPainting.cpp
+++ b/Userland/Libraries/LibGfx/GradientPainting.cpp
@@ -15,7 +15,7 @@
namespace Gfx {
-// Note: This file implements the CSS gradients for LibWeb according to the spec.
+// Note: This file implements the CSS/Canvas gradients for LibWeb according to the spec.
// Please do not make ad-hoc changes that may break spec compliance!
static float color_stop_step(ColorStop const& previous_stop, ColorStop const& next_stop, float position)
@@ -92,6 +92,8 @@ public:
Color sample_color(float loc) const
{
+ if (!isfinite(loc))
+ return Color();
if (m_sample_scale != 1.0f)
loc *= m_sample_scale;
auto repeat_wrap_if_required = [&](i64 loc) {
@@ -279,4 +281,161 @@ void RadialGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunctio
paint(radial_gradient.sample_function());
}
+// The following implements the gradient fill/stoke styles for the HTML canvas: https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles
+
+static auto make_sample_non_relative(IntPoint draw_location, auto sample)
+{
+ return [=, sample = move(sample)](IntPoint point) { return sample(point.translated(draw_location)); };
+}
+
+void CanvasLinearGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const
+{
+ // If x0 = x1 and y0 = y1, then the linear gradient must paint nothing.
+ if (m_p0 == m_p1)
+ return;
+ if (color_stops().is_empty())
+ return;
+ if (color_stops().size() < 2)
+ return paint([this](IntPoint) { return color_stops().first().color; });
+
+ auto delta = m_p1 - m_p0;
+ auto angle = AK::atan2(delta.y(), delta.x());
+ float sin_angle, cos_angle;
+ AK::sincos(angle, sin_angle, cos_angle);
+ int gradient_length = ceilf(m_p1.distance_from(m_p0));
+ auto rotated_start_point_x = m_p0.x() * cos_angle - m_p0.y() * -sin_angle;
+
+ Gradient linear_gradient {
+ GradientLine(gradient_length, color_stops(), repeat_length(), UsePremultipliedAlpha::No),
+ [=](int x, int y) {
+ return (x * cos_angle - y * -sin_angle) - rotated_start_point_x;
+ }
+ };
+
+ paint(make_sample_non_relative(physical_bounding_box.location(), linear_gradient.sample_function()));
+}
+
+void CanvasConicGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const
+{
+ if (color_stops().is_empty())
+ return;
+ if (color_stops().size() < 2)
+ return paint([this](IntPoint) { return color_stops().first().color; });
+
+ // Follows the same rendering rule as CSS 'conic-gradient' and it is equivalent to CSS
+ // 'conic-gradient(from adjustedStartAnglerad at xpx ypx, angularColorStopList)'.
+ // Here:
+ // adjustedStartAngle is given by startAngle + π/2;
+ auto conic_gradient = create_conic_gradient(color_stops(), m_center, m_start_angle + 90.0f, repeat_length(), UsePremultipliedAlpha::No);
+ paint(make_sample_non_relative(physical_bounding_box.location(), conic_gradient.sample_function()));
+}
+
+void CanvasRadialGradientPaintStyle::paint(IntRect physical_bounding_box, PaintFunction paint) const
+{
+ // 1. If x0 = x1 and y0 = y1 and r0 = r1, then the radial gradient must paint nothing. Return.
+ if (m_start_center == m_end_center && m_start_radius == m_end_radius)
+ return;
+ if (color_stops().is_empty())
+ return;
+ if (color_stops().size() < 2)
+ return paint([this](IntPoint) { return color_stops().first().color; });
+
+ auto start_radius = m_start_radius;
+ auto start_center = m_start_center;
+ auto end_radius = m_end_radius;
+ auto end_center = m_end_center;
+
+ if (end_radius == 0 && start_radius == 0)
+ return;
+
+ if (fabs(start_radius - end_radius) < 1)
+ start_radius += 1;
+
+ // Needed for the start circle > end circle case, but FIXME, this seems kind of hacky.
+ bool reverse_gradient = end_radius < start_radius;
+ if (reverse_gradient) {
+ swap(end_radius, start_radius);
+ swap(end_center, start_center);
+ }
+
+ // Spec steps: Useless for writing an actual implementation (give it a go :P):
+ //
+ // 2. Let x(ω) = (x1-x0)ω + x0
+ // Let y(ω) = (y1-y0)ω + y0
+ // Let r(ω) = (r1-r0)ω + r0
+ // Let the color at ω be the color at that position on the gradient
+ // (with the colors coming from the interpolation and extrapolation described above).
+ //
+ // 3. For all values of ω where r(ω) > 0, starting with the value of ω nearest to positive infinity and
+ // ending with the value of ω nearest to negative infinity, draw the circumference of the circle with
+ // 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)
+ };
+ }
+
+ // This is just an approximate upperbound (the gradient line class will shorten this if necessary).
+ int gradient_length = 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
+ // - Start circle outside end circle
+ // - Start circle radius == end circle radius
+ // - Start circle larger than end circle (inside end circle)
+ // - Start circle larger than end circle (outside end circle)
+ // - Start cirlce or end circle radius == 0
+
+ Gradient radial_gradient {
+ move(gradient_line),
+ [=](int x, int y) {
+ auto get_gradient_location = [&] {
+ FloatPoint point { x, y };
+ auto dist = point.distance_from(start_point);
+ if (dist == 0)
+ return 0.0f;
+ 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;
+ // FIXME: Returning nan is a hack for "Don't paint me!"
+ if (edge_dist < 0)
+ return AK::NaN<float>;
+ if (edge_dist - start_offset < 0)
+ return float(gradient_length);
+ return ((dist - start_offset) / (edge_dist - start_offset));
+ };
+ auto loc = get_gradient_location();
+ if (reverse_gradient)
+ loc = 1.0f - loc;
+ return loc * gradient_length;
+ }
+ };
+
+ paint(make_sample_non_relative(physical_bounding_box.location(), radial_gradient.sample_function()));
+}
+
}
diff --git a/Userland/Libraries/LibGfx/PaintStyle.h b/Userland/Libraries/LibGfx/PaintStyle.h
index ffa6ef0a91..19a5d14f76 100644
--- a/Userland/Libraries/LibGfx/PaintStyle.h
+++ b/Userland/Libraries/LibGfx/PaintStyle.h
@@ -147,4 +147,72 @@ private:
IntSize m_size;
};
+// The following paint styles implement the gradients required for the HTML canvas.
+// These gradients are (unlike CSS ones) not relative to the painted shape, and do not
+// support premultiplied alpha.
+
+class CanvasLinearGradientPaintStyle final : public GradientPaintStyle {
+public:
+ static NonnullRefPtr<CanvasLinearGradientPaintStyle> create(FloatPoint p0, FloatPoint p1)
+ {
+ return adopt_ref(*new CanvasLinearGradientPaintStyle(p0, p1));
+ }
+
+private:
+ virtual void paint(IntRect physical_bounding_box, PaintFunction paint) const override;
+
+ CanvasLinearGradientPaintStyle(FloatPoint p0, FloatPoint p1)
+ : m_p0(p0)
+ , m_p1(p1)
+ {
+ }
+
+ FloatPoint m_p0;
+ FloatPoint m_p1;
+};
+
+class CanvasConicGradientPaintStyle final : public GradientPaintStyle {
+public:
+ static NonnullRefPtr<CanvasConicGradientPaintStyle> create(FloatPoint center, float start_angle = 0.0f)
+ {
+ return adopt_ref(*new CanvasConicGradientPaintStyle(center, start_angle));
+ }
+
+private:
+ virtual void paint(IntRect physical_bounding_box, PaintFunction paint) const override;
+
+ CanvasConicGradientPaintStyle(FloatPoint center, float start_angle)
+ : m_center(center)
+ , m_start_angle(start_angle)
+ {
+ }
+
+ FloatPoint m_center;
+ float m_start_angle { 0.0f };
+};
+
+class CanvasRadialGradientPaintStyle final : public GradientPaintStyle {
+public:
+ static NonnullRefPtr<CanvasRadialGradientPaintStyle> create(FloatPoint start_center, float start_radius, FloatPoint end_center, float end_radius)
+ {
+ return adopt_ref(*new CanvasRadialGradientPaintStyle(start_center, start_radius, end_center, end_radius));
+ }
+
+private:
+ virtual void paint(IntRect physical_bounding_box, PaintFunction paint) const override;
+
+ CanvasRadialGradientPaintStyle(FloatPoint start_center, float start_radius, FloatPoint end_center, float end_radius)
+ : m_start_center(start_center)
+ , m_start_radius(start_radius)
+ , m_end_center(end_center)
+ , m_end_radius(end_radius)
+ {
+ }
+
+ FloatPoint m_start_center;
+ float m_start_radius { 0.0f };
+ FloatPoint m_end_center;
+ float m_end_radius { 0.0f };
+};
+
}