stylo: a colour animated far past its end weighs the two colours to zero and stays put
Generated from repros/stylo-color-animation-weights-cancel/README.md when the site was built at 7aa23e2.
What niso does
Section titled “What niso does”WPT’s css/filter-effects/animation/filter-interpolation-002.html animates
drop-shadow(20px 10px blue) to drop-shadow(20px 10px green) and samples it at
progress 2147483648 (2^31), where the shadow colour, extrapolated and clamped, is
green. niso’s CSS transitions and animations are Stylo’s, and its Web Animations
interpolate with Stylo’s Animate too, so all four subtests (CSS Transitions, with
transition: all, CSS Animations, Web Animations) get blue. niso cannot fix it; the
subtests are recorded as failing in tests/wpt/expectations.json (docs/wpt.md,
“Upstream failures”). Until cssom-inline-style three of them passed on Linux by
chance: the easing reached Stylo rounded to six significant digits, the progress
landed a little below 2^31, and the two weights no longer cancelled exactly.
Expected vs actual
Section titled “Expected vs actual”AbsoluteColor::animate (style/values/animated/color.rs:21-34) takes the
procedure’s weights, (1 - progress, progress) in f64, and hands them to
mix::mix_many as f32. At 2^31, f32 has no room for the 1: both round to 2^31,
-2147483600 + 2147483600 = 0, and mix_many (style/color/mix.rs:221-227) treats a
fold whose weights sum to 0 as one that “doesn’t contribute anything to the result”,
so the result is the start colour unchanged. Interpolation outside [0, 1] is
extrapolation (CSS Values 4, “Combining Values”), so blue + 2^31 × (green - blue) is
expected, which clamps to rgb(0, 255, 0), what the test expects.
blue to green at progress 2147483648weights as f32: -2147483600 + 2147483600 = 0expected: rgb(0, 255, 0) (green extrapolated, then clamped)actual: rgb(0, 0, 255)CARGO_TARGET_DIR=~/.capy/work/repro-target cargo runExit status 1 while the bug reproduces, 0 once fixed.
Upstream main
Section titled “Upstream main”Same code on servo/stylo main at 7f0189de128e6dbd2b504acd3aebee3befa78358
(2026-11-06): style/color/mix.rs and style/values/animated/color.rs are
unchanged since niso’s pin, so it reproduces there. Servo’s own WPT metadata
(tests/wpt/meta/css/filter-effects/animation/filter-interpolation-002.html.ini)
expects all four of these subtests to fail.
Existing issue or PR
Section titled “Existing issue or PR”None found in servo/stylo or servo/servo issues and PRs for mix_many, “color
interpolation extrapolation” or “filter-interpolation-002”.
Issue text
Section titled “Issue text”Title: Colour animation at a large progress returns the start colour (f32 weights cancel)
Body:
AbsoluteColor::animate converts the interpolation weights (1 - p, p) to f32 for
mix::mix_many. At p = 2^31, (1 - p) as f32 + p as f32 is 0 (f32 has no room for the 1), and
mix_many skips a fold whose weights sum to 0, so the result is the start colour.
WPT’s css/filter-effects/animation/filter-interpolation-002.html samples
drop-shadow(... blue) to drop-shadow(... green) at 2147483648 and expects green
(extrapolated and clamped); Stylo gives blue (Servo’s metadata expects these
subtests to fail).
let blue = AbsoluteColor::srgb_legacy(0, 0, 255, 1.0);let green = AbsoluteColor::srgb_legacy(0, 128, 0, 1.0);let mixed = blue.animate(&green, Procedure::Interpolate { progress: 2147483648.0 }).unwrap();println!("{}", mixed.to_css_string()); // rgb(0, 0, 255); expected rgb(0, 255, 0)Tested at 2dca34592ced37c6aa01733e94206a767194cf9d and main 7f0189de128e6dbd2b504acd3aebee3befa78358. Suggested fix: interpolate animation colours with the f64 weights (or only skip a fold when both weights are 0, as the comment says), rather than testing the f32 sum.