commit 02242322aef59b10c4965ca1f7d2e0a65734f34d
parent b237e89e536ff95e2e472e351dfd7d70ad974b0f
Author: Swarup Ukil <sukil@mozilla.com>
Date: Wed, 10 Dec 2025 15:52:23 +0000
Bug 2003903 - Fix interpolation failures with <arc-sweep> and <arc-size> in shape(). r=emilio,firefox-style-system-reviewers
Per shape() spec, the arc keyword interpolation rules are meant to match
existing SVG <path> interpolation rules: flags are interpolated as fractions
between zero and one, with any non-zero value considered to be a value of one.
So path() should reasonably do the same for the arc flags at 0.125 and such.
https://drafts.csswg.org/css-shapes-1/#interpolating-shape
https://svgwg.org/specs/paths/#PathElement
Differential Revision: https://phabricator.services.mozilla.com/D275122
Diffstat:
7 files changed, 36 insertions(+), 78 deletions(-)
diff --git a/servo/components/style/values/generics/basic_shape.rs b/servo/components/style/values/generics/basic_shape.rs
@@ -1329,9 +1329,12 @@ impl Animate for ArcSweep {
use num_traits::FromPrimitive;
// If an arc command has different <arc-sweep> between its starting and ending list, then
// the interpolated result uses cw for any progress value between 0 and 1.
- (*self as i32)
- .animate(&(*other as i32), procedure)
- .map(|v| ArcSweep::from_u8((v > 0) as u8).unwrap_or(ArcSweep::Ccw))
+ // Note: we cast progress from f64->f32->f64 to drop tiny noise near 0.0.
+ let progress = procedure.weights().1 as f32 as f64;
+ let procedure = Procedure::Interpolate { progress };
+ (*self as i32 as f32)
+ .animate(&(*other as i32 as f32), procedure)
+ .map(|v| ArcSweep::from_u8((v > 0.) as u8).unwrap_or(ArcSweep::Ccw))
}
}
@@ -1375,9 +1378,12 @@ impl Animate for ArcSize {
use num_traits::FromPrimitive;
// If it has different <arc-size> keywords, then the interpolated result uses large for any
// progress value between 0 and 1.
- (*self as i32)
- .animate(&(*other as i32), procedure)
- .map(|v| ArcSize::from_u8((v > 0) as u8).unwrap_or(ArcSize::Small))
+ // Note: we cast progress from f64->f32->f64 to drop tiny noise near 0.0.
+ let progress = procedure.weights().1 as f32 as f64;
+ let procedure = Procedure::Interpolate { progress };
+ (*self as i32 as f32)
+ .animate(&(*other as i32 as f32), procedure)
+ .map(|v| ArcSize::from_u8((v > 0.) as u8).unwrap_or(ArcSize::Small))
}
}
diff --git a/testing/web-platform/meta/css/css-masking/animations/clip-path-interpolation-shape.html.ini b/testing/web-platform/meta/css/css-masking/animations/clip-path-interpolation-shape.html.ini
@@ -1,36 +0,0 @@
-[clip-path-interpolation-shape.html]
- [CSS Transitions: property <clip-path> from [shape(from 5% 5px, arc to 15% -15px of 10px 20px, arc by 15% -5px of 30px 30px cw rotate 30deg large, arc to 25% 20px of 10px 5px small)\] to [shape(from 15% 15px, arc to 5% -25px of 20px 30px, arc by 25% -15px of 20px 20px cw rotate 270deg small, arc to 25% 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8% 8px, arc to 12% -18px of 13px 23px, arc by 18% -8px of 27px 27px rotate 102deg cw large, arc to 25% 20px of 10px 5px cw)\]]
- expected: FAIL
-
- [CSS Transitions with transition: all: property <clip-path> from [shape(from 5% 5px, arc to 15% -15px of 10px 20px, arc by 15% -5px of 30px 30px cw rotate 30deg large, arc to 25% 20px of 10px 5px small)\] to [shape(from 15% 15px, arc to 5% -25px of 20px 30px, arc by 25% -15px of 20px 20px cw rotate 270deg small, arc to 25% 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8% 8px, arc to 12% -18px of 13px 23px, arc by 18% -8px of 27px 27px rotate 102deg cw large, arc to 25% 20px of 10px 5px cw)\]]
- expected: FAIL
-
- [CSS Animations: property <clip-path> from [shape(from 5% 5px, arc to 15% -15px of 10px 20px, arc by 15% -5px of 30px 30px cw rotate 30deg large, arc to 25% 20px of 10px 5px small)\] to [shape(from 15% 15px, arc to 5% -25px of 20px 30px, arc by 25% -15px of 20px 20px cw rotate 270deg small, arc to 25% 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8% 8px, arc to 12% -18px of 13px 23px, arc by 18% -8px of 27px 27px rotate 102deg cw large, arc to 25% 20px of 10px 5px cw)\]]
- expected: FAIL
-
- [Web Animations: property <clip-path> from [shape(from 5% 5px, arc to 15% -15px of 10px 20px, arc by 15% -5px of 30px 30px cw rotate 30deg large, arc to 25% 20px of 10px 5px small)\] to [shape(from 15% 15px, arc to 5% -25px of 20px 30px, arc by 25% -15px of 20px 20px cw rotate 270deg small, arc to 25% 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8% 8px, arc to 12% -18px of 13px 23px, arc by 18% -8px of 27px 27px rotate 102deg cw large, arc to 25% 20px of 10px 5px cw)\]]
- expected: FAIL
-
- [CSS Transitions: property <clip-path> from [path("M 5 5 A 10,20 0 0,0 15,-15 a 30,30 30 1,1 15,-5 A 10,5 0 0,0 25 20")\] to [shape(from 15px 15px, arc to 5px -25px of 20px 30px, arc by 25px -15px of 20px 20px cw rotate 270deg small, arc to 25px 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8px 8px, arc to 12px -18px of 13px 23px, arc by 18px -8px of 27px 27px rotate 102deg cw large, arc to 25px 20px of 10px 5px cw)\]]
- expected: FAIL
-
- [CSS Transitions with transition: all: property <clip-path> from [path("M 5 5 A 10,20 0 0,0 15,-15 a 30,30 30 1,1 15,-5 A 10,5 0 0,0 25 20")\] to [shape(from 15px 15px, arc to 5px -25px of 20px 30px, arc by 25px -15px of 20px 20px cw rotate 270deg small, arc to 25px 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8px 8px, arc to 12px -18px of 13px 23px, arc by 18px -8px of 27px 27px rotate 102deg cw large, arc to 25px 20px of 10px 5px cw)\]]
- expected: FAIL
-
- [CSS Animations: property <clip-path> from [path("M 5 5 A 10,20 0 0,0 15,-15 a 30,30 30 1,1 15,-5 A 10,5 0 0,0 25 20")\] to [shape(from 15px 15px, arc to 5px -25px of 20px 30px, arc by 25px -15px of 20px 20px cw rotate 270deg small, arc to 25px 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8px 8px, arc to 12px -18px of 13px 23px, arc by 18px -8px of 27px 27px rotate 102deg cw large, arc to 25px 20px of 10px 5px cw)\]]
- expected: FAIL
-
- [Web Animations: property <clip-path> from [path("M 5 5 A 10,20 0 0,0 15,-15 a 30,30 30 1,1 15,-5 A 10,5 0 0,0 25 20")\] to [shape(from 15px 15px, arc to 5px -25px of 20px 30px, arc by 25px -15px of 20px 20px cw rotate 270deg small, arc to 25px 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8px 8px, arc to 12px -18px of 13px 23px, arc by 18px -8px of 27px 27px rotate 102deg cw large, arc to 25px 20px of 10px 5px cw)\]]
- expected: FAIL
-
- [CSS Transitions: property <clip-path> from [shape(from 5% 5px, arc to 15% -15px of 10px, arc by 15% -5px of 30% cw rotate 30deg large, arc to 25% 20px of 10% small)\] to [shape(from 15% 15px, arc to 5% -25px of 15%, arc by 25% -15px of 12rem cw rotate 270deg small, arc to 15% 20px of 20% small cw)\] at (0.3) should be [shape(from 8% 8px, arc to 12% -18px of calc(4.5% + 7px), arc by 18% -8px of calc(21% + 57.6px) rotate 102deg cw large, arc to 22% 20px of 13% cw)\]]
- expected: FAIL
-
- [CSS Transitions with transition: all: property <clip-path> from [shape(from 5% 5px, arc to 15% -15px of 10px, arc by 15% -5px of 30% cw rotate 30deg large, arc to 25% 20px of 10% small)\] to [shape(from 15% 15px, arc to 5% -25px of 15%, arc by 25% -15px of 12rem cw rotate 270deg small, arc to 15% 20px of 20% small cw)\] at (0.3) should be [shape(from 8% 8px, arc to 12% -18px of calc(4.5% + 7px), arc by 18% -8px of calc(21% + 57.6px) rotate 102deg cw large, arc to 22% 20px of 13% cw)\]]
- expected: FAIL
-
- [CSS Animations: property <clip-path> from [shape(from 5% 5px, arc to 15% -15px of 10px, arc by 15% -5px of 30% cw rotate 30deg large, arc to 25% 20px of 10% small)\] to [shape(from 15% 15px, arc to 5% -25px of 15%, arc by 25% -15px of 12rem cw rotate 270deg small, arc to 15% 20px of 20% small cw)\] at (0.3) should be [shape(from 8% 8px, arc to 12% -18px of calc(4.5% + 7px), arc by 18% -8px of calc(21% + 57.6px) rotate 102deg cw large, arc to 22% 20px of 13% cw)\]]
- expected: FAIL
-
- [Web Animations: property <clip-path> from [shape(from 5% 5px, arc to 15% -15px of 10px, arc by 15% -5px of 30% cw rotate 30deg large, arc to 25% 20px of 10% small)\] to [shape(from 15% 15px, arc to 5% -25px of 15%, arc by 25% -15px of 12rem cw rotate 270deg small, arc to 15% 20px of 20% small cw)\] at (0.3) should be [shape(from 8% 8px, arc to 12% -18px of calc(4.5% + 7px), arc by 18% -8px of calc(21% + 57.6px) rotate 102deg cw large, arc to 22% 20px of 13% cw)\]]
- expected: FAIL
diff --git a/testing/web-platform/meta/css/motion/animation/offset-path-interpolation-008.html.ini b/testing/web-platform/meta/css/motion/animation/offset-path-interpolation-008.html.ini
@@ -1,24 +0,0 @@
-[offset-path-interpolation-008.html]
- [CSS Transitions: property <offset-path> from [shape(from 5% 5px, arc to 15% -15px of 10px 20px, arc by 15% -5px of 30px 30px cw rotate 30deg large, arc to 25% 20px of 10px 5px small)\] to [shape(from 15% 15px, arc to 5% -25px of 20px 30px, arc by 25% -15px of 20px 20px cw rotate 270deg small, arc to 25% 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8% 8px, arc to 12% -18px of 13px 23px ccw small, arc by 18% -8px of 27px 27px rotate 102deg cw large, arc to 25% 20px of 10px 5px cw small )\]]
- expected: FAIL
-
- [CSS Transitions with transition: all: property <offset-path> from [shape(from 5% 5px, arc to 15% -15px of 10px 20px, arc by 15% -5px of 30px 30px cw rotate 30deg large, arc to 25% 20px of 10px 5px small)\] to [shape(from 15% 15px, arc to 5% -25px of 20px 30px, arc by 25% -15px of 20px 20px cw rotate 270deg small, arc to 25% 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8% 8px, arc to 12% -18px of 13px 23px ccw small, arc by 18% -8px of 27px 27px rotate 102deg cw large, arc to 25% 20px of 10px 5px cw small )\]]
- expected: FAIL
-
- [CSS Animations: property <offset-path> from [shape(from 5% 5px, arc to 15% -15px of 10px 20px, arc by 15% -5px of 30px 30px cw rotate 30deg large, arc to 25% 20px of 10px 5px small)\] to [shape(from 15% 15px, arc to 5% -25px of 20px 30px, arc by 25% -15px of 20px 20px cw rotate 270deg small, arc to 25% 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8% 8px, arc to 12% -18px of 13px 23px ccw small, arc by 18% -8px of 27px 27px rotate 102deg cw large, arc to 25% 20px of 10px 5px cw small )\]]
- expected: FAIL
-
- [Web Animations: property <offset-path> from [shape(from 5% 5px, arc to 15% -15px of 10px 20px, arc by 15% -5px of 30px 30px cw rotate 30deg large, arc to 25% 20px of 10px 5px small)\] to [shape(from 15% 15px, arc to 5% -25px of 20px 30px, arc by 25% -15px of 20px 20px cw rotate 270deg small, arc to 25% 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8% 8px, arc to 12% -18px of 13px 23px ccw small, arc by 18% -8px of 27px 27px rotate 102deg cw large, arc to 25% 20px of 10px 5px cw small )\]]
- expected: FAIL
-
- [CSS Transitions: property <offset-path> from [path("M 5 5 A 10,20 0 0,0 15,-15 a 30,30 30 1,1 15,-5 A 10,5 0 0,0 25 20")\] to [shape(from 15px 15px, arc to 5px -25px of 20px 30px, arc by 25px -15px of 20px 20px cw rotate 270deg small, arc to 25px 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8px 8px, arc to 12px -18px of 13px 23px ccw small, arc by 18px -8px of 27px 27px rotate 102deg cw large, arc to 25px 20px of 10px 5px cw small)\]]
- expected: FAIL
-
- [CSS Transitions with transition: all: property <offset-path> from [path("M 5 5 A 10,20 0 0,0 15,-15 a 30,30 30 1,1 15,-5 A 10,5 0 0,0 25 20")\] to [shape(from 15px 15px, arc to 5px -25px of 20px 30px, arc by 25px -15px of 20px 20px cw rotate 270deg small, arc to 25px 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8px 8px, arc to 12px -18px of 13px 23px ccw small, arc by 18px -8px of 27px 27px rotate 102deg cw large, arc to 25px 20px of 10px 5px cw small)\]]
- expected: FAIL
-
- [CSS Animations: property <offset-path> from [path("M 5 5 A 10,20 0 0,0 15,-15 a 30,30 30 1,1 15,-5 A 10,5 0 0,0 25 20")\] to [shape(from 15px 15px, arc to 5px -25px of 20px 30px, arc by 25px -15px of 20px 20px cw rotate 270deg small, arc to 25px 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8px 8px, arc to 12px -18px of 13px 23px ccw small, arc by 18px -8px of 27px 27px rotate 102deg cw large, arc to 25px 20px of 10px 5px cw small)\]]
- expected: FAIL
-
- [Web Animations: property <offset-path> from [path("M 5 5 A 10,20 0 0,0 15,-15 a 30,30 30 1,1 15,-5 A 10,5 0 0,0 25 20")\] to [shape(from 15px 15px, arc to 5px -25px of 20px 30px, arc by 25px -15px of 20px 20px cw rotate 270deg small, arc to 25px 20px of 10px 5px small cw)\] at (0.3) should be [shape(from 8px 8px, arc to 12px -18px of 13px 23px ccw small, arc by 18px -8px of 27px 27px rotate 102deg cw large, arc to 25px 20px of 10px 5px cw small)\]]
- expected: FAIL
diff --git a/testing/web-platform/tests/css/motion/animation/offset-path-interpolation-002.html b/testing/web-platform/tests/css/motion/animation/offset-path-interpolation-002.html
@@ -64,6 +64,9 @@
{at: 2, expect: "path('M 20 10 Q 48 58 280 3800')"}
]);
+ // At progress 0.125/0.875, arc flags in path() should consider
+ // non-zero interpolated values as one to match <path> behaviour.
+ // https://svgwg.org/specs/paths/#PathElement
test_interpolation({
property: 'offset-path',
from: "path('M 100 400 A 10 20 30 1 0 140 450')",
@@ -71,8 +74,8 @@
}, [
{at: -1, expect: "path('M -100 600 A -30 -20 -10 1 0 -100 610')"},
{at: 0, expect: "path('M 100 400 A 10 20 30 1 0 140 450')"},
- {at: 0.125, expect: "path('M 125 375 A 15 25 35 1 0 170 430')"},
- {at: 0.875, expect: "path('M 275 225 A 45 55 65 0 1 350 310')"},
+ {at: 0.125, expect: "path('M 125 375 A 15 25 35 1 1 170 430')"},
+ {at: 0.875, expect: "path('M 275 225 A 45 55 65 1 1 350 310')"},
{at: 1, expect: "path('M 300 200 A 50 60 70 0 1 380 290')"},
{at: 2, expect: "path('M 500 0 A 90 100 110 0 1 620 130')"}
]);
@@ -83,8 +86,8 @@
}, [
{at: -1, expect: "path('M -100 600 A -30 -20 -10 1 0 -100 610')"},
{at: 0, expect: "path('M 100 400 A 10 20 30 1 0 140 450')"},
- {at: 0.125, expect: "path('M 125 375 A 15 25 35 1 0 170 430')"},
- {at: 0.875, expect: "path('M 275 225 A 45 55 65 0 1 350 310')"},
+ {at: 0.125, expect: "path('M 125 375 A 15 25 35 1 1 170 430')"},
+ {at: 0.875, expect: "path('M 275 225 A 45 55 65 1 1 350 310')"},
{at: 1, expect: "path('M 300 200 A 50 60 70 0 1 380 290')"},
{at: 2, expect: "path('M 500 0 A 90 100 110 0 1 620 130')"}
]);
diff --git a/testing/web-platform/tests/css/motion/animation/offset-path-interpolation-004.html b/testing/web-platform/tests/css/motion/animation/offset-path-interpolation-004.html
@@ -106,6 +106,9 @@
{at: 2, expect: "path('M 250 260 H 200 V 240 H 210 V 230 L 240 210')"}
]);
+ // At progress 0.125/0.875, arc flags in path() should consider
+ // non-zero interpolated values as one to match <path> behaviour.
+ // https://svgwg.org/specs/paths/#PathElement
test_interpolation({
property: 'offset-path',
from: "path('m 10 20 a 10 20 30 1 0 40 50 a 110 120 30 1 1 140 50')",
@@ -113,8 +116,8 @@
}, [
{at: -1, expect: "path('M 2 28 A -30 -60 -10 1 0 10 30 A 70 80 -10 1 1 310 160')"},
{at: 0, expect: "path('M 10 20 A 10 20 30 1 0 50 70 A 110 120 30 1 1 190 120')"},
- {at: 0.125, expect: "path('M 11 19 A 15 30 35 1 0 55 75 A 115 125 35 1 1 175 115')"},
- {at: 0.875, expect: "path('M 17 13 A 45 90 65 0 1 85 105 A 145 155 65 0 1 85 85')"},
+ {at: 0.125, expect: "path('M 11 19 A 15 30 35 1 1 55 75 A 115 125 35 1 1 175 115')"},
+ {at: 0.875, expect: "path('M 17 13 A 45 90 65 1 1 85 105 A 145 155 65 1 1 85 85')"},
{at: 1, expect: "path('M 18 12 A 50 100 70 0 1 90 110 A 150 160 70 0 1 70 80')"},
{at: 2, expect: "path('M 26 4 A 90 180 110 0 1 130 150 A 190 200 110 0 1 -50 40')"}
]);
diff --git a/testing/web-platform/tests/svg/path/property/d-interpolation-relative-absolute.svg b/testing/web-platform/tests/svg/path/property/d-interpolation-relative-absolute.svg
@@ -106,6 +106,9 @@
{at: 2, expect: 'path("M 250 260 H 200 V 240 H 210 V 230 L 240 210 Z")'}
]);
+ // At progress 0.125/0.875, arc flags in path() should consider
+ // non-zero interpolated values as one to match <path> behaviour.
+ // https://svgwg.org/specs/paths/#PathElement
test_interpolation({
property: 'd',
from: 'path("m 10 20 a 10 20 30 1 0 40 50 a 110 120 30 1 1 140 50")',
@@ -113,8 +116,8 @@
}, [
{at: -1, expect: 'path("M 2 28 A -30 -60 -10 1 0 10 30 A 70 80 -10 1 1 310 160")'},
{at: 0, expect: 'path("M 10 20 A 10 20 30 1 0 50 70 A 110 120 30 1 1 190 120")'},
- {at: 0.125, expect: 'path("M 11 19 A 15 30 35 1 0 55 75 A 115 125 35 1 1 175 115")'},
- {at: 0.875, expect: 'path("M 17 13 A 45 90 65 0 1 85 105 A 145 155 65 0 1 85 85")'},
+ {at: 0.125, expect: 'path("M 11 19 A 15 30 35 1 1 55 75 A 115 125 35 1 1 175 115")'},
+ {at: 0.875, expect: 'path("M 17 13 A 45 90 65 1 1 85 105 A 145 155 65 1 1 85 85")'},
{at: 1, expect: 'path("M 18 12 A 50 100 70 0 1 90 110 A 150 160 70 0 1 70 80")'},
{at: 2, expect: 'path("M 26 4 A 90 180 110 0 1 130 150 A 190 200 110 0 1 -50 40")'}
]);
diff --git a/testing/web-platform/tests/svg/path/property/d-interpolation-single.svg b/testing/web-platform/tests/svg/path/property/d-interpolation-single.svg
@@ -117,6 +117,9 @@
{at: 2, expect: 'path("M 20 10 Q 48 58 280 3800")'}
]);
+ // At progress 0.125/0.875, arc flags in path() should consider
+ // non-zero interpolated values as one to match <path> behaviour.
+ // https://svgwg.org/specs/paths/#PathElement
test_interpolation({
property: 'd',
from: 'path("M 100 400 A 10 20 30 1 0 140 450")',
@@ -124,8 +127,8 @@
}, [
{at: -1, expect: 'path("M -100 600 A -30 -20 -10 1 0 -100 610")'},
{at: 0, expect: 'path("M 100 400 A 10 20 30 1 0 140 450")'},
- {at: 0.125, expect: 'path("M 125 375 A 15 25 35 1 0 170 430")'},
- {at: 0.875, expect: 'path("M 275 225 A 45 55 65 0 1 350 310")'},
+ {at: 0.125, expect: 'path("M 125 375 A 15 25 35 1 1 170 430")'},
+ {at: 0.875, expect: 'path("M 275 225 A 45 55 65 1 1 350 310")'},
{at: 1, expect: 'path("M 300 200 A 50 60 70 0 1 380 290")'},
{at: 2, expect: 'path("M 500 0 A 90 100 110 0 1 620 130")'}
]);
@@ -136,8 +139,8 @@
}, [
{at: -1, expect: 'path("M -100 600 A -30 -20 -10 1 0 -100 610")'},
{at: 0, expect: 'path("M 100 400 A 10 20 30 1 0 140 450")'},
- {at: 0.125, expect: 'path("M 125 375 A 15 25 35 1 0 170 430")'},
- {at: 0.875, expect: 'path("M 275 225 A 45 55 65 0 1 350 310")'},
+ {at: 0.125, expect: 'path("M 125 375 A 15 25 35 1 1 170 430")'},
+ {at: 0.875, expect: 'path("M 275 225 A 45 55 65 1 1 350 310")'},
{at: 1, expect: 'path("M 300 200 A 50 60 70 0 1 380 290")'},
{at: 2, expect: 'path("M 500 0 A 90 100 110 0 1 620 130")'}
]);