From 6f89f21268dd1b980e56d4002320ccbb40c6875c Mon Sep 17 00:00:00 2001 From: Camille Hodoul Date: Mon, 1 May 2017 10:55:55 +0200 Subject: [PATCH] Improve custom easing + more detailed doc about easing : fix #13 --- CHANGELOG.md | 1 + README.md | 29 +++++++-- dev/bezier.js | 104 ++++++++++++++++++++++++++++++ dev/index.html | 5 +- dist/paper-animate-browser.min.js | 2 +- dist/src/animation.js | 13 ++-- dist/src/animation.js.map | 2 +- dist/src/tween.js | 9 ++- dist/src/tween.js.map | 2 +- src/animation.ts | 19 +++--- src/tween.ts | 8 ++- test.html | 1 + tests.js | 35 ++++++++++ 13 files changed, 206 insertions(+), 24 deletions(-) create mode 100644 dev/bezier.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f000354..c93a0d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 1.2.1 * TS rewrite : TS declaration is available in dist/src/animatePaper.d.ts + * custom easings : you can now pass a `function` `(p: number) => number` to `settings.easing` * bug fix : negative absolute position supported (relative values must be of string type) * bug fix : allow 0 duration diff --git a/README.md b/README.md index 605ce56..edfae1d 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,9 @@ TypeScript declarations are available as of 1.2.1, in `dist/src/animatePaper.d.t * `settings.complete` callback takes the `Animation`object as 1st argument. * Color support for `paper.Group` animation (1.1.*) * rgb, gray, hsl, hbs Color formats are now supported (1.1.*) - * bug fix : negative absolute position supported (relative values must be of string type) - * bug fix : allow 0 duration + * bug fix : negative absolute position supported (relative values must be of string type) (1.2.*) + * bug fix : allow 0 duration (1.2.*) + * custom easings : you can now pass a `function` `(p: number) => number` to `settings.easing` (1.2.*) ## How to use : ### npm and browserify @@ -203,9 +204,24 @@ The `stop` method can take a `goToEnd` argument. If true, all the animations will take their final value and `complete` callbacks will be called. -### Add custom easing functions +### Easing -You can use `animatePaper.extendEasing(myEasingFunctions)` method to add your own easing functions or override any existing easing. +By default, the supported easing functions are : linear, swing, easeInSine, easeOutSine, easeInOutSine, easeInCirc, easeOutCirc, easeInOutCirc, easeInElastic, easeOutElastic, easeInOutElastic, easeInBack, easeOutBack, easeInOutBack, easeInBounce, easeOutBounce, easeInOutBounce, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeInQuint, easeOutQuint, easeInOutQuint, easeInExpo, easeOutExpo, easeInOutExpo. + +If you want to use more easing functions, `settings.easing` can take a `(p: number) => number` function as a value, so that you can use your own or use some from an external library such as [bezier-easing](https://github.com/gre/bezier-easing). +```js +animatePaper.animate(item,{ + properties: { + /** ... **/ + }, + settings: { + /** ... **/ + easing: BezierEasing(0, 0, 1, 0.5) + } +}); +``` + +Alternatively, you can use the `animatePaper.extendEasing(myEasingFunctions)` method to add your own easing functions or override any existing easing. The method takes only one argument : an object in which keys are easing names, and values are easing functions: @@ -217,6 +233,8 @@ animatePaper.extendEasing({ }); ``` +Learn more about easing [here](http://easings.net/). + ### Extend property hooks If you want to add support for a new property or override the library's behavior for properties that are already supported, @@ -264,7 +282,8 @@ as of 1.2.1 the lib uses TypeScript, so make your changes in src/*.ts then build ## TODOS * Change how `item.data._animatePaperVals` works to allow multiple animations of the same property at the same time. - * tests + * Change `Tween` so that we garantee values are right at `0`and `1` positions, to avoid problems with imprecise numbers (floating point). See "Negative position" in tests.js. + * Add tests ## Help needed ! diff --git a/dev/bezier.js b/dev/bezier.js new file mode 100644 index 0000000..7b4a260 --- /dev/null +++ b/dev/bezier.js @@ -0,0 +1,104 @@ +/** + * https://github.com/gre/bezier-easing + * BezierEasing - use bezier curve for transition easing function + * by Gaëtan Renaudeau 2014 - 2015 – MIT License + */ + +// 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 float32ArraySupported = typeof Float32Array === 'function'; + +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; } + +// 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; } + +// 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); } + +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; +} + +window._bezier = function bezier (mX1, mY1, mX2, mY2) { + if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { + throw new Error('bezier x values must be in [0, 1] range'); + } + + // 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); + } + } + + function getTForX (aX) { + var intervalStart = 0.0; + var currentSample = 1; + var lastSample = kSplineTableSize - 1; + + 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); + }; +}; \ No newline at end of file diff --git a/dev/index.html b/dev/index.html index 5c2f2f7..aee08c2 100644 --- a/dev/index.html +++ b/dev/index.html @@ -5,6 +5,7 @@ + animatePaper.js demo + diff --git a/tests.js b/tests.js index 8d33c3d..23c9eea 100644 --- a/tests.js +++ b/tests.js @@ -171,6 +171,41 @@ QUnit.test( "0 duration", function( assert ) { square.remove(); }, expectedTime + 1); }); +QUnit.test( "custom easing (callback)", function( assert ) { + resetCanvas(); + var scope = paper.setup('defCanvas'); + var square = new paper.Path.Rectangle(new paper.Point(150, 350), new paper.Size(50,50)); + var myBezier = _bezier(0, 0, 1, 0.5); + var myEasingUsed = 0; + var myEasing = function(p) { + myEasingUsed++; + return myBezier(p); + } + square.strokeColor = "black"; + var expectedTime = 300; + var completed = false; + animatePaper.animate(square,{ + properties: { + position: { + x: 100 + } + }, + settings: { + duration: expectedTime, + easing: myEasing, + complete: function() { + completed = true; + } + } + }); + var done = assert.async(); + setTimeout(function() { + var easingUsed = myEasingUsed; + assert.ok(myEasingUsed > 0, "Custom easing should be used. Used : " + myEasingUsed + " times"); + done(); + square.remove(); + }, expectedTime + 1); +}); function resetCanvas() {