From ddda813ea399705e11ec60508cb8b1a25fff6159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= Date: Tue, 18 Jun 2024 14:48:48 +0200 Subject: [PATCH 01/12] Bring back changes from old PR --- .../src/layoutReanimation/web/Easing.web.ts | 15 ++++++++ .../layoutReanimation/web/animationParser.ts | 22 ++++++++++-- .../web/animationsManager.ts | 33 ++++++++++++------ .../layoutReanimation/web/componentUtils.ts | 24 ++++++------- .../src/layoutReanimation/web/config.ts | 18 +++------- .../layoutReanimation/web/createAnimation.ts | 34 ++++++++++++++++--- 6 files changed, 101 insertions(+), 45 deletions(-) create mode 100644 packages/react-native-reanimated/src/layoutReanimation/web/Easing.web.ts diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/Easing.web.ts b/packages/react-native-reanimated/src/layoutReanimation/web/Easing.web.ts new file mode 100644 index 000000000000..ab6d931663fa --- /dev/null +++ b/packages/react-native-reanimated/src/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/packages/react-native-reanimated/src/layoutReanimation/web/animationParser.ts b/packages/react-native-reanimated/src/layoutReanimation/web/animationParser.ts index 42595b4fd07d..829725ca07e0 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/animationParser.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/animationParser.ts @@ -1,5 +1,8 @@ 'use strict'; +import { WebEasings } from './Easing.web'; +import type { WebEasingsNames } from './Easing.web'; + export interface ReanimatedWebTransformProperties { translateX?: string; translateY?: string; @@ -14,7 +17,7 @@ export interface ReanimatedWebTransformProperties { skewX?: string; } -interface AnimationStyle { +export interface AnimationStyle { opacity?: number; transform?: ReanimatedWebTransformProperties[]; } @@ -39,9 +42,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/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts b/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts index 6b1ff2e30419..13d05ad83a31 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts @@ -1,12 +1,18 @@ 'use strict'; -import type { AnimationConfig, AnimationNames, CustomConfig } from './config'; +import type { + AnimationConfig, + AnimationNames, + CustomConfig, + KeyframeDefinitions, +} from './config'; import { Animations } from './config'; import type { AnimatedComponentProps, LayoutAnimationStaticContext, } from '../../createAnimatedComponent/commonTypes'; import { LayoutAnimationType } from '../animationBuilder/commonTypes'; +import { createCustomKeyFrameAnimation } from './createAnimation'; import { getProcessedConfig, handleExitingAnimation, @@ -15,6 +21,7 @@ import { } from './componentUtils'; import { areDOMRectsEqual } from './domUtils'; import type { TransitionData } from './animationParser'; +import { Keyframe } from '../animationBuilder'; import { makeElementVisible } from './componentStyle'; function chooseConfig>( @@ -35,11 +42,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; } @@ -111,14 +118,19 @@ function tryGetAnimationConfig>( typeof config.constructor; const isLayoutTransition = animationType === LayoutAnimationType.LAYOUT; - const animationName = - typeof config === 'function' - ? config.presetName - : (config.constructor as ConstructorWithStaticContext).presetName; + const isCustomKeyframe = config instanceof Keyframe; + + const animationName = isCustomKeyframe + ? createCustomKeyFrameAnimation( + (config as CustomConfig).definitions as KeyframeDefinitions + ) + : typeof config === 'function' + ? config.presetName + : (config.constructor as ConstructorWithStaticContext).presetName; const shouldFail = checkUndefinedAnimationFail( animationName, - isLayoutTransition + isLayoutTransition || isCustomKeyframe ); if (shouldFail) { @@ -128,8 +140,7 @@ function tryGetAnimationConfig>( const animationConfig = getProcessedConfig( animationName, animationType, - config as CustomConfig, - animationName as AnimationNames + config as CustomConfig ); return animationConfig; diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts b/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts index 6614bf610409..09dc9552b9bf 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts @@ -1,13 +1,14 @@ 'use strict'; -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 type { TransitionData } from './animationParser'; import { TransitionGenerator } from './createAnimation'; import { scheduleAnimationCleanup } from './domUtils'; @@ -63,12 +64,12 @@ export function getReducedMotionFromConfig(config: CustomConfig) { function getDurationFromConfig( config: CustomConfig, - isLayoutTransition: boolean, - animationName: AnimationNames + animationName: string ): number { - const defaultDuration = isLayoutTransition - ? 0.3 - : Animations[animationName].duration; + const defaultDuration = + animationName in Animations + ? Animations[animationName as AnimationNames].duration + : 0.3; return config.durationV !== undefined ? config.durationV / 1000 @@ -86,17 +87,12 @@ function getReversedFromConfig(config: CustomConfig) { export function getProcessedConfig( animationName: string, animationType: LayoutAnimationType, - config: CustomConfig, - initialAnimationName: AnimationNames + config: CustomConfig ): AnimationConfig { return { animationName, animationType, - duration: getDurationFromConfig( - config, - animationType === LayoutAnimationType.LAYOUT, - initialAnimationName - ), + duration: getDurationFromConfig(config, animationName), delay: getDelayFromConfig(config), easing: getEasingFromConfig(config), callback: getCallbackFromConfig(config), diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/config.ts b/packages/react-native-reanimated/src/layoutReanimation/web/config.ts index 60307910897c..8c82027f8d08 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/config.ts +++ b/packages/react-native-reanimated/src/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/packages/react-native-reanimated/src/layoutReanimation/web/createAnimation.ts b/packages/react-native-reanimated/src/layoutReanimation/web/createAnimation.ts index ff147f5c1296..d184395fdc77 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/createAnimation.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/createAnimation.ts @@ -1,8 +1,10 @@ 'use strict'; import { TransitionType } from './config'; +import type { KeyframeDefinitions } from './config'; import { convertAnimationObjectToKeyframes } from './animationParser'; import type { + AnimationData, ReanimatedWebTransformProperties, TransitionData, } from './animationParser'; @@ -12,14 +14,14 @@ 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 transform we have to add 'px' suffix to each of translate values // that are present inside transform. // // eslint-disable-next-line @typescript-eslint/no-unused-vars -function addPxToTranslate( - transform: NonNullable -) { +function addPxToTranslate(transform: TransformType) { type RNTransformProp = (typeof transform)[number]; // @ts-ignore `existingTransform` cannot be string because in that case @@ -27,7 +29,7 @@ function addPxToTranslate( const newTransform = transform.map((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`; @@ -42,6 +44,30 @@ function addPxToTranslate( return newTransform; } +export function createCustomKeyFrameAnimation( + keyframeDefinitions: KeyframeDefinitions +) { + 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, + }; + + animationData.name = generateNextCustomKeyframeName(); + + const parsedKeyframe = convertAnimationObjectToKeyframes(animationData); + + insertWebAnimation(animationData.name, parsedKeyframe); + + return animationData.name; +} + let customKeyframeCounter = 0; function generateNextCustomKeyframeName() { From 1c0319d480baca33945a9d9d13094051756f9f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= Date: Tue, 18 Jun 2024 15:50:06 +0200 Subject: [PATCH 02/12] Add originX/Y to parsing --- .../src/layoutReanimation/web/animationParser.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/animationParser.ts b/packages/react-native-reanimated/src/layoutReanimation/web/animationParser.ts index 829725ca07e0..ec04bc78d441 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/animationParser.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/animationParser.ts @@ -60,6 +60,16 @@ export function convertAnimationObjectToKeyframes( continue; } + if (property === 'originX') { + keyframe += `left: ${values}px; `; + continue; + } + + if (property === 'originY') { + keyframe += `top: ${values}px; `; + continue; + } + if (property !== 'transform') { keyframe += `${property}: ${values}; `; continue; From ab8de265082e21e5cb74f229e29e1d8b9d9f5bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= Date: Wed, 19 Jun 2024 13:53:52 +0200 Subject: [PATCH 03/12] Change setDummyPosition name --- .../layoutReanimation/web/componentStyle.ts | 22 +++++++++---------- .../layoutReanimation/web/componentUtils.ts | 3 ++- .../src/layoutReanimation/web/domUtils.ts | 4 ++-- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/componentStyle.ts b/packages/react-native-reanimated/src/layoutReanimation/web/componentStyle.ts index ec192f6cae76..f2f82c3d70ea 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/componentStyle.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/componentStyle.ts @@ -66,19 +66,19 @@ function fixElementPosition( } } -export function setDummyPosition( - dummy: HTMLElement, +export function setElementPosition( + element: HTMLElement, snapshot: ReanimatedSnapshot ) { - dummy.style.transform = ''; - dummy.style.position = 'absolute'; - dummy.style.top = `${snapshot.top}px`; - dummy.style.left = `${snapshot.left}px`; - dummy.style.width = `${snapshot.width}px`; - dummy.style.height = `${snapshot.height}px`; - dummy.style.margin = '0px'; // tmpElement has absolute position, so margin is not necessary + element.style.transform = ''; + element.style.position = 'absolute'; + element.style.top = `${snapshot.top}px`; + element.style.left = `${snapshot.left}px`; + element.style.width = `${snapshot.width}px`; + element.style.height = `${snapshot.height}px`; + element.style.margin = '0px'; // tmpElement has absolute position, so margin is not necessary - if (dummy.parentElement) { - fixElementPosition(dummy, dummy.parentElement, snapshot); + if (element.parentElement) { + fixElementPosition(element, element.parentElement, snapshot); } } diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts b/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts index 09dc9552b9bf..405ced3bd43d 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts @@ -18,6 +18,7 @@ import { ReduceMotion } from '../../commonTypes'; import { isReducedMotion } from '../../PlatformChecker'; import { LayoutAnimationType } from '../animationBuilder/commonTypes'; import type { ReanimatedSnapshot, ScrollOffsets } from './componentStyle'; +import { setElementPosition, snapshots } from './componentStyle'; import { setDummyPosition, snapshots } from './componentStyle'; function getEasingFromConfig(config: CustomConfig): string { @@ -256,7 +257,7 @@ export function handleExitingAnimation( snapshots.set(dummy, snapshot); - setDummyPosition(dummy, snapshot); + setElementPosition(dummy, snapshot); const originalOnAnimationEnd = dummy.onanimationend; diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/domUtils.ts b/packages/react-native-reanimated/src/layoutReanimation/web/domUtils.ts index 7e918f508301..caa15645a2d3 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/domUtils.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/domUtils.ts @@ -2,7 +2,7 @@ import type { ReanimatedHTMLElement } from '../../js-reanimated'; import { isWindowAvailable } from '../../PlatformChecker'; -import { setDummyPosition, snapshots } from './componentStyle'; +import { setElementPosition, snapshots } from './componentStyle'; import { Animations } from './config'; import type { AnimationNames } from './config'; @@ -147,7 +147,7 @@ function reattachElementToAncestor(child: ReanimatedHTMLElement, parent: Node) { child.removedAfterAnimation = true; parent.appendChild(child); - setDummyPosition(child, childSnapshot); + setElementPosition(child, childSnapshot); const originalOnAnimationEnd = child.onanimationend; From 0d29d915d39993b048ecb815166fe11a1d8f7971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= Date: Wed, 19 Jun 2024 13:55:06 +0200 Subject: [PATCH 04/12] update element style --- .../web/animationsManager.ts | 3 +++ .../layoutReanimation/web/componentUtils.ts | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts b/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts index 13d05ad83a31..5c4731896961 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts @@ -17,6 +17,7 @@ import { getProcessedConfig, handleExitingAnimation, handleLayoutTransition, + maybeModifyStyleForKeyframe, setElementAnimation, } from './componentUtils'; import { areDOMRectsEqual } from './domUtils'; @@ -156,6 +157,8 @@ export function startWebLayoutAnimation< ) { const animationConfig = tryGetAnimationConfig(props, animationType); + maybeModifyStyleForKeyframe(element, props.entering as CustomConfig); + if ((animationConfig?.animationName as AnimationNames) in Animations) { maybeReportOverwrittenProperties( Animations[animationConfig?.animationName as AnimationNames].style, diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts b/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts index 405ced3bd43d..fad94e95ca74 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts @@ -101,6 +101,29 @@ export function getProcessedConfig( }; } +export function maybeModifyStyleForKeyframe( + element: HTMLElement, + config: CustomConfig +) { + if (!(config instanceof Keyframe)) { + return; + } + + for (const timestampRules of Object.values( + config.definitions as KeyframeDefinitions + )) { + if ('originX' in timestampRules || 'originY' in timestampRules) { + element.style.position = 'absolute'; + + // We need to set `animationFillMode` to `forwards`, otherwise component will go back to its position. + // This will result in wrong snapshot + element.style.animationFillMode = 'forwards'; + + return; + } + } +} + export function saveSnapshot(element: HTMLElement) { const rect = element.getBoundingClientRect(); From bfd9e7d7bfb5c349c517e787dca14e6f7d6e494e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= Date: Wed, 19 Jun 2024 13:56:10 +0200 Subject: [PATCH 05/12] Save position after animation ends --- .../layoutReanimation/web/animationsManager.ts | 2 +- .../src/layoutReanimation/web/componentUtils.ts | 17 ++++++++++++++--- .../src/layoutReanimation/web/domUtils.ts | 16 +++++++++++++--- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts b/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts index 5c4731896961..e7607b8a2499 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts @@ -94,7 +94,7 @@ function chooseAction( ) { switch (animationType) { case LayoutAnimationType.ENTERING: - setElementAnimation(element, animationConfig); + setElementAnimation(element, animationConfig, true); break; case LayoutAnimationType.LAYOUT: transitionData.reversed = animationConfig.reversed; diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts b/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts index fad94e95ca74..1dc783946f23 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts @@ -6,6 +6,7 @@ import type { AnimationConfig, AnimationNames, CustomConfig, + KeyframeDefinitions, } from './config'; import { WebEasings } from './Easing.web'; import type { WebEasingsNames } from './Easing.web'; @@ -19,7 +20,7 @@ import { isReducedMotion } from '../../PlatformChecker'; import { LayoutAnimationType } from '../animationBuilder/commonTypes'; import type { ReanimatedSnapshot, ScrollOffsets } from './componentStyle'; import { setElementPosition, snapshots } from './componentStyle'; -import { setDummyPosition, snapshots } from './componentStyle'; +import { Keyframe } from '../animationBuilder'; function getEasingFromConfig(config: CustomConfig): string { const easingName = @@ -140,7 +141,8 @@ export function saveSnapshot(element: HTMLElement) { export function setElementAnimation( element: HTMLElement, - animationConfig: AnimationConfig + animationConfig: AnimationConfig, + shouldSavePosition = false ) { const { animationName, duration, delay, easing } = animationConfig; @@ -150,11 +152,16 @@ export function setElementAnimation( element.style.animationTimingFunction = easing; element.onanimationend = () => { + if (shouldSavePosition) { + saveSnapshot(element); + } + animationConfig.callback?.(true); element.removeEventListener('animationcancel', animationCancelHandler); }; const animationCancelHandler = () => { + console.log('eoeo'); animationConfig.callback?.(false); element.removeEventListener('animationcancel', animationCancelHandler); }; @@ -172,7 +179,11 @@ export function setElementAnimation( }; if (!(animationName in Animations)) { - scheduleAnimationCleanup(animationName, duration + delay); + scheduleAnimationCleanup(animationName, duration + delay, () => { + if (shouldSavePosition) { + setElementPosition(element, snapshots.get(element)!); + } + }); } } diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/domUtils.ts b/packages/react-native-reanimated/src/layoutReanimation/web/domUtils.ts index caa15645a2d3..423f4068012c 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/domUtils.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/domUtils.ts @@ -85,7 +85,10 @@ export function insertWebAnimation(animationName: string, keyframe: string) { } } -function removeWebAnimation(animationName: string) { +function removeWebAnimation( + animationName: string, + animationRemoveCallback: () => void +) { // Without this check SSR crashes because document is undefined (NextExample on CI) if (!isWindowAvailable()) { return; @@ -101,7 +104,10 @@ function removeWebAnimation(animationName: string) { throw new Error('[Reanimated] Failed to obtain animation index.'); } + animationRemoveCallback(); + styleTag.sheet?.deleteRule(currentAnimationIndex); + animationNameList.splice(currentAnimationIndex, 1); animationNameToIndex.delete(animationName); @@ -123,7 +129,8 @@ const minimumFrames = 10; export function scheduleAnimationCleanup( animationName: string, - animationDuration: number + animationDuration: number, + animationRemoveCallback: () => void ) { // If duration is very short, we want to keep remove delay to at least 10 frames // In our case it is exactly 160/1099 s, which is approximately 0.15s @@ -132,7 +139,10 @@ export function scheduleAnimationCleanup( animationDuration + frameDurationMs * minimumFrames ); - setTimeout(() => removeWebAnimation(animationName), timeoutValue); + setTimeout( + () => removeWebAnimation(animationName, animationRemoveCallback), + timeoutValue + ); } function reattachElementToAncestor(child: ReanimatedHTMLElement, parent: Node) { From 335c2c0470ebf4757552070e3321b02df5f28cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= Date: Wed, 19 Jun 2024 14:00:13 +0200 Subject: [PATCH 06/12] Remove console.log --- .../src/layoutReanimation/web/componentUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts b/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts index 1dc783946f23..eeb7ef030027 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts @@ -161,7 +161,6 @@ export function setElementAnimation( }; const animationCancelHandler = () => { - console.log('eoeo'); animationConfig.callback?.(false); element.removeEventListener('animationcancel', animationCancelHandler); }; From 153984a67c95b17650d7645211e22c392b911cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= Date: Thu, 20 Jun 2024 12:28:39 +0200 Subject: [PATCH 07/12] Move fillmode to all keyframes --- .../src/layoutReanimation/web/componentUtils.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts b/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts index eeb7ef030027..c81020666580 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts @@ -110,16 +110,15 @@ export function maybeModifyStyleForKeyframe( return; } + // We need to set `animationFillMode` to `forwards`, otherwise component will go back to its position. + // This will result in wrong snapshot + element.style.animationFillMode = 'forwards'; + for (const timestampRules of Object.values( config.definitions as KeyframeDefinitions )) { if ('originX' in timestampRules || 'originY' in timestampRules) { element.style.position = 'absolute'; - - // We need to set `animationFillMode` to `forwards`, otherwise component will go back to its position. - // This will result in wrong snapshot - element.style.animationFillMode = 'forwards'; - return; } } From 057b963ddacadb6d2b17bd213feed1790f4b47dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= Date: Thu, 20 Jun 2024 12:29:29 +0200 Subject: [PATCH 08/12] Add warning --- .../src/layoutReanimation/web/animationsManager.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts b/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts index e7607b8a2499..494c0b8bd1b8 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts @@ -138,6 +138,20 @@ function tryGetAnimationConfig>( return null; } + if (isCustomKeyframe) { + const keyframeTimestamps = Object.keys( + (config as CustomConfig).definitions as KeyframeDefinitions + ); + + if ( + !(keyframeTimestamps.includes('100') || keyframeTimestamps.includes('to')) + ) { + console.warn( + `[Reanimated] Neither '100' nor 'to' was specified in Keyframe definition. This may result in wrong final position of your component. One possible solution is to duplicate last timestamp in definition as '100' (or 'to')` + ); + } + } + const animationConfig = getProcessedConfig( animationName, animationType, From 7842809907d3fd960062cab9d5d0c1049b9d3f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= Date: Thu, 20 Jun 2024 13:31:19 +0200 Subject: [PATCH 09/12] Fix olympic example --- .../src/examples/LayoutAnimations/OlympicAnimation.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/common-app/src/examples/LayoutAnimations/OlympicAnimation.tsx b/apps/common-app/src/examples/LayoutAnimations/OlympicAnimation.tsx index c12bac67e0fd..ddf38dcf88aa 100644 --- a/apps/common-app/src/examples/LayoutAnimations/OlympicAnimation.tsx +++ b/apps/common-app/src/examples/LayoutAnimations/OlympicAnimation.tsx @@ -72,6 +72,9 @@ export default function OlympicAnimation() { 60: { transform: [{ translateX: -13 }, { translateY: 0 }], }, + to: { + transform: [{ translateX: -13 }, { translateY: 0 }], + }, }).duration(3000); const blueRingExitAnimation = new Keyframe({ from: { @@ -104,6 +107,11 @@ export default function OlympicAnimation() { transform: [{ translateX: 1100 }, { translateY: 1100 }, { scale: 20 }], easing: Easing.quad, }, + t0: { + opacity: 0, + transform: [{ translateX: 1100 }, { translateY: 1100 }, { scale: 20 }], + easing: Easing.quad, + }, }).duration(3000); const yellowRingExitAnimation = new Keyframe({ from: { From 8321f54e0c28d1df467acb3b7eacce145d2e3158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= Date: Thu, 20 Jun 2024 13:53:27 +0200 Subject: [PATCH 10/12] Fix typo --- .../src/examples/LayoutAnimations/OlympicAnimation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/common-app/src/examples/LayoutAnimations/OlympicAnimation.tsx b/apps/common-app/src/examples/LayoutAnimations/OlympicAnimation.tsx index ddf38dcf88aa..27a3081dda3b 100644 --- a/apps/common-app/src/examples/LayoutAnimations/OlympicAnimation.tsx +++ b/apps/common-app/src/examples/LayoutAnimations/OlympicAnimation.tsx @@ -107,7 +107,7 @@ export default function OlympicAnimation() { transform: [{ translateX: 1100 }, { translateY: 1100 }, { scale: 20 }], easing: Easing.quad, }, - t0: { + to: { opacity: 0, transform: [{ translateX: 1100 }, { translateY: 1100 }, { scale: 20 }], easing: Easing.quad, From 49a9a58214d3b8e525b3589d2a792d4a120d55b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= Date: Fri, 21 Jun 2024 16:07:59 +0200 Subject: [PATCH 11/12] Add comment about duration --- .../src/layoutReanimation/web/componentUtils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts b/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts index ced4a23bf209..417f7aa10bed 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/componentUtils.ts @@ -68,6 +68,9 @@ function getDurationFromConfig( config: CustomConfig, animationName: string ): number { + // Duration in keyframe has to be in seconds. However, when using `.duration()` modifier we pass it in miliseconds. + // If `duration` was specified in config, we have to divide it by `1000`, otherwise we return value that is already in seconds. + const defaultDuration = animationName in Animations ? Animations[animationName as AnimationNames].duration From 55aab5e404ec89587b6ebcaa82852019e4998295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bert?= Date: Fri, 21 Jun 2024 16:15:09 +0200 Subject: [PATCH 12/12] Rewrite to if --- .../web/animationsManager.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts b/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts index 494c0b8bd1b8..72753c3c74d9 100644 --- a/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts +++ b/packages/react-native-reanimated/src/layoutReanimation/web/animationsManager.ts @@ -121,13 +121,18 @@ function tryGetAnimationConfig>( const isLayoutTransition = animationType === LayoutAnimationType.LAYOUT; const isCustomKeyframe = config instanceof Keyframe; - const animationName = isCustomKeyframe - ? createCustomKeyFrameAnimation( - (config as CustomConfig).definitions as KeyframeDefinitions - ) - : typeof config === 'function' - ? config.presetName - : (config.constructor as ConstructorWithStaticContext).presetName; + let animationName; + + if (isCustomKeyframe) { + animationName = createCustomKeyFrameAnimation( + (config as CustomConfig).definitions as KeyframeDefinitions + ); + } else if (typeof config === 'function') { + animationName = config.presetName; + } else { + animationName = (config.constructor as ConstructorWithStaticContext) + .presetName; + } const shouldFail = checkUndefinedAnimationFail( animationName,