mirror of
https://github.com/status-im/react-native.git
synced 2025-01-28 02:04:55 +00:00
Refactor bezier implementation from bezier-easing library
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
This commit is contained in:
parent
e82a7a8649
commit
b5985cf690
@ -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(
|
||||
|
96
Libraries/Animated/src/__tests__/bezier-test.js
Normal file
96
Libraries/Animated/src/__tests__/bezier-test.js
Normal file
@ -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<n; ++i) f(i);
|
||||
};
|
||||
}
|
||||
|
||||
describe('bezier', function(){
|
||||
it('should be a function', function(){
|
||||
expect(typeof bezier === 'function').toBe(true);
|
||||
});
|
||||
it('should creates an object', function(){
|
||||
expect(typeof bezier(0, 0, 1, 1) === 'function').toBe(true);
|
||||
});
|
||||
it('should fail with wrong arguments', function () {
|
||||
expect(function () { bezier(0.5, 0.5, -5, 0.5); }).toThrow();
|
||||
expect(function () { bezier(0.5, 0.5, 5, 0.5); }).toThrow();
|
||||
expect(function () { bezier(-2, 0.5, 0.5, 0.5); }).toThrow();
|
||||
expect(function () { bezier(2, 0.5, 0.5, 0.5); }).toThrow();
|
||||
});
|
||||
describe('linear curves', function () {
|
||||
it('should be linear', function () {
|
||||
allEquals(bezier(0, 0, 1, 1), bezier(1, 1, 0, 0), 100);
|
||||
allEquals(bezier(0, 0, 1, 1), identity, 100);
|
||||
});
|
||||
});
|
||||
describe('common properties', function () {
|
||||
it('should be the right value at extremes', function () {
|
||||
repeat(10)(function () {
|
||||
var a = Math.random(), b = 2*Math.random()-0.5, c = Math.random(), d = 2*Math.random()-0.5;
|
||||
var easing = bezier(a, b, c, d);
|
||||
expect(easing(0)).toBe(0);
|
||||
expect(easing(1)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should approach the projected value of its x=y projected curve', function () {
|
||||
repeat(10)(function () {
|
||||
var a = Math.random(), b = Math.random(), c = Math.random(), d = Math.random();
|
||||
var easing = bezier(a, b, c, d);
|
||||
var projected = bezier(b, a, d, c);
|
||||
var composed = function (x) { return projected(easing(x)); };
|
||||
allEquals(identity, composed, 100, makeAssertCloseWithPrecision(0.05));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('two same instances', function () {
|
||||
it('should be strictly equals', function () {
|
||||
repeat(10)(function () {
|
||||
var a = Math.random(), b = 2*Math.random()-0.5, c = Math.random(), d = 2*Math.random()-0.5;
|
||||
allEquals(bezier(a, b, c, d), bezier(a, b, c, d), 100, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('symetric curves', function () {
|
||||
it('should have a central value y~=0.5 at x=0.5', function () {
|
||||
repeat(10)(function () {
|
||||
var a = Math.random(), b = 2*Math.random()-0.5, c = 1-a, d = 1-b;
|
||||
var easing = bezier(a, b, c, d);
|
||||
assertClose(easing(0.5), 0.5, easing+'(0.5) should be 0.5');
|
||||
});
|
||||
});
|
||||
it('should be symetrical', function () {
|
||||
repeat(10)(function () {
|
||||
var a = Math.random(), b = 2*Math.random()-0.5, c = 1-a, d = 1-b;
|
||||
var easing = bezier(a, b, c, d);
|
||||
var sym = function (x) { return 1 - easing(1-x); };
|
||||
allEquals(easing, sym, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,82 +1,106 @@
|
||||
/**
|
||||
* https://github.com/arian/cubic-bezier
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2013 Arian Stolwijk
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
* https://github.com/gre/bezier-easing
|
||||
* BezierEasing - use bezier curve for transition easing function
|
||||
* by Gaëtan Renaudeau 2014 - 2015 – MIT License
|
||||
*
|
||||
* @providesModule bezier
|
||||
* @nolint
|
||||
*/
|
||||
|
||||
module.exports = function(x1, y1, x2, y2, epsilon){
|
||||
// These values are established by empiricism with tests (tradeoff: performance VS precision)
|
||||
var NEWTON_ITERATIONS = 4;
|
||||
var NEWTON_MIN_SLOPE = 0.001;
|
||||
var SUBDIVISION_PRECISION = 0.0000001;
|
||||
var SUBDIVISION_MAX_ITERATIONS = 10;
|
||||
|
||||
var curveX = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * v * v * t * x1 + 3 * v * t * t * x2 + t * t * t;
|
||||
};
|
||||
var kSplineTableSize = 11;
|
||||
var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0);
|
||||
|
||||
var curveY = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * v * v * t * y1 + 3 * v * t * t * y2 + t * t * t;
|
||||
};
|
||||
var float32ArraySupported = typeof Float32Array === 'function';
|
||||
|
||||
var derivativeCurveX = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * (2 * (t - 1) * t + v * v) * x1 + 3 * (-t * t * t + 2 * v * t) * x2;
|
||||
};
|
||||
function A (aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; }
|
||||
function B (aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; }
|
||||
function C (aA1) { return 3.0 * aA1; }
|
||||
|
||||
return function(t){
|
||||
// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
|
||||
function calcBezier (aT, aA1, aA2) { return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; }
|
||||
|
||||
var x = t, t0, t1, t2, x2, d2, i;
|
||||
// Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
|
||||
function getSlope (aT, aA1, aA2) { return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); }
|
||||
|
||||
// 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 binarySubdivide (aX, aA, aB, mX1, mX2) {
|
||||
var currentX, currentT, i = 0;
|
||||
do {
|
||||
currentT = aA + (aB - aA) / 2.0;
|
||||
currentX = calcBezier(currentT, mX1, mX2) - aX;
|
||||
if (currentX > 0.0) {
|
||||
aB = currentT;
|
||||
} else {
|
||||
aA = currentT;
|
||||
}
|
||||
} while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS);
|
||||
return currentT;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Failure
|
||||
return curveY(t2);
|
||||
for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) {
|
||||
intervalStart += kSampleStepSize;
|
||||
}
|
||||
--currentSample;
|
||||
|
||||
};
|
||||
// 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);
|
||||
};
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user