/*!
* Unit-Bezier 1.0.0
*
* Copyright 2015, Jacob Peters - http://jacobpeters.co
* Credit to:
* Chromium: UnitBezier Class and tests
* Firefox: nsSMILKeySpline Class and tests
* A Primer on Bézier Curves - http://pomax.github.io/bezierinfo/
* Released under the MIT license - http://opensource.org/licenses/MIT
*/
/**
* @author Jacob Peters
*/
/**
* Creates a new UnitBezier Object
* @class
* @classdesc Calculates the Y coordinate for a given X coordinate on a Bezier curve.
* @param {Array.<Number>} points The control points of a Bezier curve [x1, y1, x2, y2].
* @param {Number} epsilon An epsilon to determine the acceptable error for the answer.
*/
function UnitBezier(points, epsilon) {
'use strict';
//Calculate coefficients for sampling equations
this.coefX = [
3.0 * points[0],
3.0 * (points[2] - points[0]),
1
];
this.coefY = [
3.0 * points[1],
3.0 * (points[3] - points[1]),
1
];
this.coefX[1] -= this.coefX[0];
this.coefX[2] = this.coefX[2] - this.coefX[0] - this.coefX[1];
this.coefY[1] -= this.coefY[0];
this.coefY[2] = this.coefY[2] - this.coefY[0] - this.coefY[1];
this.epsilon = epsilon;
this.sampleTable = this.generateSampleTable();
}
/**
* Generates a look up table to speed up calculations.
* @private
* @returns {Array.<Number>} Look up table.
*/
UnitBezier.prototype.generateSampleTable = function () {
'use strict';
var sampleTable = [];
for(var i=0; i < this.sampleTableSize; ++i) {
sampleTable[i] = this.sampleCurveX(i * this.sampleStepSize);
}
return sampleTable;
};
/**
* Gets the x coordinate of the Bezier curve at a given percentage through the curve.
* @private
* @param {Number} t Percentage through the curve 0 <= t <= 1
* @returns {Number} x coordinate of curve.
*/
UnitBezier.prototype.sampleCurveX = function (t) {
'use strict';
return ((this.coefX[2] * t + this.coefX[1]) * t + this.coefX[0]) * t;
};
/**
* Gets the y coordinate of the Bezier curve at a given percentage through the curve.
* @private
* @param {Number} t Percentage through the curve 0 <= t <= 1
* @returns {Number} y coordinate of curve.
*/
UnitBezier.prototype.sampleCurveY = function (t) {
'use strict';
return ((this.coefY[2] * t + this.coefY[1]) * t + this.coefY[0]) * t;
};
/**
* Gets the derivative of the Bezier curve at a given percentage through the curve.
* @private
* @param {Number} t Percentage through the curve 0 <= t <= 1
* @returns {Number} Derivative of curve.
*/
UnitBezier.prototype.sampleCurveDerivativeX = function (t) {
'use strict';
return (3.0 * this.coefX[2] * t + 2.0 * this.coefX[1]) * t + this.coefX[0];
};
/**
* Gets the percentage through the curve that a given x coordinate appears on the curve.
* @private
* @param {Number} x Desired coordinate 0 <= x <= 1
* @returns {Number} Percentage through curve.
*/
UnitBezier.prototype.solveCurveX = function (x) {
'use strict';
//Estimate the value of t by interpolating the LUT
var tEstimates = this.esitimateT(x),
calculatedT;
//Newton Raphson will not converge if the slope is too small
if(tEstimates.slope >= 0.02) {
calculatedT = this.newtonRaphsonIterate(x, tEstimates.guessForT);
}
//For tiny slopes, the estimate is close enough
else if(tEstimates.slope < 0.00001) {
calculatedT = tEstimates.guessForT;
}
//Fallback to Binary Subdivision if all else fails
if(calculatedT === -1) {
calculatedT = this.binarySubdivide(x, tEstimates.tBoundsLow, tEstimates.tBoundsHigh);
}
return calculatedT;
};
/**
* Estimates the percentage through the curve that a given x coordinate appears on the curve with a look up table.
* @private
* @param {Number} x Desired coordinate 0 <= x <= 1
* @returns {Number} Estimated percentage through curve.
*/
UnitBezier.prototype.esitimateT = function (x) {
'use strict';
var index = 0,
interpolatedT,
dist;
//find which samples the desired x is between
for(; index < this.sampleTableSize - 1 && this.sampleTable[index] <= x + this.epsilon; ++index) {}
--index;
dist = (x - this.sampleTable[index]) / (this.sampleTable[index+1] - this.sampleTable[index]);
interpolatedT = (this.sampleStepSize * index) + (dist * this.sampleStepSize);
return {
guessForT: interpolatedT,
tBoundsLow: this.sampleStepSize * index,
tBoundsHigh: Math.min(this.sampleStepSize * (index + 1), 1),
slope: this.sampleCurveDerivativeX(interpolatedT)
};
};
/**
* @private
* @param {Number} x Desired coordinate 0 <= x <= 1
* @param {Number} estimatedT Initial estimate of percentage through curve.
* @returns {Number} Percentage through curve or -1 for a failure.
*/
UnitBezier.prototype.newtonRaphsonIterate = function (x, estimatedT) {
'use strict';
var currentX,
currentDerivitive;
for(var i=0; i < this.newtonRaphsonMaxIterations; ++i) {
currentX = this.sampleCurveX(estimatedT) - x;
currentDerivitive = this.sampleCurveDerivativeX(estimatedT);
if(Math.abs(currentX) < this.epsilon) {
return estimatedT;
}
estimatedT -= currentX / currentDerivitive;
}
console.log('x for calculated t = ' + this.sampleCurveX(estimatedT));
return -1;
};
/**
* @private
* @param {Number} x Desired coordinate 0 <= x <= 1
* @param {Number} tBoundsLow Lowest possible percentage where x could appear.
* @param {Number} tBoundsHigh Highest possible percentage where x could appear.
* @returns {Number} Percentage through curve.
*/
UnitBezier.prototype.binarySubdivide = function (x, tBoundsLow, tBoundsHigh) {
'use strict';
var currentX,
currentT,
i = this.binarySubdivideMaxIterations;
do {
currentT = tBoundsLow + ((tBoundsHigh - tBoundsLow) / 2.0);
currentX = this.sampleCurveX(currentT) - x;
if(currentX > 0.0) {
tBoundsHigh = currentT;
}
else {
tBoundsLow = currentT;
}
} while (Math.abs(currentX) > this.epsilon && --i > 0);
return currentT;
};
/**
* Calculates the Y coordinate for a given X coordinate on a Bezier curve.
* @public
* @param {Number} timePercent The x coordinate (time through animation).
* @returns {Number} The y coordinate (interpolation factor).
*/
UnitBezier.prototype.calc = function (timePercent) {
'use strict';
if(timePercent < 0.0) {
timePercent = 0.0;
}
else if (timePercent > 1.0) {
timePercent = 1.0
}
return this.sampleCurveY(this.solveCurveX(timePercent));
};
//class constants
UnitBezier.prototype.sampleTableSize = 26;
UnitBezier.prototype.sampleStepSize = 1.0 / (UnitBezier.prototype.sampleTableSize - 1);
UnitBezier.prototype.newtonRaphsonMaxIterations = 8;
UnitBezier.prototype.binarySubdivideMaxIterations = 20;
/**
* Animation Easings.
* @namespace
* @property {Array.<Number>} linear
* @property {Array.<Number>} ease
* @property {Array.<Number>} easeIn
* @property {Array.<Number>} easeInOut
* @property {Array.<Number>} easeOut
* @property {Array.<Number>} easeInBack
* @property {Array.<Number>} easeOutBack
* @property {Array.<Number>} easeInOutBack
*/
UnitBezier.easings = {
linear: [0.0, 0.0, 1.0, 1.0],
ease: [0.25, 0.1, 0.25, 1.0],
easeIn: [0.42, 0.0, 1.0, 1.0],
easeInOut: [0.42, 0.0, 0.58, 1.0],
easeOut: [0.0, 0.0, 0.58, 1.0],
easeInBack: [0.6, -0.28, 0.735, 0.045],
easeOutBack: [0.175, 0.885, 0.32, 1.275],
easeInOutBack: [0.680, -0.550, 0.265, 1.550]
};
//export for node
if(typeof module !== 'undefined')
module.exports = UnitBezier;