This repository has been archived by the owner on Feb 18, 2019. It is now read-only.
forked from facebook/react-native
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 facebook#6207 & to follow facebook#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 facebook#6433 Differential Revision: D3045854 Pulled By: vjeux fb-gh-sync-id: b3c5dba19195a6719967b4fdc8ef940cc067b1f4 shipit-source-id: b3c5dba19195a6719967b4fdc8ef940cc067b1f4
- Loading branch information
Showing
3 changed files
with
186 additions
and
74 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 kSplineTableSize = 11; | ||
var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0); | ||
|
||
var curveX = function(t){ | ||
var v = 1 - t; | ||
return 3 * v * v * t * x1 + 3 * v * t * t * x2 + t * t * t; | ||
}; | ||
var float32ArraySupported = typeof Float32Array === 'function'; | ||
|
||
var curveY = function(t){ | ||
var v = 1 - t; | ||
return 3 * v * v * t * y1 + 3 * v * t * t * y2 + t * t * t; | ||
}; | ||
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; } | ||
|
||
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; | ||
}; | ||
// 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; } | ||
|
||
return function(t){ | ||
// 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); } | ||
|
||
var x = t, t0, t1, t2, x2, d2, i; | ||
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; | ||
} | ||
|
||
// 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); | ||
}; | ||
}; |