mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 03:32:21 +08:00
feat(animation): cubic-bezier easing conversion utility (experimental) (#19788)
resolves #19789
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Animation, createAnimation } from '@ionic/core';
|
import { Animation, createAnimation, getTimeGivenProgression } from '@ionic/core';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@ -11,4 +11,22 @@ export class AnimationController {
|
|||||||
create(animationId?: string): Animation {
|
create(animationId?: string): Animation {
|
||||||
return createAnimation(animationId);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { Build, Component, ComponentInterface, Element, Event, EventEmitter, Hos
|
|||||||
import { config } from '../../global/config';
|
import { config } from '../../global/config';
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
import { getIonMode } from '../../global/ionic-global';
|
||||||
import { Animation, Gesture, GestureDetail, MenuChangeEventDetail, MenuI, Side } from '../../interface';
|
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 { GESTURE_CONTROLLER } from '../../utils/gesture';
|
||||||
import { assert, clamp, isEndSide as isEnd } from '../../utils/helpers';
|
import { assert, clamp, isEndSide as isEnd } from '../../utils/helpers';
|
||||||
import { menuController } from '../../utils/menu-controller';
|
import { menuController } from '../../utils/menu-controller';
|
||||||
@ -449,7 +449,7 @@ AFTER:
|
|||||||
* to the new easing curve, as `stepValue` is going to be given
|
* to the new easing curve, as `stepValue` is going to be given
|
||||||
* in terms of a linear curve.
|
* 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
|
this.animation
|
||||||
.easing('cubic-bezier(0.4, 0.0, 0.6, 1)')
|
.easing('cubic-bezier(0.4, 0.0, 0.6, 1)')
|
||||||
|
@ -3,7 +3,7 @@ import { Build, Component, Element, Event, EventEmitter, Method, Prop, Watch, h
|
|||||||
import { config } from '../../global/config';
|
import { config } from '../../global/config';
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
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 { 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 { assert } from '../../utils/helpers';
|
||||||
import { TransitionOptions, lifecycle, setPageHidden, transition } from '../../utils/transition';
|
import { TransitionOptions, lifecycle, setPageHidden, transition } from '../../utils/transition';
|
||||||
|
|
||||||
@ -981,9 +981,9 @@ export class Nav implements NavOutlet {
|
|||||||
*/
|
*/
|
||||||
if (!shouldComplete) {
|
if (!shouldComplete) {
|
||||||
this.sbAni.easing('cubic-bezier(1, 0, 0.68, 0.28)');
|
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 {
|
} 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);
|
(this.sbAni as Animation).progressEnd(shouldComplete ? 1 : 0, newStepValue, dur);
|
||||||
|
@ -3,7 +3,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Pr
|
|||||||
import { config } from '../../global/config';
|
import { config } from '../../global/config';
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
import { getIonMode } from '../../global/ionic-global';
|
||||||
import { Animation, AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Gesture, NavOutlet, RouteID, RouteWrite, RouterDirection, RouterOutletOptions, SwipeGestureHandler } from '../../interface';
|
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 { attachComponent, detachComponent } from '../../utils/framework-delegate';
|
||||||
import { transition } from '../../utils/transition';
|
import { transition } from '../../utils/transition';
|
||||||
|
|
||||||
@ -91,9 +91,9 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
|
|||||||
*/
|
*/
|
||||||
if (!shouldComplete) {
|
if (!shouldComplete) {
|
||||||
this.ani.easing('cubic-bezier(1, 0, 0.68, 0.28)');
|
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 {
|
} 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);
|
(this.ani as Animation).progressEnd(shouldComplete ? 1 : 0, newStepValue, dur);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'ionicons';
|
import 'ionicons';
|
||||||
|
|
||||||
export { createAnimation } from './utils/animation/animation';
|
export { createAnimation } from './utils/animation/animation';
|
||||||
|
export { getTimeGivenProgression } from './utils/animation/cubic-bezier';
|
||||||
export { createGesture } from './utils/gesture';
|
export { createGesture } from './utils/gesture';
|
||||||
export { isPlatform, Platforms, getPlatforms } from './utils/platform';
|
export { isPlatform, Platforms, getPlatforms } from './utils/platform';
|
||||||
|
|
||||||
|
@ -5,11 +5,8 @@
|
|||||||
* TODO: Reduce rounding error
|
* 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
|
* Given a cubic-bezier curve, get the x value (time) given
|
||||||
* the y value (progression).
|
* the y value (progression).
|
||||||
* Ex: cubic-bezier(0.32, 0.72, 0, 1);
|
* Ex: cubic-bezier(0.32, 0.72, 0, 1);
|
||||||
@ -19,11 +16,12 @@ export class Point {
|
|||||||
* P3: (1, 1)
|
* P3: (1, 1)
|
||||||
*
|
*
|
||||||
* If you give a cubic bezier curve that never reaches the
|
* 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) => {
|
export const getTimeGivenProgression = (p0: number[], p1: number[], p2: number[], p3: number[], progression: number): number[] => {
|
||||||
const tValues = solveCubicBezier(p0.y, p1.y, p2.y, p3.y, progression);
|
return solveCubicBezier(p0[1], p1[1], p2[1], p3[1], progression).map(tValue => {
|
||||||
return solveCubicParametricEquation(p0.x, p1.x, p2.x, p3.x, tValues[0]); // TODO: Add better strategy for dealing with multiple solutions
|
return solveCubicParametricEquation(p0[0], p1[0], p2[0], p3[0], tValue);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createAnimation } from '../animation';
|
import { createAnimation } from '../animation';
|
||||||
import { getTimeGivenProgression, Point } from '../cubic-bezier';
|
import { getTimeGivenProgression } from '../cubic-bezier';
|
||||||
import { Animation } from '../animation-interface';
|
import { Animation } from '../animation-interface';
|
||||||
|
|
||||||
describe('Animation Class', () => {
|
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)', () => {
|
describe('should properly get a time value (x value) given a progression value (y value)', () => {
|
||||||
it('cubic-bezier(0.32, 0.72, 0, 1)', () => {
|
it('cubic-bezier(0.32, 0.72, 0, 1)', () => {
|
||||||
const equation = [
|
const equation = [
|
||||||
new Point(0, 0),
|
[0, 0],
|
||||||
new Point(0.32, 0.72),
|
[0.32, 0.72],
|
||||||
new Point(0, 1),
|
[0, 1],
|
||||||
new Point(1, 1)
|
[1, 1]
|
||||||
];
|
];
|
||||||
|
|
||||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.5), 0.16);
|
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.5), [0.16]);
|
||||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.97), 0.56);
|
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.97), [0.56]);
|
||||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.33), 0.11);
|
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.33), [0.11]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cubic-bezier(1, 0, 0.68, 0.28)', () => {
|
it('cubic-bezier(1, 0, 0.68, 0.28)', () => {
|
||||||
const equation = [
|
const equation = [
|
||||||
new Point(0, 0),
|
[0, 0],
|
||||||
new Point(1, 0),
|
[1, 0],
|
||||||
new Point(0.68, 0.28),
|
[0.68, 0.28],
|
||||||
new Point(1, 1)
|
[1, 1]
|
||||||
];
|
];
|
||||||
|
|
||||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.08), 0.60);
|
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.08), [0.60]);
|
||||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.50), 0.84);
|
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.50), [0.84]);
|
||||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.94), 0.98);
|
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.94), [0.98]);
|
||||||
})
|
})
|
||||||
|
|
||||||
it('cubic-bezier(0.4, 0, 0.6, 1)', () => {
|
it('cubic-bezier(0.4, 0, 0.6, 1)', () => {
|
||||||
const equation = [
|
const equation = [
|
||||||
new Point(0, 0),
|
[0, 0],
|
||||||
new Point(0.4, 0),
|
[0.4, 0],
|
||||||
new Point(0.6, 1),
|
[0.6, 1],
|
||||||
new Point(1, 1)
|
[1, 1]
|
||||||
];
|
];
|
||||||
|
|
||||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.39), 0.43);
|
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.39), [0.43]);
|
||||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.03), 0.11);
|
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.03), [0.11]);
|
||||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.89), 0.78);
|
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.89), [0.78]);
|
||||||
})
|
})
|
||||||
|
|
||||||
it('cubic-bezier(0, 0, 0.2, 1)', () => {
|
it('cubic-bezier(0, 0, 0.2, 1)', () => {
|
||||||
const equation = [
|
const equation = [
|
||||||
new Point(0, 0),
|
[0, 0],
|
||||||
new Point(0, 0),
|
[0, 0],
|
||||||
new Point(0.2, 1),
|
[0.2, 1],
|
||||||
new Point(1, 1)
|
[1, 1]
|
||||||
];
|
];
|
||||||
|
|
||||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.95), 0.71);
|
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.95), [0.71]);
|
||||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.1), 0.03);
|
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.1), [0.03]);
|
||||||
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.70), 0.35);
|
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.70), [0.35]);
|
||||||
})
|
})
|
||||||
|
|
||||||
it('cubic-bezier(0.32, 0.72, 0, 1) (with out of bounds progression)', () => {
|
it('cubic-bezier(0.32, 0.72, 0, 1) (with out of bounds progression)', () => {
|
||||||
const equation = [
|
const equation = [
|
||||||
new Point(0, 0),
|
[0, 0],
|
||||||
new Point(0.05, 0.2),
|
[0.05, 0.2],
|
||||||
new Point(.14, 1.72),
|
[.14, 1.72],
|
||||||
new Point(1, 1)
|
[1, 1]
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(getTimeGivenProgression(...equation, 1.32)).toBeNaN();
|
expect(getTimeGivenProgression(...equation, 1.32)[0]).toBeUndefined();
|
||||||
expect(getTimeGivenProgression(...equation, -0.32)).toBeNaN();
|
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 => {
|
const shouldApproximatelyEqual = (givenValues: number[], expectedValues: number[]): void => {
|
||||||
expect(Math.abs(expectedValue - givenValue)).toBeLessThanOrEqual(0.01);
|
givenValues.forEach((givenValue, i) => {
|
||||||
|
expect(Math.abs(expectedValues[i] - givenValue)).toBeLessThanOrEqual(0.01);
|
||||||
|
});
|
||||||
}
|
}
|
Reference in New Issue
Block a user