Skip to content

Commit

Permalink
TypeScript for animations (software-mansion#2182)
Browse files Browse the repository at this point in the history
## Description

Refactor and transfter of animations module to TypeScript.
  • Loading branch information
piaskowyk authored Aug 6, 2021
1 parent 498aed5 commit 726c774
Show file tree
Hide file tree
Showing 28 changed files with 1,348 additions and 943 deletions.
8 changes: 4 additions & 4 deletions src/reanimated2/Colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,9 +634,9 @@ export function processColor(color: unknown): number | null | undefined {
return normalizedColor;
}

export function convertToHSVA(
color: unknown
): [number, number, number, number] {
export type ParsedColorArray = [number, number, number, number];

export function convertToHSVA(color: unknown): ParsedColorArray {
'worklet';
const processedColor = processColorInitially(color)!; // argb;
const a = (processedColor >>> 24) / 255;
Expand All @@ -647,7 +647,7 @@ export function convertToHSVA(
return [h, s, v, a];
}

export function toRGBA(HSVA: [number, number, number, number]): string {
export function toRGBA(HSVA: ParsedColorArray): string {
'worklet';
const { r, g, b } = HSVtoRGB(HSVA[0], HSVA[1], HSVA[2]);
return `rgba(${r}, ${g}, ${b}, ${HSVA[3]})`;
Expand Down
2 changes: 2 additions & 0 deletions src/reanimated2/Easing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import { Bezier } from './Bezier';
*/

export type EasingFn = (t: number) => number;

export type EasingFactoryFn = { factory: () => EasingFn };
/**
* A linear function, `f(t) = t`. Position correlates to elapsed time one to
* one.
Expand Down
2 changes: 1 addition & 1 deletion src/reanimated2/Hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
getTimestamp,
} from './core';
import updateProps, { updatePropsJestWrapper, colorProps } from './UpdateProps';
import { initialUpdaterRun, cancelAnimation } from './animations';
import { initialUpdaterRun, cancelAnimation } from './animation';
import { getTag } from './NativeMethods';
import NativeReanimated from './NativeReanimated';
import { makeViewDescriptorsSet, makeViewsRefSet } from './ViewDescriptorsSet';
Expand Down
2 changes: 1 addition & 1 deletion src/reanimated2/NativeReanimated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,4 @@ if (nativeShouldBeMock()) {
exportedModule = NativeReanimated;
}

export default exportedModule;
export default exportedModule as any; // TODO: just temporary type
51 changes: 51 additions & 0 deletions src/reanimated2/animation/commonTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export type PrimitiveValue = number | string;

export interface AnimationObject {
[key: string]: any;
callback: AnimationCallback;
current?: PrimitiveValue;
toValue?: AnimationObject['current'];
startValue?: AnimationObject['current'];
finished?: boolean;

__prefix?: string;
__suffix?: string;
onFrame: (animation: any, timestamp: Timestamp) => boolean;
onStart: (
nextAnimation: any,
current: any,
timestamp: Timestamp,
previousAnimation: any
) => void;
}

export interface Animation<T extends AnimationObject> extends AnimationObject {
onFrame: (animation: T, timestamp: Timestamp) => boolean;
onStart: (
nextAnimation: T,
current: T extends NumericAnimation ? number : PrimitiveValue,
timestamp: Timestamp,
previousAnimation: T
) => void;
}

export interface NumericAnimation {
current?: number;
}
export interface HigherOrderAnimation {
isHigherOrder?: boolean;
}

export type AnimationCallback = (
finished?: boolean,
current?: PrimitiveValue
) => void;

export type NextAnimation<T extends AnimationObject> = T | (() => T);

export type SharedValue = {
// TODO: just temporary mock
value: unknown;
};

export type Timestamp = number;
135 changes: 135 additions & 0 deletions src/reanimated2/animation/decay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { defineAnimation } from './util';
import {
Animation,
AnimationCallback,
AnimationObject,
PrimitiveValue,
Timestamp,
} from './commonTypes';
import { Platform } from 'react-native';

interface DecayConfig {
deceleration?: number;
velocityFactor?: number;
clamp?: number[];
velocity?: number;
}

export interface DecayAnimation extends Animation<DecayAnimation> {
lastTimestamp: Timestamp;
startTimestamp: Timestamp;
initialVelocity: number;
velocity: number;
current: PrimitiveValue;
}

export interface InnerDecayAnimation
extends Omit<DecayAnimation, 'current'>,
AnimationObject {
current: number;
}

export function withDecay(
userConfig: DecayConfig,
callback?: AnimationCallback
): Animation<DecayAnimation> {
'worklet';

return defineAnimation<DecayAnimation>(0, () => {
'worklet';
const config: Required<DecayConfig> = {
deceleration: 0.998,
velocityFactor: Platform.OS !== 'web' ? 1 : 1000,
clamp: [],
velocity: 0,
};
if (userConfig) {
Object.keys(userConfig).forEach(
(key) =>
((config as any)[key] = userConfig[key as keyof typeof userConfig])
);
}

const VELOCITY_EPS = Platform.OS !== 'web' ? 1 : 1 / 20;
const SLOPE_FACTOR = 0.1;

function decay(animation: InnerDecayAnimation, now: number): boolean {
const {
lastTimestamp,
startTimestamp,
initialVelocity,
current,
velocity,
} = animation;

const deltaTime = Math.min(now - lastTimestamp, 64);
const v =
velocity *
Math.exp(
-(1 - config.deceleration) * (now - startTimestamp) * SLOPE_FACTOR
);
animation.current =
current + (v * config.velocityFactor * deltaTime) / 1000; // /1000 because time is in ms not in s
animation.velocity = v;
animation.lastTimestamp = now;

if (config.clamp) {
if (initialVelocity < 0 && animation.current <= config.clamp[0]) {
animation.current = config.clamp[0];
return true;
} else if (
initialVelocity > 0 &&
animation.current >= config.clamp[1]
) {
animation.current = config.clamp[1];
return true;
}
}

return Math.abs(v) < VELOCITY_EPS;
}

function validateConfig(): void {
if (config.clamp) {
if (!Array.isArray(config.clamp)) {
throw Error(
`config.clamp must be an array but is ${typeof config.clamp}`
);
}
if (config.clamp.length !== 2) {
throw Error(
`clamp array must contain 2 items but is given ${config.clamp.length}`
);
}
}
if (config.velocityFactor <= 0) {
throw Error(
`config.velocityFactor must be greather then 0 but is ${config.velocityFactor}`
);
}
}

function onStart(
animation: DecayAnimation,
value: number,
now: Timestamp
): void {
animation.current = value;
animation.lastTimestamp = now;
animation.startTimestamp = now;
animation.initialVelocity = config.velocity;
validateConfig();
}

return {
onFrame: decay,
onStart,
callback,
velocity: config.velocity ?? 0,
initialVelocity: 0,
current: 0,
lastTimestamp: 0,
startTimestamp: 0,
} as DecayAnimation;
});
}
97 changes: 97 additions & 0 deletions src/reanimated2/animation/delay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { defineAnimation } from './util';
import {
Animation,
NextAnimation,
Timestamp,
HigherOrderAnimation,
PrimitiveValue,
} from './commonTypes';

export interface DelayAnimation
extends Animation<DelayAnimation>,
HigherOrderAnimation {
startTime: Timestamp;
started: boolean;
previousAnimation: DelayAnimation | null;
current: PrimitiveValue;
}

export function withDelay(
delayMs: number,
_nextAnimation: NextAnimation<DelayAnimation>
): Animation<DelayAnimation> {
'worklet';
return defineAnimation<DelayAnimation>(_nextAnimation, () => {
'worklet';
const nextAnimation =
typeof _nextAnimation === 'function' ? _nextAnimation() : _nextAnimation;

function delay(animation: DelayAnimation, now: Timestamp): boolean {
const { startTime, started, previousAnimation } = animation;

if (now - startTime > delayMs) {
if (!started) {
nextAnimation.onStart(
nextAnimation,
animation.current,
now,
previousAnimation as DelayAnimation
);
animation.previousAnimation = null;
animation.started = true;
}
const finished = nextAnimation.onFrame(nextAnimation, now);
animation.current = nextAnimation.current;
return finished;
} else if (previousAnimation) {
const finished = previousAnimation.onFrame(previousAnimation, now);
animation.current = previousAnimation.current;
if (finished) {
animation.previousAnimation = null;
}
}
return false;
}

function onStart(
animation: DelayAnimation,
value: PrimitiveValue,
now: Timestamp,
previousAnimation: DelayAnimation
): void {
animation.startTime = now;
animation.started = false;
animation.current = value;
animation.previousAnimation = previousAnimation;
}

const callback = (finished?: boolean): void => {
if (nextAnimation.callback) {
nextAnimation.callback(finished);
}
};

return {
isHigherOrder: true,
onFrame: delay,
onStart,
current: nextAnimation.current,
callback,
previousAnimation: null,
startTime: 0,
started: false,
};
});
}

/**
* @deprecated Kept for backward compatibility. Will be removed soon.
*/
export function delay(
delayMs: number,
_nextAnimation: NextAnimation<DelayAnimation>
): Animation<DelayAnimation> {
'worklet';
console.warn('Method `delay` is deprecated. Please use `withDelay` instead');
return withDelay(delayMs, _nextAnimation);
}
17 changes: 17 additions & 0 deletions src/reanimated2/animation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export {
AnimationObject,
Animation,
HigherOrderAnimation,
AnimationCallback,
NextAnimation,
SharedValue,
Timestamp,
} from './commonTypes';
export { cancelAnimation, defineAnimation } from './util';
export { withTiming } from './timing';
export { withSpring } from './spring';
export { withDecay } from './decay';
export { withDelay } from './delay';
export { withRepeat } from './repeat';
export { withSequence } from './sequence';
export { withStyleAnimation } from './styleAnimation';
Loading

0 comments on commit 726c774

Please sign in to comment.