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.
This commit is contained in:
Jelle Raaijmakers 2025-10-26 13:44:14 +01:00 committed by Jelle Raaijmakers
parent beb1d60714
commit 4dbae64dce
Notes: github-actions[bot] 2025-10-27 23:43:24 +00:00
14 changed files with 200 additions and 21 deletions

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -98,16 +99,9 @@ Optional<Gfx::AffineTransform> SVGGradientElement::gradient_transform_impl(HashT
// The gradient transform, appropriately scaled and combined with the paint transform.
Gfx::AffineTransform SVGGradientElement::gradient_paint_transform(SVGPaintContext const& paint_context) const
{
Gfx::AffineTransform gradient_paint_transform = paint_context.paint_transform;
auto const& bounding_box = paint_context.path_bounding_box;
if (gradient_units() == SVGUnits::ObjectBoundingBox) {
// Scale points from 0..1 to bounding box coordinates:
gradient_paint_transform.scale(bounding_box.width(), bounding_box.height());
} else {
// Translate points from viewport to bounding box coordinates:
gradient_paint_transform.translate(paint_context.viewport.location() - bounding_box.location());
}
auto gradient_paint_transform = Gfx::AffineTransform {};
gradient_paint_transform.set_translation(-paint_context.paint_transform.map(paint_context.path_bounding_box).location())
.multiply(paint_context.paint_transform);
if (auto transform = gradient_transform(); transform.has_value())
gradient_paint_transform.multiply(transform.value());

View file

@ -114,7 +114,6 @@ NumberPercentage SVGLinearGradientElement::end_y_impl(HashTable<SVGGradientEleme
Optional<Painting::PaintStyle> SVGLinearGradientElement::to_gfx_paint_style(SVGPaintContext const& paint_context) const
{
// FIXME: Resolve percentages properly
Gfx::FloatPoint start_point {};
Gfx::FloatPoint end_point {};
@ -125,8 +124,15 @@ Optional<Painting::PaintStyle> SVGLinearGradientElement::to_gfx_paint_style(SVGP
// box units) and then applying the transform specified by attribute gradientTransform. Percentages represent
// values relative to the bounding box for the object.
// Note: For gradientUnits="objectBoundingBox" both "100%" and "1" are treated the same.
start_point = { start_x().value(), start_y().value() };
end_point = { end_x().value(), end_y().value() };
auto const& bounding_box = paint_context.path_bounding_box;
start_point = {
bounding_box.location().x() + start_x().value() * bounding_box.width(),
bounding_box.location().y() + start_y().value() * bounding_box.height(),
};
end_point = {
bounding_box.location().x() + end_x().value() * bounding_box.width(),
bounding_box.location().y() + end_y().value() * bounding_box.height(),
};
} else {
// GradientUnits::UserSpaceOnUse
// If gradientUnits="userSpaceOnUse", x1, y1, x2, and y2 represent values in the coordinate system

View file

@ -169,15 +169,23 @@ Optional<Painting::PaintStyle> SVGRadialGradientElement::to_gfx_paint_style(SVGP
Gfx::FloatPoint end_center;
float end_radius = 0.0f;
// FIXME: Where in the spec does it say what axis the radius is relative to?
if (units == GradientUnits::ObjectBoundingBox) {
// If gradientUnits="objectBoundingBox", the user coordinate system for attributes cx, cy, r, fx, fy, and fr
// is established using the bounding box of the element to which the gradient is applied (see Object bounding box units)
// and then applying the transform specified by attribute gradientTransform. Percentages represent values relative
// to the bounding box for the object.
start_center = Gfx::FloatPoint { start_circle_x().value(), start_circle_y().value() };
start_radius = start_circle_radius().value();
end_center = Gfx::FloatPoint { end_circle_x().value(), end_circle_y().value() };
end_radius = end_circle_radius().value();
auto const& bounding_box = paint_context.path_bounding_box;
start_center = {
bounding_box.location().x() + start_circle_x().value() * bounding_box.width(),
bounding_box.location().y() + start_circle_y().value() * bounding_box.height(),
};
start_radius = start_circle_radius().value() * bounding_box.width();
end_center = {
bounding_box.location().x() + end_circle_x().value() * bounding_box.width(),
bounding_box.location().y() + end_circle_y().value() * bounding_box.height(),
};
end_radius = end_circle_radius().value() * bounding_box.width();
} else {
// GradientUnits::UserSpaceOnUse
// 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
start_circle_x().resolve_relative_to(paint_context.viewport.width()),
start_circle_y().resolve_relative_to(paint_context.viewport.height()),
};
// FIXME: Where in the spec does it say what axis the radius is relative to?
start_radius = start_circle_radius().resolve_relative_to(paint_context.viewport.width());
end_center = Gfx::FloatPoint {
end_circle_x().resolve_relative_to(paint_context.viewport.width()),

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<style>
* {
margin: 0;
}
body {
background-color: white;
}
</style>
<img src="../images/svg-gradient-objectBoundingBox-ref.png">

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

View file

@ -0,0 +1,87 @@
<!DOCTYPE html>
<link rel="match" href="../expected/svg-gradient-objectBoundingBox-ref.html" />
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-69">
<style>
svg {
border: 1px solid red;
}
</style>
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="44" height="44" fill="url(#a)" />
<defs>
<linearGradient id="a" x1=".1" y1=".1" x2=".1" y2="1" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f00" />
<stop offset="0.0001" stop-color="#0f0" stop-opacity="0.3" />
<stop offset="1" stop-color="#0f0" stop-opacity="0.7" />
</linearGradient>
</defs>
</svg>
<svg width="32" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="64" height="64" fill="url(#b)" />
<defs>
<linearGradient id="b" x1="0" y1="0" x2="0" y2="1" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f00" />
<stop offset="0.1" stop-color="#0f0" />
<stop offset="1" stop-color="#00f" />
</linearGradient>
</defs>
</svg>
<svg width="48" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="64" height="64" fill="url(#c)" />
<defs>
<linearGradient id="c" x1="0" y1="0" x2="0" y2="1" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f00" />
<stop offset="0.1" stop-color="#0f0" />
<stop offset="1" stop-color="#00f" />
</linearGradient>
</defs>
</svg>
<svg width="64" height="64" viewBox="0 32 64 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="64" height="64" fill="url(#d)" />
<defs>
<linearGradient id="d" x1="0" y1="0" x2="0" y2="1" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f00" />
<stop offset="0.1" stop-color="#0f0" />
<stop offset="1" stop-color="#00f" />
</linearGradient>
</defs>
</svg>
<svg width="64" height="64" viewBox="0 0 64 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="64" height="64" fill="url(#e)" />
<defs>
<linearGradient id="e" x1="0" y1="0" x2="0" y2="1" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f00" />
<stop offset="0.1" stop-color="#0f0" />
<stop offset="1" stop-color="#00f" />
</linearGradient>
</defs>
</svg>
<svg width="64" height="64" viewBox="32 0 32 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="64" height="64" fill="url(#f)" />
<defs>
<linearGradient id="f" x1=".1" y1="0" x2=".8" y2="0" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f00" />
<stop offset="0.1" stop-color="#0f0" />
<stop offset="1" stop-color="#00f" />
</linearGradient>
</defs>
</svg>
<svg width="64" height="64" viewBox="-20 -20 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="-20" y="-20" width="50" height="50" fill="url(#g)" />
<defs>
<linearGradient id="g" x1="10%" y1="10%" x2="10%" y2="100%" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f00" />
<stop offset="0.0001" stop-color="#0f0" stop-opacity="0.3" />
<stop offset="1" stop-color="#0f0" stop-opacity="0.7" />
</linearGradient>
</defs>
</svg>
<svg width="64" height="64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="16" y="16" width="32" height="32" fill="url(#h)" transform="rotate(45 32 32)" />
<defs>
<linearGradient id="h" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f00" />
<stop offset="1" stop-color="#0f0" />
</linearGradient>
</defs>
</svg>

View file

@ -1,6 +1,6 @@
<!DOCTYPE html>
<link rel="match" href="../expected/svg-gradient-paint-transformation-ref.html" />
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-231">
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-209">
<svg height="150" width="150" xmlns="http://www.w3.org/2000/svg">
<style>
.hi {

View file

@ -1,6 +1,6 @@
<!DOCTYPE html>
<link rel="match" href="../expected/svg-gradient-spreadMethod-ref.html" />
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-68">
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-43">
<style>
* {
margin: 0;

View file

@ -1,5 +1,11 @@
<!DOCTYPE html>
<link rel="match" href="../expected/svg-gradient-userSpaceOnUse-ref.html" />
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-69">
<style>
svg {
border: 1px solid red;
}
</style>
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="44" height="44" fill="url(#a)" />
<defs>
@ -10,3 +16,72 @@
</linearGradient>
</defs>
</svg>
<svg width="32" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="64" height="64" fill="url(#b)" />
<defs>
<linearGradient id="b" x1="0" y1="0" x2="0" y2="64" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f00" />
<stop offset="0.1" stop-color="#0f0" />
<stop offset="1" stop-color="#00f" />
</linearGradient>
</defs>
</svg>
<svg width="48" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="64" height="64" fill="url(#c)" />
<defs>
<linearGradient id="c" x1="0" y1="0" x2="0" y2="64" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f00" />
<stop offset="0.1" stop-color="#0f0" />
<stop offset="1" stop-color="#00f" />
</linearGradient>
</defs>
</svg>
<svg width="64" height="64" viewBox="0 32 64 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="64" height="64" fill="url(#d)" />
<defs>
<linearGradient id="d" x1="0" y1="0" x2="0" y2="64" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f00" />
<stop offset="0.1" stop-color="#0f0" />
<stop offset="1" stop-color="#00f" />
</linearGradient>
</defs>
</svg>
<svg width="64" height="64" viewBox="0 0 64 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="64" height="64" fill="url(#e)" />
<defs>
<linearGradient id="e" x1="0" y1="0" x2="0" y2="64" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f00" />
<stop offset="0.1" stop-color="#0f0" />
<stop offset="1" stop-color="#00f" />
</linearGradient>
</defs>
</svg>
<svg width="64" height="64" viewBox="32 0 32 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="64" height="64" fill="url(#f)" />
<defs>
<linearGradient id="f" x1="10" y1="0" x2="54" y2="0" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f00" />
<stop offset="0.1" stop-color="#0f0" />
<stop offset="1" stop-color="#00f" />
</linearGradient>
</defs>
</svg>
<svg width="64" height="64" viewBox="-20 -20 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="-20" y="-20" width="50" height="50" fill="url(#g)" />
<defs>
<linearGradient id="g" x1="10" y1="10" x2="10" y2="64" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f00" />
<stop offset="0.0001" stop-color="#0f0" stop-opacity="0.3" />
<stop offset="1" stop-color="#0f0" stop-opacity="0.7" />
</linearGradient>
</defs>
</svg>
<svg width="64" height="64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="16" y="16" width="32" height="32" fill="url(#h)" transform="rotate(45 32 32)" />
<defs>
<linearGradient id="h" gradientUnits="userSpaceOnUse">
<stop offset=".25" stop-color="#f00" />
<stop offset=".75" stop-color="#0f0" />
</linearGradient>
</defs>
</svg>

View file

@ -1,6 +1,6 @@
<!DOCTYPE html>
<link rel="match" href="../expected/svg-text-effects-ref.html" />
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-717">
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-723">
<svg height="200" width="350" xmlns="http://www.w3.org/2000/svg">
<style>
.text {