From 70370f1e4cdb62ee2c154e56fd69d12b836728a8 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Thu, 1 May 2014 11:50:19 -0500 Subject: [PATCH] Much better bezier solver --- js/animation/animation.js | 13 +- js/animation/bezier.js | 455 +++++++++++++++++++++++++++---- js/animation/timing-functions.js | 15 +- 3 files changed, 411 insertions(+), 72 deletions(-) diff --git a/js/animation/animation.js b/js/animation/animation.js index d3d8d13f56..476cf8650a 100644 --- a/js/animation/animation.js +++ b/js/animation/animation.js @@ -101,7 +101,6 @@ tf = tf(this.duration); return this._run(function(percent, now, virtual) { - console.log(percent); self.el[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + (percent * 400) + 'px, 0,0)'; }, function() { return true; @@ -122,11 +121,9 @@ * @param duration {Integer} Milliseconds to run the animation * @param easingMethod {Function} Pointer to easing function * Signature of the method should be `function(percent) { return modifiedValue; }` - * @param root {Element} Render root, when available. Used for internal - * usage of requestAnimationFrame. * @return {Integer} Identifier of animation. Can be used to stop it any time. */ - _run: function(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) { + _run: function(stepCallback, verifyCallback, completedCallback, duration, easingMethod) { var start = time(); var lastFrame = start; @@ -134,10 +131,6 @@ var dropCounter = 0; var id = counter++; - if (!root) { - root = document.body; - } - // Compacting running db automatically every few new animations if (id % 20 === 0) { var newRunning = {}; @@ -192,7 +185,7 @@ completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, percent === 1 || duration == null); } else if (render) { lastFrame = now; - ionic.requestAnimationFrame(step, root); + ionic.requestAnimationFrame(step); } }; @@ -200,7 +193,7 @@ running[id] = true; // Init first step - ionic.requestAnimationFrame(step, root); + ionic.requestAnimationFrame(step); // Return unique animation ID return id; diff --git a/js/animation/bezier.js b/js/animation/bezier.js index e025b2e81b..0ed64a807d 100644 --- a/js/animation/bezier.js +++ b/js/animation/bezier.js @@ -1,3 +1,27 @@ +/* + * Copyright (C) 2008 Apple Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ (function(ionic) { var bezierCoord = function (x,y) { @@ -13,66 +37,393 @@ ionic.Animation = ionic.Animation || {} - ionic.Animation.Bezier = { - // Quadratic bezier solver - getQuadraticBezier: function(percent,C1,C2,C3,C4) { - var pos = new bezierCoord(); - pos.x = C1.x*B1(percent) + C2.x*B2(percent) + C3.x*B3(percent) + C4.x*B4(percent); - pos.y = C1.y*B1(percent) + C2.y*B2(percent) + C3.y*B3(percent) + C4.y*B4(percent); - return pos; - }, - - // Cubic bezier solver from https://github.com/arian/cubic-bezier (MIT) - getCubicBezier: function(x1, y1, x2, y2, duration) { - // Precision - epsilon = (1000 / 60 / duration) / 4; - - var curveX = function(t){ - var v = 1 - t; - return 3 * v * v * t * x1 + 3 * v * t * t * x2 + t * t * t; + + /** + * JavaScript port of Webkit implementation of CSS cubic-bezier(p1x.p1y,p2x,p2y) by http://mck.me + * http://svn.webkit.org/repository/webkit/trunk/Source/WebCore/platform/graphics/UnitBezier.h + */ + ionic.Animation.Bezier = (function(){ + 'use strict'; + + /** + * Duration value to use when one is not specified (400ms is a common value). + * @const + * @type {number} + */ + var DEFAULT_DURATION = 400;//ms + + /** + * The epsilon value we pass to UnitBezier::solve given that the animation is going to run over |dur| seconds. + * The longer the animation, the more precision we need in the timing function result to avoid ugly discontinuities. + * http://svn.webkit.org/repository/webkit/trunk/Source/WebCore/page/animation/AnimationBase.cpp + */ + var solveEpsilon = function(duration) { + return 1.0 / (200.0 * duration); + }; + + /** + * Defines a cubic-bezier curve given the middle two control points. + * NOTE: first and last control points are implicitly (0,0) and (1,1). + * @param p1x {number} X component of control point 1 + * @param p1y {number} Y component of control point 1 + * @param p2x {number} X component of control point 2 + * @param p2y {number} Y component of control point 2 + */ + var unitBezier = function(p1x, p1y, p2x, p2y) { + + // private members -------------------------------------------- + + // Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1). + + /** + * X component of Bezier coefficient C + * @const + * @type {number} + */ + var cx = 3.0 * p1x; + + /** + * X component of Bezier coefficient B + * @const + * @type {number} + */ + var bx = 3.0 * (p2x - p1x) - cx; + + /** + * X component of Bezier coefficient A + * @const + * @type {number} + */ + var ax = 1.0 - cx -bx; + + /** + * Y component of Bezier coefficient C + * @const + * @type {number} + */ + var cy = 3.0 * p1y; + + /** + * Y component of Bezier coefficient B + * @const + * @type {number} + */ + var by = 3.0 * (p2y - p1y) - cy; + + /** + * Y component of Bezier coefficient A + * @const + * @type {number} + */ + var ay = 1.0 - cy - by; + + /** + * @param t {number} parametric timing value + * @return {number} + */ + var sampleCurveX = function(t) { + // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule. + return ((ax * t + bx) * t + cx) * t; }; - - var curveY = function(t){ - var v = 1 - t; - return 3 * v * v * t * y1 + 3 * v * t * t * y2 + t * t * t; + + /** + * @param t {number} parametric timing value + * @return {number} + */ + var sampleCurveY = function(t) { + return ((ay * t + by) * t + cy) * t; }; - - 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; + + /** + * @param t {number} parametric timing value + * @return {number} + */ + var sampleCurveDerivativeX = function(t) { + return (3.0 * ax * t + 2.0 * bx) * t + cx; }; - - return function(t) { - - var x = t, t0, t1, t2, x2, d2, i; - + + /** + * Given an x value, find a parametric value it came from. + * @param x {number} value of x along the bezier curve, 0.0 <= x <= 1.0 + * @param epsilon {number} accuracy limit of t for the given x + * @return {number} the t value corresponding to x + */ + var solveCurveX = function(x, epsilon) { + var t0; + var t1; + var t2; + var x2; + var d2; + var i; + // 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; + for (t2 = x, i = 0; i < 8; i++) { + x2 = sampleCurveX(t2) - x; + if (Math.abs (x2) < epsilon) { + return t2; + } + d2 = sampleCurveDerivativeX(t2); + if (Math.abs(d2) < 1e-6) { + break; + } t2 = t2 - x2 / d2; } - - t0 = 0, t1 = 1, t2 = x; - - if (t2 < t0) return curveY(t0); - if (t2 > t1) return curveY(t1); - - // 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; + + // Fall back to the bisection method for reliability. + t0 = 0.0; + t1 = 1.0; + t2 = x; + + if (t2 < t0) { + return t0; + } + if (t2 > t1) { + return t1; + } + + while (t0 < t1) { + x2 = sampleCurveX(t2); + if (Math.abs(x2 - x) < epsilon) { + return t2; + } + if (x > x2) { + t0 = t2; + } else { + t1 = t2; + } t2 = (t1 - t0) * 0.5 + t0; } - - // Failure - return curveY(t2); + + // Failure. + return t2; }; - } - }; + + /** + * @param x {number} the value of x along the bezier curve, 0.0 <= x <= 1.0 + * @param epsilon {number} the accuracy of t for the given x + * @return {number} the y value along the bezier curve + */ + var solve = function(x, epsilon) { + return sampleCurveY(solveCurveX(x, epsilon)); + }; + + // public interface -------------------------------------------- + + /** + * Find the y of the cubic-bezier for a given x with accuracy determined by the animation duration. + * @param x {number} the value of x along the bezier curve, 0.0 <= x <= 1.0 + * @param duration {number} the duration of the animation in milliseconds + * @return {number} the y value along the bezier curve + */ + return function(x, duration) { + return solve(x, solveEpsilon(+duration || DEFAULT_DURATION)); + }; + }; + + // http://www.w3.org/TR/css3-transitions/#transition-timing-function + return { + /** + * @param x {number} the value of x along the bezier curve, 0.0 <= x <= 1.0 + * @param duration {number} the duration of the animation in milliseconds + * @return {number} the y value along the bezier curve + */ + linear: unitBezier(0.0, 0.0, 1.0, 1.0), + + /** + * @param x {number} the value of x along the bezier curve, 0.0 <= x <= 1.0 + * @param duration {number} the duration of the animation in milliseconds + * @return {number} the y value along the bezier curve + */ + ease: unitBezier(0.25, 0.1, 0.25, 1.0), + + /** + * @param x {number} the value of x along the bezier curve, 0.0 <= x <= 1.0 + * @param duration {number} the duration of the animation in milliseconds + * @return {number} the y value along the bezier curve + */ + easeIn: unitBezier(0.42, 0, 1.0, 1.0), + + /** + * @param x {number} the value of x along the bezier curve, 0.0 <= x <= 1.0 + * @param duration {number} the duration of the animation in milliseconds + * @return {number} the y value along the bezier curve + */ + easeOut: unitBezier(0, 0, 0.58, 1.0), + + /** + * @param x {number} the value of x along the bezier curve, 0.0 <= x <= 1.0 + * @param duration {number} the duration of the animation in milliseconds + * @return {number} the y value along the bezier curve + */ + easeInOut: unitBezier(0.42, 0, 0.58, 1.0), + + /** + * @param p1x {number} X component of control point 1 + * @param p1y {number} Y component of control point 1 + * @param p2x {number} X component of control point 2 + * @param p2y {number} Y component of control point 2 + * @param x {number} the value of x along the bezier curve, 0.0 <= x <= 1.0 + * @param duration {number} the duration of the animation in milliseconds + * @return {number} the y value along the bezier curve + */ + cubicBezier: function(p1x, p1y, p2x, p2y, x, duration) { + return unitBezier(p1x, p1y, p2x, p2y)(x, duration); + } + }; + })(); +/** + * Various fast approximations and alternates to cubic-bezier easing functions. + * http://www.w3.org/TR/css3-transitions/#transition-timing-function + */ +var Easing = (function(){ + 'use strict'; + + /** + * @const + */ + var EASE_IN_OUT_CONST = 0.5 * Math.pow(0.5, 1.925); + + return { + + /** + * @param x {number} the value of x along the curve, 0.0 <= x <= 1.0 + * @return {number} the y value along the curve + */ + linear: function(x) { + return x; + }, + +// /** +// * @param x {number} the value of x along the curve, 0.0 <= x <= 1.0 +// * @return {number} the y value along the curve +// */ +// ease: function(x) { +// // TODO: find fast approximations +// return x; +// }, + + /** + * @param x {number} the value of x along the curve, 0.0 <= x <= 1.0 + * @return {number} the y value along the curve + */ + easeInApprox: function(x) { + // very close approximation to cubic-bezier(0.42, 0, 1.0, 1.0) + return Math.pow(x, 1.685); + }, + + /** + * @param x {number} the value of x along the curve, 0.0 <= x <= 1.0 + * @return {number} the y value along the curve + */ + easeInQuadratic: function(x) { + return (x * x); + }, + + /** + * @param x {number} the value of x along the curve, 0.0 <= x <= 1.0 + * @return {number} the y value along the curve + */ + easeInCubic: function(x) { + return (x * x * x); + }, + + /** + * @param x {number} the value of x along the curve, 0.0 <= x <= 1.0 + * @return {number} the y value along the curve + */ + easeOutApprox: function(x) { + // very close approximation to cubic-bezier(0, 0, 0.58, 1.0) + return 1 - Math.pow(1-x, 1.685); + }, + + /** + * @param x {number} the value of x along the curve, 0.0 <= x <= 1.0 + * @return {number} the y value along the curve + */ + easeOutQuadratic: function(x) { + x -= 1; + return 1 - (x * x); + }, + + /** + * @param x {number} the value of x along the curve, 0.0 <= x <= 1.0 + * @return {number} the y value along the curve + */ + easeOutCubic: function(x) { + x -= 1; + return 1 + (x * x * x); + }, + + /** + * @param x {number} the value of x along the curve, 0.0 <= x <= 1.0 + * @return {number} the y value along the curve + */ + easeInOutApprox: function(x) { + // very close approximation to cubic-bezier(0.42, 0, 0.58, 1.0) + if (x < 0.5) { + return EASE_IN_OUT_CONST * Math.pow(x, 1.925); + + } else { + return 1 - EASE_IN_OUT_CONST * Math.pow(1-x, 1.925); + } + }, + + /** + * @param x {number} the value of x along the curve, 0.0 <= x <= 1.0 + * @return {number} the y value along the curve + */ + easeInOutQuadratic: function(x) { + if (x < 0.5) { + return (2 * x * x); + + } else { + x -= 1; + return 1 - (2 * x * x); + } + }, + + /** + * @param x {number} the value of x along the curve, 0.0 <= x <= 1.0 + * @return {number} the y value along the curve + */ + easeInOutCubic: function(x) { + if (x < 0.5) { + return (4 * x * x * x); + + } else { + x -= 1; + return 1 + (4 * x * x * x); + } + }, + + /** + * @param x {number} the value of x along the curve, 0.0 <= x <= 1.0 + * @return {number} the y value along the curve + */ + easeInOutQuartic: function(x) { + if (x < 0.5) { + return (8 * x * x * x * x); + + } else { + x -= 1; + return 1 + (8 * x * x * x * x); + } + }, + + /** + * @param x {number} the value of x along the curve, 0.0 <= x <= 1.0 + * @return {number} the y value along the curve + */ + easeInOutQuintic: function(x) { + if (x < 0.5) { + return (16 * x * x * x * x * x); + + } else { + x -= 1; + return 1 + (16 * x * x * x * x * x); + } + } + }; +})(); })(ionic); diff --git a/js/animation/timing-functions.js b/js/animation/timing-functions.js index 5b066d277e..5fba8c0347 100644 --- a/js/animation/timing-functions.js +++ b/js/animation/timing-functions.js @@ -7,32 +7,27 @@ ionic.Animation.TimingFn = { 'linear': function(duration) { return function(t) { - return t; + return ionic.Animation.Bezier.linear(t, duration); } }, 'ease': function(duration) { - var bz = ionic.Animation.Bezier.getCubicBezier(0.25, 0.1, 0.25, 1.0, duration); return function(t) { - return bz(t); + return ionic.Animation.Bezier.ease(t, duration); } }, 'ease-in': function(duration) { - //0.42, 0.0, 1.0, 1.0 - var bz = ionic.Animation.Bezier.getCubicBezier(0.42, 0.0, 1.0, 1.0, duration); return function(t) { - return bz(t); + return ionic.Animation.Bezier.easeIn(t, duration); } }, 'ease-out': function(duration) { - var bz = ionic.Animation.Bezier.getCubicBezier(0.0, 0.0, 0.58, 1.0, duration); return function(t) { - return bz(t); + return ionic.Animation.Bezier.easeOut(t, duration); } }, 'ease-in-out': function(duration) { - var bz = ionic.Animation.Bezier.getCubicBezier(0.42, 0.0, 0.58, 1.0, duration); return function(t) { - return bz(t); + return ionic.Animation.Bezier.easeInOut(t, duration); } } };