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.
Author: https://github.com/gmta
Commit: 4dbae64dce
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/6592
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
<img src="../images/svg-gradient-objectBoundingBox-ref.png">
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
|
@ -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>
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||