1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 07:58:11 +00:00

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.
This commit is contained in:
MacDue 2023-01-17 19:52:02 +00:00 committed by Andreas Kling
parent f3c0987afe
commit 1a89d77688
2 changed files with 228 additions and 1 deletions

View file

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

View file

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