Skip to content

Commit cf8c6b8

Browse files
authored
Update web lerpDouble to match C++ behaviour (#21010)
This updates the web_ui implementation of lerpDouble to match the behaviour of the C++ engine implementation in dart:ui. Specifically this covers the following changes: * #20871: stricter handling of NaN and infinity * #20879: Improve the precision of lerpDouble lerpDouble: stricter handling of NaN and infinity (#20871) ---------------------------------------------------------- Previously, the behaviour of lerpDouble with respect to NaN and infinity was relatively complex and difficult to reason about. This patch simplifies the behaviour with respect to those conditions and adds documentation and tests. In general, if `a == b` or both values are null, infinite, or NaN, `a` is returned. Otherwise we require `a` and `b` and `t` to be finite or null and the result of the linear interpolation is returned. Improve the precision of lerpDouble (#20879) -------------------------------------------- Reduces errors caused by the loss of floating point precision when the two extrema of the lerp differ significantly in magnitude. Previously, we used the calculation: a + (b - a) * t When the difference in magnitude between `a` and `b` exceeds the precision representable by double-precision floating point math, `b - a` results in the larger-magnitude value of `a` or `b`. The error between the value produced and the correct value is then scaled by t. A simple example of the impact can be seen when `a` is significantly larger in magnitude than `b`. In that case, `b - a` results in `a` and when `t` is 1.0, the resulting value is `a - (a) * 1.0 == 0`. The patch transforms the computation to the mathematically-equivalent expression: a * (1.0 - t) + b * t By scaling each value independently, the behaviour is more accurate. From the point of view of performance, this adds an extra multiplication, but multiplication is relatively cheap and the behaviour is significantly better. This patch also adds a `precisionErrorTolerance` constant to test_utils.dart and migrates existing tests to use `closeTo()` for testing. The tests themselves *do* currently use values that have an exact floating-point representation, but we should allow for flexibility in future implementation changes.
1 parent cef383d commit cf8c6b8

File tree

3 files changed

+184
-5
lines changed

3 files changed

+184
-5
lines changed

lib/web_ui/lib/src/ui/lerp.dart

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,34 @@
55
// @dart = 2.10
66
part of ui;
77

8+
/// Linearly interpolate between two numbers, `a` and `b`, by an extrapolation
9+
/// factor `t`.
10+
///
11+
/// When `a` and `b` are equal or both NaN, `a` is returned. Otherwise, if
12+
/// `a`, `b`, and `t` are required to be finite or null, and the result of `a +
13+
/// (b - a) * t` is returned, where nulls are defaulted to 0.0.
814
double? lerpDouble(num? a, num? b, double t) {
9-
if (a == null && b == null) {
10-
return null;
15+
if (a == b || (a?.isNaN == true) && (b?.isNaN == true)) {
16+
return a?.toDouble();
1117
}
1218
a ??= 0.0;
1319
b ??= 0.0;
14-
return (a + (b - a) * t).toDouble();
20+
assert(a.isFinite, 'Cannot interpolate between finite and non-finite values');
21+
assert(b.isFinite, 'Cannot interpolate between finite and non-finite values');
22+
assert(t.isFinite, 't must be finite when interpolating between values');
23+
return a * (1.0 - t) + b * t;
1524
}
1625

26+
/// Linearly interpolate between two doubles.
27+
///
28+
/// Same as [lerpDouble] but specialized for non-null `double` type.
1729
double _lerpDouble(double a, double b, double t) {
18-
return a + (b - a) * t;
30+
return a * (1.0 - t) + b * t;
1931
}
2032

33+
/// Linearly interpolate between two integers.
34+
///
35+
/// Same as [lerpDouble] but specialized for non-null `int` type.
2136
double _lerpInt(int a, int b, double t) {
22-
return a + (b - a) * t;
37+
return a * (1.0 - t) + b * t;
2338
}

lib/web_ui/test/lerp_test.dart

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// @dart = 2.10
6+
import 'package:test/bootstrap/browser.dart';
7+
import 'package:test/test.dart';
8+
9+
import 'package:ui/ui.dart';
10+
11+
/// The epsilon of tolerable double precision error.
12+
///
13+
/// This is used in various places in the framework to allow for floating point
14+
/// precision loss in calculations. Differences below this threshold are safe
15+
/// to disregard.
16+
const double precisionErrorTolerance = 1e-10;
17+
18+
void main() {
19+
internalBootstrapBrowserTest(() => testMain);
20+
}
21+
22+
// These tests should be kept in sync with the VM tests in
23+
// testing/dart/lerp_test.dart.
24+
void testMain() {
25+
test('lerpDouble should return null if and only if both inputs are null', () {
26+
expect(lerpDouble(null, null, 1.0), isNull);
27+
expect(lerpDouble(5.0, null, 0.25), isNotNull);
28+
expect(lerpDouble(null, 5.0, 0.25), isNotNull);
29+
30+
expect(lerpDouble(5, null, 0.25), isNotNull);
31+
expect(lerpDouble(null, 5, 0.25), isNotNull);
32+
});
33+
34+
test('lerpDouble should treat a null input as 0 if the other input is non-null', () {
35+
expect(lerpDouble(null, 10.0, 0.25), closeTo(2.5, precisionErrorTolerance));
36+
expect(lerpDouble(10.0, null, 0.25), closeTo(7.5, precisionErrorTolerance));
37+
38+
expect(lerpDouble(null, 10, 0.25), closeTo(2.5, precisionErrorTolerance));
39+
expect(lerpDouble(10, null, 0.25), closeTo(7.5, precisionErrorTolerance));
40+
});
41+
42+
test('lerpDouble should handle interpolation values < 0.0', () {
43+
expect(lerpDouble(0.0, 10.0, -5.0), closeTo(-50.0, precisionErrorTolerance));
44+
expect(lerpDouble(10.0, 0.0, -5.0), closeTo(60.0, precisionErrorTolerance));
45+
46+
expect(lerpDouble(0, 10, -5), closeTo(-50, precisionErrorTolerance));
47+
expect(lerpDouble(10, 0, -5), closeTo(60, precisionErrorTolerance));
48+
});
49+
50+
test('lerpDouble should return the start value at 0.0', () {
51+
expect(lerpDouble(2.0, 10.0, 0.0), 2.0);
52+
expect(lerpDouble(10.0, 2.0, 0.0), 10.0);
53+
54+
expect(lerpDouble(2, 10, 0), 2);
55+
expect(lerpDouble(10, 2, 0), 10);
56+
});
57+
58+
test('lerpDouble should interpolate between two values', () {
59+
expect(lerpDouble(0.0, 10.0, 0.25), closeTo(2.5, precisionErrorTolerance));
60+
expect(lerpDouble(10.0, 0.0, 0.25), closeTo(7.5, precisionErrorTolerance));
61+
62+
expect(lerpDouble(0, 10, 0.25), closeTo(2.5, precisionErrorTolerance));
63+
expect(lerpDouble(10, 0, 0.25), closeTo(7.5, precisionErrorTolerance));
64+
65+
// Exact answer: 20.0 - 1.0e-29
66+
expect(lerpDouble(10.0, 1.0e30, 1.0e-29), closeTo(20.0, precisionErrorTolerance));
67+
68+
// Exact answer: 5.0 + 5.0e29
69+
expect(lerpDouble(10.0, 1.0e30, 0.5), closeTo(5.0e29, precisionErrorTolerance));
70+
});
71+
72+
test('lerpDouble should return the end value at 1.0', () {
73+
expect(lerpDouble(2.0, 10.0, 1.0), 10.0);
74+
expect(lerpDouble(10.0, 2.0, 1.0), 2.0);
75+
76+
expect(lerpDouble(0, 10, 5), 50);
77+
expect(lerpDouble(10, 0, 5), -40);
78+
79+
expect(lerpDouble(1.0e30, 10.0, 1.0), 10.0);
80+
expect(lerpDouble(10.0, 1.0e30, 0.0), 10.0);
81+
});
82+
83+
test('lerpDouble should handle interpolation values > 1.0', () {
84+
expect(lerpDouble(0.0, 10.0, 5.0), closeTo(50.0, precisionErrorTolerance));
85+
expect(lerpDouble(10.0, 0.0, 5.0), closeTo(-40.0, precisionErrorTolerance));
86+
87+
expect(lerpDouble(0, 10, 5), closeTo(50, precisionErrorTolerance));
88+
expect(lerpDouble(10, 0, 5), closeTo(-40, precisionErrorTolerance));
89+
});
90+
91+
test('lerpDouble should return input value in all cases if begin/end are equal', () {
92+
expect(lerpDouble(10.0, 10.0, 5.0), 10.0);
93+
expect(lerpDouble(10.0, 10.0, double.nan), 10.0);
94+
expect(lerpDouble(10.0, 10.0, double.infinity), 10.0);
95+
expect(lerpDouble(10.0, 10.0, -double.infinity), 10.0);
96+
97+
expect(lerpDouble(10, 10, 5.0), 10.0);
98+
expect(lerpDouble(10, 10, double.nan), 10.0);
99+
expect(lerpDouble(10, 10, double.infinity), 10.0);
100+
expect(lerpDouble(10, 10, -double.infinity), 10.0);
101+
102+
expect(lerpDouble(double.nan, double.nan, 5.0), isNaN);
103+
expect(lerpDouble(double.nan, double.nan, double.nan), isNaN);
104+
expect(lerpDouble(double.nan, double.nan, double.infinity), isNaN);
105+
expect(lerpDouble(double.nan, double.nan, -double.infinity), isNaN);
106+
107+
expect(lerpDouble(double.infinity, double.infinity, 5.0), double.infinity);
108+
expect(lerpDouble(double.infinity, double.infinity, double.nan), double.infinity);
109+
expect(lerpDouble(double.infinity, double.infinity, double.infinity), double.infinity);
110+
expect(lerpDouble(double.infinity, double.infinity, -double.infinity), double.infinity);
111+
112+
expect(lerpDouble(-double.infinity, -double.infinity, 5.0), -double.infinity);
113+
expect(lerpDouble(-double.infinity, -double.infinity, double.nan), -double.infinity);
114+
expect(lerpDouble(-double.infinity, -double.infinity, double.infinity), -double.infinity);
115+
expect(lerpDouble(-double.infinity, -double.infinity, -double.infinity), -double.infinity);
116+
});
117+
118+
test('lerpDouble should throw AssertionError if interpolation value is NaN and a != b', () {
119+
expectAssertion(() => lerpDouble(0.0, 10.0, double.nan));
120+
});
121+
122+
test('lerpDouble should throw AssertionError if interpolation value is +/- infinity and a != b', () {
123+
expectAssertion(() => lerpDouble(0.0, 10.0, double.infinity));
124+
expectAssertion(() => lerpDouble(0.0, 10.0, -double.infinity));
125+
});
126+
127+
test('lerpDouble should throw AssertionError if either start or end are NaN', () {
128+
expectAssertion(() => lerpDouble(double.nan, 10.0, 5.0));
129+
expectAssertion(() => lerpDouble(0.0, double.nan, 5.0));
130+
});
131+
132+
test('lerpDouble should throw AssertionError if either start or end are +/- infinity', () {
133+
expectAssertion(() => lerpDouble(double.infinity, 10.0, 5.0));
134+
expectAssertion(() => lerpDouble(-double.infinity, 10.0, 5.0));
135+
expectAssertion(() => lerpDouble(0.0, double.infinity, 5.0));
136+
expectAssertion(() => lerpDouble(0.0, -double.infinity, 5.0));
137+
});
138+
}
139+
140+
/// Asserts that `callback` throws an [AssertionError].
141+
///
142+
/// Verifies that the specified callback throws an [AssertionError] when
143+
/// running in with assertions enabled. When asserts are not enabled, such as
144+
/// when running using a release-mode VM with default settings, this acts as a
145+
/// no-op.
146+
void expectAssertion(Function callback) {
147+
bool assertsEnabled = false;
148+
assert(() {
149+
assertsEnabled = true;
150+
return true;
151+
}());
152+
if (assertsEnabled) {
153+
bool threw = false;
154+
try {
155+
callback();
156+
} catch (e) {
157+
expect(e is AssertionError, true);
158+
threw = true;
159+
}
160+
expect(threw, true);
161+
}
162+
}

testing/dart/lerp_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import 'package:test/test.dart';
99

1010
import 'test_util.dart';
1111

12+
// These tests should be kept in sync with the web tests in
13+
// lib/web_ui/test/lerp_test.dart.
1214
void main() {
1315
test('lerpDouble should return null if and only if both inputs are null', () {
1416
expect(lerpDouble(null, null, 1.0), isNull);

0 commit comments

Comments
 (0)