UNPKG

10.4 kBPlain TextView Raw
1'use strict';
2import type { EasingFunction } from '../../Easing';
3import { Easing } from '../../Easing';
4import { withDelay, withSequence, withTiming } from '../../animation';
5import type {
6 AnimationFunction,
7 EntryExitAnimationFunction,
8 IEntryExitAnimationBuilder,
9 KeyframeProps,
10 StylePropsWithArrayTransform,
11} from './commonTypes';
12import type { StyleProps } from '../../commonTypes';
13import type { TransformArrayItem } from '../../helperTypes';
14import { ReduceMotion } from '../../commonTypes';
15import {
16 assertEasingIsWorklet,
17 getReduceMotionFromConfig,
18} from '../../animation/util';
19
20interface KeyframePoint {
21 duration: number;
22 value: number | string;
23 easing?: EasingFunction;
24}
25interface ParsedKeyframesDefinition {
26 initialValues: StyleProps;
27 keyframes: Record<string, KeyframePoint[]>;
28}
29class InnerKeyframe implements IEntryExitAnimationBuilder {
30 durationV?: number;
31 delayV?: number;
32 reduceMotionV: ReduceMotion = ReduceMotion.System;
33 callbackV?: (finished: boolean) => void;
34 definitions: Record<string, KeyframeProps>;
35
36 /*
37 Keyframe definition should be passed in the constructor as the map
38 which keys are between range 0 - 100 (%) and correspond to the point in the animation progress.
39 */
40 constructor(definitions: Record<string, KeyframeProps>) {
41 this.definitions = definitions;
42 }
43
44 private parseDefinitions(): ParsedKeyframesDefinition {
45 /*
46 Each style property contain an array with all their key points:
47 value, duration of transition to that value, and optional easing function (defaults to Linear)
48 */
49 const parsedKeyframes: Record<string, KeyframePoint[]> = {};
50 /*
51 Parsing keyframes 'from' and 'to'.
52 */
53 if (this.definitions.from) {
54 if (this.definitions['0']) {
55 throw new Error(
56 "[Reanimated] You cannot provide both keyframe 0 and 'from' as they both specified initial values."
57 );
58 }
59 this.definitions['0'] = this.definitions.from;
60 delete this.definitions.from;
61 }
62 if (this.definitions.to) {
63 if (this.definitions['100']) {
64 throw new Error(
65 "[Reanimated] You cannot provide both keyframe 100 and 'to' as they both specified values at the end of the animation."
66 );
67 }
68 this.definitions['100'] = this.definitions.to;
69 delete this.definitions.to;
70 }
71 /*
72 One of the assumptions is that keyframe 0 is required to properly set initial values.
73 Every other keyframe should contain properties from the set provided as initial values.
74 */
75 if (!this.definitions['0']) {
76 throw new Error(
77 "[Reanimated] Please provide 0 or 'from' keyframe with initial state of your object."
78 );
79 }
80 const initialValues: StyleProps = this.definitions['0'] as StyleProps;
81 /*
82 Initialize parsedKeyframes for properties provided in initial keyframe
83 */
84 Object.keys(initialValues).forEach((styleProp: string) => {
85 if (styleProp === 'transform') {
86 if (!Array.isArray(initialValues.transform)) {
87 return;
88 }
89 initialValues.transform.forEach((transformStyle, index) => {
90 Object.keys(transformStyle).forEach((transformProp: string) => {
91 parsedKeyframes[makeKeyframeKey(index, transformProp)] = [];
92 });
93 });
94 } else {
95 parsedKeyframes[styleProp] = [];
96 }
97 });
98
99 const duration: number = this.durationV ? this.durationV : 500;
100 const animationKeyPoints: Array<string> = Array.from(
101 Object.keys(this.definitions)
102 );
103
104 const getAnimationDuration = (
105 key: string,
106 currentKeyPoint: number
107 ): number => {
108 const maxDuration = (currentKeyPoint / 100) * duration;
109 const currentDuration = parsedKeyframes[key].reduce(
110 (acc: number, value: KeyframePoint) => acc + value.duration,
111 0
112 );
113 return maxDuration - currentDuration;
114 };
115
116 /*
117 Other keyframes can't contain properties that were not specified in initial keyframe.
118 */
119 const addKeyPoint = ({
120 key,
121 value,
122 currentKeyPoint,
123 easing,
124 }: {
125 key: string;
126 value: string | number;
127 currentKeyPoint: number;
128 easing?: EasingFunction;
129 }): void => {
130 if (!(key in parsedKeyframes)) {
131 throw new Error(
132 "[Reanimated] Keyframe can contain only that set of properties that were provide with initial values (keyframe 0 or 'from')"
133 );
134 }
135
136 if (__DEV__ && easing) {
137 assertEasingIsWorklet(easing);
138 }
139
140 parsedKeyframes[key].push({
141 duration: getAnimationDuration(key, currentKeyPoint),
142 value,
143 easing,
144 });
145 };
146 animationKeyPoints
147 .filter((value: string) => parseInt(value) !== 0)
148 .sort((a: string, b: string) => parseInt(a) - parseInt(b))
149 .forEach((keyPoint: string) => {
150 if (parseInt(keyPoint) < 0 || parseInt(keyPoint) > 100) {
151 throw new Error(
152 '[Reanimated] Keyframe should be in between range 0 - 100.'
153 );
154 }
155 const keyframe: KeyframeProps = this.definitions[keyPoint];
156 const easing = keyframe.easing;
157 delete keyframe.easing;
158 const addKeyPointWith = (key: string, value: string | number) =>
159 addKeyPoint({
160 key,
161 value,
162 currentKeyPoint: parseInt(keyPoint),
163 easing,
164 });
165 Object.keys(keyframe).forEach((key: string) => {
166 if (key === 'transform') {
167 if (!Array.isArray(keyframe.transform)) {
168 return;
169 }
170 keyframe.transform.forEach((transformStyle, index) => {
171 Object.keys(transformStyle).forEach((transformProp: string) => {
172 addKeyPointWith(
173 makeKeyframeKey(index, transformProp),
174 transformStyle[
175 transformProp as keyof typeof transformStyle
176 ] as number | string // Here we assume that user has passed props of proper type.
177 // I don't think it's worthwhile to check if he passed i.e. `Animated.Node`.
178 );
179 });
180 });
181 } else {
182 addKeyPointWith(key, keyframe[key]);
183 }
184 });
185 });
186 return { initialValues, keyframes: parsedKeyframes };
187 }
188
189 duration(durationMs: number): InnerKeyframe {
190 this.durationV = durationMs;
191 return this;
192 }
193
194 delay(delayMs: number): InnerKeyframe {
195 this.delayV = delayMs;
196 return this;
197 }
198
199 withCallback(callback: (finsihed: boolean) => void): InnerKeyframe {
200 this.callbackV = callback;
201 return this;
202 }
203
204 reduceMotion(reduceMotionV: ReduceMotion): this {
205 this.reduceMotionV = reduceMotionV;
206 return this;
207 }
208
209 private getDelayFunction(): AnimationFunction {
210 const delay = this.delayV;
211 const reduceMotion = this.reduceMotionV;
212 return delay
213 ? // eslint-disable-next-line @typescript-eslint/no-shadow
214 (delay, animation) => {
215 'worklet';
216 return withDelay(delay, animation, reduceMotion);
217 }
218 : (_, animation) => {
219 'worklet';
220 animation.reduceMotion = getReduceMotionFromConfig(reduceMotion);
221 return animation;
222 };
223 }
224
225 build = (): EntryExitAnimationFunction => {
226 const delay = this.delayV;
227 const delayFunction = this.getDelayFunction();
228 const { keyframes, initialValues } = this.parseDefinitions();
229 const callback = this.callbackV;
230
231 return () => {
232 'worklet';
233 const animations: StylePropsWithArrayTransform = {};
234
235 /*
236 For each style property, an animations sequence is created that corresponds with its key points.
237 Transform style properties require special handling because of their nested structure.
238 */
239 const addAnimation = (key: string) => {
240 const keyframePoints = keyframes[key];
241 // in case if property was only passed as initial value
242 if (keyframePoints.length === 0) {
243 return;
244 }
245 const animation = delayFunction(
246 delay,
247 keyframePoints.length === 1
248 ? withTiming(keyframePoints[0].value, {
249 duration: keyframePoints[0].duration,
250 easing: keyframePoints[0].easing
251 ? keyframePoints[0].easing
252 : Easing.linear,
253 })
254 : withSequence(
255 ...keyframePoints.map((keyframePoint: KeyframePoint) =>
256 withTiming(keyframePoint.value, {
257 duration: keyframePoint.duration,
258 easing: keyframePoint.easing
259 ? keyframePoint.easing
260 : Easing.linear,
261 })
262 )
263 )
264 );
265 if (key.includes('transform')) {
266 if (!('transform' in animations)) {
267 animations.transform = [];
268 }
269 animations.transform!.push(<TransformArrayItem>{
270 [key.split(':')[1]]: animation,
271 });
272 } else {
273 animations[key] = animation;
274 }
275 };
276 Object.keys(initialValues).forEach((key: string) => {
277 if (key.includes('transform')) {
278 initialValues[key].forEach(
279 (transformProp: Record<string, number | string>, index: number) => {
280 Object.keys(transformProp).forEach((transformPropKey: string) => {
281 addAnimation(makeKeyframeKey(index, transformPropKey));
282 });
283 }
284 );
285 } else {
286 addAnimation(key);
287 }
288 });
289 return {
290 animations,
291 initialValues,
292 callback,
293 };
294 };
295 };
296}
297
298function makeKeyframeKey(index: number, transformProp: string) {
299 'worklet';
300 return `${index}_transform:${transformProp}`;
301}
302
303// TODO TYPESCRIPT This is a temporary type to get rid of .d.ts file.
304export declare class ReanimatedKeyframe {
305 constructor(definitions: Record<string, KeyframeProps>);
306 duration(durationMs: number): ReanimatedKeyframe;
307 delay(delayMs: number): ReanimatedKeyframe;
308 reduceMotion(reduceMotionV: ReduceMotion): ReanimatedKeyframe;
309 withCallback(callback: (finished: boolean) => void): ReanimatedKeyframe;
310}
311
312// TODO TYPESCRIPT This temporary cast is to get rid of .d.ts file.
313export const Keyframe = InnerKeyframe as unknown as typeof ReanimatedKeyframe;