From b5985cf6908365e28bff1f3dd15c44d7586a6433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Sat, 12 Mar 2016 14:12:33 -0800 Subject: [PATCH] Refactor bezier implementation from bezier-easing library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary:fast & accurate implementation See https://github.com/gre/bezier-easing the library is embedded in React Native fixes #6207 & to follow #6340 (or to replace it) cc vjeux tests --- [the lib tests](https://github.com/gre/bezier-easing/blob/master/test/test.js) ensure the library is accurate. It is tested that the library have a precision better than ±0.000001 . performance --- On my macbook pro, [the lib benchmark](https://github.com/gre/bezier-easing/blob/master/benchmark.js) have: ``` BezierEasing: instanciation x 1,043,725 ops/sec ±1.46% (82 runs sampled) BezierEasing: call x 7,866,642 ops/sec ±0.93% (85 runs sampled) BezierEasing: instanciation + call x 803,051 ops/sec ±1.58% (74 runs sampled) ``` Closes https://github.com/facebook/react-native/pull/6433 Differential Revision: D3045854 Pulled By: vjeux fb-gh-sync-id: b3c5dba19195a6719967b4fdc8ef940cc067b1f4 shipit-source-id: b3c5dba19195a6719967b4fdc8ef940cc067b1f4 --- Libraries/Animated/src/Easing.js | 12 +- .../Animated/src/__tests__/bezier-test.js | 96 +++++++++++ Libraries/Animated/src/bezier.js | 152 ++++++++++-------- 3 files changed, 186 insertions(+), 74 deletions(-) create mode 100644 Libraries/Animated/src/__tests__/bezier-test.js diff --git a/Libraries/Animated/src/Easing.js b/Libraries/Animated/src/Easing.js index a8685a7182d641..a3021f3e6369bc 100644 --- a/Libraries/Animated/src/Easing.js +++ b/Libraries/Animated/src/Easing.js @@ -104,17 +104,9 @@ class Easing { x1: number, y1: number, x2: number, - y2: number, - epsilon?: ?number, + y2: number ): (t: number) => number { - if (epsilon === undefined) { - // epsilon determines the precision of the solved values - // a good approximation is: - var duration = 500; // duration of animation in milliseconds. - epsilon = (1000 / 60 / duration) / 4; - } - - return _bezier(x1, y1, x2, y2, epsilon); + return _bezier(x1, y1, x2, y2); } static in( diff --git a/Libraries/Animated/src/__tests__/bezier-test.js b/Libraries/Animated/src/__tests__/bezier-test.js new file mode 100644 index 00000000000000..dfbd818250feac --- /dev/null +++ b/Libraries/Animated/src/__tests__/bezier-test.js @@ -0,0 +1,96 @@ +/* eslint-disable */ + +jest.dontMock('bezier'); +var bezier = require('bezier'); + +var identity = function (x) { return x; }; + +function assertClose (a, b, precision) { + expect(a).toBeCloseTo(b, 3); +} + +function makeAssertCloseWithPrecision (precision) { + return function (a, b, message) { + assertClose(a, b, message, precision); + }; +} + +function allEquals (be1, be2, samples, assertion) { + if (!assertion) assertion = assertClose; + for (var i=0; i<=samples; ++i) { + var x = i / samples; + assertion(be1(x), be2(x), 'comparing '+be1+' and '+be2+' for value '+x); + } +} + +function repeat (n) { + return function (f) { + for (var i=0; i 0.0) { + aB = currentT; + } else { + aA = currentT; + } + } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS); + return currentT; + } - // First try a few iterations of Newton's method -- normally very fast. - for (t2 = x, i = 0; i < 8; i++){ - x2 = curveX(t2) - x; - if (Math.abs(x2) < epsilon) { return curveY(t2); } - d2 = derivativeCurveX(t2); - if (Math.abs(d2) < 1e-6) { break; } - t2 = t2 - x2 / d2; + function newtonRaphsonIterate (aX, aGuessT, mX1, mX2) { + for (var i = 0; i < NEWTON_ITERATIONS; ++i) { + var currentSlope = getSlope(aGuessT, mX1, mX2); + if (currentSlope === 0.0) { + return aGuessT; } + var currentX = calcBezier(aGuessT, mX1, mX2) - aX; + aGuessT -= currentX / currentSlope; + } + return aGuessT; + } - t0 = 0; - t1 = 1; - t2 = x; + module.exports = function bezier (mX1, mY1, mX2, mY2) { + if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { // eslint-disable-line yoda + throw new Error('bezier x values must be in [0, 1] range'); + } - if (t2 < t0) { return curveY(t0); } - if (t2 > t1) { return curveY(t1); } + // Precompute samples table + var sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize); + if (mX1 !== mY1 || mX2 !== mY2) { + for (var i = 0; i < kSplineTableSize; ++i) { + sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2); + } + } - // Fallback to the bisection method for reliability. - while (t0 < t1){ - x2 = curveX(t2); - if (Math.abs(x2 - x) < epsilon) { return curveY(t2); } - if (x > x2) { t0 = t2; } - else { t1 = t2; } - t2 = (t1 - t0) * 0.5 + t0; - } + function getTForX (aX) { + var intervalStart = 0.0; + var currentSample = 1; + var lastSample = kSplineTableSize - 1; + + for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) { + intervalStart += kSampleStepSize; + } + --currentSample; - // Failure - return curveY(t2); + // Interpolate to provide an initial guess for t + var dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]); + var guessForT = intervalStart + dist * kSampleStepSize; - }; + var initialSlope = getSlope(guessForT, mX1, mX2); + if (initialSlope >= NEWTON_MIN_SLOPE) { + return newtonRaphsonIterate(aX, guessForT, mX1, mX2); + } else if (initialSlope === 0.0) { + return guessForT; + } else { + return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2); + } + } -}; + return function BezierEasing (x) { + if (mX1 === mY1 && mX2 === mY2) { + return x; // linear + } + // Because JavaScript number are imprecise, we should guarantee the extremes are right. + if (x === 0) { + return 0; + } + if (x === 1) { + return 1; + } + return calcBezier(getTForX(x), mY1, mY2); + }; + };