diff --git a/angular/src/providers/animation-controller.ts b/angular/src/providers/animation-controller.ts index 2b2a4c3506..abd620c571 100644 --- a/angular/src/providers/animation-controller.ts +++ b/angular/src/providers/animation-controller.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Animation, createAnimation } from '@ionic/core'; +import { Animation, createAnimation, getTimeGivenProgression } from '@ionic/core'; @Injectable({ providedIn: 'root', @@ -11,4 +11,22 @@ export class AnimationController { create(animationId?: string): Animation { return createAnimation(animationId); } + + /** + * EXPERIMENTAL + * + * Given a progression and a cubic bezier function, + * this utility returns the time value(s) at which the + * cubic bezier reaches the given time progression. + * + * If the cubic bezier never reaches the progression + * the result will be an empty array. + * + * This is most useful for switching between easing curves + * when doing a gesture animation (i.e. going from linear easing + * during a drag, to another easing when `progressEnd` is called) + */ + easingTime(p0: number[], p1: number[], p2: number[], p3: number[], progression: number): number[] { + return getTimeGivenProgression(p0, p1, p2, p3, progression); + } } diff --git a/core/src/components/menu/menu.tsx b/core/src/components/menu/menu.tsx index 6a3d741bf2..42cb233717 100644 --- a/core/src/components/menu/menu.tsx +++ b/core/src/components/menu/menu.tsx @@ -3,7 +3,7 @@ import { Build, Component, ComponentInterface, Element, Event, EventEmitter, Hos import { config } from '../../global/config'; import { getIonMode } from '../../global/ionic-global'; import { Animation, Gesture, GestureDetail, MenuChangeEventDetail, MenuI, Side } from '../../interface'; -import { Point, getTimeGivenProgression } from '../../utils/animation/cubic-bezier'; +import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier'; import { GESTURE_CONTROLLER } from '../../utils/gesture'; import { assert, clamp, isEndSide as isEnd } from '../../utils/helpers'; import { menuController } from '../../utils/menu-controller'; @@ -449,7 +449,7 @@ AFTER: * to the new easing curve, as `stepValue` is going to be given * in terms of a linear curve. */ - newStepValue += getTimeGivenProgression(new Point(0, 0), new Point(0.4, 0), new Point(0.6, 1), new Point(1, 1), clamp(0, adjustedStepValue, 1)); + newStepValue += getTimeGivenProgression([0, 0], [0.4, 0], [0.6, 1], [1, 1], clamp(0, adjustedStepValue, 1))[0]; this.animation .easing('cubic-bezier(0.4, 0.0, 0.6, 1)') diff --git a/core/src/components/nav/nav.tsx b/core/src/components/nav/nav.tsx index c7dfea4522..ad0a107acb 100644 --- a/core/src/components/nav/nav.tsx +++ b/core/src/components/nav/nav.tsx @@ -3,7 +3,7 @@ import { Build, Component, Element, Event, EventEmitter, Method, Prop, Watch, h import { config } from '../../global/config'; import { getIonMode } from '../../global/ionic-global'; import { Animation, AnimationBuilder, ComponentProps, FrameworkDelegate, Gesture, NavComponent, NavOptions, NavOutlet, NavResult, RouteID, RouteWrite, RouterDirection, TransitionDoneFn, TransitionInstruction, ViewController } from '../../interface'; -import { Point, getTimeGivenProgression } from '../../utils/animation/cubic-bezier'; +import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier'; import { assert } from '../../utils/helpers'; import { TransitionOptions, lifecycle, setPageHidden, transition } from '../../utils/transition'; @@ -981,9 +981,9 @@ export class Nav implements NavOutlet { */ if (!shouldComplete) { this.sbAni.easing('cubic-bezier(1, 0, 0.68, 0.28)'); - newStepValue += getTimeGivenProgression(new Point(0, 0), new Point(1, 0), new Point(0.68, 0.28), new Point(1, 1), stepValue); + newStepValue += getTimeGivenProgression([0, 0], [1, 0], [0.68, 0.28], [1, 1], stepValue)[0]; } else { - newStepValue += getTimeGivenProgression(new Point(0, 0), new Point(0.32, 0.72), new Point(0, 1), new Point(1, 1), stepValue); + newStepValue += getTimeGivenProgression([0, 0], [0.32, 0.72], [0, 1], [1, 1], stepValue)[0]; } (this.sbAni as Animation).progressEnd(shouldComplete ? 1 : 0, newStepValue, dur); diff --git a/core/src/components/router-outlet/route-outlet.tsx b/core/src/components/router-outlet/route-outlet.tsx index 1542709b76..018d99011a 100644 --- a/core/src/components/router-outlet/route-outlet.tsx +++ b/core/src/components/router-outlet/route-outlet.tsx @@ -3,7 +3,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Pr import { config } from '../../global/config'; import { getIonMode } from '../../global/ionic-global'; import { Animation, AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Gesture, NavOutlet, RouteID, RouteWrite, RouterDirection, RouterOutletOptions, SwipeGestureHandler } from '../../interface'; -import { Point, getTimeGivenProgression } from '../../utils/animation/cubic-bezier'; +import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier'; import { attachComponent, detachComponent } from '../../utils/framework-delegate'; import { transition } from '../../utils/transition'; @@ -91,9 +91,9 @@ export class RouterOutlet implements ComponentInterface, NavOutlet { */ if (!shouldComplete) { this.ani.easing('cubic-bezier(1, 0, 0.68, 0.28)'); - newStepValue += getTimeGivenProgression(new Point(0, 0), new Point(1, 0), new Point(0.68, 0.28), new Point(1, 1), step); + newStepValue += getTimeGivenProgression([0, 0], [1, 0], [0.68, 0.28], [1, 1], step)[0]; } else { - newStepValue += getTimeGivenProgression(new Point(0, 0), new Point(0.32, 0.72), new Point(0, 1), new Point(1, 1), step); + newStepValue += getTimeGivenProgression([0, 0], [0.32, 0.72], [0, 1], [1, 1], step)[0]; } (this.ani as Animation).progressEnd(shouldComplete ? 1 : 0, newStepValue, dur); diff --git a/core/src/index.ts b/core/src/index.ts index c58483b7cc..139f129a95 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -1,6 +1,7 @@ import 'ionicons'; export { createAnimation } from './utils/animation/animation'; +export { getTimeGivenProgression } from './utils/animation/cubic-bezier'; export { createGesture } from './utils/gesture'; export { isPlatform, Platforms, getPlatforms } from './utils/platform'; diff --git a/core/src/utils/animation/cubic-bezier.ts b/core/src/utils/animation/cubic-bezier.ts index 4a8b10d1b5..8d80b2f91b 100644 --- a/core/src/utils/animation/cubic-bezier.ts +++ b/core/src/utils/animation/cubic-bezier.ts @@ -5,11 +5,8 @@ * TODO: Reduce rounding error */ -export class Point { - constructor(public x: number, public y: number) {} -} - /** + * EXPERIMENTAL * Given a cubic-bezier curve, get the x value (time) given * the y value (progression). * Ex: cubic-bezier(0.32, 0.72, 0, 1); @@ -19,11 +16,12 @@ export class Point { * P3: (1, 1) * * If you give a cubic bezier curve that never reaches the - * provided progression, this function will return NaN. + * provided progression, this function will return an empty array. */ -export const getTimeGivenProgression = (p0: Point, p1: Point, p2: Point, p3: Point, progression: number) => { - const tValues = solveCubicBezier(p0.y, p1.y, p2.y, p3.y, progression); - return solveCubicParametricEquation(p0.x, p1.x, p2.x, p3.x, tValues[0]); // TODO: Add better strategy for dealing with multiple solutions +export const getTimeGivenProgression = (p0: number[], p1: number[], p2: number[], p3: number[], progression: number): number[] => { + return solveCubicBezier(p0[1], p1[1], p2[1], p3[1], progression).map(tValue => { + return solveCubicParametricEquation(p0[0], p1[0], p2[0], p3[0], tValue); + }); }; /** diff --git a/core/src/utils/animation/test/animation.spec.ts b/core/src/utils/animation/test/animation.spec.ts index 519770bcec..6ecf4ed8ca 100644 --- a/core/src/utils/animation/test/animation.spec.ts +++ b/core/src/utils/animation/test/animation.spec.ts @@ -1,5 +1,5 @@ import { createAnimation } from '../animation'; -import { getTimeGivenProgression, Point } from '../cubic-bezier'; +import { getTimeGivenProgression } from '../cubic-bezier'; import { Animation } from '../animation-interface'; describe('Animation Class', () => { @@ -313,70 +313,83 @@ describe('cubic-bezier conversion', () => { describe('should properly get a time value (x value) given a progression value (y value)', () => { it('cubic-bezier(0.32, 0.72, 0, 1)', () => { const equation = [ - new Point(0, 0), - new Point(0.32, 0.72), - new Point(0, 1), - new Point(1, 1) + [0, 0], + [0.32, 0.72], + [0, 1], + [1, 1] ]; - shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.5), 0.16); - shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.97), 0.56); - shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.33), 0.11); + shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.5), [0.16]); + shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.97), [0.56]); + shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.33), [0.11]); }); it('cubic-bezier(1, 0, 0.68, 0.28)', () => { const equation = [ - new Point(0, 0), - new Point(1, 0), - new Point(0.68, 0.28), - new Point(1, 1) + [0, 0], + [1, 0], + [0.68, 0.28], + [1, 1] ]; - shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.08), 0.60); - shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.50), 0.84); - shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.94), 0.98); + shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.08), [0.60]); + shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.50), [0.84]); + shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.94), [0.98]); }) it('cubic-bezier(0.4, 0, 0.6, 1)', () => { const equation = [ - new Point(0, 0), - new Point(0.4, 0), - new Point(0.6, 1), - new Point(1, 1) + [0, 0], + [0.4, 0], + [0.6, 1], + [1, 1] ]; - shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.39), 0.43); - shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.03), 0.11); - shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.89), 0.78); + shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.39), [0.43]); + shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.03), [0.11]); + shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.89), [0.78]); }) it('cubic-bezier(0, 0, 0.2, 1)', () => { const equation = [ - new Point(0, 0), - new Point(0, 0), - new Point(0.2, 1), - new Point(1, 1) + [0, 0], + [0, 0], + [0.2, 1], + [1, 1] ]; - shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.95), 0.71); - shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.1), 0.03); - shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.70), 0.35); + shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.95), [0.71]); + shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.1), [0.03]); + shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.70), [0.35]); }) it('cubic-bezier(0.32, 0.72, 0, 1) (with out of bounds progression)', () => { const equation = [ - new Point(0, 0), - new Point(0.05, 0.2), - new Point(.14, 1.72), - new Point(1, 1) + [0, 0], + [0.05, 0.2], + [.14, 1.72], + [1, 1] ]; - expect(getTimeGivenProgression(...equation, 1.32)).toBeNaN(); - expect(getTimeGivenProgression(...equation, -0.32)).toBeNaN(); + expect(getTimeGivenProgression(...equation, 1.32)[0]).toBeUndefined(); + expect(getTimeGivenProgression(...equation, -0.32)[0]).toBeUndefined(); + }) + + it('cubic-bezier(0.21, 1.71, 0.88, 0.9) (multiple solutions)', () => { + const equation = [ + [0, 0], + [0.21, 1.71], + [0.88, 0.9], + [1, 1] + ]; + + shouldApproximatelyEqual(getTimeGivenProgression(...equation, 1.02), [0.35, 0.87]); }) }) }); -const shouldApproximatelyEqual = (givenValue: number, expectedValue: number): boolean => { - expect(Math.abs(expectedValue - givenValue)).toBeLessThanOrEqual(0.01); +const shouldApproximatelyEqual = (givenValues: number[], expectedValues: number[]): void => { + givenValues.forEach((givenValue, i) => { + expect(Math.abs(expectedValues[i] - givenValue)).toBeLessThanOrEqual(0.01); + }); } \ No newline at end of file