diff --git a/src/reanimated2/layoutReanimation/web/Easing.web.ts b/src/reanimated2/layoutReanimation/web/Easing.web.ts new file mode 100644 index 000000000000..ab6d931663fa --- /dev/null +++ b/src/reanimated2/layoutReanimation/web/Easing.web.ts @@ -0,0 +1,15 @@ +'use strict'; + +// Those are the easings that can be implemented using Bezier curves. +// Others should be done as CSS animations +export const WebEasings = { + linear: [0, 0, 1, 1], + ease: [0.42, 0, 1, 1], + quad: [0.11, 0, 0.5, 0], + cubic: [0.32, 0, 0.67, 0], + sin: [0.12, 0, 0.39, 0], + circle: [0.55, 0, 1, 0.45], + exp: [0.7, 0, 0.84, 0], +}; + +export type WebEasingsNames = keyof typeof WebEasings; diff --git a/src/reanimated2/layoutReanimation/web/animationParser.ts b/src/reanimated2/layoutReanimation/web/animationParser.ts index 954b590418de..243c25380bd1 100644 --- a/src/reanimated2/layoutReanimation/web/animationParser.ts +++ b/src/reanimated2/layoutReanimation/web/animationParser.ts @@ -1,6 +1,8 @@ 'use strict'; import type { TransformsStyle } from 'react-native'; +import { WebEasings } from './Easing.web'; +import type { WebEasingsNames } from './Easing.web'; export interface ReanimatedWebTransformProperties { translateX?: string; @@ -16,7 +18,7 @@ export interface ReanimatedWebTransformProperties { skewX?: string; } -interface AnimationStyle { +export interface AnimationStyle { opacity?: number; transform?: ReanimatedWebTransformProperties[]; } @@ -41,9 +43,24 @@ export function convertAnimationObjectToKeyframes( let keyframe = `@keyframes ${animationObject.name} { `; for (const [timestamp, style] of Object.entries(animationObject.style)) { - keyframe += `${timestamp}% { `; + const step = + timestamp === 'from' ? 0 : timestamp === 'to' ? 100 : timestamp; + + keyframe += `${step}% { `; for (const [property, values] of Object.entries(style)) { + if (property === 'easing') { + const easingName = ( + values.name in WebEasings ? values.name : 'linear' + ) as WebEasingsNames; + + keyframe += `animation-timing-function: cubic-bezier(${WebEasings[ + easingName + ].toString()});`; + + continue; + } + if (property !== 'transform') { keyframe += `${property}: ${values}; `; continue; diff --git a/src/reanimated2/layoutReanimation/web/animationsManager.ts b/src/reanimated2/layoutReanimation/web/animationsManager.ts index 0221d6904d1e..13c88e6463ae 100644 --- a/src/reanimated2/layoutReanimation/web/animationsManager.ts +++ b/src/reanimated2/layoutReanimation/web/animationsManager.ts @@ -1,6 +1,11 @@ 'use strict'; -import type { AnimationConfig, AnimationNames, CustomConfig } from './config'; +import type { + AnimationConfig, + AnimationNames, + CustomConfig, + KeyframeDefinitions, +} from './config'; import { Animations } from './config'; import type { AnimatedComponentProps, @@ -8,7 +13,10 @@ import type { } from '../../../createAnimatedComponent/commonTypes'; import { LayoutAnimationType } from '../animationBuilder/commonTypes'; import type { StyleProps } from '../../commonTypes'; -import { createAnimationWithExistingTransform } from './createAnimation'; +import { + createAnimationWithExistingTransform, + createCustomKeyFrameAnimation, +} from './createAnimation'; import { extractTransformFromStyle, getProcessedConfig, @@ -19,6 +27,7 @@ import { import { areDOMRectsEqual } from './domUtils'; import type { TransformsStyle } from 'react-native'; import type { TransitionData } from './animationParser'; +import { Keyframe } from '../animationBuilder'; import { makeElementVisible } from './componentStyle'; function chooseConfig>( @@ -39,11 +48,11 @@ function chooseConfig>( function checkUndefinedAnimationFail( initialAnimationName: string, - isLayoutTransition: boolean + needsCustomization: boolean ) { // This prevents crashes if we try to set animations that are not defined. - // We don't care about layout transitions since they're created dynamically - if (initialAnimationName in Animations || isLayoutTransition) { + // We don't care about layout transitions or custom keyframes since they're created dynamically + if (initialAnimationName in Animations || needsCustomization) { return false; } @@ -96,6 +105,7 @@ function tryGetAnimationConfigWithTransform< typeof config.constructor; const isLayoutTransition = animationType === LayoutAnimationType.LAYOUT; + const isCustomKeyframe = config instanceof Keyframe; const initialAnimationName = typeof config === 'function' ? config.presetName @@ -103,7 +113,7 @@ function tryGetAnimationConfigWithTransform< const shouldFail = checkUndefinedAnimationFail( initialAnimationName, - isLayoutTransition + isLayoutTransition || isCustomKeyframe ); if (shouldFail) { @@ -112,15 +122,24 @@ function tryGetAnimationConfigWithTransform< const transform = extractTransformFromStyle(props.style as StyleProps); - const animationName = - transform && animationType !== LayoutAnimationType.EXITING + let animationName = initialAnimationName; + + if (isCustomKeyframe) { + animationName = createCustomKeyFrameAnimation( + (config as CustomConfig).definitions as KeyframeDefinitions, + transform + ); + } else { + animationName = transform ? createAnimationWithExistingTransform(initialAnimationName, transform) : initialAnimationName; + } const animationConfig = getProcessedConfig( animationName, animationType, config as CustomConfig, + isLayoutTransition || isCustomKeyframe, initialAnimationName as AnimationNames ); diff --git a/src/reanimated2/layoutReanimation/web/componentUtils.ts b/src/reanimated2/layoutReanimation/web/componentUtils.ts index 293a2344e2ee..b601c7cc4ccb 100644 --- a/src/reanimated2/layoutReanimation/web/componentUtils.ts +++ b/src/reanimated2/layoutReanimation/web/componentUtils.ts @@ -1,14 +1,15 @@ 'use strict'; import type { TransformsStyle } from 'react-native'; -import { Animations, TransitionType, WebEasings } from './config'; +import { Animations, TransitionType } from './config'; import type { AnimationCallback, AnimationConfig, AnimationNames, CustomConfig, - WebEasingsNames, } from './config'; +import { WebEasings } from './Easing.web'; +import type { WebEasingsNames } from './Easing.web'; import { convertTransformToString } from './animationParser'; import type { TransitionData } from './animationParser'; import { TransitionGenerator } from './createAnimation'; @@ -66,10 +67,10 @@ export function getReducedMotionFromConfig(config: CustomConfig) { function getDurationFromConfig( config: CustomConfig, - isLayoutTransition: boolean, + needsCustomization: boolean, animationName: AnimationNames ): number { - const defaultDuration = isLayoutTransition + const defaultDuration = needsCustomization ? 0.3 : Animations[animationName].duration; @@ -111,6 +112,7 @@ export function getProcessedConfig( animationName: string, animationType: LayoutAnimationType, config: CustomConfig, + needsCustomization: boolean, initialAnimationName: AnimationNames ): AnimationConfig { return { @@ -118,7 +120,7 @@ export function getProcessedConfig( animationType, duration: getDurationFromConfig( config, - animationType === LayoutAnimationType.LAYOUT, + needsCustomization, initialAnimationName ), delay: getDelayFromConfig(config), diff --git a/src/reanimated2/layoutReanimation/web/config.ts b/src/reanimated2/layoutReanimation/web/config.ts index 60307910897c..8c82027f8d08 100644 --- a/src/reanimated2/layoutReanimation/web/config.ts +++ b/src/reanimated2/layoutReanimation/web/config.ts @@ -37,10 +37,12 @@ import { } from './animation/Stretch.web'; import { ZoomIn, ZoomInData, ZoomOut, ZoomOutData } from './animation/Zoom.web'; -import type { AnimationData } from './animationParser'; +import type { AnimationData, AnimationStyle } from './animationParser'; export type AnimationCallback = ((finished: boolean) => void) | null; +export type KeyframeDefinitions = Record; + export interface AnimationConfig { animationName: string; animationType: LayoutAnimationType; @@ -59,6 +61,7 @@ export interface CustomConfig { reduceMotionV?: ReduceMotion; callbackV?: AnimationCallback; reversed?: boolean; + definitions?: KeyframeDefinitions; } export enum TransitionType { @@ -111,18 +114,5 @@ export const Animations = { ...RollOut, }; -// Those are the easings that can be implemented using Bezier curves. -// Others should be done as CSS animations -export const WebEasings = { - linear: [0, 0, 1, 1], - ease: [0.42, 0, 1, 1], - quad: [0.11, 0, 0.5, 0], - cubic: [0.32, 0, 0.67, 0], - sin: [0.12, 0, 0.39, 0], - circle: [0.55, 0, 1, 0.45], - exp: [0.7, 0, 0.84, 0], -}; - export type AnimationNames = keyof typeof Animations; export type LayoutTransitionsNames = keyof typeof AnimationsData; -export type WebEasingsNames = keyof typeof WebEasings; diff --git a/src/reanimated2/layoutReanimation/web/createAnimation.ts b/src/reanimated2/layoutReanimation/web/createAnimation.ts index 2b53943455f9..9717a20f9fae 100644 --- a/src/reanimated2/layoutReanimation/web/createAnimation.ts +++ b/src/reanimated2/layoutReanimation/web/createAnimation.ts @@ -1,6 +1,7 @@ 'use strict'; import { Animations, AnimationsData, TransitionType } from './config'; +import type { KeyframeDefinitions } from './config'; import { convertAnimationObjectToKeyframes } from './animationParser'; import type { AnimationData, @@ -13,12 +14,12 @@ import { SequencedTransition } from './transition/Sequenced.web'; import { FadingTransition } from './transition/Fading.web'; import { insertWebAnimation } from './domUtils'; +type TransformType = NonNullable; + // Translate values are passed as numbers. However, if `translate` property receives number, it will not automatically // convert it to `px`. Therefore if we want to keep existing transform we have to add 'px' suffix to each of translate values // that are present inside transform. -function addPxToTranslate( - existingTransform: NonNullable -) { +function addPxToTranslate(existingTransform: TransformType) { type RNTransformProp = (typeof existingTransform)[number]; // @ts-ignore `existingTransform` cannot be string because in that case @@ -27,7 +28,7 @@ function addPxToTranslate( (transformProp: RNTransformProp) => { const newTransformProp: ReanimatedWebTransformProperties = {}; for (const [key, value] of Object.entries(transformProp)) { - if (key.includes('translate')) { + if (key.includes('translate') && typeof value === 'number') { // @ts-ignore After many trials we decided to ignore this error - it says that we cannot use 'key' to index this object. // Sadly it doesn't go away after using cast `key as keyof TransformProperties`. newTransformProp[key] = `${value}px`; @@ -71,13 +72,13 @@ function addExistingTransform( */ export function createAnimationWithExistingTransform( animationName: string, - existingTransform: NonNullable, - layoutTransition?: AnimationData + existingTransform: TransformType, + customData?: AnimationData ) { let newAnimationData; - if (layoutTransition) { - newAnimationData = layoutTransition; + if (customData) { + newAnimationData = customData; } else { if (!(animationName in Animations)) { return ''; @@ -100,6 +101,39 @@ export function createAnimationWithExistingTransform( return keyframeName; } +export function createCustomKeyFrameAnimation( + keyframeDefinitions: KeyframeDefinitions, + transform: TransformsStyle['transform'] +) { + for (const value of Object.values(keyframeDefinitions)) { + if (value.transform) { + value.transform = addPxToTranslate(value.transform as TransformType); + } + } + + const animationData: AnimationData = { + name: '', + style: keyframeDefinitions, + duration: -1, + }; + + if (transform) { + return createAnimationWithExistingTransform( + animationData.name, + transform, + animationData + ); + } + + animationData.name = generateNextCustomKeyframeName(); + + const parsedKeyframe = convertAnimationObjectToKeyframes(animationData); + + insertWebAnimation(animationData.name, parsedKeyframe); + + return animationData.name; +} + let customKeyframeCounter = 0; function generateNextCustomKeyframeName() {