diff --git a/lib/web_ui/lib/src/ui/lerp.dart b/lib/web_ui/lib/src/ui/lerp.dart index 196a2aa15fc74..364209411fa6e 100644 --- a/lib/web_ui/lib/src/ui/lerp.dart +++ b/lib/web_ui/lib/src/ui/lerp.dart @@ -5,19 +5,34 @@ // @dart = 2.10 part of ui; +/// Linearly interpolate between two numbers, `a` and `b`, by an extrapolation +/// factor `t`. +/// +/// When `a` and `b` are equal or both NaN, `a` is returned. Otherwise, if +/// `a`, `b`, and `t` are required to be finite or null, and the result of `a + +/// (b - a) * t` is returned, where nulls are defaulted to 0.0. double? lerpDouble(num? a, num? b, double t) { - if (a == null && b == null) { - return null; + if (a == b || (a?.isNaN == true) && (b?.isNaN == true)) { + return a?.toDouble(); } a ??= 0.0; b ??= 0.0; - return (a + (b - a) * t).toDouble(); + assert(a.isFinite, 'Cannot interpolate between finite and non-finite values'); + assert(b.isFinite, 'Cannot interpolate between finite and non-finite values'); + assert(t.isFinite, 't must be finite when interpolating between values'); + return a * (1.0 - t) + b * t; } +/// Linearly interpolate between two doubles. +/// +/// Same as [lerpDouble] but specialized for non-null `double` type. double _lerpDouble(double a, double b, double t) { - return a + (b - a) * t; + return a * (1.0 - t) + b * t; } +/// Linearly interpolate between two integers. +/// +/// Same as [lerpDouble] but specialized for non-null `int` type. double _lerpInt(int a, int b, double t) { - return a + (b - a) * t; + return a * (1.0 - t) + b * t; } diff --git a/lib/web_ui/test/lerp_test.dart b/lib/web_ui/test/lerp_test.dart new file mode 100644 index 0000000000000..433ffbd066c01 --- /dev/null +++ b/lib/web_ui/test/lerp_test.dart @@ -0,0 +1,162 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.10 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; + +import 'package:ui/ui.dart'; + +/// The epsilon of tolerable double precision error. +/// +/// This is used in various places in the framework to allow for floating point +/// precision loss in calculations. Differences below this threshold are safe +/// to disregard. +const double precisionErrorTolerance = 1e-10; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +// These tests should be kept in sync with the VM tests in +// testing/dart/lerp_test.dart. +void testMain() { + test('lerpDouble should return null if and only if both inputs are null', () { + expect(lerpDouble(null, null, 1.0), isNull); + expect(lerpDouble(5.0, null, 0.25), isNotNull); + expect(lerpDouble(null, 5.0, 0.25), isNotNull); + + expect(lerpDouble(5, null, 0.25), isNotNull); + expect(lerpDouble(null, 5, 0.25), isNotNull); + }); + + test('lerpDouble should treat a null input as 0 if the other input is non-null', () { + expect(lerpDouble(null, 10.0, 0.25), closeTo(2.5, precisionErrorTolerance)); + expect(lerpDouble(10.0, null, 0.25), closeTo(7.5, precisionErrorTolerance)); + + expect(lerpDouble(null, 10, 0.25), closeTo(2.5, precisionErrorTolerance)); + expect(lerpDouble(10, null, 0.25), closeTo(7.5, precisionErrorTolerance)); + }); + + test('lerpDouble should handle interpolation values < 0.0', () { + expect(lerpDouble(0.0, 10.0, -5.0), closeTo(-50.0, precisionErrorTolerance)); + expect(lerpDouble(10.0, 0.0, -5.0), closeTo(60.0, precisionErrorTolerance)); + + expect(lerpDouble(0, 10, -5), closeTo(-50, precisionErrorTolerance)); + expect(lerpDouble(10, 0, -5), closeTo(60, precisionErrorTolerance)); + }); + + test('lerpDouble should return the start value at 0.0', () { + expect(lerpDouble(2.0, 10.0, 0.0), 2.0); + expect(lerpDouble(10.0, 2.0, 0.0), 10.0); + + expect(lerpDouble(2, 10, 0), 2); + expect(lerpDouble(10, 2, 0), 10); + }); + + test('lerpDouble should interpolate between two values', () { + expect(lerpDouble(0.0, 10.0, 0.25), closeTo(2.5, precisionErrorTolerance)); + expect(lerpDouble(10.0, 0.0, 0.25), closeTo(7.5, precisionErrorTolerance)); + + expect(lerpDouble(0, 10, 0.25), closeTo(2.5, precisionErrorTolerance)); + expect(lerpDouble(10, 0, 0.25), closeTo(7.5, precisionErrorTolerance)); + + // Exact answer: 20.0 - 1.0e-29 + expect(lerpDouble(10.0, 1.0e30, 1.0e-29), closeTo(20.0, precisionErrorTolerance)); + + // Exact answer: 5.0 + 5.0e29 + expect(lerpDouble(10.0, 1.0e30, 0.5), closeTo(5.0e29, precisionErrorTolerance)); + }); + + test('lerpDouble should return the end value at 1.0', () { + expect(lerpDouble(2.0, 10.0, 1.0), 10.0); + expect(lerpDouble(10.0, 2.0, 1.0), 2.0); + + expect(lerpDouble(0, 10, 5), 50); + expect(lerpDouble(10, 0, 5), -40); + + expect(lerpDouble(1.0e30, 10.0, 1.0), 10.0); + expect(lerpDouble(10.0, 1.0e30, 0.0), 10.0); + }); + + test('lerpDouble should handle interpolation values > 1.0', () { + expect(lerpDouble(0.0, 10.0, 5.0), closeTo(50.0, precisionErrorTolerance)); + expect(lerpDouble(10.0, 0.0, 5.0), closeTo(-40.0, precisionErrorTolerance)); + + expect(lerpDouble(0, 10, 5), closeTo(50, precisionErrorTolerance)); + expect(lerpDouble(10, 0, 5), closeTo(-40, precisionErrorTolerance)); + }); + + test('lerpDouble should return input value in all cases if begin/end are equal', () { + expect(lerpDouble(10.0, 10.0, 5.0), 10.0); + expect(lerpDouble(10.0, 10.0, double.nan), 10.0); + expect(lerpDouble(10.0, 10.0, double.infinity), 10.0); + expect(lerpDouble(10.0, 10.0, -double.infinity), 10.0); + + expect(lerpDouble(10, 10, 5.0), 10.0); + expect(lerpDouble(10, 10, double.nan), 10.0); + expect(lerpDouble(10, 10, double.infinity), 10.0); + expect(lerpDouble(10, 10, -double.infinity), 10.0); + + expect(lerpDouble(double.nan, double.nan, 5.0), isNaN); + expect(lerpDouble(double.nan, double.nan, double.nan), isNaN); + expect(lerpDouble(double.nan, double.nan, double.infinity), isNaN); + expect(lerpDouble(double.nan, double.nan, -double.infinity), isNaN); + + expect(lerpDouble(double.infinity, double.infinity, 5.0), double.infinity); + expect(lerpDouble(double.infinity, double.infinity, double.nan), double.infinity); + expect(lerpDouble(double.infinity, double.infinity, double.infinity), double.infinity); + expect(lerpDouble(double.infinity, double.infinity, -double.infinity), double.infinity); + + expect(lerpDouble(-double.infinity, -double.infinity, 5.0), -double.infinity); + expect(lerpDouble(-double.infinity, -double.infinity, double.nan), -double.infinity); + expect(lerpDouble(-double.infinity, -double.infinity, double.infinity), -double.infinity); + expect(lerpDouble(-double.infinity, -double.infinity, -double.infinity), -double.infinity); + }); + + test('lerpDouble should throw AssertionError if interpolation value is NaN and a != b', () { + expectAssertion(() => lerpDouble(0.0, 10.0, double.nan)); + }); + + test('lerpDouble should throw AssertionError if interpolation value is +/- infinity and a != b', () { + expectAssertion(() => lerpDouble(0.0, 10.0, double.infinity)); + expectAssertion(() => lerpDouble(0.0, 10.0, -double.infinity)); + }); + + test('lerpDouble should throw AssertionError if either start or end are NaN', () { + expectAssertion(() => lerpDouble(double.nan, 10.0, 5.0)); + expectAssertion(() => lerpDouble(0.0, double.nan, 5.0)); + }); + + test('lerpDouble should throw AssertionError if either start or end are +/- infinity', () { + expectAssertion(() => lerpDouble(double.infinity, 10.0, 5.0)); + expectAssertion(() => lerpDouble(-double.infinity, 10.0, 5.0)); + expectAssertion(() => lerpDouble(0.0, double.infinity, 5.0)); + expectAssertion(() => lerpDouble(0.0, -double.infinity, 5.0)); + }); +} + +/// Asserts that `callback` throws an [AssertionError]. +/// +/// Verifies that the specified callback throws an [AssertionError] when +/// running in with assertions enabled. When asserts are not enabled, such as +/// when running using a release-mode VM with default settings, this acts as a +/// no-op. +void expectAssertion(Function callback) { + bool assertsEnabled = false; + assert(() { + assertsEnabled = true; + return true; + }()); + if (assertsEnabled) { + bool threw = false; + try { + callback(); + } catch (e) { + expect(e is AssertionError, true); + threw = true; + } + expect(threw, true); + } +} diff --git a/testing/dart/lerp_test.dart b/testing/dart/lerp_test.dart index d9e43092c95e7..9bc15f4f5034d 100644 --- a/testing/dart/lerp_test.dart +++ b/testing/dart/lerp_test.dart @@ -9,6 +9,8 @@ import 'package:test/test.dart'; import 'test_util.dart'; +// These tests should be kept in sync with the web tests in +// lib/web_ui/test/lerp_test.dart. void main() { test('lerpDouble should return null if and only if both inputs are null', () { expect(lerpDouble(null, null, 1.0), isNull);