Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(animation): cubic-bezier easing conversion utility (experimental) #19788

Merged
merged 6 commits into from Oct 31, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 19 additions & 1 deletion 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',
Expand All @@ -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);
}
}
4 changes: 2 additions & 2 deletions core/src/components/menu/menu.tsx
Expand Up @@ -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';
Expand Down Expand Up @@ -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)')
Expand Down
6 changes: 3 additions & 3 deletions core/src/components/nav/nav.tsx
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions core/src/components/router-outlet/route-outlet.tsx
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions 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';

Expand Down
14 changes: 6 additions & 8 deletions core/src/utils/animation/cubic-bezier.ts
Expand Up @@ -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);
Expand All @@ -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);
});
};

/**
Expand Down
87 changes: 50 additions & 37 deletions 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', () => {
Expand Down Expand Up @@ -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);
});
}