Skip to content

Commit 4dbae64

Browse files
committed
LibWeb: Unify objectBoundingBox and userSpaceOnUse coord transformations
There's a fairly complicated interaction between an SVG gradient's paint transformation and the gradient coordinate transformation required to correctly draw gradient fills. This was especially noticeable when scaling down an SVG, resulting in broken gradient coordinates and graphical glitches. This changes the objectBoundingBox units to immediately map to the bounding box's coordinate system, so we can unify the gradient paint transformation logic and make it a lot simpler. We only need to undo the bounding box offset and apply the paint transformation to fix a lot of gradient fill bugs.
1 parent beb1d60 commit 4dbae64

14 files changed

+200
-21
lines changed

Libraries/LibWeb/SVG/SVGGradientElement.cpp

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
3+
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
34
*
45
* SPDX-License-Identifier: BSD-2-Clause
56
*/
@@ -98,16 +99,9 @@ Optional<Gfx::AffineTransform> SVGGradientElement::gradient_transform_impl(HashT
9899
// The gradient transform, appropriately scaled and combined with the paint transform.
99100
Gfx::AffineTransform SVGGradientElement::gradient_paint_transform(SVGPaintContext const& paint_context) const
100101
{
101-
Gfx::AffineTransform gradient_paint_transform = paint_context.paint_transform;
102-
auto const& bounding_box = paint_context.path_bounding_box;
103-
104-
if (gradient_units() == SVGUnits::ObjectBoundingBox) {
105-
// Scale points from 0..1 to bounding box coordinates:
106-
gradient_paint_transform.scale(bounding_box.width(), bounding_box.height());
107-
} else {
108-
// Translate points from viewport to bounding box coordinates:
109-
gradient_paint_transform.translate(paint_context.viewport.location() - bounding_box.location());
110-
}
102+
auto gradient_paint_transform = Gfx::AffineTransform {};
103+
gradient_paint_transform.set_translation(-paint_context.paint_transform.map(paint_context.path_bounding_box).location())
104+
.multiply(paint_context.paint_transform);
111105

112106
if (auto transform = gradient_transform(); transform.has_value())
113107
gradient_paint_transform.multiply(transform.value());

Libraries/LibWeb/SVG/SVGLinearGradientElement.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ NumberPercentage SVGLinearGradientElement::end_y_impl(HashTable<SVGGradientEleme
114114

115115
Optional<Painting::PaintStyle> SVGLinearGradientElement::to_gfx_paint_style(SVGPaintContext const& paint_context) const
116116
{
117-
// FIXME: Resolve percentages properly
118117
Gfx::FloatPoint start_point {};
119118
Gfx::FloatPoint end_point {};
120119

@@ -125,8 +124,15 @@ Optional<Painting::PaintStyle> SVGLinearGradientElement::to_gfx_paint_style(SVGP
125124
// box units) and then applying the transform specified by attribute ‘gradientTransform’. Percentages represent
126125
// values relative to the bounding box for the object.
127126
// Note: For gradientUnits="objectBoundingBox" both "100%" and "1" are treated the same.
128-
start_point = { start_x().value(), start_y().value() };
129-
end_point = { end_x().value(), end_y().value() };
127+
auto const& bounding_box = paint_context.path_bounding_box;
128+
start_point = {
129+
bounding_box.location().x() + start_x().value() * bounding_box.width(),
130+
bounding_box.location().y() + start_y().value() * bounding_box.height(),
131+
};
132+
end_point = {
133+
bounding_box.location().x() + end_x().value() * bounding_box.width(),
134+
bounding_box.location().y() + end_y().value() * bounding_box.height(),
135+
};
130136
} else {
131137
// GradientUnits::UserSpaceOnUse
132138
// If gradientUnits="userSpaceOnUse", ‘x1’, ‘y1’, ‘x2’, and ‘y2’ represent values in the coordinate system

Libraries/LibWeb/SVG/SVGRadialGradientElement.cpp

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,15 +169,23 @@ Optional<Painting::PaintStyle> SVGRadialGradientElement::to_gfx_paint_style(SVGP
169169
Gfx::FloatPoint end_center;
170170
float end_radius = 0.0f;
171171

172+
// FIXME: Where in the spec does it say what axis the radius is relative to?
172173
if (units == GradientUnits::ObjectBoundingBox) {
173174
// If gradientUnits="objectBoundingBox", the user coordinate system for attributes ‘cx’, ‘cy’, ‘r’, ‘fx’, ‘fy’, and ‘fr’
174175
// is established using the bounding box of the element to which the gradient is applied (see Object bounding box units)
175176
// and then applying the transform specified by attribute ‘gradientTransform’. Percentages represent values relative
176177
// to the bounding box for the object.
177-
start_center = Gfx::FloatPoint { start_circle_x().value(), start_circle_y().value() };
178-
start_radius = start_circle_radius().value();
179-
end_center = Gfx::FloatPoint { end_circle_x().value(), end_circle_y().value() };
180-
end_radius = end_circle_radius().value();
178+
auto const& bounding_box = paint_context.path_bounding_box;
179+
start_center = {
180+
bounding_box.location().x() + start_circle_x().value() * bounding_box.width(),
181+
bounding_box.location().y() + start_circle_y().value() * bounding_box.height(),
182+
};
183+
start_radius = start_circle_radius().value() * bounding_box.width();
184+
end_center = {
185+
bounding_box.location().x() + end_circle_x().value() * bounding_box.width(),
186+
bounding_box.location().y() + end_circle_y().value() * bounding_box.height(),
187+
};
188+
end_radius = end_circle_radius().value() * bounding_box.width();
181189
} else {
182190
// GradientUnits::UserSpaceOnUse
183191
// If gradientUnits="userSpaceOnUse", ‘cx’, ‘cy’, ‘r’, ‘fx’, ‘fy’, and ‘fr’ represent values in the coordinate system
@@ -191,7 +199,6 @@ Optional<Painting::PaintStyle> SVGRadialGradientElement::to_gfx_paint_style(SVGP
191199
start_circle_x().resolve_relative_to(paint_context.viewport.width()),
192200
start_circle_y().resolve_relative_to(paint_context.viewport.height()),
193201
};
194-
// FIXME: Where in the spec does it say what axis the radius is relative to?
195202
start_radius = start_circle_radius().resolve_relative_to(paint_context.viewport.width());
196203
end_center = Gfx::FloatPoint {
197204
end_circle_x().resolve_relative_to(paint_context.viewport.width()),
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<style>
3+
* {
4+
margin: 0;
5+
}
6+
body {
7+
background-color: white;
8+
}
9+
</style>
10+
<img src="../images/svg-gradient-objectBoundingBox-ref.png">
3.93 KB
Loading
-1.98 KB
Loading
243 Bytes
Loading
612 Bytes
Loading
74 Bytes
Loading
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<!DOCTYPE html>
2+
<link rel="match" href="../expected/svg-gradient-objectBoundingBox-ref.html" />
3+
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-69">
4+
<style>
5+
svg {
6+
border: 1px solid red;
7+
}
8+
</style>
9+
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
10+
<rect x="10" y="10" width="44" height="44" fill="url(#a)" />
11+
<defs>
12+
<linearGradient id="a" x1=".1" y1=".1" x2=".1" y2="1" gradientUnits="objectBoundingBox">
13+
<stop offset="0" stop-color="#f00" />
14+
<stop offset="0.0001" stop-color="#0f0" stop-opacity="0.3" />
15+
<stop offset="1" stop-color="#0f0" stop-opacity="0.7" />
16+
</linearGradient>
17+
</defs>
18+
</svg>
19+
<svg width="32" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
20+
<rect x="0" y="0" width="64" height="64" fill="url(#b)" />
21+
<defs>
22+
<linearGradient id="b" x1="0" y1="0" x2="0" y2="1" gradientUnits="objectBoundingBox">
23+
<stop offset="0" stop-color="#f00" />
24+
<stop offset="0.1" stop-color="#0f0" />
25+
<stop offset="1" stop-color="#00f" />
26+
</linearGradient>
27+
</defs>
28+
</svg>
29+
<svg width="48" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
30+
<rect x="0" y="0" width="64" height="64" fill="url(#c)" />
31+
<defs>
32+
<linearGradient id="c" x1="0" y1="0" x2="0" y2="1" gradientUnits="objectBoundingBox">
33+
<stop offset="0" stop-color="#f00" />
34+
<stop offset="0.1" stop-color="#0f0" />
35+
<stop offset="1" stop-color="#00f" />
36+
</linearGradient>
37+
</defs>
38+
</svg>
39+
<svg width="64" height="64" viewBox="0 32 64 32" fill="none" xmlns="http://www.w3.org/2000/svg">
40+
<rect x="0" y="0" width="64" height="64" fill="url(#d)" />
41+
<defs>
42+
<linearGradient id="d" x1="0" y1="0" x2="0" y2="1" gradientUnits="objectBoundingBox">
43+
<stop offset="0" stop-color="#f00" />
44+
<stop offset="0.1" stop-color="#0f0" />
45+
<stop offset="1" stop-color="#00f" />
46+
</linearGradient>
47+
</defs>
48+
</svg>
49+
<svg width="64" height="64" viewBox="0 0 64 8" fill="none" xmlns="http://www.w3.org/2000/svg">
50+
<rect x="0" y="0" width="64" height="64" fill="url(#e)" />
51+
<defs>
52+
<linearGradient id="e" x1="0" y1="0" x2="0" y2="1" gradientUnits="objectBoundingBox">
53+
<stop offset="0" stop-color="#f00" />
54+
<stop offset="0.1" stop-color="#0f0" />
55+
<stop offset="1" stop-color="#00f" />
56+
</linearGradient>
57+
</defs>
58+
</svg>
59+
<svg width="64" height="64" viewBox="32 0 32 64" fill="none" xmlns="http://www.w3.org/2000/svg">
60+
<rect x="0" y="0" width="64" height="64" fill="url(#f)" />
61+
<defs>
62+
<linearGradient id="f" x1=".1" y1="0" x2=".8" y2="0" gradientUnits="objectBoundingBox">
63+
<stop offset="0" stop-color="#f00" />
64+
<stop offset="0.1" stop-color="#0f0" />
65+
<stop offset="1" stop-color="#00f" />
66+
</linearGradient>
67+
</defs>
68+
</svg>
69+
<svg width="64" height="64" viewBox="-20 -20 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
70+
<rect x="-20" y="-20" width="50" height="50" fill="url(#g)" />
71+
<defs>
72+
<linearGradient id="g" x1="10%" y1="10%" x2="10%" y2="100%" gradientUnits="objectBoundingBox">
73+
<stop offset="0" stop-color="#f00" />
74+
<stop offset="0.0001" stop-color="#0f0" stop-opacity="0.3" />
75+
<stop offset="1" stop-color="#0f0" stop-opacity="0.7" />
76+
</linearGradient>
77+
</defs>
78+
</svg>
79+
<svg width="64" height="64" fill="none" xmlns="http://www.w3.org/2000/svg">
80+
<rect x="16" y="16" width="32" height="32" fill="url(#h)" transform="rotate(45 32 32)" />
81+
<defs>
82+
<linearGradient id="h" gradientUnits="objectBoundingBox">
83+
<stop offset="0" stop-color="#f00" />
84+
<stop offset="1" stop-color="#0f0" />
85+
</linearGradient>
86+
</defs>
87+
</svg>

0 commit comments

Comments
 (0)