diff options
author | MacDue <macdue@dueutil.tech> | 2023-01-17 19:52:02 +0000 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2023-01-22 18:15:52 +0100 |
commit | 1a89d776885dfbde280d50cfea14c99d66823f87 (patch) | |
tree | 138a3b57e7838104acde1e80441f1a62a6aa7551 | |
parent | f3c0987afe4148d204143f09acade8bd9d178321 (diff) | |
download | serenity-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.cpp | 161 | ||||
-rw-r--r-- | Userland/Libraries/LibGfx/PaintStyle.h | 68 |
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 }; +}; + } |